Skip to content

Latest commit

 

History

History
230 lines (174 loc) · 13.2 KB

File metadata and controls

230 lines (174 loc) · 13.2 KB

Apple TV / Android TV (DS-3)

Full Munib Tracker product app on living-room devices via the React Native TV fork and Expo’s Build for TV guide.

Approach

Item Detail
Package Same apps/app (not a separate monorepo app)
RN "react-native": "npm:react-native-tvos@0.86.0-2" (Expo SDK 57)
Toggle EXPO_TV=1 during prebuild / EAS
Plugin @react-native-tvos/config-tv with Leanback banner + Apple TV icons/top shelf
Chrome Side rail always (WideAppTabs); focus rings on PressableScale

Phone ↔ TV always needs a clean prebuild (pnpm prebuild:app:tv vs pnpm cleanbuild:app:android / ios).

Versioning

Marketing semver is unified across phone, TV, and web via Release Please (expo release type + .env.example x-release-please-version markers). Build / version-code integers stay per surface and are not owned by Release Please (bump manually or via EAS autoIncrement on store upload).

Surface Marketing semver (same product version) Build / version code (independent)
Android phone EXPO_ANDROID_APP_VERSION EXPO_ANDROID_VERSION_CODE
Android TV / Fire TV EXPO_ANDROID_TV_APP_VERSION EXPO_ANDROID_TV_VERSION_CODE
iPhone EXPO_IOS_APP_VERSION EXPO_IOS_APP_BUILD_NUMBER
Apple TV EXPO_TVOS_APP_VERSION EXPO_TVOS_APP_BUILD_NUMBER

Phone + Android TV share package app.munibtracker on Play — versionCodes must stay unique and increasing across both form factors (do not reuse a phone code on TV or vice versa). See apps/app/.env.example and RELEASES.md.

Brand assets

Generated by pnpm generate:app:brand-assets into apps/app/assets/images/tv/:

File Role
android-banner.png Leanback row banner (320×180) — in APK
android-icon.png Android TV launcher icon (512×512) — in APK
tvos-icon-*.png Apple TV App Icon layers
tvos-topshelf-*.png Top Shelf / wide Top Shelf
firetv-background-1920x1080.png Amazon console background (not in APK)
firetv-icon-1280x720.png Amazon console large icon
firetv-icon-512.png / firetv-icon-114.png Amazon console icons

Do not run brand generation unless you intend to overwrite PNGs. Fire TV files are for Amazon Developer Console → Appstore Details only.

Store / screenshot notes: STORE_ASSETS.md.

Store screenshots (prepared; opt-in capture)

Scripts validate by default and do not write images until RUN_CAPTURE=1:

pnpm screenshots:tvos          # validate Apple TV pipeline
pnpm screenshots:android-tv    # validate Android TV / Fire TV pipeline
pnpm screenshots:tv            # both (still validate-only without RUN_CAPTURE)
pnpm screenshots:validate      # includes TV structure checks

# Later, when submitting TV SKUs (requires EXPO_TV=1 build + TV sim/emulator):
RUN_CAPTURE=1 pnpm screenshots:tvos
RUN_CAPTURE=1 LOCALES=en THEMES=dark pnpm screenshots:android-tv

Output (only with RUN_CAPTURE=1):

  • store-assets/captures-native/tvos|android-tv/<locale>/<theme>/
  • store-assets/ios/screenshots/apple-tv-1080p/<locale>/
  • store-assets/android/screenshots/android-tv-1080p/<locale>/

Scenes: lib/tv-scenes.mjs (home, tracker, library, quran, zikr, settings). Not CI-automated; not Maestro on TV.

Commands

# Generate tvOS + Leanback native projects (always --clean)
pnpm prebuild:app:tv
pnpm prebuild:app:tv:android
pnpm prebuild:app:tv:ios
# aliases:
pnpm cleanbuild:app:tv
pnpm cleanbuild:app:tv:android
pnpm cleanbuild:app:tv:ios

# Run (after TV prebuild)
pnpm dev:app:tv:android
pnpm dev:app:tv:ios

# Local signed release (mirrors phone release:app:*)
pnpm release:app:android-tv          # Leanback AAB → android/play-upload/tv/
pnpm release:app:android-tv:apk      # Leanback APK (Amazon Fire TV / sideload)
pnpm release:app:android-tv:upload   # Play internal track (same package; enable TV form factor)
pnpm release:app:tvos                # tvOS IPA → ios/app-store-export-tvos/ (macOS)
pnpm release:app:tvos:upload         # altool --type appletvos
pnpm release:app:tvos:upload:validate

