Skip to content

feat(send): real on-chain confirmation + failure-state screen#45

Merged
epicexcelsior merged 18 commits into
anonmesh:v3from
epicexcelsior:epic/pigeon-loader-confirmation
May 13, 2026
Merged

feat(send): real on-chain confirmation + failure-state screen#45
epicexcelsior merged 18 commits into
anonmesh:v3from
epicexcelsior:epic/pigeon-loader-confirmation

Conversation

@epicexcelsior
Copy link
Copy Markdown
Collaborator

@epicexcelsior epicexcelsior commented May 13, 2026

Summary

Closes the authenticity gap left by PR #44. The send flow now waits for real on-chain confirmation before showing the success screen, and routes to a dedicated FailureCard for txs that submit-then-fail (expired blockhash, program error, timeout).

Stacked on PR #44base should ideally be epic/pigeon-loader-component but is set to v3 for visibility. The first 8 commits (61d2c14..53022a2) are PR #44's; this PR's net contribution is the 7 commits at the top (fddf96c..HEAD). When PR #44 merges, the diff here collapses to just Phase 2.

What's in (Phase 2 commits only)

4ad3142 chore(send): SuccessCard reflects real confirmation
da2a3da feat(send): real on-chain confirmation in ReviewCard handleConfirm
e132de8 feat(send): FailureCard + /send/failure route
4dda12e feat(send): error translation map for FailureCard copy
fddf96c feat(network): add getSignatureStatus + confirmTransaction polling

Service layer

  • IRpcAdapter extended with getSignatureStatus(signature). Direct + Mesh + Isolated implementations.
  • confirmTransaction(rpcAdapter, signature, { signal? }) polls until 'confirmed' / 'finalized' / on-chain error / overall budget timeout.
    • Online: 500ms cadence, 60s budget
    • Mesh: 1000ms cadence, 120s budget (per-call 30s timeout — needs headroom for ≥3 stuck calls)
    • Per-call rejections caught inside the loop so one hung getSignatureStatus doesn't abort confirmation
    • AbortSignal-driven cancellation for unmount cleanup

Send flow

ReviewCard becomes 2-phase: submit → confirm. Loader stays visible throughout, sublabel updates "Submitting""Confirming on devnet". Outcomes:

  • confirmed/send/success (now an honest success — "Confirmed" pill / green, subtitle "Settlement confirmed on devnet.")
  • failed/send/failure with friendly subtitle + status pill computed at the moment of failure via describeSendFailure(), plus raw error pre-formatted for the toggle
  • cancelled → no-op (caller unmounted via back-navigation; abort cleaned up the loop)

FailureCard

Mirrors SuccessCard structure with red X ring (SVG-based, spring-in on mount), shockwave in red tone, haptics.warning() on mount. Glass card shows status pill + signature (failed txs still have on-chain records — copyable + Explorer link). Collapsible "View error details" toggle reveals the raw RPC error in mono font with a Copy button. Footer: Try again (router.back, slider reset preserved) + Done (back to wallet).

Inline <Stack.Screen options={{ gestureEnabled: false }} /> on the route prevents swipe-back to a stale Review state. Same pattern as app/dev/pigeon-loader.tsx — no _layout.tsx edit needed.

Honesty improvements

