Settings Screen
System Intent
- What this is: The settings screen (
SettingsScreenContent) is a React Native screen that lets authenticated users manage device registration (pairing/unpairing Meta View smart glasses), sign out of their account, and toggle developer debug mode. Device registration is coordinated across an in-app button tap, a native iOS module (WearablesModule), the Meta View companion app (an OS-level external app), and a URL-scheme callback — making foreground-return state refresh a critical concern.
Mermaid Diagram
flowchart TD
SettingsScreen["SettingsScreen (LoggingContextProvider)"]
SettingsScreen --> SettingsScreenContent
SettingsScreenContent --> useFocusEffect["useFocusEffect\n(reads isAnyDeviceRegisted on RN nav focus)"]
SettingsScreenContent --> AppStateListener["AppState listener\n(reads isAnyDeviceRegisted + resets isToggling\non app foreground return)"]
SettingsScreenContent --> handleToggleRegistration
handleToggleRegistration --> |"isRegistered=false"| startRegistration["WearablesModule.startRegistration()\n(resolves immediately, opens Meta View via URL scheme)"]
handleToggleRegistration --> |"isRegistered=true"| startUnregistration["WearablesModule.startUnregistration()\n(awaits full native result)"]
startRegistration --> MetaView["Meta View (OS-level app)\nUser completes pairing"]
MetaView --> |"encache:// URL callback"| WearablesAppDelegateSubscriber
WearablesAppDelegateSubscriber --> |"handleUrl() — sets registered state natively"| AppForeground["App returns to foreground"]
AppForeground --> AppStateListener
startUnregistration --> |"result: outcome, durationMs, states, errorMessage"| RefreshAfterUnreg["setIsRegistered + setIsToggling(false)"]
Flows
Flow: deviceRegistration
- Test files:
main/app/__tests__/settings-screen.test.tsx - Core files:
main/app/app/settings.tsx, main/app/wearables-module/ios/WearablesModule.swift, main/app/wearables-module/ios/WearablesAppDelegateSubscriber.swift
Types
WearablesModule.startRegistration() -> Promise<void>
Resolves in milliseconds after opening the Meta View companion app via URL scheme.
Does NOT wait for pairing to complete.
WearablesModule.startUnregistration() -> Promise<UnregistrationResult>
UnregistrationResult {
outcome: string ("success" | "error" | "timeout" | "already_unregistered")
durationMs: number
registrationStateBefore: string
registrationStateAfter: string
errorMessage: string | null
}
WearablesModule.isAnyDeviceRegisted() -> boolean
Synchronous. Reads current native registration state.
Paths
| path | input | output | path-type | notes |
deviceRegistration.start | User presses "Register" button | startRegistration() called; button disabled (isToggling=true) | happy path | isToggling is NOT reset in the finally block — AppState listener resets it when app foregrounds |
deviceRegistration.foregroundReturn | App returns to foreground (AppState active) | isRegistered refreshed via isAnyDeviceRegisted(); isToggling reset to false | happy path | Triggered by AppState.addEventListener('change', ...) — not by useFocusEffect |
deviceRegistration.error | startRegistration() throws | isToggling reset to false in catch block; button re-enabled | error | User can retry |
deviceUnregistration.start | User presses "Unregister" button | startUnregistration() called; button disabled | happy path | Awaits full native result before re-enabling button |
deviceUnregistration.complete | startUnregistration() resolves | isRegistered refreshed; isToggling reset to false; button re-enabled | happy path | Outcome logged via useLogging (wearable_unregistration_completed) |
deviceUnregistration.error | startUnregistration() throws | isToggling reset to false in catch block | error | isRegistered is re-read via isAnyDeviceRegisted() in finally-equivalent |
Pseudocode
// State refresh on RN navigation focus (handles in-app navigation)
useFocusEffect(() => {
setIsRegistered(WearablesModule.isAnyDeviceRegisted())
})
// State refresh on OS foreground return (handles Meta View → Encache transition)
useEffect(() => {
const sub = AppState.addEventListener('change', (nextState) => {
if (appStateRef.current !== 'active' && nextState === 'active') {
setIsRegistered(WearablesModule.isAnyDeviceRegisted())
setIsToggling(false)
}
appStateRef.current = nextState
})
return () => sub.remove()
}, [])
// Registration handler
handleToggleRegistration:
setIsToggling(true)
if isRegistered:
result = await WearablesModule.startUnregistration()
log('wearable_unregistration_completed', result fields)
setIsRegistered(WearablesModule.isAnyDeviceRegisted())
setIsToggling(false)
else:
await WearablesModule.startRegistration()
// Do NOT reset isToggling — AppState listener handles it on foreground return
catch:
setIsToggling(false)
Key invariant: startRegistration() opens Meta View via URL scheme and resolves in milliseconds. The button must remain disabled (isToggling=true) until the user completes pairing in Meta View and the OS returns the Encache app to the foreground. useFocusEffect does NOT fire on OS foreground return — only AppState does.
Flow: signOut
- Test files:
main/app/__tests__/settings-screen.test.tsx - Core files:
main/app/app/settings.tsx
Types
No input payload. Calls useAuth().signOut() then navigates to /(auth)/login.
Paths
| path | input | output | path-type | notes |
signOut.success | User presses "Sign out" | signOut() called; router navigates to /(auth)/login | happy path | Logged before action |
Flow: debugModeToggle
- Test files:
main/app/__tests__/settings-screen.test.tsx - Core files:
main/app/app/settings.tsx, main/app/lib/debug-mode.ts
Paths
| path | input | output | path-type | notes |
debugModeToggle | User flips the switch | setDebugMode(newValue) persisted via expo-secure-store key encache.debug_mode | happy path | Value read on focus via getDebugMode() |
Logs
| Source | Location |
wearable_unregistration_completed | Frontend logger (useLogging flow=settings) — includes outcome, duration_ms, registration_state_before, registration_state_after, error_message |
sign_out_requested | Frontend logger (useLogging flow=settings) — includes username_present |
Deployment
- Mechanism:
local only (React Native / Expo screen, no server deployment) - Deploy command:
# Run from main/app/
npx expo start
- Notes: iOS only for device registration (WearablesModule is an iOS native module). The
AppState listener and isAnyDeviceRegisted sync are both iOS-specific concerns.