Skip to content

chore: back-merge main → dev (2026-06-10, post #2199/#2202/#2204)#2205

Merged
Hugo0 merged 14 commits into
devfrom
chore/sync-main-into-dev-0610
Jun 10, 2026
Merged

chore: back-merge main → dev (2026-06-10, post #2199/#2202/#2204)#2205
Hugo0 merged 14 commits into
devfrom
chore/sync-main-into-dev-0610

Conversation

@Hugo0

@Hugo0 Hugo0 commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

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

  • typecheck clean
  • full jest green (the 17 countryCurrencyMapping failures in a fresh worktree are the known missing-public/flags env artifact — pass with assets present)

Risk

None beyond what already shipped on main.

jjramirezn and others added 14 commits June 8, 2026 01:08
… 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
@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

This 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.

Changes

Stale-Session and KYC Infrastructure

Layer / File(s) Summary
Wallet credential staleness detection utilities
src/utils/walletCredential.utils.ts, src/utils/__tests__/walletCredential.utils.test.ts
Introduces isStaleClientForUser, isStaleKeyError, and createStaleSessionError functions to detect and construct stale-session/webAuthn key failures, with test coverage for address comparison and error classification patterns.
Stale-session guard hook
src/hooks/wallet/useStaleSessionGuard.ts
New client-side React hook providing a memoized (error: unknown) => boolean callback that detects stale-session errors via shared utilities, triggers logoutUser(), and signals the caller to stop processing on match.
Kernel client ownership assertion
src/context/kernelClient.context.tsx
Adds assertClientOwnedByUser callback to validate derived smart-account address against logged-in user, logs out and throws stale-session error on mismatch; applies assertion to cached and newly built clients, and differentiates stale-key errors (logout) from other build errors (Sentry capture).
Sumsub KYC hook Manteca country validation
src/hooks/useSumsubKycFlow.ts, src/hooks/__tests__/useSumsubKycFlow.test.ts
Enhances KYC initiation to normalize and validate target country codes for Manteca support by uppercasing input and checking isMantecaSupportedCountryCode; clears user-initiated guard on terminal errors to prevent stale websocket approvals; expands test coverage for country gating and terminal-error guard cleanup.
Shared stale-key error handling in useZeroDev
src/hooks/useZeroDev.ts
Refactors to import and use shared isStaleKeyError and createStaleSessionError utilities instead of local classifier, triggers logoutUser() and throws standardized stale-session error on detection.

Stale-Session Integration in Flows

Layer / File(s) Summary
Stale-session guards in QR pay and withdraw flows
src/app/(mobile-ui)/qr-pay/page.tsx, src/app/(mobile-ui)/withdraw/manteca/page.tsx
Integrates stale-session guard into QR pay Manteca payment completion error handler and Manteca withdrawal backend/catch error paths; checks for wrong-passkey/stale-session and returns early when detected, with proper callback dependency updates.

KYC Flow Enhancements Across Components

Layer / File(s) Summary
KYC call site parameter updates
src/app/(mobile-ui)/add-money/[country]/bank/page.tsx, src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx, src/components/AddWithdraw/AddWithdrawCountriesList.tsx, src/components/Claim/Link/MantecaFlowManager.tsx, src/components/Claim/Link/views/BankFlowManager.view.tsx
Updates sumsubFlow.handleInitiateKyc calls across add-money, withdraw, and claim flows to pass country ID (selectedCountry?.id or currentCountry?.id) and needs-enrollment gate expressions as additional arguments, aligning with the enhanced hook parameter signature.

Card Page and eSign Content

Layer / File(s) Summary
Card page outer-gate refactor to 404 boundary
src/app/(mobile-ui)/card/page.tsx
Changes pre-launch outer gate from client-side redirect (router.replace('/shhhhh')) to server-side notFound() 404 boundary; removes useRouter dependency and updates associated comment to reflect new behavior.
Card eSign marketing page and routing
src/app/[locale]/(marketing)/card-esign/page.tsx, src/i18n/config.ts, src/components/Card/CardTermsScreen.tsx
Adds localized card-eSign marketing page with static params, dynamic-params false, and generateMetadata that builds SEO metadata from MDX frontmatter; adds 'card-esign' route slug; updates card terms E-Sign link to new peanut.me/en/card-esign page.
Content submodule update
src/content
Advances submodule reference to new commit containing localized card-eSign content and MDX metadata.

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

enhancement

Suggested reviewers

  • jjramirezn
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the pull request as a routine back-merge from main to dev, with specific PR references for context.
Description check ✅ Passed The description is well-detailed, explaining the purpose, carried changes, test gates, and risk assessment—all directly related to the back-merge changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • unit: 1348 ran, 0 failed, 0 skipped, 21.3s

📊 Coverage (unit)

metric %
statements 50.8%
branches 31.8%
functions 37.6%
lines 50.7%
⏱ 10 slowest test cases
time test
0.4s src/app/actions/__tests__/api-headers.test.ts › should include Content-Type in updateUserById
0.3s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › every stamp stays within canvas at any count
0.3s src/app/actions/__tests__/api-headers-extended.test.ts › should not include apiKey in updateUserById body
0.2s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle valid 9-digit US account
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle too long for US account
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle invalid ETH address (too short)
0.1s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › Perk claimed shows shake class + go home button
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle maximum length (17 digits) US account
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle valid ETH address
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle minimum length (6 digits) US account
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

@coderabbitai coderabbitai Bot added the enhancement New feature or request label Jun 10, 2026
@Hugo0 Hugo0 merged commit d3031d5 into dev Jun 10, 2026
16 of 20 checks passed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 win

Remove the dead isRiskAccepted state before merge.

CI is already red here because isRiskAccepted is 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 into OnrampConfirmationModal.

🤖 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 win

Add a refreshToken() regression here.

These cases only prove the first initiateSumsubKyc() call is sanitized. The fix also depends on targetCountryRef.current being reused on later token refreshes; if that path drops AR/BR, the SDK can refresh into a different applicant action even though the initial request was correct. Please add one follow-up assertion that result.current.refreshToken() re-sends the sanitized targetCountry after 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

📥 Commits

Reviewing files that changed from the base of the PR and between 1d7d2e5 and b22704d.

📒 Files selected for processing (19)
  • src/app/(mobile-ui)/add-money/[country]/bank/page.tsx
  • src/app/(mobile-ui)/card/page.tsx
  • src/app/(mobile-ui)/qr-pay/page.tsx
  • src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx
  • src/app/(mobile-ui)/withdraw/manteca/page.tsx
  • src/app/[locale]/(marketing)/card-esign/page.tsx
  • src/components/AddWithdraw/AddWithdrawCountriesList.tsx
  • src/components/Card/CardTermsScreen.tsx
  • src/components/Claim/Link/MantecaFlowManager.tsx
  • src/components/Claim/Link/views/BankFlowManager.view.tsx
  • src/content
  • src/context/kernelClient.context.tsx
  • src/hooks/__tests__/useSumsubKycFlow.test.ts
  • src/hooks/useSumsubKycFlow.ts
  • src/hooks/useZeroDev.ts
  • src/hooks/wallet/useStaleSessionGuard.ts
  • src/i18n/config.ts
  • src/utils/__tests__/walletCredential.utils.test.ts
  • src/utils/walletCredential.utils.ts

Comment on lines 543 to 551
.catch((error) => {
console.error(`Error lazy-building kernel client for chain ${chainId}:`, error)
captureException(error)
if (isStaleKeyError(error)) {
logoutUser()
} else {
captureException(error)
}
throw error
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants