A local-first React Native (Expo) app that turns your phone into a remote for a Samsung Smart TV, tuned for The Frame (2020, model LS03T) and other 2019+ Tizen TVs that use token authentication.
Everything runs on-device: discovered TVs and pairing tokens are stored locally (tokens in the OS secure enclave). There is no account, no cloud sync, and no analytics.
- 📺 Discovery — finds Samsung TVs on your Wi-Fi via mDNS with an IP-scan fallback
- 🔐 Secure pairing — token handshake over
wss://…:8002; the token is stored in the Keychain/Keystore and reused so you only approve the popup once - 🎛️ Modern Samsung remote — power, a D-pad with OK, Back/Home, a volume rocker (mute in the centre), a channel rocker (guide in the centre) and play/pause
- 🚀 App shortcuts — one-tap Netflix, Prime Video and Disney+ via the TV's REST API
Layered and dependency-inverted: domain/ has zero RN/Expo imports; adapters
implement I-prefixed interfaces; the composition root (src/di/) registers the
concrete adapters and the UI resolves them with useService.
app/ Expo Router screens (thin)
components/ Presentational React components (SamsungRemote, …)
src/
domain/
models/ RemoteKey & SamsungApp catalogues, ConnectionState, TVDevice, TVModel
validators/ HostValidator
repositories/ interfaces/ (ITVRepository, ITokenStore) + implementations/
(AsyncStorage TV repo, secure token store) + RepositoryContainer
services/
remote/ interfaces/ (IRemoteTransport, IAppLauncher, IDeviceInfoProvider)
+ providers/ (Samsung WebSocket transport, REST app launcher,
device-info) + RemoteSession
discovery/ IDiscoveryService + Zeroconf/IP-scan + DiscoverTVs
tv/ SavedTVsService
logging/ createLogger
di/ ServiceContainer + providers + ServiceContext + useService
hooks/ useRemoteSession, useDiscovery, useSavedTVs
utils/ base64, http
GET http://<ip>:8001/api/v2/→ detect the model and token support.- Open
wss://<ip>:8002/api/v2/channels/samsung.remote.control?name=<base64>. - On
ms.channel.unauthorizedthe TV shows an Allow/Deny popup. - On approval the TV returns
ms.channel.connectwithdata.token; the token is persisted and reused (&token=…) so future connections skip the popup. - Keys →
ms.remote.control(SendRemoteKey); app launch →POST /api/v2/applications/<appId>.
On iOS the
NSAllowsArbitraryLoadsexception inapp.jsonlets the app talk to the TV's self-signedwsscertificate. On Android the platform WebSocket may reject self-signed certs — iOS is the primary target.
npm install
npm start # then press i (iOS) / a (Android)npm test # run the unit suite
npm run test:coverage # with coverage (src/** ≥ 80%)
npm run type-check # tsc --noEmit
npm run lintThe Samsung protocol is covered by unit tests that drive a mocked WebSocket
through the full handshake (unauthorized → token → connected → sendKey), so the
wire format is verified without a physical TV. Validate against your own Frame on
the same Wi-Fi network.
Uses EAS with local signing credentials (credentialsSource: "local" in
eas.json). Copy the template and fill in your own certificate paths and
passwords; the real files are git-ignored and never committed:
cp credentials.json.example credentials.json # then edit with your values
# place the referenced files under ios_certs/ and android_certs/npm run build:development-device
npm run build:productionThere are no runtime secrets to configure: the app is local-only (no Supabase/analytics), so there is no
.env. If you ever add a cloud key, inject it viaapp.config.js→extraand read it through a config provider.
MIT