Conversation
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).
…user) + formatted demo history
- 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
There was a problem hiding this comment.
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
📒 Files selected for processing (6)
src/constants/analytics.consts.tssrc/hooks/wallet/__tests__/useGrantSessionKey.test.tssrc/hooks/wallet/__tests__/useGrantSessionKey.test.tsxsrc/hooks/wallet/useGrantSessionKey.tssrc/utils/__tests__/kernelNonceRepair.utils.test.tssrc/utils/kernelNonceRepair.utils.ts
| 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 | ||
| } |
There was a problem hiding this comment.
🩺 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)
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (1)
src/types/api.generated.tsis excluded by!**/*.generated.*
📒 Files selected for processing (12)
src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsxsrc/app/(mobile-ui)/qr-pay/page.tsxsrc/app/actions/types/users.types.tssrc/components/Home/ActivationCTAs.tsxsrc/components/Home/__tests__/ActivationCTAs.test.tsxsrc/interfaces/interfaces.tssrc/types/api.openapi.jsonsrc/types/capabilities.tssrc/utils/capability-gate.test.tssrc/utils/capability-gate.tssrc/utils/provider-rejection.utils.test.tssrc/utils/provider-rejection.utils.ts
💤 Files with no reviewable changes (2)
- src/interfaces/interfaces.ts
- src/app/actions/types/users.types.ts
| "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"] | ||
| } |
There was a problem hiding this comment.
🗄️ 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.
| 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') } |
There was a problem hiding this comment.
🎯 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.
fix(deposit): update Bridge SEPA account holder name + deposit details bugs
# Conflicts: # src/components/TransactionDetails/provider-rows/BridgeDepositInstructions.tsx
chore: backmerge main into dev (prod hotfixes)
|
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
…o-dev-20260716 chore: back-merge main → dev (manicero badge, pre SP-151 release)
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)
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.Profile-edit fixes (#2388)
Verified users can save email without a name; backend save errors surface instead of false-success; typed update payload.
On prod, a chain shows only if its
chain-rollout-*flag is ON. All 13chain-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
Improvements
Bug Fixes