refactor(scanner): route-based QR scanner (fix black preview + frozen screen)#85
Draft
epicexcelsior wants to merge 3 commits into
Draft
refactor(scanner): route-based QR scanner (fix black preview + frozen screen)#85epicexcelsior wants to merge 3 commits into
epicexcelsior wants to merge 3 commits into
Conversation
d78415f to
c5767b7
Compare
Two related iOS bugs observed on a personal-team dev build: 1. CameraView mounted before permission resolved → black preview that never recovers until app restart. On iOS the component caches its permission state at mount; flipping null→granted at runtime doesn't re-init the preview. Gate the mount on `permission.granted === true` so the conditional flip causes a fresh mount with permission in place. 2. PeersDrawer's QR onResult silently dropped any QR that wasn't a plain LXMF hash — group QRs, Solana addresses, malformed/empty payloads all closed the modal with no feedback, leaving users thinking "scanner is broken." Surface explicit alerts: - lxmf-group → "looks like a channel QR, use Join channel" - solana → "wallet address, use Send screen" - unknown → show first 64 chars of raw payload so user can debug Also adds a __DEV__ console.log of the parsed result so we can adb logcat-trace exactly what came out of the scanner.
expo-camera@17 split audio recording into a separate package (expo-audio). iOS native side hard-links the ExpoAudio module even when the JS-level use is barcode-scanning only, so the dev-client build fails with "Cannot find native module expoAudio" without the peer dep installed. `npx expo install expo-audio` added 1.1.1 to deps and registered the config plugin in app.json. No source code changes — this is pure native bridge availability.
3db3829 to
590ae74
Compare
QRScannerModal was a free-floating <Modal>. Rendered inside the join-channel sheet (itself a <Modal>) it stacked two native iOS windows — black camera preview, and corrupted touch handling on dismiss (frozen screen, camera never released). A single modal (Send, peers drawer) worked; only nesting broke. - app/scan.tsx route + scan() promise API (src/services/qrScan), usable identically from any screen or modal; one camera/permission lifecycle; re-entrancy-guarded so overlapping calls can't stack routes. - Convert the join-channel sheet to app/join-channel.tsx (transparentModal owning its slide, like receive) so the scanner composes over it natively — no nested-modal conflict. - Update RecipientPicker, PeersDrawer, and the join flow to await scan(). Delete QRScannerModal and JoinGroupModal.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The QR scanner was
QRScannerModal, a free-floating core RN<Modal>. Rendered inside the join-channel sheet (itself a<Modal>), the two stacked into separate native iOS windows — black camera preview, and corrupted touch handling on dismiss (frozen screen, camera session never released). A single modal (Send recipient, peers drawer) worked fine; only the nested case broke.This replaces the scanner with an expo-router route, the pattern the app already uses for
receive/contacts/send.Changes
app/scan.tsx— full-screen scanner route, driven by a promise APIscan()(src/services/qrScan.ts).const r = await scan()works identically from any screen or modal. One scanner, one camera/permission lifecycle, one parse path. Re-entrancy-guarded — a second call while one is in flight returns the same promise instead of stacking a duplicate route.app/join-channel.tsx— the join sheet, converted from<Modal>to atransparentModalroute that owns its slide animation (same approach asreceive). The scanner route composes over it natively — no nested-modal conflict, no hide/remount workaround.RecipientPicker,PeersDrawer, and the join flow toawait scan().QRScannerModalandJoinGroupModal.Routes compose; nested
<Modal>s don't. This is the template for migrating the remaining modal components later.Why not just gate the modal's
visibleThat patches the symptom while keeping two presentation systems (RN
<Modal>windows + navigator routes) that don't compose. Making the scanner a route removes the bug class by construction.Test plan
Device-verified (iOS dev client):
Pending:
Notes
QRScannerModal.LxmfContext.handleJoinGroupdoesn'tawait lxmf.joinGroup, so a failed join can report success — worth a separate look.