# EAS (profiles development_tv / preview_tv / production_tv)
pnpm build:app:android:tv
pnpm build:app:ios:tv
pnpm build:app:android:preview:tv
pnpm build:app:ios:preview:tv
pnpm submit:app:android:tv
pnpm submit:app:ios:tv

# Back to phone
pnpm cleanbuild:app:android
pnpm cleanbuild:app:ios

Guards: Phone release:app:android / release:app:ios refuse a Leanback/tvOS tree; TV release scripts refuse a phone tree. Switch with a clean prebuild.

Artifacts

Command Output
release:app:android-tv apps/app/android/play-upload/tv/app-release.aab
release:app:android-tv:apk apps/app/android/play-upload/tv/app-release.apk
release:app:tvos apps/app/ios/app-store-export-tvos/*.ipa

Fire TV banner/icons stay Amazon-console-only (firetv-*); the APK is the Leanback binary above.

EAS profiles: development_tv, preview_tv, production_tv in apps/app/eas.json (env.EXPO_TV=1). TV builds strip extra.eas.build.experimental.ios.appExtensions (widgets/Watch/Intents). submit.production_tv mirrors phone submit.

tvOS credentials: same bundle ID / certs as iOS, but different provisioning profiles. Automatic signing fails without a registered Apple TV development device — pnpm release:app:tvos uses manual signing with a TVOS_APP_STORE profile (ExportOptions.tvos.plist, override name via TVOS_PROVISIONING_PROFILE). When shipping both iOS and tvOS, keep tvOS credentials local (EAS-stored profiles are iOS-only).

Android TV emulator controls

The emulator sidebar buttons (◁ Back · ○ Home · □ Overview) are phone navigation. On Android TV AVDs they often do nothing — that is expected.

Use a remote / D-pad instead:

  1. Click the TV emulator window so it has keyboard focus.
  2. Keyboard: Arrow keys = D-pad, Enter = select, Esc = back.
  3. Or send keys via adb:
pnpm --filter app tv:remote home
pnpm --filter app tv:remote back
pnpm --filter app tv:remote up    # also: down left right select apps

Optional: ANDROID_SERIAL=emulator-5554 if several devices are connected.

If the Munib splash stays forever, Metro may be unreachable — pnpm dev:app:tv:android now sets REACT_NATIVE_PACKAGER_HOSTNAME=10.0.2.2 and adb reverse for emulators. Manually:

adb reverse tcp:8081 tcp:8081
adb shell am start -a android.intent.action.VIEW \
  -d 'exp+munib-tracker://expo-development-client/?url=http%3A%2F%2F10.0.2.2%3A8081'

Do not open the LAN IP (192.168.x.x) from the TV emulator — Dev Client hangs on splash, then ANRs.

react-native.config.js excludes widgets / quick-actions / speech-recognition / react-native-webview / expo-web-browser when EXPO_TV=1 (needs a TV prebuild / pod install to drop those from Fabric codegen). Without excluding webview, RCTThirdPartyComponentsProvider registers RNCWebView with a nil Class and aborts at JS load. On tvOS, plugins/withTvExpoModuleExcludes.cjs also patches use_expo_modules!(exclude: …) because some podspecs (e.g. expo-quick-actions) incorrectly declare :tvos and would still link without that.

Phone-only plugins (omitted when EXPO_TV=1)

@bacons/apple-targets, App Intents, external commands, Wear OS, Android widgets, expo-quick-actions.

Metro (EXPO_TV=1): phone-only packages resolve to stubs under apps/app/src/lib/tv-stubs/ so imports cannot crash Leanback/tvOS JS. Covered today: expo-speech-recognition, react-native-android-widget, expo-quick-actions, expo-web-browser, react-native-webview, react-native-pager-view, expo-notifications, expo-location, expo-sensors, expo-haptics, expo-local-authentication, expo-image-picker, expo-document-picker, expo-sharing, expo-store-review, expo-speech, expo-apple-authentication, expo-navigation-bar. Autolink excludes the same packages via react-native.config.js (+ Podfile use_expo_modules!(exclude: …) for modules whose podspecs incorrectly claim tvOS). plugins/withTvNilSafeFabricComponents.cjs also nil-safes RCTThirdPartyComponentsProvider.mm so any remaining unlinked Fabric Class does not abort at JS load.

Runtime degradations (Platform.isTV / isTV())

Feature TV behavior
Location Manual city search only (no GPS)
Notifications / Live Activity / push token Settings empty state; scheduler no-op
Widgets / Watch / Wear / Siri / quick actions Not built / not synced
Qibla Static bearing from city; no live compass
Verse detector Unavailable empty state
App lock / biometrics / voice shortcuts Hidden in Settings
Camera / gallery / document pickers Canceled / toast
Haptics / store review No-op
Social OAuth Best-effort on remote; QR companion login preferred (POST /auth/tv/pairing + phone /tv-pair)
Side rail Collapses to icons when focus is in content; expands with labels + brand when focused
Back / Menu Ladder: stack pop → expand rail + preferred tab focus → confirm exit (TvRemoteBridge)
Play/Pause remote Toggles in-app audio when a track is loaded
Lock-screen media Intentional no-op on TV
Focus Rings + scale-on-focus (reduce-motion gated); hasTVPreferredFocus; TvFocusGuide autoFocus (focus memory)
Copy tTv() prefers *.tv / *Tv Select wording; high-traffic “Tap …” strings have TV variants

Focus & remote (native feel)

Piece Where
Preferred focus Button.preferredFocus, Continue cards, rail after Back, confirm-dialog Cancel, first Settings/Library rows
Focus guides tv-focus-guide.tsxautoFocus (focus memory), trap edges on chips / segmented / content; sheets auto-focus
Non-focusable scroll tv-scroll-view.tsx / tv-flat-list.tsx — scroll chrome must not be a D-pad target (focusable={false} on TV). Used by ScreenLayout, sheets, home, search, lists, and toolbars. Keep scrollEnabled so focus moves between children and the view still scrolls to them.
Viewport CTAs Short screens (intro): also scrollEnabled={false} + CTA in TvFocusGuide autoFocus
nextFocus* Rail tabs nextFocusRight → content guide
Scale on focus pressable-scale.tsx (~1.08×, skipped when reduce motion)
Remote bridge tv-remote-bridge.tsx — Play/Pause → audio; Menu/Back ladder
QR login TV login shows QR; phone opens /tv-pair?code=… and claims via API

Simulator tip: click the TV window so it has keyboard focus, then Arrow keys = D-pad and Enter = Select. Mouse clicks alone often do not advance focus.

Layout widths (10-foot)

Token / rule Value / behavior
TvLayout.contentPaddingX 48 — overscan inset on ScreenLayout / sheets
ScreenLayout default on TV Uncapped (fills the tab pane). Learn prose routes stay at MaxContentWidth (800) for readability.
TvLayout.listDetailMaxWidth 1720 — Qur’an / Hadith list–detail shells
TvLayout.detailPaneMinWidth / MaxWidth 420 / 720 — secondary panes
Selected vs focused Soft fill (accentSoft) + hairline for selected; solid accent / thick ring only for D-pad focus. Never paint accent borders on selected rows that also receive a focus ring.
Search placeholders Prefer short keys on TV (e.g. quran.search vs long phone placeholders)

QA checklist

  • Android TV emulator: cold start → home side rail; D-pad between tabs
  • Apple TV simulator: same
  • Back from nested screen pops stack; from tab content expands rail; from rail root shows exit confirm
  • Play/Pause toggles Qur’an / audio mini-player when media is active
  • Focus scale + ring on primary tiles; reduce-motion disables scale
  • Continue card receives preferred focus when present
  • Mark Salah / tracker update (Select wording; large prayer rows)
  • Manual city change updates prayer times (no GPS chrome)
  • Open Qur’an surah, search, dua/zikr lists with focus
  • Settings appearance + language; phone-only rows empty-stated
  • Login: QR pairing panel; guest secondary; phone /tv-pair claims session
  • Intro: guest-first on last slide; location step overscan + no reminders prompt
  • Tasbeeh: Press Select to count
  • Qibla shows TV hint (no magnetometer)
  • Sheets / mini-player respect overscan padding
  • Leanback banner / Apple TV icon appear after TV prebuild
  • Phone prebuild still includes widgets / Watch after reverting EXPO_TV

Related: DEVICES.md · NATIVE_SURFACES.md · STORE_ASSETS.md · apps/app/AGENTS.md