Skip to content

Prod Release Sprint 151 — KYC verdict rendering · Solana/Tron/Base withdrawals · profile fixes (2026-07-16)#2407

Merged
Hugo0 merged 235 commits into
mainfrom
dev
Jul 16, 2026
Merged

Prod Release Sprint 151 — KYC verdict rendering · Solana/Tron/Base withdrawals · profile fixes (2026-07-16)#2407
Hugo0 merged 235 commits into
mainfrom
dev

Conversation

@Hugo0

@Hugo0 Hugo0 commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Prod Release Sprint 151 — Rhino chain expansion + profile fixes (FE)

dev → main. Deploy AFTER peanut-api-ts #1172 (needs the Rhino delivery-verification BE + its migration).

Payload (2 themes)

Rhino chain expansion (#2398, #2401)

  • New withdraw destinations: Solana, Tron, Base, Avalanche, HyperEVM, Ink, Katana, Linea, Mantle, Plasma, Stable, Tempo (SCROLL removed — Rhino disabled it). Per-family recipient validation (EVM 0x / Solana + Tron base58).
  • New deposit chains: Tempo, Kaia, Plasma (USDT-only chains annotated).
  • CHAIN_REGISTRY — one source of truth for every FE chain fact (7 hand-maintained maps consolidated into derived views, equality-proven).
  • Per-chain PostHog rollout flags (chain-rollout-*) — launch chains one at a time with a toggle, no deploy. Staging/preview bypass; prod fails closed. Doctrine: engineering/patterns/feature-gates.md.
  • Polish: Linea logo fix, deposit fee note, receipt explorer links.

Profile-edit fixes (#2388)

Verified users can save email without a name; backend save errors surface instead of false-success; typed update payload.

⚠️ Rollout-flag state at cutover

On prod, a chain shows only if its chain-rollout-* flag is ON. All 13 chain-rollout-* flags are ON (Hugo, 2026-07-14) — so every chain incl. Solana/Tron goes live with the FE deploy. Re-flip any in PostHog before deploy if a staged launch is wanted.

QA

1,831 tests; live-verified on staging. Testing guide in the Notion release page.

Summary by CodeRabbit

  • New Features

    • Added native demo mode with simulated balances, transactions, onboarding, and payments.
    • Added smart-paste extraction (incl. PIX keys and recipients) powered by clipboard detection, plus suggestion UI.
    • Added a connectivity banner and improved QR scanning using native clipboard detection and camera readiness.
  • Improvements

    • Made chain availability rollout-aware across deposit/withdraw selectors and flows.
    • Updated withdraw UX text (“You’re withdrawing”) and added a small bridging-fee note for non-offramp flows.
    • Enhanced non‑EVM recipient handling (Solana/Tron) with address-family validation and automatic input reset when switching chains.
  • Bug Fixes

    • Fixed explorer linking for withdrawal receipts and improved empty-state handling in graphs.

Two native passkey reliability fixes behind the May-18 Play rejection
and a known multi-account signing bug.

Silent "Set it up" no-op (rejection cause):
- checkPasskeySupport() now actually queries the native plugin's
  isSupported() in Capacitor instead of assuming support
- SetupPasskey re-checks on tap and always surfaces an actionable
  message — the button can never silently do nothing
- clearer Android NotAllowedError help copy (Google account / Play
  Services vs private-mode/cancel)

Multi-account signing (re-applies closed PR #2189):
- createNativeSignMessageCallback pins allowCredentials to the kernel's
  own authenticatorId when the caller supplies none, so a second
  peanut.me passkey on the device can't be substituted. Additive and
  native-only; requires a 2-account real-device smoke test.
Two native passkey reliability fixes behind the May-18 Play rejection
and a known multi-account signing bug.

Silent "Set it up" no-op (rejection cause):
- checkPasskeySupport() now actually queries the native plugin's
  isSupported() in Capacitor instead of assuming support
- SetupPasskey re-checks on tap and always surfaces an actionable
  message — the button can never silently do nothing
- clearer Android NotAllowedError help copy (Google account / Play
  Services vs private-mode/cancel)

Multi-account signing (re-applies closed PR #2189):
- createNativeSignMessageCallback pins allowCredentials to the kernel's
  own authenticatorId when the caller supplies none, so a second
  peanut.me passkey on the device can't be substituted. Additive and
  native-only; requires a 2-account real-device smoke test.
Native-enablement fixes that were sitting uncommitted in the working tree.

- android/build.gradle: force Java 17 across all modules (capacitor-android
  compiles at 21 → "compiled with a more recent Java" Gradle failures). Java 17
  is the Capacitor 8 / AGP baseline.
- card-comparison.ts: drop the `'use server'` directive. Server Actions can't
  exist in a static export (output: 'export') and would break the native build.
  The file only does a public dolarapi.com fetch + arithmetic (no secrets), so
  it's safe to run client-side; its dep getCurrencyPrice is not a server action.
- AndroidManifest + res/xml/network_security_config.xml: permit cleartext ONLY
  to 10.0.2.2 / localhost so the emulator can reach a local backend. The network
  config takes precedence on API 24+, so production https traffic is unaffected.

Note: capacitor.build.gradle / capacitor.settings.gradle / capacitor-passkey.xml
are cap-sync-generated and intentionally not committed here.
Native-enablement fixes that were sitting uncommitted in the working tree.

- android/build.gradle: force Java 17 across all modules (capacitor-android
  compiles at 21 → "compiled with a more recent Java" Gradle failures). Java 17
  is the Capacitor 8 / AGP baseline.
- card-comparison.ts: drop the `'use server'` directive. Server Actions can't
  exist in a static export (output: 'export') and would break the native build.
  The file only does a public dolarapi.com fetch + arithmetic (no secrets), so
  it's safe to run client-side; its dep getCurrencyPrice is not a server action.
- AndroidManifest + res/xml/network_security_config.xml: permit cleartext ONLY
  to 10.0.2.2 / localhost so the emulator can reach a local backend. The network
  config takes precedence on API 24+, so production https traffic is unaffected.

Note: capacitor.build.gradle / capacitor.settings.gradle / capacitor-passkey.xml
are cap-sync-generated and intentionally not committed here.
Audit-driven security fixes (peanut-ui):

- MoreInfo: remove the `html` prop / dangerouslySetInnerHTML sink. The only
  caller (ValidatedInput) passed help-text that's always rendered safely now;
  the sink was a latent XSS vector if user-controlled text ever reached it.
- MermaidRenderer (dev): render caught errors via textContent instead of
  interpolating the (untrusted) diagram source into innerHTML.
- vercel.json: add baseline security headers (nosniff, X-Frame-Options
  SAMEORIGIN, Referrer-Policy, HSTS) and a clickjacking CSP
  (frame-ancestors 'self'; object-src 'none'; base-uri 'self'). A full
  script-src allow-list is deferred to a report-only rollout.
- PeanutDebug: add a NODE_ENV==='production' early return so the hardcoded
  harness private key + localStorage signer writes are dead-code-eliminated
  from prod bundles, not just skipped at runtime.
Audit-driven security fixes (peanut-ui):

- MoreInfo: remove the `html` prop / dangerouslySetInnerHTML sink. The only
  caller (ValidatedInput) passed help-text that's always rendered safely now;
  the sink was a latent XSS vector if user-controlled text ever reached it.
- MermaidRenderer (dev): render caught errors via textContent instead of
  interpolating the (untrusted) diagram source into innerHTML.
- vercel.json: add baseline security headers (nosniff, X-Frame-Options
  SAMEORIGIN, Referrer-Policy, HSTS) and a clickjacking CSP
  (frame-ancestors 'self'; object-src 'none'; base-uri 'self'). A full
  script-src allow-list is deferred to a report-only rollout.
- PeanutDebug: add a NODE_ENV==='production' early return so the hardcoded
  harness private key + localStorage signer writes are dead-code-eliminated
  from prod bundles, not just skipped at runtime.
Fill the edge-to-edge status-bar inset on the setup/onboarding flow with the
brand periwinkle (secondary-3) and pad by env(safe-area-inset-top) so the
feedback ribbon isn't jammed under the status icons on Android 15 (targetSdk 36,
edge-to-edge forced). No-op on web and pre-edge-to-edge Android.
Fill the edge-to-edge status-bar inset on the setup/onboarding flow with the
brand periwinkle (secondary-3) and pad by env(safe-area-inset-top) so the
feedback ribbon isn't jammed under the status icons on Android 15 (targetSdk 36,
edge-to-edge forced). No-op on web and pre-edge-to-edge Android.
Everything needed for Play review beyond the passkey fixes.

Reviewer/demo mode (entered via the `demo` invite code, native-only):
- utils/reviewer.ts gate; pre-filled balance + history (constants/demo-data.ts)
  overlaid in useWallet / useTransactionHistory
- KYC skipped (useCapabilities, useSumsubKycFlow)
- money-movement simulated at the execution primitives (useSpendBundle,
  useZeroDev) — no chain, no funds; safe on mainnet
- flag set on `demo` invite (services/invites.ts), cleared on logout

Build rot guard + versioning:
- native-build.js fails loudly on server-only routes not covered by the
  disable list (route handlers / force-dynamic)
- android versionCode derives from git commit count (env-overridable),
  versionName from package.json; never below 2

Release flow:
- scripts/native-release.sh (pnpm native:release) derives version and runs
  build -> cap sync -> signed bundleRelease
- docs/NATIVE-RELEASE.md runbook

Native plugins (low-risk polish):
- @capacitor/haptics (success buzz on send), @capacitor/keyboard (resize)

Setup status-bar safe zone:
- (setup)/layout.tsx fills the edge-to-edge status-bar inset with the brand
  periwinkle so the feedback ribbon isn't jammed under the status icons
Everything needed for Play review beyond the passkey fixes.

Reviewer/demo mode (entered via the `demo` invite code, native-only):
- utils/reviewer.ts gate; pre-filled balance + history (constants/demo-data.ts)
  overlaid in useWallet / useTransactionHistory
- KYC skipped (useCapabilities, useSumsubKycFlow)
- money-movement simulated at the execution primitives (useSpendBundle,
  useZeroDev) — no chain, no funds; safe on mainnet
- flag set on `demo` invite (services/invites.ts), cleared on logout

Build rot guard + versioning:
- native-build.js fails loudly on server-only routes not covered by the
  disable list (route handlers / force-dynamic)
- android versionCode derives from git commit count (env-overridable),
  versionName from package.json; never below 2

Release flow:
- scripts/native-release.sh (pnpm native:release) derives version and runs
  build -> cap sync -> signed bundleRelease
- docs/NATIVE-RELEASE.md runbook

Native plugins (low-risk polish):
- @capacitor/haptics (success buzz on send), @capacitor/keyboard (resize)

Setup status-bar safe zone:
- (setup)/layout.tsx fills the edge-to-edge status-bar inset with the brand
  periwinkle so the feedback ribbon isn't jammed under the status icons
- rename reviewer.ts→demo.ts; isReviewerMode→isDemoMode, etc.
- hard-stop in kernelClient: in demo mode sendUserOperation returns a fake
  hash and never submits — covers every flow (send/recover/session-key), not
  just the simulated primitives. Guarantees no real transaction in demo mode.
- trim verbose comments.
- rename reviewer.ts→demo.ts; isReviewerMode→isDemoMode, etc.
- hard-stop in kernelClient: in demo mode sendUserOperation returns a fake
  hash and never submits — covers every flow (send/recover/session-key), not
  just the simulated primitives. Guarantees no real transaction in demo mode.
- trim verbose comments.
…setup

The synthetic demo user resolves async via useUserQuery, so there is a brief
window where user is null. Three redirect paths bounced the session back to
setup before DEMO_USER settled. Guard each with isDemoMode():
- (mobile-ui) layout: skip the /setup redirect (loading gate holds instead)
- useAccountSetupRedirect: never redirect a demo session to /setup/finish
- LandingPageCapacitorGate: cold-start routes a demo session to /home
…setup

The synthetic demo user resolves async via useUserQuery, so there is a brief
window where user is null. Three redirect paths bounced the session back to
setup before DEMO_USER settled. Guard each with isDemoMode():
- (mobile-ui) layout: skip the /setup redirect (loading gate holds instead)
- useAccountSetupRedirect: never redirect a demo session to /setup/finish
- LandingPageCapacitorGate: cold-start routes a demo session to /home
Manage the release ops gaps:
- .github/workflows/android-release.yml: tag/dispatch-triggered job that decodes
  the upload keystore from CI secrets, builds a signed AAB via native:release
  (versionCode = run_number), and uploads to a Play track. Removes the
  one-laptop release dependency; gate via a `production` GitHub Environment.
- docs/NATIVE-RELEASE.md: rewritten for current reality — Node 22 / JDK 17
  toolchain, branch merge order, full local-dev setup (AirPlay :5001, engineering
  stubs, perk key, 10.0.2.2 cleartext, demo invite), keystore secret-manager +
  recovery + assetlinks coupling, Capgo prod channel / staged rollout / rollback,
  and the CI secrets inventory.
- android-release.yml: write the full production NEXT_PUBLIC_* set into
  .env.production.local (identical block to ios-release.yml); PEANUT_API_URL now
  sourced from a repo var. CAPACITOR_BUILD/IS_NATIVE_BUILD/GIT_COMMIT_HASH are
  auto-baked by next.config.native.js, so they're omitted.
- NATIVE-RELEASE.md §11: iOS pipeline now lives on feat/ci-ios; replace the stale
  'in progress' stub with a pointer to docs/NATIVE-RELEASE-IOS.md.
- .github/workflows/ios-release.yml: build the static export, cap sync ios, then
  import the Apple Distribution cert (apple-actions/import-codesign-certs), install
  the provisioning profile from a base64 secret, xcodebuild archive/exportArchive
  with manual signing (CURRENT_PROJECT_VERSION = run number), and upload via
  apple-actions/upload-testflight-build. IPA uploaded as an artifact.
- docs/NATIVE-RELEASE-IOS.md: pipeline overview, one-time signing-material setup,
  cert/profile rotation, manual App Store promotion, and the iOS secrets table.
- Mirrors android-release.yml's no-fastlane style; no Ruby toolchain.

Stacks on feat/native-review-readiness (needs scripts/native-build.js); independent
of the Android CI branch.
cap add ios + plugins via SPM, Info.plist usage strings (camera/photos/FaceID),
associated-domains entitlement (webcredentials/applinks peanut.me), bundle
me.peanut.wallet. Simulator build pending an SPM artifact-cache clear.
The inviter-username gates and the signup field fired their validation
lookup 750ms after any non-empty input, so typing a single character
produced a misleading "No Peanut member with that username" /
"must be at least 4 characters" error before the handle could possibly
be valid.

Add an optional `shouldValidate` predicate to ValidatedInput: when it
fails, the field is held in the neutral "changing" state (no lookup,
no error UI, Next disabled) instead of being marked invalid. Apply it
on both invite gates and the signup field, gated on
`>= USERNAME_MIN_LENGTH` (4, mirroring the backend username regex).
The inviter-username gates and the signup field fired their validation
lookup 750ms after any non-empty input, so typing a single character
produced a misleading "No Peanut member with that username" /
"must be at least 4 characters" error before the handle could possibly
be valid.

Add an optional `shouldValidate` predicate to ValidatedInput: when it
fails, the field is held in the neutral "changing" state (no lookup,
no error UI, Next disabled) instead of being marked invalid. Apply it
on both invite gates and the signup field, gated on
`>= USERNAME_MIN_LENGTH` (4, mirroring the backend username regex).
jjramirezn and others added 2 commits July 14, 2026 11:02
- deploy path now goes through ensureRootValidatorMigrated: the hand-rolled
  confirm only checked 'deployed', which a reverted migration inside a
  successful bundle satisfies while leaving the account on v0.0.2 — the
  approval signed next would be silently dead (the exact class this PR
  repairs); the gate verifies the swap against on-chain ground truth and
  rebuilds the client
- a flaky validNonceFrom read no longer aborts healthy grants — proceed on
  pre-floor-check behavior and flag it (floored stragglers are still caught
  by the sweep + /fix-card-signature)
- repairEnableNonce: confirm-poll survives flaky reads (tap and gas are
  already spent); a lagging 'undeployed' fresh read is retryable, never
  'contact support'; cap check runs against fresh state only
- preflight-repair analytics captured after success, not before the tap
…ce-repair

feat(card): repair enable-bricked kernels inline in the grant preflight

@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

🤖 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/utils/kernelNonceRepair.utils.ts`:
- Around line 58-73: Update repairEnableNonce around the pre-send readNonceState
call so transient read failures are caught and converted to the existing
retryable KernelNonceRepairPendingError, matching the post-send polling
behavior. Use deps.validNonceFrom as the documented fallback when re-reading
nonce state fails, or revise the documentation if the intended behavior is only
to retry as pending; ensure validNonceFrom is no longer unused.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 20fd4bb7-ff09-4826-94bf-a1a193d1e9af

📥 Commits

Reviewing files that changed from the base of the PR and between a91e1a7 and a9cb2d9.

📒 Files selected for processing (6)
  • src/constants/analytics.consts.ts
  • src/hooks/wallet/__tests__/useGrantSessionKey.test.ts
  • src/hooks/wallet/__tests__/useGrantSessionKey.test.tsx
  • src/hooks/wallet/useGrantSessionKey.ts
  • src/utils/__tests__/kernelNonceRepair.utils.test.ts
  • src/utils/kernelNonceRepair.utils.ts

Comment on lines +58 to +73
export interface NoncePublicClient {
getCode(args: { address: Address }): Promise<Hex | undefined>
readContract(args: { address: Address; abi: unknown; functionName: string }): Promise<unknown>
}

export interface RepairEnableNonceDeps {
publicClient: NoncePublicClient
accountAddress: Address
/** The floor observed by the caller's read — fallback when re-reads flake. */
validNonceFrom: number
/** Sends one root userOp through the user's kernel client (one passkey tap). */
sendUserOp: (call: { to: Hex; value: bigint; data: Hex }) => Promise<unknown>
/** On-chain confirmation attempts / spacing (overridable for tests). */
retries?: number
intervalMs?: number
}

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Unprotected first read contradicts the documented "fallback when re-reads flake" behavior for validNonceFrom.

deps.validNonceFrom is declared as a fallback for flaky re-reads but is never referenced in the function body. Consequently, the pre-send readNonceState call at Line 114 is the one unguarded read in this module — a transient RPC error here throws raw out of repairEnableNonce instead of degrading to a retryable KernelNonceRepairPendingError, unlike the (correctly try/catch-wrapped) post-send poll a few lines below.

🛡️ Proposed fix: tolerate a flaky pre-send read the same way the poll does
-    const fresh = await readNonceState(deps.publicClient, deps.accountAddress)
-    if (!fresh.deployed) throw new KernelNonceRepairPendingError()
+    let fresh: NonceState
+    try {
+        fresh = await readNonceState(deps.publicClient, deps.accountAddress)
+    } catch {
+        // Same rationale as the poll below: a flaky read must not turn a
+        // retryable situation into a hard failure.
+        throw new KernelNonceRepairPendingError()
+    }
+    if (!fresh.deployed) throw new KernelNonceRepairPendingError()

If deps.validNonceFrom was actually meant to be a data-based fallback (rather than just retry-as-pending), remove the now-inaccurate doc comment or wire it in explicitly.

Also applies to: 105-121

🤖 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/utils/kernelNonceRepair.utils.ts` around lines 58 - 73, Update
repairEnableNonce around the pre-send readNonceState call so transient read
failures are caught and converted to the existing retryable
KernelNonceRepairPendingError, matching the post-send polling behavior. Use
deps.validNonceFrom as the documented fallback when re-reading nonce state
fails, or revise the documentation if the intended behavior is only to retry as
pending; ensure validNonceFrom is no longer unused.

Bridge completed a legal entity migration on 2026-06-27; EUR/GBP vIBANs now
reflect the new entity name 'Bridge Building S.A.' (was 'Bridge Building
Sp. Z.o.o.'). Update the FE fallback account holder name to match and avoid
sender name-mismatch confusion at deposit time.
The old fix only touched the fallback constant, which the FE uses solely when
Bridge omits account_holder_name (GBP faster_payments). For EUR/SEPA Bridge
returns the field directly and can still send the stale 'Sp. Z.o.o.' name, so
the fallback never applied and the sender's Confirmation-of-Payee check kept
flagging a mismatch.

Route both display sites through resolveBridgeAccountHolderName, which maps the
stale/absent legacy name to the current entity ('Bridge Building S.A.') while
passing through any other value. Banking Circle accepts either name so payments
still settle — this just removes the name-mismatch warning.
the capability gate, provider-rejection surfaces, qr-pay and
ActivationCTAs now branch on rail.resolved - the BE-derived verdict -
instead of re-deriving fixable/terminal from status + reason codes +
action kinds. a local fallback mirroring the BE collapse rules covers
rails from older/cached responses and gets deleted with the BE's step-5
cleanup. gate priority order is unchanged; all 32 pre-existing gate
tests pass through the new path unmodified, plus 7 new tests pinning
that the verdict wins over contradictory legacy fields.

hygiene riding along: api.generated.ts regenerated from the PR-2
openapi snapshot (resolved on both capability routes, provide-email/
restart-identity kinds now present); dead tosStatus/tosAcceptedAt
types deleted (zero live reads); InitiateKycResponse.tosStatus dropped
(BE never emits it).
…view round)

/code-review (47-agent verified pass) confirmed 8 findings; all fixed:

fallback fidelity (the live path until the BE ships resolved):
- railVerdict keys on STATUS first - action kinds only refine within a
  status. a blocked rail carrying a stale sumsub/tos/wait action stays
  blocked (the old gate never offered those CTAs on blocked rails);
  requires-info keeps its wait/self-serve/document-punt tiers
- ladder parity restored: the blocked-family CTA is decided on the FIRST
  blocked rail only (a terminal block beats a sibling's restart-identity,
  which cannot unblock it and burns Sumsub attempts); fixable-with-action
  outranks pending, action-less fixable ranks below it (the old 7b
  placement); requires-info + contact-support no longer outranks a
  fixable sumsub sibling

one collapse, one copy source (was four diverging hand-rolled copies):
- railVerdict + railUserMessage are exported; provider-rejection.utils,
  qr-pay and ActivationCTAs consume them (deriveProviderRejection gains
  an optional nextActions param for the fallback's action-kind
  refinement - callers without it keep pure status semantics)

qr-pay: a provide-email verdict maps to the unavailable modal, never the
document-upload flow (that surface has no email form; the upload flow
dead-ends an email-blocked user)

tests: +6 fallback-fidelity pins on the gate, +11 new
provider-rejection.utils suite, +1 qr-pay provide-email pin
…rdict

refactor(kyc): render the backend ResolvedRail verdict (step 4)

@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: 6

🤖 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/app/`(mobile-ui)/qr-pay/page.tsx:
- Around line 211-239: Update the candidate selection in the QR KYC
state-resolution logic to prioritize candidates whose blocking selfHealKind is
restart-identity before checking verdict status. Ensure requires-info candidates
with that action resolve to PROVIDER_RESTART_IDENTITY, while preserving the
existing country_not_supported handling and other blocked/fixable outcomes.
- Around line 201-205: Update the QR payment rail candidate logic around
actionByKey and railsForProvider so rails are filtered to targetMantecaCountry
before computing verdicts. Apply the same country filter to the enabled-pay
check, and add targetMantecaCountry to the relevant memo dependency array.

In `@src/types/api.openapi.json`:
- Around line 559-580: Add the "restart-identity" enum value to every
next-action kind schema in the OpenAPI definition: both top-level and
resolved.nextAction schemas for /users/me and /users/capabilities. Update all
four corresponding kind enums while preserving their existing values.

In `@src/utils/capability-gate.ts`:
- Around line 157-201: Update railVerdict and the legacy-collapse path to derive
the verdict from operationStatus(rail, op) when an operation is requested, while
keeping rail.resolved authoritative. Ensure a rail-level enabled status with an
operation-level requires-info status produces the appropriate non-enabled
verdict and prevents incorrect needs-enrollment fallback. Add a regression test
covering top-level enabled with operations.deposit set to requires-info.
- Around line 214-220: Update railUserMessage and verdictMessage to use nullish
fallback for the authoritative resolved.blocking.userMessage value, preserving
an intentionally empty string and falling back only when the value is null or
undefined; keep the existing reason.userMessage and final null behavior
unchanged.

In `@src/utils/provider-rejection.utils.ts`:
- Around line 83-95: Update the MANTECA restart-identity detection around
isRestartIdentity to search all candidates for a blocked verdict whose blocking
selfHealKind is restart-identity or code is country_not_supported, rather than
relying only on the first blocked candidate. Preserve the existing state
selection and surfaced behavior, while ensuring a later eligible rail can select
the identity-repair path.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 866394fb-6ee1-465a-b33c-140b3c14a93f

📥 Commits

Reviewing files that changed from the base of the PR and between a9cb2d9 and 5d5d66c.

⛔ Files ignored due to path filters (1)
  • src/types/api.generated.ts is excluded by !**/*.generated.*
📒 Files selected for processing (12)
  • src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx
  • src/app/(mobile-ui)/qr-pay/page.tsx
  • src/app/actions/types/users.types.ts
  • src/components/Home/ActivationCTAs.tsx
  • src/components/Home/__tests__/ActivationCTAs.test.tsx
  • src/interfaces/interfaces.ts
  • src/types/api.openapi.json
  • src/types/capabilities.ts
  • src/utils/capability-gate.test.ts
  • src/utils/capability-gate.ts
  • src/utils/provider-rejection.utils.test.ts
  • src/utils/provider-rejection.utils.ts
💤 Files with no reviewable changes (2)
  • src/interfaces/interfaces.ts
  • src/app/actions/types/users.types.ts

Comment thread src/app/(mobile-ui)/qr-pay/page.tsx
Comment thread src/app/(mobile-ui)/qr-pay/page.tsx
Comment on lines +559 to +580
"kind": {
"anyOf": [
{
"type": "string",
"enum": ["sumsub"]
},
{
"type": "string",
"enum": ["accept-tos"]
},
{
"type": "string",
"enum": ["wait"]
},
{
"type": "string",
"enum": ["contact-support"]
},
{
"type": "string",
"enum": ["provide-email"]
}

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Add restart-identity to every next-action kind enum.

railVerdict explicitly consumes action.kind === 'restart-identity', but these four OpenAPI enums cannot represent it. Generated clients or validators may reject valid legacy Manteca repair actions. Add it to both top-level and resolved.nextAction schemas for /users/me and /users/capabilities.

Also applies to: 641-644, 1676-1697, 1758-1761

🤖 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/types/api.openapi.json` around lines 559 - 580, Add the
"restart-identity" enum value to every next-action kind schema in the OpenAPI
definition: both top-level and resolved.nextAction schemas for /users/me and
/users/capabilities. Update all four corresponding kind enums while preserving
their existing values.

Comment on lines +157 to +201
export function railVerdict(rail: RailCapability, byKey: Map<string, NextAction>): ResolvedRail {
if (rail.resolved) return rail.resolved

if (rail.status === 'enabled' || rail.status === 'pending') {
// an enabled rail's advisory hint rides as the verdict's nextAction
// (the BE emits at most one hint per rail)
const hint = railHintActions(rail, byKey)[0]
return { status: rail.status, ...(hint ? { nextAction: hint } : {}) }
}

const actions = railActions(rail, byKey)
const blocking = (selfHealable: boolean, selfHealKind?: NonNullable<ResolvedRail['blocking']>['selfHealKind']) => ({
code: rail.reason?.code ?? 'unknown',
userMessage: rail.reason?.userMessage ?? '',
selfHealable,
...(selfHealKind ? { selfHealKind } : {}),
...(rail.reason?.details ? { details: rail.reason.details } : {}),
})

if (rail.status === 'blocked') {
const email = actions.find((action) => action.kind === 'provide-email')
if (email) return { status: 'fixable', blocking: blocking(true, 'provide-email'), nextAction: email }
const restart = actions.find((action) => action.kind === 'restart-identity')
if (restart) return { status: 'blocked', blocking: blocking(true, 'restart-identity'), nextAction: restart }
return { status: 'blocked', blocking: blocking(false, 'contact-support') }
}

// requires-info: an actionable step outranks a wait marker on the same rail
const nextAction = actions.find((action) => action.kind !== 'wait') ?? actions[0]
if (nextAction?.kind === 'wait') return { status: 'pending', nextAction }
if (nextAction && nextAction.kind !== 'contact-support') {
const selfHealKind =
nextAction.kind === 'sumsub'
? ('document-resubmit' as const)
: nextAction.kind === 'provide-email'
? ('provide-email' as const)
: nextAction.kind === 'restart-identity'
? ('restart-identity' as const)
: undefined
return { status: 'fixable', blocking: blocking(true, selfHealKind), nextAction }
}
// actionless (or contact-support-only): the document-punt tier — the BE
// routes these through the resubmit endpoint without a capability action;
// NO nextAction on the verdict, which is what ranks it below pending
return { status: 'fixable', blocking: blocking(true, 'document-resubmit') }

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Derive legacy verdicts from the requested operation status.

For a cached rail with status: 'enabled' but operations.deposit: 'requires-info', railVerdict() returns enabled; ready then fails and the gate incorrectly falls through to needs-enrollment. Pass operationStatus(rail, op) into the legacy collapse while keeping rail.resolved authoritative.

Proposed fix
-export function railVerdict(rail: RailCapability, byKey: Map<string, NextAction>): ResolvedRail {
+export function railVerdict(
+    rail: RailCapability,
+    byKey: Map<string, NextAction>,
+    legacyStatus: RailCapability['status'] = rail.status
+): ResolvedRail {
     if (rail.resolved) return rail.resolved

-    if (rail.status === 'enabled' || rail.status === 'pending') {
+    if (legacyStatus === 'enabled' || legacyStatus === 'pending') {
         const hint = railHintActions(rail, byKey)[0]
-        return { status: rail.status, ...(hint ? { nextAction: hint } : {}) }
+        return { status: legacyStatus, ...(hint ? { nextAction: hint } : {}) }
     }

-    if (rail.status === 'blocked') {
+    if (legacyStatus === 'blocked') {
 const candidates = filterRailsByScope(state.rails, scope).map((rail) => ({
     rail,
-    verdict: railVerdict(rail, actionByKey),
+    verdict: railVerdict(rail, actionByKey, operationStatus(rail, op)),
 }))

Add a regression test for top-level enabled plus operation-level requires-info.

Also applies to: 282-286

🤖 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/utils/capability-gate.ts` around lines 157 - 201, Update railVerdict and
the legacy-collapse path to derive the verdict from operationStatus(rail, op)
when an operation is requested, while keeping rail.resolved authoritative.
Ensure a rail-level enabled status with an operation-level requires-info status
produces the appropriate non-enabled verdict and prevents incorrect
needs-enrollment fallback. Add a regression test covering top-level enabled with
operations.deposit set to requires-info.

Comment thread src/utils/capability-gate.ts
Comment thread src/utils/provider-rejection.utils.ts
kushagrasarathe and others added 2 commits July 16, 2026 15:10
# Conflicts:
#	src/components/TransactionDetails/provider-rows/BridgeDepositInstructions.tsx
chore: backmerge main into dev (prod hotfixes)
@cursor

cursor Bot commented Jul 16, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

Two interacting defects made 'Select new method' unusable when saved
bank accounts exist:

1. Crypto click navigated to /withdraw/crypto before an amount was set.
   That page's no-amount guard bounces straight back to /withdraw, and
   since da943d6 (mobile-release, merged to dev via #2393) its unmount
   cleanup calls resetWithdrawFlow(), wiping selectedMethod and
   showAllWithdrawMethods — landing the user on the saved-accounts list
   instead of the amount input. Set the method in context only (same
   contract as bank-country selection); the withdraw page navigates to
   /withdraw/crypto after Continue, with the amount set.

2. The default-view effect re-ran on every user refetch (window focus,
   4s pending-rail poll — each dispatches a fresh user object into
   redux) and unconditionally forced the saved-accounts view, yanking
   an open country list back after a few seconds. Latch the default
   view once per mount; refetches now only sync the accounts list.
… add-flow default

code-review round: the withdraw crypto click now goes through
handleMethodSelected (one code path for 'a withdraw method was selected'
— analytics payload + method shape can't drift), the add flow skips its
localstorage re-read + state churn once the default view is latched, and
the regression tests run against the real WithdrawFlowContextProvider
instead of a hand-rolled context copy that could drift from it.
…od-bounce

fix(withdraw): stop method selection bouncing back to saved accounts
@jjramirezn jjramirezn changed the title Prod Release Sprint 151 — Rhino chain expansion · Solana/Tron/Base withdrawals · profile fixes (2026-07-13) Prod Release Sprint 151 — KYC verdict rendering · Solana/Tron/Base withdrawals · profile fixes (2026-07-16) Jul 16, 2026
…o-dev-20260716

chore: back-merge main → dev (manicero badge, pre SP-151 release)
@Hugo0
Hugo0 merged commit 5ab943f into main Jul 16, 2026
24 of 27 checks passed
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.

5 participants