Repository navigation
refactor(qr-pay): split the 1.9k-line page into a features/ flow (TASK-21457) - #3008
Conversation
…rting a second Init publishes oneSignalInitialized before its first login() resolves, so an opt-in in that window ran syncExternalIdLink again and reached adapter.login twice for the same id — the double-record race behind TASK-22209 on one more path. A sync for an id whose login is in flight now joins that promise; failures still clear it so the next sync retries.
…ssued-on fix(receipt): remove issued-on date from PDF receipt footer
Conflicts in useHomeFlow: dev (#2929, TASK-22142) removed avatarName and seeds the top-nav chip from the username directly, which supersedes this branch's avatar-seeding fix. Took dev's version of the hook and its test.
The name row above the pill now shows the full name or nothing. Falling back to the username printed the handle twice on a page whose pill already reads peanut.me/<username>. ProfileHeader drops the row on an empty name rather than rendering an empty label, so the two callers without a pill (public profile, profile edit) keep theirs — both already fall back to the username themselves before passing it. The showFullName preference still gates it: opting out means there is no name to show. Avatar initials are unaffected, they already fell back to the username.
The five-tap switch let any production install self-assign to the Capgo `staging` channel, which receives every `dev` merge. The gesture controls discoverability, not eligibility, and Capgo's self-assignment setting is global — it cannot tell an internal tester from a customer — so the two together were the whole boundary whenever self-assignment was open. Gate the join on the `beta-ota-channel` cohort again, with the failure mode that made the last one useless removed. The cohort now gates the JOIN only: - The card renders on every native build, as it does today. A missing or false flag can no longer hide the switch, which is how the previous gate disabled itself for its own testers and looked identical to exclusion. - A blocked device says why on screen and names the fix, instead of a gesture that silently does nothing. - The off switch stays live whatever the cohort says. Offboarding someone mid-beta must not strand them on beta code with no way back to the store bundle. nonProdBypass keeps staging and preview builds open — they are internal by construction — so the cohort only has to exist for the production binary. TASK-22248
…spend Ceremony telemetry shows the mixed spend's second sheet (the UserOp, 1.5 s after the Rain admin signature) is the one that gets dismissed: Android NotAllowedError and iOS LOGIN_CANCELED both cluster at that gap, and the retry then costs four sheets. The overlay between the two taps said only "Verifying security…", which reads as "done". - ModalsContext: the overlay takes a variant; 'next-passkey' swaps the copy. - useSpendBundle passes it before tap #2. useSignSpendBundle showed no overlay at all between its two taps; it now shows the same beat. - en / es-419 / pt-BR copy (es-AR inherits es-419). Stopgap until the one-tap path (ui#2959) removes the second sheet.
… reveal re-entry Six days of ceremony telemetry (ui#2870) left three blind spots and one loop: - Step-up assertions (card details, PIN, add bank) carried no purpose and showed as `unknown` — 99 of them. They are now `step_up`. - Only link creation was bracketed as a flow, so QR pay, withdraws, direct send and card actions had to be reconstructed from per-user gaps. Both spend engines now bracket every call as `spend:<kind>` / `sign_spend:<kind>` with the chosen strategy; link_create nests inside. - `signup_login_error` had no `native` flag, so the repeated-login failures could not be split native vs web. - One device fired 13 step-ups in 27 s on /card, each failing in <10 ms with overlapped=true: useCardReveal had no re-entry guard, so taps while the sheet was up opened a second ceremony that failed instantly. Guarded.
… again Telemetry: log in, open the card, second passkey sheet 7 s later (login → step-up). A login assertion seconds old is as fresh as a step-up one, and the API now mints a step-up proof alongside the session token on /passkeys/login/verify (peanut-api-ts: login-mints-step-up-token). The ZeroDev SDK discards the verify body, so the fetch wrapper that already recovers the session JWT on native now runs on every platform and stashes the step-up proof under the same ceremony-window rule; the guard commits it to the step-up cache only when the ceremony resolves. On web the session token still comes from the cookie — only the proof is taken from the body. Side effect worth having: non-2xx verify responses are now reported to Sentry on web too, which is what the "Login not verified" retries need. The cache moved to step-up-cache.ts so the ceremony guard can seed it without a step-up.ts ↔ passkeyCeremony.utils import cycle.
public/press is 13.8 MB across 30 files — founder photos, the brand guidelines PDF and an EPS logotype. Next copies public/ into out/ wholesale, independent of routes, so all of it ships inside every native binary and every OTA bundle even though its only consumer, /[locale]/press, is in the [locale] directory this script already disables. The assets are PNG/PDF/EPS, so they compress badly and land in the OTA zip near full size, for a route that 404s in the app. Reuses the existing pruneExportedAssets pass that already drops the PWA .mov files and the /dev pages.
A bundle applied from appMovedToBackground() starts its notifyAppReady countdown as the app is leaving the foreground, and the countdown keeps running while the OS has the process frozen. One Android 11 device called notifyAppReady 6 ms after the deadline and had the bundle rolled back (PEANUT-UI-SVE). Android floors this at 30 s for a pending bundle, so 15 s was inert there and binding on iOS.
fix: receipt drawer clipped the avatar, profile repeated the username
…tive-export perf(native): prune the press kit from the static export
…deadlock fix(ota): quit instead of deadlocking the restart on affected Android binaries
Chip (major): the flag made the ephemeral signer production-reachable and only the boolean gate was tested. These enter useSpendBundle with a real mixed routing and assert the two money invariants: a settled ephemeral op is what is returned and stamped (receipt hash, not the userOp hash, no passkey signatures), and a reverted or pre-broadcast failure falls back to the passkey path against the SAME preparation — one prepareWithdrawal per attempt, the coordinator's adminNonce being the mutex that keeps the two attempts mutually exclusive.
…authoritative backend - Scope the pre-flight min guard to external-wallet claims only. A claim into the user's own Peanut balance is forced down the same isXChain branch but settles to Arbitrum USDC with no product minimum; blocking it dead-ended the very "claim to your Peanut balance" fallback the copy points at (review: MAJOR). - Stop sending a client-reported amount. claim-xchain now forwards the deposit identity (chainId, depositIdx, contractVersion) so the backend reads the claim amount from chain (authoritative); withdraw/pay-request drop payAmountUsd since the backend derives their amount from the charge. - Keep the dynamic sda.minDepositLimitUsd check as a fail-open backstop for when the backend can't derive the amount, and the pre-flight static floor as the first-line UX block. - Add executeClaimXChain regression tests: sub-minimum blocks before signing/ submitting, boundary passes, priceless/no-min-route cases don't block, and the deposit identity is forwarded (review: MAJOR, no money-path test). Pairs with the peanut-api-ts server-authoritative guard.
… only real receipts Chip (major x2): getPatchedSudoValidator sat outside the helper's catch, so a rejection there (cookie cleared mid-session, SDK transport) escaped as a failed spend after /prepare had run — the passkey path never ran and no fallback event fired. It now yields ok:false like every other failure. And an unresolved receipt (bundler timeout, rescue miss) was stamped with the userOp hash as if it were a tx hash. The branch now stamps only a receipt's transactionHash; with none it reports the op as submitted, the same as the passkey path, leaves the intent PENDING for webhook reconciliation, and tags the success event receipt=unresolved. Returning ok:false there was rejected on purpose: the fallback would race a possibly landed op on the same account nonce, fail with AA25 after the money moved, and invite a user retry against a fresh preparation.
… joining Replaces the PostHog cohort with the badge the gesture now awards. The cohort never existed, and a flag that has to be hand-created in a dashboard is the same setup step that left this switch invisible for months; the badge is created by the act of tapping, so there is nothing to remember. The fifth tap claims PEANUT_TEAM and refetches the user before revealing the card. Revealing first would show a disabled toggle and an "ask for access" line for a round trip, on the very gesture that just granted access. A failed claim still reveals the card: a device already on beta needs the off switch, whatever the network did. What the badge is: a record of who opted in and a handle to revoke. It is not an access boundary — anyone who performs the gesture awards it to themselves, and Capgo's channel self-assignment setting remains the real one. What it buys over the bare gesture is a list and a way to take it back. It is never rendered — not in a profile row, not in the home feed, not as a celebration toast. It says "team" and is handed out on a gesture, so showing it would let any customer wear Peanut staff colours in a payments app. Needs peanut-api-ts#PEANUT_TEAM (POST /badge/team) deployed first: without it the claim fails, the card still reveals, and joining stays blocked with copy that says to tap again. TASK-22248
…laim-settle fix(native): OTA beta switch off PostHog, Peanut-only receive, claim settles on CLAIMED
…d-reveal-guard fix(passkey): tag step-up ceremonies, bracket every spend, guard card reveal re-entry
…sheet-copy feat(spend): tell the user a second passkey sheet follows on a mixed spend
# Conflicts: # src/services/__tests__/step-up.test.ts # src/services/step-up.ts
…d its own flag QR pay, Manteca withdraw and the card lock/cancel modals sign a mixed spend through useSignSpendBundle and hand the UserOp to the backend to broadcast. That engine still costs two passkey sheets (Rain admin EIP-712 + UserOp); ui#2959 only covers the broadcasting engine. signMixedEphemeralSpend is the sign-only twin of tryMixedEphemeralSpend: the same single enable-signature tap, then the ephemeral key signs the admin EIP-712 and the UserOp silently, uninstall last, and the unbroadcast artifact goes back in the same SignedSpendArtifact shape. Nothing is sent from the client, so a signing failure falls back to the two-tap path with nothing at stake. Own flag (session_key_spend_sign) rather than the broadcasting engine's: a permission that fails on-chain surfaces here as the backend's broadcast reverting, with no client-side retry. Stays off until ui#2959 has proven the ERC-1271 ordering on production contracts. The permission lifetime is 10 min for this path (backend broadcasts after Manteca settles, with a receipt re-poll on timeout); the op stays single-use through its nonce.
…or cannot be resolved Same gap Chip found on the broadcasting engine (ui#2959): the validator lookup sat outside the helper's catch, so a rejection escaped as a failed spend after /prepare had run. It now yields ok:false and the two-tap path signs against the same preparation.
Decision: no rollout gate. The fallback is the safety mechanism — every failure inside the one-tap branch (validator, preflight, reverted op) falls through to the two-tap passkey path against the same Rain preparation, and the coordinator's adminNonce keeps the two attempts mutually exclusive. A gate only decided who proved that first. - Runtime gates gone: no PostHog flag, no device opt-in, no /dev toggle page. Both spend engines take the ephemeral path unconditionally. - Build gate gone with them: NEXT_PUBLIC_SESSION_KEY_SPEND is read nowhere, so the workflow lines are dropped and the web build gets the path too. - Engine B (QR pay, Manteca withdraw, card lock/cancel) folded in from the stacked PR, same treatment. - Tests keep every branch case; the flag-off cases are gone with the flag. - useSpendBundle's callback deps now list getPatchedSudoValidator.
The About screen read App.getInfo(), which is frozen at the .0 the binary shipped with — an install running OTA 1.1.2 still reported 1.1.0. Prefer the Capgo bundle actually executing for the release segments and keep the binary's CI build number as the fourth.
…honest copy
Address chip re-review (Peanut-balance carve-out untested + inconsistent). The
prior revision skipped the guard for non-external claims, but a sub-minimum
link on a non-Arbitrum chain claimed into the user's own Peanut balance still
takes the isXChain branch and still bridges via Rhino, which parks it. So:
- Guard EVERY cross-chain claim (external wallet and own Peanut balance alike);
only the copy branches — external keeps the network-picker message (now
"Try a different network", dropping the dead-end "claim to your Peanut
balance" fallback), internal gets a new errors.belowMinimumCrossChain
("...ask the sender to reclaim it") in all four locales.
- Always forward amountUsd so the FE dynamic backstop covers both destinations.
- Extract the decision into a pure belowClaimBridgeMinimum() helper and unit-
test it (same-chain skip, unknown-amount skip, per-chain floors, boundary).
Pairs with the peanut-api-ts fail-closed server-authoritative guard.
…t three /users/me deliberately returns the badge so the beta switch can read its own permission, so hiding it from the public profile server-side does not cover the surfaces that render the CALLER's own list. Two still showed it: the /badges page and the full history page — a customer who tapped five times would find "Peanut Team" on their own profile. Move the rule into displayableBadges() and use it at all four call sites, so the next surface to render a badge list inherits it instead of re-deciding. The home feed keeps its separate BETA_TESTER rule, which is about first impressions rather than about permission records.
Chip (major): the sign-only engine hands the op to the backend, so a reverted ephemeral op comes back as a backend error and a retry would enter the one-tap block again — no two-tap fallback — and the Manteca withdraw broadcaster's receipt waiter does not check success. Engine B leaves this PR; it returns with a client-side revert marker and the backend success check. Engine A's fallback is complete and ships now.
…ile-filter fix(badges): filter PEANUT_TEAM on every own-profile surface
There was a problem hiding this comment.
Chip review — no blocking findings — this is not an approval
Clean at the pinned head. The QR-pay split preserves payment initiation, signing and completion, KYC and error precedence, amount and balance gates, and the success/perk lifecycle; exact-head CI is green.
Findings
- MAJOR · src/components/TransactionDetails/strategies/registry.ts:30 · [claude-opus] IntentKind is derived from a /history/{entryId} kind enum the pinned peanut-api-ts does not declare
registry.tsnow derives the FE's whole intent vocabulary from the generated types:export type IntentKind = paths['/history/{entryId}']['get']['parameters']['query']['kind'], and the comment above it promises that a backend enum addition 'becomes a COMPILE ERROR on the next type regen'. That promise depends on the backend declaring the kind as a literal union.
Evidence: the committed src/types/api.openapi.json / src/types/api.generated.ts:5603 carry the full 18-value enum (ONRAMP | ... | CHARGEBACK | PERK_REWARD), added by this diff. The pinned peanut-api-ts policy branch declares it loosely — peanut-api-ts/src/routes/user/history.ts:230-238, getHistoryEntrySchema.querystring = Type.Object({ kind: Type.String() }), with the validation done at runtime via VALID_WIRE_KINDS (line 259) rather than in the schema. Every other new path in this regen (/rain/cards/withdraw/prepare/cancel, /rain/cards/withdraw/stamp, /users/saved-addresses, /user/crisp-token, /status/summary, the /healthz/* set) does exist on the pinned branch, so this one entry is the sole drift.
This does not break peanut-api-ts, and nothing breaks at runtime today — the generated file is committed. The failure is on the next regen: pnpm gen:api:live against the merged backend collapses IntentKind to string, STRATEGIES becomes Record<string, TransactionStrategy> (which accepts any subset without error), isIntentKind widens to value is string, and the exhaustiveness guarantee this commit was written to provide disappears silently — exactly the TASK-21403 failure shape the comment cites. pnpm check:api in CI would also start failing or, worse, quietly rewrite the file.
A change like this normally ships as a pair, and the backend half (tightening kind to Type.Union of the TransactionIntentKind literals plus PERK_REWARD) is most likely an open peanut-api-ts PR that is not in this checkout — this is not a claim the author forgot it. Flagging it so the merge order is explicit: confirm the peanut-api-ts schema change is merged before or with this, or the derived-type safety net is inert. If the backend half is not landing, pin the union locally in registry.ts and keep the Record<IntentKind, …> check meaningful rather than deriving from a string.
Checked clean
- Confirmed the detached worktree head, merge base, trusted author, and dev base exactly match the supplied review coordinates.
- Compared the QR-pay split against its immediate pre-split image, including nuqs parameter decoding, keyed scan remounts, capability/KYC gating, scan retries and outcome classification, open- and fixed-amount handling, signed-spend request shapes, and success/perk state; no reachable behavior regression survived.
- Adversarially traced invalid and recurring QR inputs, provider and region blocks, retry and order-not-ready transitions, new scans during in-flight work, balance and limit failures, stale-session/signing failures, and backend completion failures; the moved guards and unchanged state-matrix coverage preserve the intended outcomes.
- Reviewed the stacked tech-debt integration seams relevant to this head, including workflow trigger expansion, dependency removal, auth/wallet money boundaries, and the final dev merge; no QR-path conflict resolution or privilege escalation was introduced.
- Exact-head required CI passed, including unit, typecheck, ESLint, format, DS lint, native export, analysis, deploy preview, visual ds-shots, and the aggregate gate.
- Focused local Jest was not rerun because the detached worktree has no installed dependencies; the exact-head unit check provides the execution result.
Security review: did not run — openrouter-http-402. This review is one reviewer short.
Third opinion by claude-opus: 1 finding(s), marked with the model name. It answers only product truth, missing tests and the cross-repo contract, so treat its findings as advice.
Exact head: 245330f134e0 · Context: repo · Took 6m
…ng qr-pay branch (stacked-PR order: #3008 -> tech-debt, this on top)
|
/chip review |
There was a problem hiding this comment.
Chip review — no blocking findings — this is not an approval
The QR-pay implementation and tech-debt merge adaptation are clean, but the retargeted 472-commit range cannot pass the required provenance gate because GitHub enumerates only 250 PR commits.
Findings
- MAJOR · .github/workflows/bot-approval.yml:122 · Reduce the PR below the provenance gate's commit ceiling
Against tech-debt, GitHub reports 472 commits for this PR. Both latest bot-approval runs on this exact head reach this fail-closed guard after listCommits returns only 250 entries, so the required provenance check cannot inspect every commit and the head is not mergeable despite green product CI. Merge the dev carrier into tech-debt in staged human-owned updates, or otherwise split/rebase the range so this PR exposes at most 250 commits, then rerun the gate.
Inline anchors unavailable for 1 finding(s); the findings remain in this summary.
Checked clean
- Confirmed the detached worktree head, merge base, trusted author, and live tech-debt base exactly match the supplied review coordinates.
- Decomposed the 472-commit carrier into already-landed dev history, the three QR-pay refactor commits, and the final dev merge; reviewed the QR split against its immediate pre-split image rather than treating inherited history as newly authored code.
- Traced QR parameter decoding, provider remounts, capability and KYC gates, retry and outcome transitions, open- and fixed-amount handling, signed-spend request shapes, balance and limit failures, backend completion, and success/perk state; no reachable behavior regression survived.
- Inspected the final merge with remerge-diff. Its only content conflict adapts the residence-polish handlers and tests from Redux dispatches to SetupFlowContext setters, and the resulting state updates are correct.
- Exact-head product checks are green, including unit, typecheck, ESLint, format, DS lint and shots, native export, analysis, deploy preview, human-authors, and ci-success; the two latest bot-approval runs fail solely on the 472-versus-250 commit enumeration mismatch reported above.
- Focused local Jest was not rerun because the detached worktree has no installed dependencies; the exact-head unit check provides the execution result.
Security review: did not run — openrouter-http-402. This review is one reviewer short.
Third opinion by claude-opus: 0 finding(s), marked with the model name. It answers only product truth, missing tests and the cross-repo contract, so treat its findings as advice.
Exact head: 245330f134e0 · Context: repo · Took 16m
…ress The features/ rebuild of the qr-pay flow predates dev's entity-split funding change (33cb733 / 09199fa, for the 2026-09-14 Manteca per-entity balance split), so useQrPayFlow signed to the legacy per-rail constants and the next dev merge would have dropped the fix silently (the file dev changed no longer exists here). Hand-ported from dev: pickMantecaDepositAddress + friends in src/utils/manteca.utils.ts (with tests), depositAddress/legalEntity on QrPaymentLock and WithdrawPriceLock, the qr-pay signSpend recipient, the withdraw/manteca signSpend recipient (this branch also rewrote that page), and the recipient-wiring test group in qr-pay-states.
…KYC views Dev's shared restart-cooldown dialog (99bfe77) postdates this branch's last dev merge, and the qr-pay views it changed were rebuilt into features/ here — so a 429 on restart left the user staring at a prompt whose only CTA the backend refuses, with no way out. Hand-ported from dev: the 429 cooldown parse in the restart server action (with tests), errorCooldown/dismissErrorCooldown through useSumsubKycFlow and useMultiPhaseKycFlow, KycRestartCooldownModal mounted by SumsubKycModals (onCooldownClose), the cooldown i18n keys in all four locales, and the prompt gating (!kycPromptDismissed && !sumsubFlow.errorCooldown, dismiss leaves the flow) in QrPayProviderRejectionView and QrPayKycGateView, pinned by the cooldown test in qr-pay-states.
… flow (PR #3005) The features/withdraw rebuild kept #3005's charge-pinned broadcast (setupAmountRef) but dropped the max-withdrawal half: the balance tap was gone from the amount step, and the crypto path validated and signed the displayed 2-decimal amount — so 'withdraw everything' stranded the sub-cent remainder as un-withdrawable dust again (TASK-21899). Re-ported onto this branch's architecture: isMaxWithdrawal on the flow context, balanceFillAmount/onBalanceFilled through WithdrawRoot and WithdrawAmountView (retired on any edit), and resolveWithdrawAmount in the crypto page (validated, quoted, pinned at charge creation, and used as every fallback the pinned amount had). Tests: the five balance-fill cases in withdraw-states and the frozen-spend group (dust remainder, balance drop, balance rise) in crypto-withdraw-confirm, driven through the real setup path so the pin is exercised.
…l capture, pay the hover ratchet Three review findings on the features/ rebuild: - QrPayPageLoading lost the old layout="fill" when rewritten to width/height, parking the mascot at the container's top-left; inset-0 h-full w-full restores the fill so object-contain centers it again. - usePerkHoldToClaim's deferred REWARD_CLAIM_DISMISSED cancel lived in an instance ref, and the page remounts the flow under a new qrCode|timestamp key — a new instance could never cancel the old instance's capture, so every keyed remount fired a phantom dismissal. The pending timer is module-scoped now (pinned by a keyed-remount test). - Revert the hoverNoActiveFiles 44->45 baseline ratchet and give the two relocated hover:text-black links their active:text-black instead — the ratchet exists to be paid, not raised.
|
Heads-up: I pushed 4 commits ( What changed (all are dev work that landed after this branch's last dev merge, in files the split rebuilt — the next dev merge would have conflict-dropped them):
Local gate before push: prettier clean, Known remaining drift, deliberately NOT touched (for the next dev merge, Kush): dev's qr-pay page/tests moved further after this branch's merge-base — non-PAID status handling tests ( |
…to refactor/TASK-21854-logic-ui-separation
removeParamStep() clears the trigger through a raw window.history.replaceState that nuqs does not observe, so stepFromURL keeps reporting 'claim' for the rest of the mount — and fetchUser() during the claim replaces the user object, re-running the trigger effect mid-claim. That re-run fired a SECOND claim POST, which rejected against the already-claimed link and painted an error over the success screen. A consume-once ref (armed only when a step was actually present) swallows the re-run. The pinning test now stages the real prod sequence — a user identity swap after the first claim — instead of relying on a stable user object that made the double-fire unreachable in the harness.
…afe storage, one flow surface - useAddMoneyFlow: handleBack's if (countryFromQuery) branch was unreachable — every ?country= render path returns a component that owns its own back behavior, so the handler never runs with a country in the URL. Deleted, with its pinning test replaced by one that records why. - card/utils: the celebration gate read raw window.localStorage inside a render-time useState initializer; the bare accessor throws where site data is blocked. Now goes through safe-storage. - QrPayFormView read useWallet() right next to useQrPayFlow(), which already holds the wallet; the flow now surfaces balance so views read one object. Also dropped InvitesGraph/index.tsx's unused GraphMode re-export (types live in types.ts per the export rules).
|
/chip review |
There was a problem hiding this comment.
Chip review — no blocking findings — this is not an approval
Two prior major findings remain: required provenance CI cannot enumerate this 476-commit PR, and the intent-kind exhaustiveness type is still based on a frontend-only enum absent from the pinned backend schema. The QR-pay changes since the previously reviewed head are otherwise clean, and all exact-head code checks pass.
Findings
-
MAJOR · .github/workflows/bot-approval.yml:122 · Reduce the PR below the provenance gate's commit ceiling
This head is still 476 commits ahead of the exact base, while GitHub's pull-request commits API enumerates at most 250. Bothbot-approvalandhuman-authorsfail closed at this head, andci-successconsequently fails, so an approval or rerun cannot make required CI green. Merge the dev carrier intotech-debtfirst, or split/rebase the stack until this PR exposes at most 250 commits. -
MAJOR · src/components/TransactionDetails/strategies/registry.ts:30 · Define the intent-kind enum in the backend schema
IntentKindis still derived from a vendored union on/history/{entryId}, while the pinnedpeanut-api-tsroute and OpenAPI schema declarekindas a plain string. On the next automated sync from staging, this type widens tostring, so the finiteRecord<IntentKind, ...>no longer forces a mapping for a newly added backend kind and that row falls through at runtime. Declare the accepted wire-kind literals, including syntheticPERK_REWARD, in the backend route schema, regenerate its OpenAPI, then sync the frontend snapshot. -
MINOR · src/utils/residence-availability.ts:41 · [claude-opus] Residence card omits SPEI for non-Mexican Bridge residences, against the Bridge cohort rule
BRIDGE_RAILSis['eurSepa', 'gbpFps', 'usdAch']and the comment above it justifies dropping SPEI_MX fromREGION_RAIL_MAPon the grounds that "Bridge mints MXN accounts for Mexican residents". product/countries.md contradicts that directly, in the section headed "THE BRIDGE COHORT RULE (verified in code + production, 2026-09-02)": "One Bridge verification opens ALL FOUR Bridge rails — SEPA (EUR), ACH/wire (USD), Faster Payments (GBP) and SPEI (MXN) — to the verified user, whatever their residence", because "Bridge entitlement is per-ENDORSEMENT, not per-country ... with no residence branch". product/providers/fiat/eligibility.md line 392 backs it with production evidence — Nigerian-resident customers holding approvedspeiendorsements. The pinned peanut-api-ts agrees:REGION_RAIL_MAP.EUand.NAin src/kyc/rails.consts.ts both includeSPEI_MX, so the mirror is faithful for the other three rails and diverges only here.
The code is the side that's wrong, but the harm is small: this is an advisory "Available with {country}" list at signup, so it under-lists rather than over-promises, and MXN is a near-dead rail by volume. The fix is to add 'spei' to BRIDGE_RAILS (the rail !== home filter in bankRailsFor already stops Mexico listing it twice) and update the three pinned expectations in src/utils/tests/residence-availability.test.ts. If the residence scoping is in fact real and countries.md is the stale side, that file should be corrected instead — it is the source of truth support and marketing copy are generated from.
Inline anchors unavailable for 2 finding(s); the findings remain in this summary.
Checked clean
- Confirmed the detached worktree is at the supplied head and the supplied base is the merge base.
- Read the exact-head CI failures: both provenance jobs see 476 commits but can enumerate only 250; unit, typecheck, ESLint, format, native export, and design-system checks pass.
- Compared the frontend intent registry and generated snapshot with the pinned peanut-api-ts route and OpenAPI schema.
- Reviewed the QR-pay delta since the previously reviewed head: API-served Manteca deposit-recipient validation, restart cooldown exit, full-balance withdrawal pinning, loading layout, and cross-remount perk analytics cleanup.
- Reviewed workflow permission and credential-persistence changes; secret-bearing jobs remain isolated from PR-authored execution.
Security review by moonshotai/kimi-k3: 0 finding(s), marked with the model name. It reads the diff only and answers only security, privacy and money, so treat its findings as advice.
Third opinion by claude-opus: 1 finding(s), marked with the model name. It answers only product truth, missing tests and the cross-repo contract, so treat its findings as advice.
Exact head: de3903698714 · Context: repo, sibling · Took 18m
…c-ui-separation refactor: TASK-21854 logic/UI separation — extract inline-state pages and monoliths onto features/
Summary
Rebuild of the qr-pay page (TASK-21457, DS tech-debt project). The 1,919-line
app/(mobile-ui)/qr-pay/page.tsxmonolith (11 early-return render branches, ~20 useState, no step variable) becomes afeatures/payments/flows/qr-pay/flow, modeled onsemantic-request: a 19-line entry, one provider, one behavior hook, and no-prop views. Behavior-parity migration — the DOM, copy, i18n keys, and API calls are unchanged.Note on the diff: this branch carries
origin/tech-debtmerged onto dev (the qr-pay split needsuseFlowStepper's home + the redux deletion). Until the official tech-debt→dev merge-back lands, the diff shows all of tech-debt. The qr-pay work itself is commits7accc694,08083f69,4a59d521. Merge-resolution notes (including dev withdraw fixes that had no home in the rebuilt structure, flagged for the merge-back) live in the state log.Task
TASK-21457
What moved where
app/(mobile-ui)/qr-pay/page.tsx(19 lines)switch(view)QrPayPage.tsxQrPayFlowContext.tsx+qr-pay-flow.types.tsuseQrPayFlow.tsuseMultiPhaseKycFlow/useCapabilitiesuntouched)useQrPayKycGate.tsusePerkHoldToClaim.tsderive-view.tsuseQrFailureCopy.tsviews/— Form, Success, KycGate, Blocked (maintenance/init-error/order-not-ready), ProviderRejection, PageLoadingDesign notes / accepted trade-offs
useFlowStepperdeliberately NOT adopted. Every qr-pay screen is outcome-derived (KYC gate, scan errors, payment success) — there are zero user-navigable steps, so a URL step param would let?step=successdeep-link a payment that never happened.deriveQrPayViewencodes the exact precedence ladder (the old "this must come BEFORE" ordering comments are now one readable function).keyonqrCode|t) replaces the oldresetState()-in-an-effect; a late in-flightsetIsSuccessnow lands on a dead provider instead of a fresh scan's state.?qrCode/?t/?type; strict-mode-safe perk-dismissal analytics (regression-tested); guarded currency fetch; timer cleanup consolidated; deprecatednext/imagelayout="fill"removed; balance/entry-guard errors derived instead of effect-latched.Tests
The 2,050-line qr-pay state-matrix suite runs unchanged through the page entry — the harness only gained
NuqsTestingAdapter(~13-line diff). Classifier test moved with its file. NewusePerkHoldToClaimStrictMode suite (fails against the old implementation). 503 suites / 6,203 tests green; typecheck, prettier, build, ds-lint ratchet all pass.Risks / breaking changes
handleMantecaPaymentis byte-equivalent to the old page modulo renames; independently reviewed hunk-by-hunk against the monolith (internal adversarial review: no dropped guard, no inverted condition found).QA
npm test— qr-pay suite exercises the full state matrix (KYC gates, scan failures, payment errors, success + perk claim)./qr-pay?qrCode=qr3via the Nutcrackere2e-qr-pay-arscenario path.Screenshots
Sandbox capture (local qa stack, seeded users, mobile Chrome UA). PR = this branch on :3055; baseline = dev (
367da9366) on :3050, same users/API/DB. Thepr-assets-3008branch holds these images only — delete it after merge./qr-pay?qrCode=qr3&type=ARGENTINA_QR3, manteca-AR user, 375×667/qr-payno params, 375×667Not reachable in sandbox (not faked): FORM/SUCCESS (needs
sandbox.manteca.devcredentials —qr3falls through to the prod decoder and 500s, pere2e-qr-pay-ar), AWAITING_MERCHANT, ORDER_NOT_READY, MAINTENANCE, PROVIDER_REJECTION.Stack (updated 2026-09-07)
Base retargeted
dev→tech-debt. This PR is the bottom of a 2-PR stack and carries dev's latest (incl. the DS changes) into tech-debt:tech-debt— dev's recent commits + the qr-pay splittech-debtwhen this mergesThe diff therefore shows dev-side commits (residence polish, avatars, review sheet, …) riding along — intended, this PR is the dev-carrier for the stack.