chore: back-merge main → dev (2026-06-10, post #2199/#2202/#2204)#2205
Conversation
… sites Pairs with peanut-api-ts#995. Five call sites of `sumsubFlow.handleInitiateKyc` knew the user's chosen country (selectedCountry.id / currentCountry.id / URL param) but dropped it on the floor — the 4th `targetCountry` arg was just omitted. That cascaded on the BE: no `pendingMantecaGeo` on metadata → no `-XX` suffix on the Sumsub action externalId → no PENDING Manteca rail pre-stamped → when the action went GREEN, the webhook handler couldn't determine which exchange to submit to and bailed silently (companion PR adds an applicant- country fallback + Sentry capture for that case). Sites fixed: - components/AddWithdraw/AddWithdrawCountriesList.tsx (2 call sites) - components/Claim/Link/views/BankFlowManager.view.tsx (2 call sites) - components/Claim/Link/MantecaFlowManager.tsx - app/(mobile-ui)/add-money/[country]/bank/page.tsx - app/(mobile-ui)/withdraw/[country]/bank/page.tsx UnlockedRegions.view.tsx intentionally NOT patched: at LATAM-macro click the country isn't known (no per-country picker at that surface). The companion BE fallback handles that case from the applicant's extracted country.
The card campaign isn't fully online yet, so users without flow early access should not discover the /shhhhh closed-beta landing by hitting /card. Swap the outer-gate redirect for notFound() — /card now behaves as if it doesn't exist for the ungated. Access is unchanged for legit paths: existing/in-progress card holders pass the active-card precedence check, and entrants through /shhhhh get their flowEarlyAccess stamp (via ?press_door=1) before /card mounts. The /shhhhh page itself stays public and fully visible.
fix(card): 404 the /card gate instead of redirecting to /shhhhh
…rce re-auth On a shared device with two passkeys for the same RP, the credential restore path can pair a session with the OTHER account's passkey. Every sign site reads the kernel client through getClientForChain, but nothing verified the client's smart-account address belonged to the logged-in user — so a wrong-owner signature would be produced and later rejected (AA24 on-chain, or an invalid ERC-1271 admin signature for Rain). Guard at the single choke point (getClientForChain / ensureClientForChain) so all six sign sites — and any future one — are covered: if the active client's address doesn't match the user's smart-wallet address, purge the stored key, force logout, and throw. Also force logout when the reactive AA24/wapk detector fires in useZeroDev; previously it showed the "session expired" toast but left the user signed in, so they stayed stuck. The address check is exact for derived-address (post-migration) accounts. Pre-migration accounts inject their address, so it can't detect a mismatch for them; those remain backstopped by the broadcast-ordering config.
The access-point guard catches post-migration accounts (their address is derived from the passkey, so a wrong key yields a different address). But a pre-migration (migration) account's address is injected from the backend user record, so a wrong-passkey session has a matching address and slips through — it would sign for the wrong owner and fail with AA24. Derive the address the passkey actually maps to (createKernelAccount with the legacy validator and no injected address — the same derivation the SDK runs internally to locate the account it migrates) and require it to match the injected address. A mismatch means the wrong passkey owns the session, so bail to a clean re-auth before any signature is produced. Also share the stale-error plumbing (isStaleKeyError, createStaleSessionError) between useZeroDev and the kernel context, skip retries on the deterministic stale error during init, and force logout from the lazy build path too.
The proactive pre-migration check (deriving the key's address and comparing to the injected one) carried an unacceptable failure mode: if that derivation were ever wrong for a legitimate account, the user would be thrown into an unrecoverable logout loop — every re-auth re-derives the same wrong address and bounces them, and since each passkey IS an account they have no other key to escape with. That's breaking the app, not a clean logout. Drop it. Pre-migration is now covered reactively instead: detect the actual AA24 / wapk error coming back and force a clean re-auth. This can only fire on a real failure, so it can never false-positive into a lockout. - New useStaleSessionGuard hook: force logout when an API error/response is a stale-credential error (AA24 / wapk). - Wire it into the Manteca withdraw flow (sign-then-broadcast), where the backend surfaces the broadcast error — the access-point guard can't see it for migration accounts since their address is injected, not key-derived. Post-migration accounts remain covered proactively (their address IS derived from the key, so that guard cannot false-positive on a working account).
QR pay is the other sign-then-broadcast caller. Adopt useStaleSessionGuard so a wrong-passkey session (backend rejects the signed UserOp with AA24 / wapk) forces a clean logout instead of a generic "could not complete payment" message, matching the Manteca withdraw flow.
…guard fix(wallet): reject mismatched kernel client at the sign choke point + force re-auth
…uard on every terminal error Three hook-level holes around the new targetCountry plumbing: 1. Choke-point gating: call sites pass the raw destination country for every latam-region country, but the BE only ever consumes targetCountry as a Manteca geo — an unsupported value (MX, CL, …) stamps a poisoned pendingMantecaGeo (first-write-wins) that bails every later geo resolution. Drop non-AR/BR values in the hook so no call site can regress this. 2. Native token-refresh parity: the Capacitor refresh callback omitted targetCountry (web refreshToken and the 5s poller pass it), so a native token expiry mid-action minted a token for a different, suffix-less applicant action than the one the user was inside. 3. Generalize the 4527d38 race fix: it cleared userInitiatedRef only on the unsupported-region branch, but every other terminal-error exit (response.error — e.g. region_not_supported 400 —, no-token, native failure, throw; same in handleRestartIdentity/handleSelfHealResubmit) left the guard set while restoring prevStatusRef, so a late websocket APPROVED replay fired onKycSuccess on top of the rendered error — the exact "You're all set" loop again. Tests use a PENDING→APPROVED two-event sequence so the userInitiatedRef guard is isolated from the prevStatusRef guard.
isMantecaSupportedCountryCode uppercases internally, so a lowercase route-param country passed the predicate but was forwarded raw; the BE normalizes anyway, but the hook's contract is normalized-or-dropped.
Rain's onboarding form requires a partner-hosted E-Sign Consent (their template with 'Rain'→company name); their hosted page also surfaces Rain branding and routes support to support@rain.xyz, conflicting with our no-provider-names-to-customers posture. - new route /[locale]/card-esign (legal ContentPage), content bumped to peanut-content@f204f50 (Peanut-branded Electronic Communications Notice) - ROUTE_SLUGS += card-esign - onboarding E-Sign Consent checkbox now links peanut.me/en/card-esign instead of legal.raincards.xyz Page + link ship together so there's no broken-link window.
feat(card): self-hosted Peanut E-Sign Consent page + onboarding link
…ountry fix(cross-region): pass targetCountry from country-aware KYC initiate sites
WalkthroughThis PR implements stale-session error handling infrastructure, enhances KYC flows with country-specific targeting for Manteca support, integrates session guards into payment and withdrawal flows, refactors the card page outer-gate to use 404 boundaries, and adds a localized card-eSign marketing page. ChangesStale-Session and KYC Infrastructure
Stale-Session Integration in Flows
KYC Flow Enhancements Across Components
Card Page and eSign Content
🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
Comment |
🧪 UI test report — ✅ all greenSuites
📊 Coverage (unit)
⏱ 10 slowest test cases
|
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/app/(mobile-ui)/add-money/[country]/bank/page.tsx (1)
64-66:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winRemove the dead
isRiskAcceptedstate before merge.CI is already red here because
isRiskAcceptedis never read, and the setter only writes back to the same unused state. Drop the state pair and the two reset calls, or wire the value intoOnrampConfirmationModal.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/app/`(mobile-ui)/add-money/[country]/bank/page.tsx around lines 64 - 66, The isRiskAccepted state and its setter (isRiskAccepted / setIsRiskAccepted) are dead — remove the useState declaration and any calls that reset it (e.g., setIsRiskAccepted(false)) from the component, or alternatively pass isRiskAccepted and setIsRiskAccepted into OnrampConfirmationModal so the modal reads/controls the value; update all references to either delete them or wire them into OnrampConfirmationModal accordingly to eliminate the unused state and clear the CI failure.Source: Pipeline failures
🧹 Nitpick comments (1)
src/hooks/__tests__/useSumsubKycFlow.test.ts (1)
112-152: ⚡ Quick winAdd a
refreshToken()regression here.These cases only prove the first
initiateSumsubKyc()call is sanitized. The fix also depends ontargetCountryRef.currentbeing reused on later token refreshes; if that path dropsAR/BR, the SDK can refresh into a different applicant action even though the initial request was correct. Please add one follow-up assertion thatresult.current.refreshToken()re-sends the sanitizedtargetCountryafter a successful LATAM initiate.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/hooks/__tests__/useSumsubKycFlow.test.ts` around lines 112 - 152, Add a follow-up assertion that refreshToken() re-sends the same sanitized targetCountry after the initial initiateSumsubKyc() call: after calling result.current.handleInitiateKyc(...) and asserting mockInitiate was called, call await act(async () => await result.current.refreshToken()) and then assert mockInitiate was called again with an objectContaining the same sanitized targetCountry (e.g., 'AR' or 'BR') and same regionIntent/crossRegion flags; reference the hook useSumsubKycFlow, the handler handleInitiateKyc, the refresh method refreshToken, and the mock mockInitiate so the test verifies targetCountryRef.current is preserved across token refreshes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/context/kernelClient.context.tsx`:
- Around line 543-551: The catch block that currently logs and rethrows the
original low-level error should normalize stale-key failures to the
deterministic session-expired error before rethrowing: inside the catch for the
lazy-build (the block referencing isStaleKeyError, logoutUser,
captureException), when isStaleKeyError(error) is true call logoutUser() and
then throw a new, standardized session-expired error (e.g., new
Error('session-expired') or an error object with a clear code like { code:
'session-expired' }) instead of rethrowing the original low-level error; for
non-stale errors keep captureException(error) and rethrow the original error as
before.
---
Outside diff comments:
In `@src/app/`(mobile-ui)/add-money/[country]/bank/page.tsx:
- Around line 64-66: The isRiskAccepted state and its setter (isRiskAccepted /
setIsRiskAccepted) are dead — remove the useState declaration and any calls that
reset it (e.g., setIsRiskAccepted(false)) from the component, or alternatively
pass isRiskAccepted and setIsRiskAccepted into OnrampConfirmationModal so the
modal reads/controls the value; update all references to either delete them or
wire them into OnrampConfirmationModal accordingly to eliminate the unused state
and clear the CI failure.
---
Nitpick comments:
In `@src/hooks/__tests__/useSumsubKycFlow.test.ts`:
- Around line 112-152: Add a follow-up assertion that refreshToken() re-sends
the same sanitized targetCountry after the initial initiateSumsubKyc() call:
after calling result.current.handleInitiateKyc(...) and asserting mockInitiate
was called, call await act(async () => await result.current.refreshToken()) and
then assert mockInitiate was called again with an objectContaining the same
sanitized targetCountry (e.g., 'AR' or 'BR') and same regionIntent/crossRegion
flags; reference the hook useSumsubKycFlow, the handler handleInitiateKyc, the
refresh method refreshToken, and the mock mockInitiate so the test verifies
targetCountryRef.current is preserved across token refreshes.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 4bf3c755-667e-41d8-88db-548e6d2c46b3
📒 Files selected for processing (19)
src/app/(mobile-ui)/add-money/[country]/bank/page.tsxsrc/app/(mobile-ui)/card/page.tsxsrc/app/(mobile-ui)/qr-pay/page.tsxsrc/app/(mobile-ui)/withdraw/[country]/bank/page.tsxsrc/app/(mobile-ui)/withdraw/manteca/page.tsxsrc/app/[locale]/(marketing)/card-esign/page.tsxsrc/components/AddWithdraw/AddWithdrawCountriesList.tsxsrc/components/Card/CardTermsScreen.tsxsrc/components/Claim/Link/MantecaFlowManager.tsxsrc/components/Claim/Link/views/BankFlowManager.view.tsxsrc/contentsrc/context/kernelClient.context.tsxsrc/hooks/__tests__/useSumsubKycFlow.test.tssrc/hooks/useSumsubKycFlow.tssrc/hooks/useZeroDev.tssrc/hooks/wallet/useStaleSessionGuard.tssrc/i18n/config.tssrc/utils/__tests__/walletCredential.utils.test.tssrc/utils/walletCredential.utils.ts
| .catch((error) => { | ||
| console.error(`Error lazy-building kernel client for chain ${chainId}:`, error) | ||
| captureException(error) | ||
| if (isStaleKeyError(error)) { | ||
| logoutUser() | ||
| } else { | ||
| captureException(error) | ||
| } | ||
| throw error | ||
| }) |
There was a problem hiding this comment.
Normalize stale-key lazy-build failures before rethrowing.
Line 545 detects stale-session errors but rethrows the original low-level error. This bypasses the standardized stale-session error contract and can leak AA24/wapk messages to downstream handlers expecting the deterministic session-expired error.
Proposed fix
.catch((error) => {
console.error(`Error lazy-building kernel client for chain ${chainId}:`, error)
if (isStaleKeyError(error)) {
+ if (user?.user.userId) updateUserPreferences(user.user.userId, { webAuthnKey: undefined })
logoutUser()
+ throw createStaleSessionError(error)
} else {
captureException(error)
}
throw error
})🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/context/kernelClient.context.tsx` around lines 543 - 551, The catch block
that currently logs and rethrows the original low-level error should normalize
stale-key failures to the deterministic session-expired error before rethrowing:
inside the catch for the lazy-build (the block referencing isStaleKeyError,
logoutUser, captureException), when isStaleKeyError(error) is true call
logoutUser() and then throw a new, standardized session-expired error (e.g., new
Error('session-expired') or an error object with a clear code like { code:
'session-expired' }) instead of rethrowing the original low-level error; for
non-stale errors keep captureException(error) and rethrow the original error as
before.
Summary
Routine back-merge so the next release PR doesn't conflict. Carries from main: #2194 (cross-region unsupported message), #2199 (targetCountry plumbing + hook hardening), #2202 (stale-credential guard), #2204 (card e-sign page), content bumps.
Merge was conflict-free (19 files).
Gates
countryCurrencyMappingfailures in a fresh worktree are the known missing-public/flagsenv artifact — pass with assets present)Risk
None beyond what already shipped on main.