Send screen: QR scan + Solana Pay URI parser#37
Conversation
The component's own comment notes that horizontal padding is owned by WalletScreen's grid (paddingHorizontal: 16, mirrored by WALLET_PAD = 32 in the cardW math). The H_PAD reference on the wrapper View was dead code left over from an earlier refactor — at runtime it raises "Property 'H_PAD' doesn't exist" the moment the wallet tab mounts. Drop the inline paddingHorizontal entirely.
Replaces the placeholder "QR scan coming soon" alert with the existing QRScannerModal already used by PeersDrawer and JoinGroupModal. Adds a parseSolanaPayUri helper to src/services/solanaPayUri.ts as the single source of truth for both bare base58 and full Solana Pay URI decoding (amount, spl-token, label, message, memo, reference). QRScannerModal's solana variant now carries the parsed params instead of just the address, and the inline parsing is delegated to the new helper so future consumers don't drift. RecipientPicker uses the params: on scan, it sets the recipient address and — when an amount is requested without an spl-token — pushes straight to the amount step with the amount prefilled, so the user lands on the keypad ready to confirm. spl-token requests are intentionally ignored for amount prefill: the send flow is currently SOL-only (TokenPicker.isSendable filters all SPL entries pending the legacy transferInstruction T22 fix), so we honor the recipient but let the user choose the amount in SOL. AmountKeypad reads the optional amount param and falls back to "0" so manual continue from the recipient step is unchanged.
When a user denies camera permission with "don't ask again", the only path to grant is via OS Settings. expo-camera's useCameraPermissions only re-checks on mount + on requestPermission(), so returning from Settings with a fresh grant left the modal stuck on the denial UI until the app was killed and reopened. useCameraPermissions returns a third element — a silent getter that reads OS state without showing the prompt. Subscribe to AppState while the scanner is visible and call the getter on each foreground; grants made in Settings now reflect live without a re-mount, denials stay silent (no prompt loop), and there's no effect when the scanner is closed.
There was a problem hiding this comment.
Pull request overview
This PR wires up QR scanning in the send recipient step and centralizes Solana Pay decoding into a shared parseSolanaPayUri helper, enabling the send flow to prefill the recipient (and optionally the amount) from scanned Solana/Solana Pay QR codes.
Changes:
- Added
parseSolanaPayUri(andParsedSolanaPaytype) to decode bare base58 recipients andsolana:URIs with common Solana Pay params. - Updated
QRScannerModalto delegate Solana parsing toparseSolanaPayUriand return optional parsed fields (amount,splToken, etc.). - Replaced the send recipient placeholder “coming soon” scan handler with
QRScannerModal, and allowed/send/amountto initialize from an optionalamountroute param.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| mobile_app/src/services/solanaPayUri.ts | Adds ParsedSolanaPay and parseSolanaPayUri to parse bare addresses and Solana Pay URIs. |
| mobile_app/components/messages/QRScannerModal.tsx | Routes Solana QR parsing through the new helper and enriches the solana scan result shape. |
| mobile_app/components/send/RecipientPicker.tsx | Mounts QRScannerModal, opens it from “Scan QR”, and pushes to amount step when appropriate. |
| mobile_app/components/send/AmountKeypad.tsx | Initializes the keypad state from an optional amount query param. |
| mobile_app/components/nodes/PendingCosigns.tsx | Removes the stale H_PAD horizontal padding usage from the wrapper view. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| export function parseSolanaPayUri(input: string): ParsedSolanaPay | null { | ||
| const s = input?.trim(); | ||
| if (!s) return null; | ||
|
|
||
| if (BASE58_RE.test(s)) { | ||
| return { recipient: s }; |
Builder had fixture coverage but the new parser shipped against the camera path with only ad-hoc fixtures. Adds testParseSolanaPayUri to the existing tier-0 services validator (no new test infra) covering the four areas reviewers flagged: bare base58, solana: URIs with and without params, invalid inputs, and spl-token / reference filtering. Also asserts the silent-drop policy on amount edge cases (zero, negative, NaN, 10-decimal overflow) — recipient still surfaces so the keypad picks up the address while the user enters the amount manually.
|
Coverage added in 5a81a51 — extended
Also locks in the silent-drop policy on amount edge cases (zero, negative, NaN, 10-decimal overflow): recipient still surfaces so the keypad picks up the address while the user enters the amount manually. |
Summary
Replaces the placeholder "QR scan coming soon" alert in the recipient
step with the existing
QRScannerModalalready used byPeersDrawerand
JoinGroupModal. Adds aparseSolanaPayUrihelper as the singlesource of truth for both bare base58 and full Solana Pay URI decoding
(amount, spl-token, label, message, memo, reference).
After scan:
amountand nospl-token, we pushstraight to the amount step with the value prefilled — user lands on
the keypad ready to confirm
spl-tokenis present, the address is honored but the amount isignored, since the send flow is currently SOL-only
(
TokenPicker.isSendablefilters every SPL entry pending the legacytransferInstructionT22 fix). User picks the amount in SOL.closed; nothing is set
Changes
src/services/solanaPayUri.ts— addsParsedSolanaPaytype andparseSolanaPayUri(input). Builder unchanged.components/messages/QRScannerModal.tsx—solanavariant nowcarries optional
amount/splToken/label/message/memo/reference. Inline parsing delegates to the helper. Otherconsumers (PeersDrawer, JoinGroupModal) only check
result.type/result.hash/result.addressand are unaffected by the addition.components/send/RecipientPicker.tsx— addsscannerOpenstate,mounts
QRScannerModal, replaces the placeholderhandleScan.components/send/AmountKeypad.tsx— reads optionalamountqueryparam, falls back to
"0"so manual recipient → continue isunchanged.
Branch base
Cuts off
upstream/v3@29b4825. Carries the sameH_PADcherry-pickas #30/#31/#32/#34/#35/#36 so
tscpasses; no-ops on whichever PRmerges first.
Test plan
recipient screen, Continue enabled
solana:<addr>(no params) → recipient prefills, stays onrecipient screen
solana:<addr>?amount=0.05→ lands on amount keypad with0.05prefilled, Review transfer enabled if balance allowssolana:<addr>?amount=0.05&spl-token=<mint>→ recipientprefills, stays on recipient (amount ignored, no force-route)
solana:<addr>?amount=99999(over balance) → keypad shows"Amount over balance" warning; user can edit down
alert; recipient unchanged
untouched
0(no prefill regression)
Validation gates
npx tsc --noEmit— 0 errorsnpm run lint— 0 errors (2 warnings pre-existing inPendingCosigns.tsxfrom PR V3 magic branch #29 squash; not in scope here)npm run validate:tier0:services— passnode ./scripts/validate-tier0-config.mjs— pass