Surface Phase 1 (PR #44) Phase 2 (this PR)
Success pill "Awaiting confirmation" (optimistic) "Confirmed" (verified)
Success subtitle "Explorer state can lag..." (hedge) "Settlement confirmed on devnet."
Post-submission failures Silently misrepresented as success Dedicated FailureCard with reason + raw error
Loader during poll n/a (didn't poll) "Confirming on devnet" sublabel

Test plan

  • Real devnet send → loader sublabel "Submitting" → "Confirming on devnet" → SuccessCard with "Confirmed" pill (verified on chain)
  • Stale-blockhash failure (slide-to-send after ~90s of idle on Review) → FailureCard "Blockhash expired" with friendly subtitle
  • Forced timeout (airplane-mode mid-poll) → FailureCard "Network timeout" / "Mesh timeout"
  • On-chain error (force a bad recipient / wrong-program SPL) → FailureCard "Program error" with raw error visible via toggle
  • "View error details" toggle expands and collapses, Copy button works
  • "Try again" returns to Review with form state intact (slider re-enabled)
  • "Done" returns to wallet
  • Cancellation: send → mid-poll, back-button → no ghost router.replace
  • Wallet rejection: still inline error on Review (no regression from PR feat(ui): pigeon-loader sending screen + send-flow wire-up #44)
  • Mesh mode: longer cadence + budget, mesh-specific pill copy
  • No regressions: Wallet, Receive, Peers, Settings tabs

Lane note

  • ReviewCard.tsx — QVAC's branch also edits this file (memo plumbing). My changes are state + control flow in handleConfirm; theirs are prop signature + Memo DetailRow. 3-way merge expected clean (same as PR feat(ui): pigeon-loader sending screen + send-flow wire-up #44).
  • _layout.tsx — untouched (inline Stack.Screen options handle the new route).
  • package.json — untouched (no new deps).

Squash recommended

Net Phase 2: ~+500 LOC across 7 commits. Squash on merge.

Out of scope (deliberate)

  • WebSocket onSignature subscription — polling works for both transports.
  • Tx replacement / cancel API.
  • Push notifications for backgrounded long mesh txs.
  • "Retry on online" recovery suggestions for mesh failures.

Phantom-style full-screen loading overlay for transactional flows.
Uses animated WebP via the already-installed expo-image (no new deps).

- assets/animations/sending.webp  — 240x240 q70 animated WebP, ~350KB
- components/ui/PigeonLoader.tsx  — Modal overlay with optional cancel
- app/dev/pigeon-loader.tsx       — preview route for testing without a real tx

Reachable for QA via: anonmesh://dev/pigeon-loader

Component is unwired; send-flow integration will follow in a separate PR
to keep this lane-clean against components/send/.
Device review showed two issues:

1. Ghost frames stuck on screen during loop. Root cause: ffmpeg's webp
   encoder writes frames with dispose=none + blend=alpha, so each frame
   composites onto the previous. Sparse transparent regions left
   trailing pixels.

   Fix: pre-composite every source frame onto a solid #00080c (theme
   background) via Pillow, then save without alpha. No alpha plane =
   every frame is fully opaque = ghosting is impossible.

   Side effect: 358KB -> 292KB (smaller, no alpha plane).

2. Loader felt too small. Image render 160 -> 220 (≈37% bigger).
   Source resolution bumped 240 -> 280 to keep sharpness at the
   larger render size on 3x devices.

3. Backdrop opacity dropped from 92% to fully opaque, matching the
   Phantom-wallet pattern for blocking loaders. Also avoids the
   visible-rectangle artifact that would appear if a fully-opaque
   sprite sat on a translucent backdrop of a slightly different
   blended color.
Elevate the send loader from a generic spinner into a brand moment that
tells the AnonMesh routing story while a tx is in flight.

Changes:

- Bigger hero — pigeon render 220 -> 280pt (fills the central area)
- Larger label (24pt semibold), sublabel hidden by default — less text
- Subtle pulse on the pigeon (1.0 ↔ 1.03 / 1.8s sine) so the image
  doesn't sit still during the WebP's quieter frames
- Ambient mesh background — 12 drifting cyan nodes + 19 connecting
  lines on the brand primary color, driven by a single shared phase
  via Reanimated worklets. Renders behind the pigeon at low opacity.
  Tells the "your tx is hopping across peers" story visually.
- New success state (`status: 'loading' | 'success'`):
  pigeon crossfades out, Feather check springs in with a brand-green
  glow ring. Fires the success notification haptic.
- Mount haptic — medium impact when the loader becomes visible,
  announcing the blocking state.

Asset:

- assets/animations/sending.webp re-encoded with alpha so the mesh BG
  reads through the transparent regions around the pigeon. Avoids
  ghosting via explicit per-frame disposal=2 (background) in the
  WebP encode (Pillow), so each frame fully replaces the previous.
  320x320 q=70 alpha_q=80 → 614KB.

Dev route:

- "Full lifecycle" buttons play sending → success → dismiss so the
  whole sequence can be felt without a real tx
- Manual status toggle exposes the loading↔success transition
- Sublabel and cancel button toggles still present

Deps: zero new packages. Uses expo-image, expo-haptics,
react-native-reanimated, react-native-svg — all already installed.
Two device-review fixes:

1. Mesh BG redrawn from the actual AnonMesh logo geometry.
   - Node positions extracted from assets/icons/anonmesh_white_icon.png
     via Pillow distance-transform (6 nodes, 10 edges).
   - Logo scaled 1.15× the screen, centered. The pigeon mounts in
     front of the central node, exactly like the brand icon's core.
   - Static dots dropped in favor of stable topology that matches the
     logo silhouette.
   - One bright "message" dot per edge, all driven by a single
     11-second shared phase. Each message travels half the cycle
     (visible) then fades out for the other half. Phases staggered
     so messages don't all arrive simultaneously.
   - Color cohered to brand primary cyan at three opacity tiers:
     edges 15%, nodes 20%, traveling messages 85%.

2. Success ring rebuilt in SVG to kill the octagon artifact.
   The previous View used borderRadius=48 + borderWidth=2.5 +
   elevation=10. On Android, large-radius rounded views with
   elevation shadows render with quantized polygon edges — that
   was the "incomplete shape inside the circle" symptom.
   Now: <Svg> with three concentric Circles (outer halo, mid halo,
   ring stroke) for guaranteed anti-aliased curvature and a soft
   glow that doesn't rely on platform shadow clipping.
Three improvements rolled together; the perf one matters most.

1. Center node aligned to screen center.
   The logo's central node sits at (0.596, 0.417) in source coords, not
   (0.5, 0.5). Previously the bounding box was centered, which made the
   visual center sit right-of-center on screen. New positioning anchors
   that single node to (width/2, height/2). The pigeon now flies over
   the logo's actual core, and the whole pattern reads as the brand mark
   instead of a generic web.

2. Scaled 1.15× → 1.45×.
   Outer nodes intentionally drift past the screen edges. Pattern reads
   as ambient / abstract rather than "here is a small diagram." Static
   node/edge opacities lowered to compensate (14% / 12%) so the mesh
   stays subordinate to the pigeon.

3. Animated messages moved off react-native-svg onto Animated.View
   transforms. SVG <Circle> with useAnimatedProps was triggering native
   tree updates each frame across 10 elements — that's where the perf
   cost was hiding. Each message is now a positioned Animated.View
   driven by useAnimatedStyle, so the transform updates run UI-thread
   only and never round-trip to RN's view manager.

   Side benefits from the rewrite:
   - Smoothstep easing on the position so dots accelerate out of one
     node and decelerate into the next. Reads as more deliberate motion.
   - Three-stage opacity curve: fade in over 18% of the visible window,
     hold at peak, fade out over the last 18%. No more abrupt blink-on
     / blink-off at edge endpoints.
   - Stacked core + halo (one bright inner, one softer outer) for a
     glow without relying on platform shadow primitives.
   - Visible window dropped to 42% per cycle, travel period 11s → 7s.
     Net result: fewer dots on screen at once but each one is brighter
     and faster, which reads as more pronounced motion without crowding.

Static layer (nodes + edges) still SVG — drawn once, zero per-frame
cost.
Mesh BG didn't add enough to justify the visual + perf cost on top of
the pigeon. Loader is now just the dark backdrop, pigeon, and label.
Cleaner base to layer alternative polish on.
Three polish layers replacing the dropped mesh BG. None of them add
motion that competes with the pigeon during normal loading.

1. Radial spotlight backdrop.
   Static SVG <RadialGradient> centered on screen — cyan primary at
   ~11% opacity in the core, fading to transparent at 100%. The void
   backdrop now reads as an intentional stage rather than dead black.
   Zero motion, single-render, no per-frame cost.

2. SlideLabel — animated label transitions.
   When the label text changes (Sending → Sent on success, or any
   other override), the outgoing text eases up and fades out while the
   new text rises up and fades in. State change feels announced
   instead of just swapped. The transition is keyed on text equality
   so identical re-renders don't trigger it.

3. Success shockwave.
   Single ring positioned at screen center, hidden until the success
   transition. Fires alongside the check spring with the same 80ms
   delay so the ring reads as the wave's anchor. Scales 0.85 → 3.6
   over 720ms with opacity 0.55 → 0, easing out cubic. The wave moves
   off-screen by the end of the animation, which is the intended
   "exclamation point" feel for the success moment.

Reset on status reversal (success → loading) snaps all three success
animations to their idle state, so the dev route's manual toggle stays
clean across rapid changes.
Loader now appears for real sends. Path:

1. Slider hit -> setIsConfirming(true), setTxStatus('loading')
   PigeonLoader Modal fades in (built-in animationType="fade"), pigeon
   pulses, mount haptic fires.
2. await sendSolTransfer/sendSplTransfer -> result.signature
3. saveAddressBookRecipient(to) (in the same try block)
4. setTxStatus('success') -> loader crossfades to check ring,
   shockwave + confirm haptic, label slides Sending -> Sent.
5. setTimeout 1200ms -> router.replace('/send/success', ...). The
   1.2s hold lets the success state actually register before nav.
6. ReviewCard unmounts, loader Modal closes alongside, Success
   screen with explorer link is now visible.

Error path: catch -> setTxStatus('loading'), setIsConfirming(false),
slider resets. Loader fades out, error panel visible on Review for
retry. Drops the previous `finally { setIsConfirming(false) }` since
the success path now owns its own dismissal via navigation.

Lane note: QVAC's epic/qvac-integration branch also edits
ReviewCard.tsx (memo prop plumbing). The diffs do not overlap line-
for-line — my edits are around state declarations and try/catch
control flow; theirs are around prop signature, fee estimate args,
and adding a Memo DetailRow. 3-way merge should clear without
manual intervention, but whoever lands second should verify.
Standard wallet pattern: the loader does loading, the success screen
does the success moment. One haptic, one check, no contrived hold.

Bug fix:
  Previously the loader fired haptics.confirm() in its success state,
  held 1.2s, navigated. Then SuccessCard fired haptics.confirm() on
  mount. Two confirm haptics 1.2s apart on every send. Two check
  rings (animated then static). Removed by:
  - dropping setTxStatus('success') + setTimeout from ReviewCard;
    we navigate immediately when sendSolTransfer/sendSplTransfer
    returns the signature
  - SuccessCard now owns the celebration: SVG check ring + shockwave
    spring in on mount, single confirm haptic, lifted from the
    PigeonLoader work (same dimensions, same easing)

Other:
  - SuccessCard title "transfer in motion" → "sent" (present tense
    on a screen the user lands on AFTER submit)
  - Loader's status prop kept in PigeonLoader API for future non-send
    consumers (refresh/connect/mesh-ack flows), just unused here
"Submitted to devnet" → "Awaiting confirmation".

The screen mounts on signature receipt, but the tx isn't yet confirmed
on chain (no polling on this code path). The previous copy claimed a
status we hadn't verified. The new copy is what's actually true at
that moment. Subtitle already acknowledges the lag.

Proper fix (status polling + failure-state route) lives in a follow-up
PR; this is the cheap honesty change.
Extend IRpcAdapter with getSignatureStatus(signature) so callers can
poll for on-chain confirmation in a transport-blind way. Direct and
mesh implementations both delegate to their existing infrastructure:

- DirectRpcAdapter.getSignatureStatus: connection.getSignatureStatus
  with searchTransactionHistory=false (recent submission only)
- MeshRpcAdapter.getSignatureStatus: routes through the same LXMF
  relay path as the existing RPC calls, using getSignatureStatuses
  with a single-element array (the only multi-sig form web3.js's
  JSON-RPC exposes)
- IsolatedRpcAdapter: throws "No Solana route available" matching
  the existing pattern

Adds confirmTransaction(rpcAdapter, signature, { signal? }) in
sendTransaction.ts:

- Polls every 500ms online / 1000ms mesh
- Overall budget 60s online / 120s mesh (per-call mesh timeout is
  30s — need headroom for ≥3 stuck calls)
- Per-call rejections are caught and logged inside the loop so a
  single hung getSignatureStatus doesn't abort confirmation
- Returns ConfirmResult discriminated union: 'confirmed' | 'failed'
  (reason: 'on-chain' | 'timeout') | 'cancelled' for AbortSignal use
- Cancellation triggers an early sleep-exit so unmount doesn't have
  to wait out a full pollInterval before returning

No call sites wired yet. ReviewCard rewire + UI states land in
follow-up commits on this branch.
src/services/sendErrorMessages.ts (pure, no React).

describeSendFailure(result, mode) maps a ConfirmResult failure into:
  { subtitle, pillLabel }

Pattern-matches common Solana errors against a combined haystack of:
- summarizeError's extracted view (message, name, raw, cause)
- JSON.stringify of the raw err for object-form TransactionError keys
  like { InstructionError: [...] }

Covered cases: blockhash expired, insufficient funds, missing account,
program error, generic on-chain failure, network timeout (mode-aware:
"Mesh timeout" vs "Network timeout"). Anything unmatched falls through
to a generic message that directs the user to the raw error toggle.

formatRawError(err) produces the multi-line monospace block for the
FailureCard's collapsible "View error details" section.
Mirrors SuccessCard structure with the same SVG ring spring + shockwave
pattern; tone shifts to colors.error and the inner icon is an X drawn
as two SVG lines instead of a Feather check.

Component (FailureCard.tsx):
- haptics.warning() on mount (not confirm — different signal)
- Title "couldn't send" (lowercase, brand-consistent)
- Subtitle + pillLabel arrive as props from ReviewCard, computed via
  describeSendFailure() so the failure copy is captured at the moment
  of failure (not recomputed later in case mode changes)
- Amount tile so the user sees what failed
- Glass card with status pill + signature (when present; even failed
  txs land on chain and have copyable explorer-linkable signatures)
- "View error details" collapsible toggle revealing rawError in
  monospace with a Copy button
- Footer: Done (secondary, back to wallet) + Try again (primary,
  router.back to Review with state intact)
- View on explorer button below the error detail when a signature
  exists (failed txs are still inspectable)

Route (app/send/failure.tsx):
- Thin wrapper, reads params via useLocalSearchParams
- Inline <Stack.Screen options={{ gestureEnabled: false }} /> to
  prevent swipe-back to a phantom Review state (no _layout.tsx edit
  needed — same pattern as app/dev/pigeon-loader.tsx)
- asString() helper to defend against query-string arrays from
  Expo Router when params are bracketed

Not wired into ReviewCard yet — that's the next commit.
Send flow becomes two-phase:

1. Submit (existing sendSolTransfer / sendSplTransfer call) → signature
2. Confirm (new): poll getSignatureStatus via confirmTransaction until
   the chain reports 'confirmed' or 'finalized', errors out, or the
   budget elapses

State:
- txPhase: 'submitting' | 'confirming' | null  drives loader sublabel
- confirmAbortRef: AbortController for cancellation on unmount

PigeonLoader sublabel updates as the phase advances. The loader stays
visible through both phases so users see the actual work happening.
Sublabels:
  'submitting' → "Submitting"
  'confirming' → "Confirming on devnet"

Outcome routing:
- 'confirmed' → router.replace('/send/success', { txId, ... })
- 'failed'    → router.replace('/send/failure', { subtitle, pillLabel,
                  rawError, txId, ... }) with copy computed by
                  describeSendFailure() at the moment of failure
- 'cancelled' → return silently. Caller already unmounted.

The unmount useEffect aborts any in-flight confirmation. Combined with
the 'cancelled' result branch, this prevents two classes of bugs:
- ghost router.replace on a phantom ReviewCard after user back-swipes
- orphan polling loops that nobody is listening to

Existing submission-stage error path is unchanged — wallet rejection,
RPC unreachable, validation failures still surface inline on Review.
Phase 1 SuccessCard mounted on signature receipt with an "Awaiting
confirmation" pill — honest about not having verified the chain.
Phase 2 confirms before navigation, so the copy can land:

- Subtitle: "Settlement confirmed on devnet." (no more lag caveat
  because we've already waited for the lag)
- Pill: "Confirmed" / green tone (no more "Awaiting" / cyan)

Behavior unchanged otherwise: check ring spring + shockwave + single
confirm haptic on mount.
…arry-through)

Stack carries PR anonmesh#44's PigeonLoader through this branch, so the
accessibility fix needs to land here too. Mechanical mirror of the
fix on epic/pigeon-loader-component:

`accessible` + alert role moved from the backdrop View to the content
stack so the optional Cancel Pressable remains independently focusable
for screen readers.
…replace, timeout retry guard

1. AbortSignal listener leak in confirmTransaction's per-poll sleep.
   Each iteration's `signal.addEventListener('abort', onAbort, { once: true })`
   was never removed when the timer fired normally — only when abort
   itself fired. Across a 60s budget at 500ms cadence, that's up to 120
   dead listeners on the same controller before unmount. Now the timer
   handler explicitly removeEventListener's the onAbort, so listeners
   are bounded to one live entry per cycle.

2. router.replace → router.push for the failure path.
   With replace, /send/review got swapped out of the stack before
   navigation to /send/failure. router.back() from FailureCard landed
   on /send/amount instead of /send/review, losing the form state.
   With push, /send/review stays on the stack, "Try again" returns
   the user exactly where they were with amount/recipient intact.
   Success path stays as replace (terminal state, going back is
   meaningless).

3. Timeout-failure double-send guard.
   reason='timeout' means the tx submitted and we stopped watching
   before getting a terminal status. On online mode, the blockhash
   may still be live for up to 30s after the 60s budget — meaning
   the tx COULD still confirm. If the user taps "Try again" in that
   window, they double-send. New plumbing:
   - ReviewCard passes `reason` as a route param
   - app/send/failure.tsx reads it and computes `isTimeout`
   - FailureCard hides "Try again" entirely on timeout, swaps Done
     to primary, and bumps "View on explorer" from secondary to
     primary so it reads as the recommended action

User intending to retry on a true timeout just taps Done → opens
wallet → re-enters Send. One extra tap to avoid a real money risk.
# Conflicts:
#	mobile_app/components/send/ReviewCard.tsx
#	mobile_app/components/send/SuccessCard.tsx
@epicexcelsior epicexcelsior merged commit 8218930 into anonmesh:v3 May 13, 2026
@epicexcelsior
Copy link
Copy Markdown
Collaborator Author

@copilot review this PR please

@epicexcelsior epicexcelsior deleted the epic/pigeon-loader-confirmation branch May 15, 2026 09:09
epicexcelsior added a commit to epicexcelsior/anon0mesh that referenced this pull request May 16, 2026
Adds `docs/SMOKE_TEST.md` — the 5-minute manual gate that runs before
every tester APK release. Seven numbered steps with explicit pass/fail
criteria, each linked to an AUDIT / ROADMAP ID for traceability.

Steps:
1. Cold install + onboarding
2. Local wallet generation
3. DEVNET banner visible (Tier 0.5 / B1)
4. Send 0.001 SOL to self (PR anonmesh#45 confirmation)
5. Nodes screen honesty (no fixture data)
6. Seed reveal biometric gate
7. Airplane-mode toggle

Adapted from § 10 of LOCAL_NOTES/REVIEW-2026-05-13/raw-tracks/
OFFGRID_FALLBACK_AUDIT.md ("Off-grid testing playbook") for the
team's actual release workflow. Includes a sign-off table.
epicexcelsior added a commit that referenced this pull request May 16, 2026
* chore(preview): wrap dead Yield/Swap/Send panel controls in PreviewedActions

These three wallet panels are not exported from `components/wallet/index.ts`
today (per PR #47) but the source still ships in the bundle and contains
live Pressables that look real. Wrap the dead CTAs in <PreviewedActions>
and add a <PreviewBadge label="…coming soon"> above each panel so that if
anything ever does render them, the affordance reads as preview-only.

- YieldPanel: deposit/withdraw vault buttons wrapped (the expand/collapse
  Pressable is purely local UI state, left alone).
- SwapPanel: SWAP + "new order" CTAs wrapped (flip-asset Pressable left).
- SendPanel: "send privately" + "new transfer" CTAs wrapped (asset picker
  and MAX shortcut left — local state only).

Per AUDIT A6 / ROADMAP § 0.A.8.

* feat(a11y): honor reduce-motion on MeshMap + RadarScan loops

Two of the largest infinite Animated.loop / withRepeat(-1) sites that were
deferred from PR #52. Adds `useReducedMotion()` guards using the same
pattern as PulseDot / Skeleton / NetworkStatusBadge / PigeonLoader.

- MeshMap: the "me" node pulse halo + ghost skeleton breathe both honor
  reduce-motion. Pinned to mid-frame opacity so the indicators still read
  but don't loop. No structural changes.
- RadarScan: rotation sweep pinned at 0deg + center dot held at resting
  scale under reduce-motion. Static ring guides still convey state.

Both visual-only — happy path unchanged.

Per ROADMAP Tier 4.1 (a11y motion audit).

* docs(release): pre-release smoke-test playbook (ROADMAP Tier 1.2)

Adds `docs/SMOKE_TEST.md` — the 5-minute manual gate that runs before
every tester APK release. Seven numbered steps with explicit pass/fail
criteria, each linked to an AUDIT / ROADMAP ID for traceability.

Steps:
1. Cold install + onboarding
2. Local wallet generation
3. DEVNET banner visible (Tier 0.5 / B1)
4. Send 0.001 SOL to self (PR #45 confirmation)
5. Nodes screen honesty (no fixture data)
6. Seed reveal biometric gate
7. Airplane-mode toggle

Adapted from § 10 of LOCAL_NOTES/REVIEW-2026-05-13/raw-tracks/
OFFGRID_FALLBACK_AUDIT.md ("Off-grid testing playbook") for the
team's actual release workflow. Includes a sign-off table.

* chore(diagnostics): warn on 3 silent-failure sites (TxDetailModal + MeshRpcAdapter)

Drops overlap w/ #53 (ReceivePanel + LxmfContext warns) — #53 owns those.
Per AUDIT § 3 silent failure inventory.
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