Skip to content

refactor(scanner): route-based QR scanner (fix black preview + frozen screen)#85

Draft
epicexcelsior wants to merge 3 commits into
anonmesh:stagingfrom
epicexcelsior:epic/iou-qr-camera-fix
Draft

refactor(scanner): route-based QR scanner (fix black preview + frozen screen)#85
epicexcelsior wants to merge 3 commits into
anonmesh:stagingfrom
epicexcelsior:epic/iou-qr-camera-fix

Conversation

@epicexcelsior
Copy link
Copy Markdown
Collaborator

@epicexcelsior epicexcelsior commented May 19, 2026

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 API scan() (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 a transparentModal route that owns its slide animation (same approach as receive). The scanner route composes over it natively — no nested-modal conflict, no hide/remount workaround.
  • Updated RecipientPicker, PeersDrawer, and the join flow to await scan().
  • Deleted QRScannerModal and JoinGroupModal.

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 visible

That 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):

  • Join channel → SCAN QR CODE → camera shows (was black)
  • Cancel/X from the join scanner → no frozen screen, camera released, returns to the sheet smoothly
  • Open/close all three scanner entry points

Pending:

  • Re-verify scan results: Send (Solana addr → fills recipient), peers drawer (peer QR → opens thread; unknown → alert)
  • Android hardware-back from the scanner and the join sheet
  • Scan a real channel QR → actually joins (needs a 2nd device)

Notes

  • Squash-merge — history has intermediate add-then-delete churn on QRScannerModal.
  • Pure JS (no native rebuild). Reaches users via a staging→v3 promotion.
  • Out of scope / pre-existing (not this PR): LxmfContext.handleJoinGroup doesn't await lxmf.joinGroup, so a failed join can report success — worth a separate look.

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.
@epicexcelsior epicexcelsior force-pushed the epic/iou-qr-camera-fix branch from 3db3829 to 590ae74 Compare May 25, 2026 02:15
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.
@epicexcelsior epicexcelsior changed the title fix(qr-scan): gate CameraView on permission + surface unrecognized QR refactor(scanner): route-based QR scanner (fix black preview + frozen screen) May 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant