Skip to content

feat(card): resolve Immersve spendable balance from on-chain allowance - #33743

Merged
Brunonascdev merged 78 commits into
mainfrom
feat/mm-card-immersve-balance
Jul 24, 2026
Merged

feat(card): resolve Immersve spendable balance from on-chain allowance#33743
Brunonascdev merged 78 commits into
mainfrom
feat/mm-card-immersve-balance

Conversation

@Brunonascdev

@Brunonascdev Brunonascdev commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Description

Immersve funding assets previously left spendableBalance / spendingCap empty 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:

  1. Reads ERC-20 balanceOf + allowance on-chain (spender from the Immersve feature-flag config) and sets spendable balance to min(balance, allowance).
  2. Resumes returning Immersve users onto their existing card program + funding source via getResumeCardInfo, instead of always creating/resolving against the default funding channel.
  3. Adds Arbitrum Sepolia USDC funding token mapping for Immersve networks that use that chain.

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

Feature: Immersve Card spendable balance

  Scenario: Card Home shows on-chain spendable balance for an Immersve card
    Given the user has completed Immersve onboarding with an active card
    And the card feature flag includes immersve.spenderAddress
    And the funding wallet has USDC balance and a non-zero allowance to the spender

    When the user opens Card Home
    Then the funding asset shows spendable balance as min(wallet balance, allowance)

  Scenario: Returning Immersve user resumes the existing card program
    Given the user previously completed Immersve onboarding on a non-default program
    And the Immersve account already has a card with funding sources

    When the user signs in again via Immersve resume onboarding
    Then the app selects the existing cardProgramId
    And the app reuses the existing funding source id without creating a new one

Screenshots/Recordings

Before

N/A

After

N/A

Pre-merge author checklist

Performance checks (if applicable)

  • I've tested on Android
    • Ideally on a mid-range device; emulator is acceptable
  • I've tested with a power user scenario
    • Use these power-user SRPs to import wallets with many accounts and tokens
  • I've instrumented key operations with Sentry traces for production performance metrics

For performance guidelines and tooling, see the Performance Guide.

Pre-merge reviewer checklist

  • I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed).
  • I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots.

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 balanceOf and allowance on the funding network (spender from immersve.spenderAddress in the card feature flag) and setting spendableBalance to min(balance, allowance) with spendingCap from 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.

Brunonascdev and others added 30 commits July 13, 2026 17:04
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>
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>
…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>
…ersve-kyc-webview

Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	app/core/NavigationService/types.ts
Resolve Immersve Card conflicts in favor of pending-verification /
contact-patch UX, and fix SignUp phone region nav typing via
navigateWithDetails.
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.
@Brunonascdev Brunonascdev self-assigned this Jul 23, 2026
@github-actions

Copy link
Copy Markdown
Contributor

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.

@metamask-ci metamask-ci Bot added the team-card Card Team label Jul 23, 2026
@Brunonascdev
Brunonascdev marked this pull request as ready for review July 23, 2026 22:33
@Brunonascdev
Brunonascdev requested a review from a team as a code owner July 23, 2026 22:33
Comment thread app/core/Engine/controllers/card-controller/providers/ImmersveProvider.ts Outdated
Comment thread app/components/UI/Card/components/Onboarding/ImmersveFundingApproval.tsx Outdated
@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

🧪 Flaky unit test detection

Run history flaky detection

View recent run history

Historical 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:

File 7d 15d 30d
app/components/UI/Card/components/Onboarding/SignUp.test.tsx 0/163 0/273 0/288
app/core/Engine/controllers/card-controller/CardController.test.ts 0/163 0/273 0/288

AI-detected flaky patterns

app/components/UI/Card/components/Onboarding/SignUp.test.tsx

  • J3 — Missing jest.clearAllMocks()/resetAllMocks() between tests (high)
    • Multiple top-level mock fns (mockImmersveSignIn, mockGetFundingSources, mockCreateFundingSource, mockGetSpendingPrerequisites, etc.) have .mockResolvedValue/.mockReturnValue and .mockRejectedValue set inside individual it() blocks (e.g. the three Immersve flow tests and error-handling tests). beforeEach only calls jest.clearAllMocks() (which preserves implementations). Without jest.resetAllMocks() in afterEach, mock state leaks across tests and can produce order-dependent failures. The PR change itself added another mockResolvedValue in beforeEach, but does not address the root isolation gap.
    • Suggested fix in app/components/UI/Card/components/Onboarding/SignUp.test.tsx:285:
      -beforeEach(() => {
      -    jest.clearAllMocks();
      -    mockUseCardPostAuthRedirect.mockReturnValue(undefined);
      -    mockNavigate = jest.fn();
      -    mockGoBack = jest.fn();
      -    // ...
      -    const cardFlagSelectors = jest.requireMock(
      -      '../../../../../selectors/featureFlagController/card',
      -    );
      -    const actualCardFlagSelectors = jest.requireActual(
      -      '../../../../../selectors/featureFlagController/card',
      -    );
      -    (
      -      cardFlagSelectors.selectImmersveOnboardingEnabled as jest.Mock
      -    // ...
      -    mockGetResumeCardInfo.mockResolvedValue(null);
      -    store = createTestStore();
      -  });
      +beforeEach(() => {
      +    jest.clearAllMocks();
      +    mockUseCardPostAuthRedirect.mockReturnValue(undefined);
      +    mockNavigate = jest.fn();
      +    mockGoBack = jest.fn();
      +    // ...
      +    const cardFlagSelectors = jest.requireMock(
      +      '../../../../../selectors/featureFlagController/card',
      +    );
      +    const actualCardFlagSelectors = jest.requireActual(
      +      '../../../../../selectors/featureFlagController/card',
      +    );
      +    (
      +      cardFlagSelectors.selectImmersveOnboardingEnabled as jest.Mock
      +    // ...
      +    mockGetResumeCardInfo.mockResolvedValue(null);
      +    store = createTestStore();
      +  });
      +
      +afterEach(() => {
      +    jest.resetAllMocks();
      +  });
  • J5 — Incomplete mock store state (medium)
    • createTestStore defaults to an empty object and only populates a narrow slice of backgroundState (GeolocationController + partial CardController). Many tests rely on this default store (or pass only geoLocation/onboarding). The component, its hooks (useEmailVerificationSend, useDebouncedValue, useRegions, useImmersve*), and selectors access additional Redux slices (metamask, settings, full engine.backgroundState, feature flags, etc.). Missing slices can produce intermittent selector errors or undefined behavior under varying test order or React render timing.
    • Suggested fix in app/components/UI/Card/components/Onboarding/SignUp.test.tsx:60:
      -const createTestStore = (initialState: Record<string, unknown> = {}) => {
      -  const { geoLocation, selectedCardProgramId, ...cardState } = initialState;
      -  const engineState = {
      -    backgroundState: {
      -      GeolocationController:
      -        typeof geoLocation === 'string' ? { location: geoLocation } : undefined,
      -      CardController: {
      -    // ...
      -
      -describe('SignUp Component', () => {
      -  let store: ReturnType<typeof createTestStore>;
      -  // ...
      -  beforeEach(() => {
      -    // ...
      -    mockGetResumeCardInfo.mockResolvedValue(null);
      -    store = createTestStore();
      -  });
      +const createTestStore = (initialState: Record<string, unknown> = {}) => {
      +  const { geoLocation, selectedCardProgramId, ...cardState } = initialState;
      +  const engineState = {
      +    backgroundState: {
      +      GeolocationController:
      +        typeof geoLocation === 'string' ? { location: geoLocation } : undefined,
      +      CardController: {
      +        // ... full mock as in other Card tests
      +      },
      +      // ... other controllers
      +    },
      +  };
      +  return configureStore({
      +    reducer: rootReducer, // or the actual reducers used by the app
      +    preloadedState: {
      +      engine: engineState,
      +      metamask: mockMetamaskState,
      +      settings: settingsInitialState,
      +      onboarding: { ...defaultOnboardingState, ...cardState },
      +      ...initialState,
      +    },
      +  });
      +};

app/core/Engine/controllers/card-controller/CardController.test.ts

  • J6 — Arbitrary setTimeout/sleep used as a synchronization barrier (high)
    • This helper is used throughout the test file (e.g. to poll on mock.calls.length before proceeding with auth/logout flows) as a real-timer synchronization barrier. It relies on wall-clock Date.now() for the deadline and setImmediate (real macrotask) in a busy-wait loop. Per the loaded skill (J6), arbitrary timer-based barriers are a top source of intermittent CI failures under load; the function comment even acknowledges timing sensitivity around microtask vs macrotask draining. No fake timers are used in this file. Historical data showed zero failures but the pattern is present and risky.
    • Suggested fix in app/core/Engine/controllers/card-controller/CardController.test.ts:
      -async function waitForCondition(
      -  predicate: () => boolean,
      -  timeoutMs = 1000,
      -): Promise<void> {
      -  const deadline = Date.now() + timeoutMs;
      -  while (!predicate()) {
      -    if (Date.now() > deadline) {
      -      throw new Error('waitForCondition predicate never became true');
      -    }
      -    await new Promise<void>((resolve) => {
      -      setImmediate(resolve);
      -    });
      -  }
      -}
      +async function waitForCondition(
      +  predicate: () => boolean,
      +  maxIterations = 100,
      +): Promise<void> {
      +  for (let i = 0; i < maxIterations; ++i) {
      +    if (predicate()) {
      +      return;
      +    }
      +    await new Promise<void>((resolve) => {
      +      setImmediate(resolve);
      +    });
      +  }
      +  throw new Error('waitForCondition predicate never became true');
      +}

This check is informational only and does not block merging.

@github-actions github-actions Bot added the risk:high AI analysis: high risk label Jul 23, 2026
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.
@github-actions github-actions Bot added risk:medium AI analysis: medium risk and removed risk:high AI analysis: high risk labels Jul 23, 2026
SignUp now resumes via useImmersveResumeOnboarding, which calls
getResumeCardInfo after SIWE; without the mock the flow aborted early.
@github-actions

Copy link
Copy Markdown
Contributor

🔍 Smart E2E Test Selection

  • Selected E2E tags: SmokeMoney, SmokeSwap, SmokeConfirmations
  • Selected Performance tags: None (no tests recommended)
  • Risk Level: medium
  • AI Confidence: 85%
click to see 🤖 AI reasoning details

E2E Test Selection:
All changes in this PR are focused on the MetaMask Card (Immersve) functionality:

  1. CardController.ts: Added getResumeCardInfo() method that fetches card program ID and funding source IDs from the Immersve provider.
  2. ImmersveProvider.ts: Major additions including:
    • getResumeCardInfo() method to retrieve card details for resume flows
    • resolveOnChainSpendableBalance() to read actual ERC-20 balance and allowance on-chain (replacing the previous empty string approach)
    • createFundingNetworkProvider() for connecting to Base Mainnet, Base Sepolia, and Arbitrum Sepolia networks
    • Arbitrum Sepolia network support
  3. onChainAllowance.ts: New utility file for reading ERC-20 allowance and balance on-chain using ethers.js
  4. immersveFunding.ts: Added Arbitrum Sepolia network mapping
  5. constants.ts: Added Arbitrum Sepolia RPC URL and USDC token address
  6. useImmersveResumeOnboarding.ts: Updated to use getResumeCardInfo() to pre-populate card program ID and funding source ID during resume onboarding
  7. featureFlagController/card/index.ts: Added spenderAddress field to Immersve program config (used for on-chain allowance checks)

The primary impact is on the Card (Immersve) onboarding/resume flow and funding source balance display. The SmokeMoney tag directly covers Card Home and Add Funds flows. Per the tag description, when selecting SmokeMoney for Card Add Funds flows that execute swaps, also select SmokeSwap and SmokeConfirmations. The changes to on-chain balance resolution and resume onboarding could affect the Add Funds flow which may involve swap paths.

Performance Test Selection:
The changes are focused on Card/Immersve controller logic, on-chain balance reading, and resume onboarding flows. None of these changes affect performance-sensitive paths like app launch, login, account list rendering, swap quote fetching, or asset loading. The on-chain balance reading is a new async operation but it's within the Card controller scope and not measured by any performance test scenario.

View GitHub Actions results

@cursor cursor 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.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

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

@sonarqubecloud

Copy link
Copy Markdown

@Brunonascdev
Brunonascdev added this pull request to the merge queue Jul 24, 2026
Merged via the queue into main with commit 093b66b Jul 24, 2026
175 of 176 checks passed
@Brunonascdev
Brunonascdev deleted the feat/mm-card-immersve-balance branch July 24, 2026 15:22
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 24, 2026
@metamask-ci metamask-ci Bot added the release-8.6.0 Issue or pull request that will be included in release 8.6.0 label Jul 24, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

release-8.6.0 Issue or pull request that will be included in release 8.6.0 risk:medium AI analysis: medium risk size-L team-card Card Team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants