feat(card): resolve Immersve spendable balance from on-chain allowance - #33743
Conversation
Adds Immersve as a second ICardProvider behind the immersveOnboardingEnabled flag (inert until enabled): SIWE auth lifecycle (login-init autoSignup -> login-complete -> JWT + cardholderAccountId; refresh via /auth/token; JWT exp read from the token), env->URL mapping (exp->dev, rc/prod->prod), country->provider routing via setSelectedCountry, account-bound sessions, onboarding pass-throughs (funding-source, contact-details, spending-prerequisites, create-card), and the useImmersveSiweAuth hook. Card-read methods stubbed pending endpoints. Unit tests included. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…card-immersve-provider-foundation
Adds the shared onboarding state-machine engine consumed by new-user onboarding and the auth-screen resume: deriveNextImmersveAction (pure) maps spending-prerequisites actionTypes to the next step (contact/kyc/expected_spend/funding/pending/active), and useImmersveSpendingPrerequisites fetches + derives + polls while pending. Adds CardController pass-throughs (createFundingSource, getSpendingPrerequisites, createCard, patchContactDetails) with the standard withAuthRetry wrapper. Inert behind the immersveOnboardingEnabled flag. Tests included. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds the on-chain funding step for Immersve onboarding: encodeSmartContractWrite generically encodes the smart_contract_write instruction returned by spending-prerequisites (abi-driven, so it survives approve-vs-deposit variance) and immersveNetworkToCaipChainId maps the program network to a CAIP chain id. useImmersveFunding wires createFundingSource / createCard controller pass-throughs and executeFunding, which ensures the Base network exists and submits the encoded approve via awaitTransactionConfirmed (reusing the useCardDelegation primitives, no delegation challenge/signature/callback). Inert behind the immersveOnboardingEnabled flag. Tests included. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Immersve cards are tied to one network+asset and the fundingChannelId is not static, so it is now resolved at runtime: createFundingSource lists funding channels (GET /api/accounts/:id/funding-channels), matches item.fundingTypeName against the new immersve.fundingType flag, and uses the resolved id. Constant program config is hardcoded in ImmersveProvider (kycType, kycHiddenSteps=['region','contact-channels'], spendableCurrency='USD', spendableAmount=999999999) and removed from the flag. Country routing moves to a top-level immersveCountries flag (out of the immersve block); CardController reads it and SignUp wires setSelectedCountry on selection + treats Immersve countries as supported (no waitlist), all gated by immersveOnboardingEnabled. Tests updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ils) SignUp immersve mode: hide password, email-only (no validation), account picker below email binding SIWE to the selected account. Next runs SIWE + createFundingSource (failure = already a cardholder -> block re-signup), persists immersveFundingSourceId, then routes to the phone step. SetPhoneNumber gains an immersve mode (no verification) that submits patchContactDetails and hands off to the KYC pending step (interim; branch 6 replaces the terminus). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add clientApplicationId to ImmersveProgramConfig and read it via a provider getter that prefers the flag value, falling back to the env config. Used in login-init and token refresh so the id is remotely configurable per env. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Funding channels are defined on the partner account (fixed per program/env), not the individual cardholder. Add partnerAccountId to the immersve feature flag and have #resolveFundingChannelId list channels under it. The /api/funding-sources POST body still creates the source for the cardholder. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Immersve onboarding flow collects a phone number but does not send a verification code (contact details are PATCHed directly, no OTP). Hide the "We'll send you a confirmation code there." description and the "…receiving SMS to verify…" legal copy below the Next button when isImmersve. The Baanx flow is unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replaces SetPhoneNumber's interim KYC_PENDING terminus with the Immersve
KYC onboarding slice (direct integration, no backend/webhooks):
- ImmersveKYCProcessing: progress orchestrator (clones VerifyingVeriffKYC).
Drives useImmersveSpendingPrerequisites off the persisted
immersveFundingSourceId; opens the hosted KYC webview on `kyc`, polls
while `pending` (30s cutoff -> KYC_PENDING), routes `rejected`
(blocked/kyc_check_failed) -> KYC_FAILED, and parks approved
(funding/active) on an interim terminus (branch 6b wires SpendingLimit).
- ImmersveKYCModal: transparent-modal WebView (clones WaitlistFormModal)
with a single status state (loading/loaded/error). Completion detected via
navigation to the kycRedirectUrl sentinel;
mediaPlaybackRequiresUserGesture={false} for Android Sumsub/Onfido camera.
- Driver: widen CardPrerequisiteStatus with blocked/kyc_check_failed and add
a `rejected` ImmersveNextAction (checked before the pending fallback so a
blocked account doesn't poll forever).
- Route + nav-type wiring (KYC_PROCESSING onboarding screen, IMMERSVE_KYC
modal), CardScreens.KYC_PROCESSING, en.json strings, tests.
Inert behind the off immersveOnboardingEnabled flag.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rror useImmersveSpendingPrerequisites.refresh computed the error message inside a functional setState updater (runs in React's render phase, so a throw there surfaces as an uncatchable render error) and then re-threw. On an expected spending-prerequisites API error this crashed ImmersveKYCProcessing with "Render Error". - Compute getCardProviderErrorMessage(e) eagerly in the catch scope; keep the updater trivial. Resolve to null instead of re-throwing so consumers can't crash. Errors are exposed via the hook's `error` field. - ImmersveKYCProcessing surfaces `error` instead of an eternal spinner. - Add the missing error-path coverage (hook + screen). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ic lookup)
The funding channel is a stable per-program/env value, so listing channels and
matching fundingTypeName at runtime was unnecessary indirection.
- Flag: rename immersve.fundingType -> fundingChannelId; keep partnerAccountId
(dormant, reserved for future URL endpoints).
- ImmersveProvider: delete #resolveFundingChannelId + the channel response
interfaces; createFundingSource reads requireProgramValue('fundingChannelId')
straight into the /api/funding-sources POST body (no GET, no match). Narrow
the requireProgramValue union to cardProgramId | fundingChannelId.
- Tests updated to assert the POST uses the flag id and no funding-channels GET
is made; unconfigured-error case swapped to fundingChannelId.
Reverses the dynamic-resolution parts of the earlier funding-channel work.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A 403 (e.g. FUNDING_SOURCE_EXISTS on createFundingSource) was mapped to an auth-token error, forcing token refresh + session-expiry and redirecting the user to the authentication screen. Only 401 now signals a revoked access token; 403 maps to a new Forbidden code carrying the provider machine code, which is surfaced on the UI for triage. Applies to both Baanx and Immersve. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
SafeAreaView computed a zero top inset in this modal presentation, clipping the back button under the status bar / notch. Match the ForgotPasswordModal standard: explicit useSafeAreaInsets() padding on a plain View plus HeaderStandard for the back button. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…oring SignUp's Immersve continue handler blindly called createFundingSource, which 403s FUNDING_SOURCE_EXISTS for anyone who already onboarded. Now: SIWE → getFundingSources (new GET /api/accounts/:id/funding-sources), create only if none → read spending-prerequisites → route where the user stopped: contact → phone, kyc/pending → KYC_PROCESSING, funding → SpendingLimit, rejected → KYC_FAILED, all done → "you already have an account" toast + Card Home. A shared useImmersveOnboardingRouter centralizes the action→destination mapping (also used by KYC_PROCESSING for its terminal transitions, replacing the interim KYC_PENDING placeholder). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two UX fixes to the Immersve KYC processing step: - Entry: forward the already-derived kycUrl from SignUp so KYC_PROCESSING opens the webview immediately instead of re-polling and flashing the "awaiting approval" spinner first. - Close: the KYC webview is a transparentModal that keeps the processing screen mounted without blurring it, so useFocusEffect never detected the close, leaving a blank screen with no polling. The modal now invokes a callback (mirroring RegionSelectorModal's registry) so the screen re-polls on close and, when KYC is still outstanding, prompts the user to reopen verification with a freshly polled url instead of stranding them. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…card-immersve-kyc-webview
…l style Fixes ImmersveFundingApproval swapping between the confirm button and a full-screen spinner on every 5s background poll: the poll gate had been widened to also poll while 'funding', but that's the state the user sits in before tapping approve too. Revert the hook widening; the settlement wait after submit is now a screen-local poll instead. The screen also now mirrors SpendingLimit.tsx's onboarding layout — a read-only settings card (account, USDC on Base) with a single persistent confirm button whose own isLoading/isDisabled reflect busy state, never swapping the whole layout. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… + icon - ImmersveFundingApproval now mirrors SpendingLimit.tsx's own onboarding layout (SafeAreaView/HeaderStandard/KeyboardAwareScrollView, header copy classes, ActivityIndicator loading state) instead of OnboardingStep's slightly different spacing, for visual parity. - useEnsureCardNetworkExists only added networks from the production-curated PopularList, so approving funding against the Base Sepolia sandbox failed with "Network not found in PopularList for chain ID eip155:84532". Added a small Card-scoped test-network fallback (Base Sepolia) it falls back to. - The token icon used the live write.contractAddress, which is Base Sepolia's test USDC and isn't indexed by the icon CDN. Icon lookup now always uses the real Base-mainnet USDC address (display only — the approve tx still uses the real API-provided contract address). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ovider-foundation
…immersve-prerequisites-driver
…-immersve-funding
…ersve-kyc-webview Co-authored-by: Cursor <cursoragent@cursor.com> # Conflicts: # app/core/NavigationService/types.ts
…ovider-foundation
…immersve-prerequisites-driver
…-immersve-funding
Resolve Immersve Card conflicts in favor of pending-verification / contact-patch UX, and fix SignUp phone region nav typing via navigateWithDetails.
…mm-card-immersve-balance
Avoid LIVENESS_MISMATCH when cardFeature points at a different network than the cardholder's existing card by preferring that card's cardProgramId and funding source.
…mm-card-immersve-balance
|
CLA Signature Action: All authors have signed the CLA. You may need to manually re-run the blocking PR check if it doesn't pass in a few minutes. |
🧪 Flaky unit test detectionRun history flaky detectionHistorical failure rate is a hint, not proof — review each suggestion in context. See the flaky-test-detection skill for the full pattern reference and manual audit workflow. Failures / runs sampled per window:
AI-detected flaky patterns
|
Restore ImmersveFundingApproval to main so funding always uses BAANX_MAX_LIMIT, matching polish PR #33655. Also restore a dropped provisioning comment and align the ImmersveProvider mock typing.
Map funding networks to the correct chain id/RPC so Arbitrum Sepolia allowance reads no longer hit Base. Also drop a duplicate selectedCardProgramId key that broke tsc.
…mm-card-immersve-balance
SignUp now resumes via useImmersveResumeOnboarding, which calls getResumeCardInfo after SIWE; without the mock the flow aborted early.
🔍 Smart E2E Test Selection
click to see 🤖 AI reasoning detailsE2E Test Selection:
The primary impact is on the Card (Immersve) onboarding/resume flow and funding source balance display. The Performance Test Selection: |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit a422e92. Configure here.
|




Description
Immersve funding assets previously left
spendableBalance/spendingCapempty because the Immersve API reports a funding-source balance of 0. Card Home therefore could not show how much a user can actually spend.This PR:
balanceOf+allowanceon-chain (spender from the Immersve feature-flag config) and sets spendable balance tomin(balance, allowance).getResumeCardInfo, instead of always creating/resolving against the default funding channel.Immersve Funding Approval continues to always submit ERC-20 approve with
BAANX_MAX_LIMIT(no spending-limit selector), matching polish PR #33655.Changelog
CHANGELOG entry: Added Immersve Card spendable balance based on on-chain wallet balance and allowance
Related issues
Refs: null
Manual testing steps
Screenshots/Recordings
Before
N/A
After
N/A
Pre-merge author checklist
Performance checks (if applicable)
trace()for usage andaddTokenfor an exampleFor performance guidelines and tooling, see the Performance Guide.
Pre-merge reviewer checklist
Note
Medium Risk
Touches Immersve funding resolution and live RPC reads for balances; misconfigured spender or RPC could show wrong or empty spendable amounts, but changes are gated by feature flag and fail soft on errors.
Overview
Immersve Card Home can now show how much the user can actually spend by reading ERC-20
balanceOfandallowanceon the funding network (spender fromimmersve.spenderAddressin the card feature flag) and settingspendableBalanceto min(balance, allowance) withspendingCapfrom allowance. On-chain reads are skipped when the spender is unset; RPC failures leave balances empty.Returning Immersve users resume via new
getResumeCardInfo(card program + funding source IDs from the existing card). Resume onboarding applies the stored program and reuses the existing funding source instead of always resolving against the default funding channel.Arbitrum Sepolia is added as an Immersve funding network (RPC, USDC address, CAIP mapping). Tests cover allowance math, resume routing, and provider behavior.
Reviewed by Cursor Bugbot for commit a422e92. Bugbot is set up for automated code reviews on this repo. Configure here.