Register Device Button: No Loading Feedback + Stale Label After Registration
Metadata
- Date:
2026-04-26 - Status:
fixed - Severity:
medium - Related issue/ticket:
N/A - Owner:
N/A
About
Overview: - Bug 1: Pressing the "Register Device" button produces no visual feedback. The button should grey out immediately on press and remain greyed out while the user is in the native registration flow (Meta View app). Instead the button appears to do nothing until the user returns. - Bug 2: After completing the registration flow in Meta View and returning to the settings screen, the button label still reads "Register Device" instead of updating to "Unregister Device". The user must navigate away from and back into settings before the label updates. - These bugs degrade user trust: with no feedback the user may press the button multiple times, and after returning from registration the UI appears broken.
Technical Questions: - startRegistration() is an iOS AsyncFunction that calls Wearables.shared.startRegistration(), which opens the Meta View companion app via URL scheme and resolves in milliseconds — it does not wait for the user to complete pairing. - The registration completes only when Meta View redirects back via the encache:// URL scheme and WearablesAppDelegateSubscriber.handleUrl() processes the callback. - useFocusEffect fires on React Navigation focus events (in-app navigation). It does NOT fire when the iOS app returns to the foreground after being backgrounded (e.g., after the user comes back from Meta View). Therefore the stale-label fix using useFocusEffect alone is insufficient. - React Native's AppState API fires change events when the app transitions between active, inactive, and background states. An AppState listener is needed to detect the app-return and re-read registration state.
Resources: - main/app/app/settings.tsx — SettingsScreenContent component - main/app/wearables-module/ios/WearablesModule.swift — startRegistration() and startUnregistrationInternal() - main/app/wearables-module/ios/WearablesAppDelegateSubscriber.swift — URL callback handler that completes registration - main/app/__tests__/settings-screen.test.tsx — existing test suite
Steps to cause failure
flowchart LR
A[User opens Settings] --> B[Presses Register Device]
B --> C[startRegistration resolves in ms]
C --> D[App transitions to background - Meta View opens]
D --> E[User completes pairing in Meta View]
E --> F[Meta View redirects back via encache:// URL]
F --> G[App returns to foreground - Settings screen visible]
G --> H[useFocusEffect does NOT fire - AppState not wired]
H --> I[Button still shows Register Device - stale state] System
flowchart TD
SettingsScreen --> handleToggleRegistration
handleToggleRegistration --> |"isRegistered=false"| startRegistration
startRegistration --> |"resolves in ~ms"| NativeMetaView["Meta View App (OS level)"]
NativeMetaView --> |"encache:// URL callback"| WearablesAppDelegateSubscriber
WearablesAppDelegateSubscriber --> |"handleUrl()"| RegistrationComplete["isAnyDeviceRegisted() = true"]
SettingsScreen --> |"useFocusEffect fires on RN nav focus only"| isAnyDeviceRegisted
AppState --> |"change → active"| isAnyDeviceRegisted Registration state is updated by the URL callback handler asynchronously. The settings screen must listen to AppState changes to detect the app-return and re-read registration state.
Reproduction Details
- Open the Encache app on a device with no registered glasses.
- Navigate to Settings.
- Observe the "Register Device" button.
- Press the "Register" button — observe: no visual feedback, button is not greyed out.
- Complete pairing in Meta View and return to Encache Settings.
- Observe: button still shows "Register" / "Register Device" — stale label.
- Navigate away from Settings and back — button now correctly shows "Unregister".
Reproduction test (unit preferred): main/app/__tests__/settings-screen.test.tsx — new tests button remains disabled while app is backgrounded for registration and refreshes registration state when app returns to foreground
Notes for PR
Bug 1 fix: After startRegistration() resolves, do NOT call setIsToggling(false) in the finally block for the registration path. For the registration (not unregistration) path, leave isToggling = true to keep the button visually greyed out and disabled while the user completes pairing in Meta View. The AppState listener will reset isToggling = false and refresh isRegistered when the app returns to foreground.
Bug 2 fix: Add a useEffect that subscribes to AppState.addEventListener('change', ...). When the state transitions to 'active', call setIsRegistered(WearablesModule.isAnyDeviceRegisted()). Also reset isToggling to false on this transition so the button is interactive again. The listener should be cleaned up on unmount.
Audit Log
| ID | Action | Note | Context |
|---|---|---|---|
| 1 | Create audit log | Initialize bug investigation | Two UX bugs on settings Register Device button |
| 2 | Read settings.tsx | Found handleToggleRegistration and useFocusEffect implementation | settings.tsx lines 36-65 |
| 3 | Read WearablesModule.swift | Confirmed startRegistration resolves immediately after opening Meta View | WearablesModule.swift + WearablesAppDelegateSubscriber.swift |
| 4 | Run existing tests | All 13 tests pass | No existing test covers AppState-based refresh or persistent loading state |
| 5 | Root cause identified | Bug 1: finally block resets isToggling before user is in Meta View (React batching or instant resolve). Bug 2: useFocusEffect does not fire on app foreground return — AppState listener needed | Evidence: WearablesAppDelegateSubscriber confirms registration only completes on URL callback |
| 6 | Write failing regression tests | Tests for persistent disabled state and AppState-triggered refresh | settings-screen.test.tsx |
| 7 | Confirm tests fail before fix | Both new tests failed, existing "updates to Unregister" test also failed | 3 of 15 tests failed |
| 8 | Apply fix to settings.tsx | Added AppState listener; registration path no longer resets isToggling in finally — leaves button disabled until app foregrounds | settings.tsx lines 43-91 |
| 9 | All 15 tests pass after fix | Including 2 new regression tests and updated existing test | npx jest settings-screen.test.tsx |
| 10 | Causality verified | Reverted fix → 3 tests failed; restored fix → 15 pass | Confirmed fix is load-bearing |
| 11 | Code review gate | No Critical/Major findings; DRY, YAGNI, test coverage all pass | Inline review |
Verification
- [x] Reproduced failure before fix
- [x] Reproduction test fails before fix
- [x] Root cause identified with evidence
- [x] Fix applied at source (no workaround-only patch)
- [x] Reproduction test passes after fix
- [x] Reproduction path now passes
- [x] Regression test added/updated — 2 new tests + 1 updated test in
main/app/__tests__/settings-screen.test.tsx - [x] Verified no duplicate solved-bug log exists for same root cause