Skip to content

Send screen: QR scan + Solana Pay URI parser#37

Merged
epicexcelsior merged 4 commits into
anonmesh:v3from
epicexcelsior:epic/send-qr-scan
May 10, 2026
Merged

Send screen: QR scan + Solana Pay URI parser#37
epicexcelsior merged 4 commits into
anonmesh:v3from
epicexcelsior:epic/send-qr-scan

Conversation

@epicexcelsior
Copy link
Copy Markdown
Collaborator

@epicexcelsior epicexcelsior commented May 8, 2026

Summary

Replaces the placeholder "QR scan coming soon" alert in the recipient
step with the existing QRScannerModal already used by PeersDrawer
and JoinGroupModal. Adds a parseSolanaPayUri helper as the single
source of truth for both bare base58 and full Solana Pay URI decoding
(amount, spl-token, label, message, memo, reference).

After scan:

  • recipient address is set on the picker
  • if the URI carries an amount and no spl-token, we push
    straight to the amount step with the value prefilled — user lands on
    the keypad ready to confirm
  • if spl-token is present, the address is honored but the amount is
    ignored, since the send flow is currently SOL-only
    (TokenPicker.isSendable filters every SPL entry pending the legacy
    transferInstruction T22 fix). User picks the amount in SOL.
  • non-Solana QRs (LXMF, garbage) surface a polite alert; modal stays
    closed; nothing is set

Changes

  • src/services/solanaPayUri.ts — adds ParsedSolanaPay type and
    parseSolanaPayUri(input). Builder unchanged.
  • components/messages/QRScannerModal.tsxsolana variant now
    carries optional amount / splToken / label / message /
    memo / reference. Inline parsing delegates to the helper. Other
    consumers (PeersDrawer, JoinGroupModal) only check result.type /
    result.hash / result.address and are unaffected by the addition.
  • components/send/RecipientPicker.tsx — adds scannerOpen state,
    mounts QRScannerModal, replaces the placeholder handleScan.
  • components/send/AmountKeypad.tsx — reads optional amount query
    param, falls back to "0" so manual recipient → continue is
    unchanged.

Branch base

Cuts off upstream/v3@29b4825. Carries the same H_PAD cherry-pick
as #30/#31/#32/#34/#35/#36 so tsc passes; no-ops on whichever PR
merges first.

Test plan

  • tap Scan QR → camera opens with viewfinder
  • scan a bare base58 Solana address → recipient prefills, stays on
    recipient screen, Continue enabled
  • scan solana:<addr> (no params) → recipient prefills, stays on
    recipient screen
  • scan solana:<addr>?amount=0.05 → lands on amount keypad with
    0.05 prefilled, Review transfer enabled if balance allows
  • scan solana:<addr>?amount=0.05&spl-token=<mint> → recipient
    prefills, stays on recipient (amount ignored, no force-route)
  • scan solana:<addr>?amount=99999 (over balance) → keypad shows
    "Amount over balance" warning; user can edit down
  • scan invalid QR (lxmf://hash, plain text) → "QR not recognised"
    alert; recipient unchanged
  • cancel scanner via close button → modal dismisses, address state
    untouched
  • camera permission denied → existing denial UI shows (unchanged)
  • manual flow (paste address → Continue) → keypad opens with 0
    (no prefill regression)

Validation gates

  • npx tsc --noEmit — 0 errors
  • npm run lint — 0 errors (2 warnings pre-existing in
    PendingCosigns.tsx from PR V3 magic branch #29 squash; not in scope here)
  • npm run validate:tier0:services — pass
  • node ./scripts/validate-tier0-config.mjs — pass
  • Seeker device smoke — owed (camera + viewfinder + parse paths)

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.
@epicexcelsior epicexcelsior changed the title Send screen: QR scan + Solana Pay URI parser (B.3) Send screen: QR scan + Solana Pay URI parser May 8, 2026
@epicexcelsior epicexcelsior marked this pull request as draft May 8, 2026 09:06
@epicexcelsior epicexcelsior requested a review from Copilot May 8, 2026 09:14
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.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 (and ParsedSolanaPay type) to decode bare base58 recipients and solana: URIs with common Solana Pay params.
  • Updated QRScannerModal to delegate Solana parsing to parseSolanaPayUri and return optional parsed fields (amount, splToken, etc.).
  • Replaced the send recipient placeholder “coming soon” scan handler with QRScannerModal, and allowed /send/amount to initialize from an optional amount route 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.

Comment on lines +53 to +58
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.
@epicexcelsior
Copy link
Copy Markdown
Collaborator Author

Coverage added in 5a81a51 — extended scripts/validate-tier0-services.mjs with testParseSolanaPayUri() covering the four areas you flagged:

  • bare base58 — 32-char + 44-char, with and without leading/trailing whitespace
  • solana: URIs — no params, amount only, full param set with URL-decoded label/message, case-insensitive scheme
  • invalid inputs — empty, whitespace-only, bare solana:, unsupported schemes (https, lxmf), garbage payloads, recipients with disallowed base58 chars (0OIl)
  • spl-token / reference filtering — valid mint surfaces, bogus mint dropped (recipient still returns), multi-reference array kept, mixed valid+invalid references filtered down to valids only, all-invalid references → field omitted

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. npm run validate:tier0:services passes locally; same script that already gates the build.

@epicexcelsior epicexcelsior marked this pull request as ready for review May 8, 2026 09:23
@epicexcelsior epicexcelsior merged commit 0b21176 into anonmesh:v3 May 10, 2026
@epicexcelsior epicexcelsior deleted the epic/send-qr-scan branch May 15, 2026 09:09
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.

2 participants