From 3bf2cf93009b2a7deabf06a42864e7b1d24f5315 Mon Sep 17 00:00:00 2001 From: Curtis David Date: Mon, 3 Nov 2025 19:41:15 -0500 Subject: [PATCH 1/6] test: add assertions to claim activity view (#22075) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR adds assertions to claim flow ## **Changelog** CHANGELOG entry: ## **Related issues** Fixes: ## **Manual testing steps** ```gherkin Feature: my feature name Scenario: user [verb for user action] Given [describe expected initial app state] When user [verb for user action] Then [describe expected outcome] ``` ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **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] > Adds mocks to surface REDEEM activity after claiming and updates e2e to verify activity details, amounts, and balance refresh. > > - **Mocks (Polymarket)**: > - Add `POLYMARKET_CLAIMED_POSITIONS_ACTIVITY_RESPONSE` and new helper `POLYMARKET_ADD_CLAIMED_POSITIONS_TO_ACTIVITY_MOCKS` to prepend `REDEEM` transactions to `activity`. > - Wire new activity mock into claim flow; keep resolved positions empty post-claim and refresh USDC balance. > - **E2E Tests**: > - Update `predict-claim-positions.spec.ts` to verify claim button disappears, navigate to Activity → Predictions, open each claimed item, and assert formatted amounts; verify wallet balance on iOS. > - Adjust synchronization handling and mock sequencing in the claim flow. > - **Page Objects**: > - Add `amountDisplay` getter to `predictionsActivityDetails` for asserting displayed amounts. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit d77f5ec11493e010c4be36d58b4fb47345bbd70c. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- .../polymarket-activity-response.ts | 53 +++++++++++++++++ .../polymarket/polymarket-mocks.ts | 59 ++++++++++++++++++- .../predictionsActivityDetails.ts | 6 ++ .../predict/predict-claim-positions.spec.ts | 54 ++++++++++++----- 4 files changed, 157 insertions(+), 15 deletions(-) diff --git a/e2e/api-mocking/mock-responses/polymarket/polymarket-activity-response.ts b/e2e/api-mocking/mock-responses/polymarket/polymarket-activity-response.ts index eff7b8fc4896..1a8ff9a52b89 100644 --- a/e2e/api-mocking/mock-responses/polymarket/polymarket-activity-response.ts +++ b/e2e/api-mocking/mock-responses/polymarket/polymarket-activity-response.ts @@ -345,3 +345,56 @@ export const POLYMARKET_ACTIVITY_RESPONSE = [ profileImageOptimized: '', }, ]; + +export const POLYMARKET_CLAIMED_POSITIONS_ACTIVITY_RESPONSE = [ + { + proxyWallet: PROXY_WALLET_ADDRESS, + timestamp: 1762189059, + conditionId: + '0xbf97a1420a810787dc6ffa2810f1d1d91977267e542b8685ecd21e622567a46c', + type: 'REDEEM', + size: 15, + usdcSize: 15, + transactionHash: + '0x205120808686f9164ca306404e6221cd0fda524002587a410bebdff3f7d3858c', + price: 0, + asset: '', + side: '', + outcomeIndex: 999, + title: 'Blue Jays vs. Mariners', + slug: 'mlb-tor-sea-2025-10-17', + icon: 'https://polymarket-upload.s3.us-east-2.amazonaws.com/Repetitive-markets/MLB.jpg', + eventSlug: 'mlb-tor-sea-2025-10-17', + outcome: '', + name: 'cropMaster', + pseudonym: 'Nonstop-Suitcase', + bio: '', + profileImage: '', + profileImageOptimized: '', + }, + { + proxyWallet: PROXY_WALLET_ADDRESS, + timestamp: 1762189060, + conditionId: + '0xa13312b2cc64532aed2a446b66e5a2d8d8b440b24d7213d33b6dae6a58c33223', + type: 'REDEEM', + size: 5, + usdcSize: 5, + transactionHash: + '0x205120808686f9164ca306404e6221cd0fda524002587a410bebdff3f7d3858d', + price: 0, + asset: '', + side: '', + outcomeIndex: 999, + title: 'Steelers vs. Bengals', + slug: 'nfl-pit-cin-2025-10-16', + icon: 'https://polymarket-upload.s3.us-east-2.amazonaws.com/nfl.png', + eventSlug: 'nfl-pit-cin-2025-10-16', + outcome: '', + name: 'cropMaster', + pseudonym: 'Nonstop-Suitcase', + bio: '', + profileImage: '', + profileImageOptimized: '', + }, +]; diff --git a/e2e/api-mocking/mock-responses/polymarket/polymarket-mocks.ts b/e2e/api-mocking/mock-responses/polymarket/polymarket-mocks.ts index e98f80bce90e..2d24fde30957 100644 --- a/e2e/api-mocking/mock-responses/polymarket/polymarket-mocks.ts +++ b/e2e/api-mocking/mock-responses/polymarket/polymarket-mocks.ts @@ -14,7 +14,10 @@ import { POLYMARKET_EVENT_DETAILS_SPURS_PELICANS_RESPONSE, } from './polymarket-event-details-response'; import { POLYMARKET_UPNL_RESPONSE } from './polymarket-upnl-response'; -import { POLYMARKET_ACTIVITY_RESPONSE } from './polymarket-activity-response'; +import { + POLYMARKET_ACTIVITY_RESPONSE, + POLYMARKET_CLAIMED_POSITIONS_ACTIVITY_RESPONSE, +} from './polymarket-activity-response'; import { POLYMARKET_ORDER_BOOK_RESPONSE, POLYMARKET_ZOHRAN_ORDER_BOOK_RESPONSE, @@ -985,6 +988,60 @@ export const POLYMARKET_REMOVE_CLAIMED_POSITIONS_MOCKS = async ( })); }; +/** + * Post-claim mock that adds REDEEM transactions to the activity endpoint + * After claiming, REDEEM type transactions should appear in the activity feed + * @param mockServer - The mockttp server instance + */ +export const POLYMARKET_ADD_CLAIMED_POSITIONS_TO_ACTIVITY_MOCKS = async ( + mockServer: Mockttp, +) => { + // Override the activity mock to include REDEEM transactions for claimed positions + await mockServer + .forGet('/proxy') + .matching((request) => { + const url = new URL(request.url).searchParams.get('url'); + return Boolean( + url && + /^https:\/\/data-api\.polymarket\.com\/activity\?user=0x[a-fA-F0-9]{40}$/.test( + url, + ), + ); + }) + .asPriority(PRIORITY.API_OVERRIDE) // Higher priority to override the original activity mock + .thenCallback((request) => { + const url = new URL(request.url).searchParams.get('url'); + const userMatch = url?.match(/user=(0x[a-fA-F0-9]{40})/); + const userAddress = userMatch ? userMatch[1] : USER_WALLET_ADDRESS; + + // Map claimed positions to use the actual user address + const claimedPositionsWithUserAddress = + POLYMARKET_CLAIMED_POSITIONS_ACTIVITY_RESPONSE.map((activity) => ({ + ...activity, + proxyWallet: userAddress, + })); + + // Map existing activity to use the actual user address + const existingActivityWithUserAddress = POLYMARKET_ACTIVITY_RESPONSE.map( + (activity) => ({ + ...activity, + proxyWallet: userAddress, + }), + ); + + // Add the REDEEM transactions at the beginning of the activity array (most recent first) + const activityWithClaims = [ + ...claimedPositionsWithUserAddress, + ...existingActivityWithUserAddress, + ]; + + return { + statusCode: 200, + json: activityWithClaims, + }; + }); +}; + /** * Post-cash-out mock that removes the cashed out position from positions endpoint * and adds a SELL transaction to the activity endpoint diff --git a/e2e/pages/Transactions/predictionsActivityDetails.ts b/e2e/pages/Transactions/predictionsActivityDetails.ts index 30751c980b31..77c0a745d349 100644 --- a/e2e/pages/Transactions/predictionsActivityDetails.ts +++ b/e2e/pages/Transactions/predictionsActivityDetails.ts @@ -15,6 +15,12 @@ class PredictActivityDetails { ); } + get amountDisplay(): DetoxElement { + return Matchers.getElementByID( + PredictActivityDetailsSelectorsIDs.AMOUNT_DISPLAY, + ); + } + async tapBackButton(): Promise { await Gestures.waitAndTap(this.backButton); } diff --git a/e2e/specs/predict/predict-claim-positions.spec.ts b/e2e/specs/predict/predict-claim-positions.spec.ts index 6c36737cbe80..7760c1176bc7 100644 --- a/e2e/specs/predict/predict-claim-positions.spec.ts +++ b/e2e/specs/predict/predict-claim-positions.spec.ts @@ -14,16 +14,20 @@ import { POLYMARKET_REMOVE_CLAIMED_POSITIONS_MOCKS, POLYMARKET_TRANSACTION_SENTINEL_MOCKS, POLYMARKET_UPDATE_USDC_BALANCE_MOCKS, + POLYMARKET_ADD_CLAIMED_POSITIONS_TO_ACTIVITY_MOCKS, } from '../../api-mocking/mock-responses/polymarket/polymarket-mocks'; import { Mockttp } from 'mockttp'; import { setupRemoteFeatureFlagsMock } from '../../api-mocking/helpers/remoteFeatureFlagsHelper'; import PredictClaimPage from '../../pages/Predict/PredictClaimPage'; - +import TabBarComponent from '../../pages/wallet/TabBarComponent'; +import ActivitiesView from '../../pages/Transactions/ActivitiesView'; +import PredictActivityDetails from '../../pages/Transactions/predictionsActivityDetails'; import { POLYMARKET_RESOLVED_LOST_POSITIONS_RESPONSE, POLYMARKET_WINNING_POSITIONS_RESPONSE, } from '../../api-mocking/mock-responses/polymarket/polymarket-positions-response'; import { PredictHelpers } from './helpers/predict-helpers'; +import { POLYMARKET_CLAIMED_POSITIONS_ACTIVITY_RESPONSE } from '../../api-mocking/mock-responses/polymarket/polymarket-activity-response'; /* Test Scenario: Claim positions @@ -56,7 +60,7 @@ describe(SmokePredictions('Predictions'), () => { await PredictHelpers.setPortugalLocation(); await loginToApp(); - // Claim button is animated - disabling sync to prevent test hang + // Claim button is animated - disabling sync on iOS to prevent test hang await device.disableSynchronization(); await WalletView.tapOnPredictionsTab(); @@ -68,15 +72,18 @@ describe(SmokePredictions('Predictions'), () => { await WalletView.tapClaimButton(); await Assertions.expectElementToBeVisible(PredictClaimPage.container); - // Set up mocks to remove claimed positions after tapping claim button - await POLYMARKET_REMOVE_CLAIMED_POSITIONS_MOCKS(mockServer); - await POLYMARKET_UPDATE_USDC_BALANCE_MOCKS(mockServer, 'claim'); - await PredictClaimPage.tapClaimConfirmButton(); - await device.enableSynchronization(); + + await POLYMARKET_UPDATE_USDC_BALANCE_MOCKS(mockServer, 'claim'); + await POLYMARKET_REMOVE_CLAIMED_POSITIONS_MOCKS(mockServer); + await POLYMARKET_ADD_CLAIMED_POSITIONS_TO_ACTIVITY_MOCKS(mockServer); await Assertions.expectElementToBeVisible(WalletView.container); + await device.enableSynchronization(); + await Assertions.expectElementToNotBeVisible(WalletView.claimButton, { + description: 'Claim button should not be visible', + }); /* Verify that all resolved positions (lost positions + winning positions) are removed after claiming Resolved positions include both: @@ -94,13 +101,32 @@ describe(SmokePredictions('Predictions'), () => { }); } - await Assertions.expectElementToNotBeVisible(WalletView.claimButton, { - description: 'Claim button should not be visible', - }); - /* there is a bug where balances are not updating quick enough. - Leaving this commented for now. Once the bug is fixed we shoudl uncomment. - */ - // await Assertions.expectTextDisplayed('$48.16'); + await TabBarComponent.tapActivity(); + + await ActivitiesView.tapOnPredictionsTab(); + + for (const position of POLYMARKET_CLAIMED_POSITIONS_ACTIVITY_RESPONSE) { + await ActivitiesView.tapPredictPosition(position.title); + await Assertions.expectElementToBeVisible( + PredictActivityDetails.container, + { + description: `Activity details should be visible for "${position.title}"`, + }, + ); + // Verify the balance is displayed correctly (formatted as $XX.XX) + const expectedBalance = `$${position.usdcSize.toFixed(2)}`; + await Assertions.expectTextDisplayed(expectedBalance, { + description: `Balance should be displayed as "${expectedBalance}" for "${position.title}"`, + }); + await PredictActivityDetails.tapBackButton(); + } + + await TabBarComponent.tapWallet(); + + // Verify balance on iOS only. Android balances take a while to refresh. + if (device.getPlatform() === 'ios') { + await Assertions.expectTextDisplayed('$48.16'); + } }, ); }); From 0b3c7401ccfc380bd23d0e2f7d38d91db4b53e74 Mon Sep 17 00:00:00 2001 From: Bruno Nascimento Date: Mon, 3 Nov 2025 22:47:02 -0300 Subject: [PATCH 2/6] fix(card): delegation issues (#22058) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR addresses several issues affecting the Card experience, ensuring proper asset display, navigation flow, and data consistency across components. Fixes - Fixed an issue where assets were not loading correctly after opening the Card Home screen. - Corrected balance handling on the Change Asset Bottom Sheet — it now displays `availableBalance` for enabled tokens and the user’s total balance for disabled ones. - Restored asset icons on all asset-related bottom sheets. - Fixed incorrect titles on the Spending Limit screen: - Selecting a token that isn’t enabled now correctly shows “Change token and network.” - Pressing “Manage spending limit” on Card Home now correctly shows “Enable token.” - Resolved concurrency issues caused by promise caching in the `useLoadCardData` hook. ## **Changelog** CHANGELOG entry: Fixed issue where assets failed to load after opening the Card Home screen. CHANGELOG entry: Fixed balance display on the Change Asset Bottom Sheet to correctly show availableBalance for enabled tokens and user balance for disabled tokens. CHANGELOG entry: Restored missing asset icons on asset bottom sheets. CHANGELOG entry: Fixed incorrect Spending Limit title when selecting a token that’s not enabled (now shows “Change token and network”). CHANGELOG entry: Fixed incorrect Spending Limit title when pressing “Manage spending limit” on Card Home (now shows “Enable token”). CHANGELOG entry: Resolved concurrency and caching issues in useLoadCardData hook. ## **Related issues** Fixes: ## **Manual testing steps** ```gherkin Feature: my feature name Scenario: user [verb for user action] Given [describe expected initial app state] When user [verb for user action] Then [describe expected outcome] ``` ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [x] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **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] > Replaces balance logic with aggregated hook, adds pull-to-refresh and spending limit progress/warning on Card Home, fixes navigation/metrics/caching, strengthens delegation/external wallet flows, and adds extensive tests. > > - **Card Home UI**: > - Add pull-to-refresh and toast on enable-card failure. > - Show Spending Limit progress bar and close-to-limit warning; hide controls when enabling assets/card. > - Use `balanceFormatted` fallback when fiat unavailable; update metrics gating and properties. > - **Asset Selection / Spending Limit**: > - Use `useAssetBalances` with per-token keying; show correct balances (available vs wallet), restore icons, exclude Solana where needed. > - Adjust manage flow routing/params and block navigation during delegation; improved input validation and button states. > - **Hooks & Data Flow**: > - Replace `useAssetBalance`/`useAssetsList` with `useAssetBalances` (map-based, multi-token). > - Revise `useLoadCardData` with explicit (re)fetch controls, cache-clearing on auth errors, and concurrency fixes. > - New hooks/util: delegation handling (user-cancel aware), external wallet details with timeout, delegation settings caching, chain/name mapping, safe chainId formatting. > - **SDK**: > - Simplify provider init; add allowance querying robustness (skip non-EVM, timeout) and remove unused mapping. > - **State & i18n**: > - Add global cache clear on logout; new error string for enable-card. > - **Tests**: > - Add/expand tests for AssetSelectionBottomSheet, SpendingLimit, CardHome, delegation, balances, external wallet details, delegation settings, priority token loading, and utilities; update snapshots. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 2f102292d3f1f74bcdcb06efb4ea99ab32842dc3. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- .../UI/Card/Views/CardHome/CardHome.test.tsx | 456 ++++--- .../UI/Card/Views/CardHome/CardHome.tsx | 221 +++- .../__snapshots__/CardHome.test.tsx.snap | 126 ++ .../SpendingLimit/SpendingLimit.test.tsx | 554 ++++++--- .../Views/SpendingLimit/SpendingLimit.tsx | 333 +++--- .../AssetSelectionBottomSheet.test.tsx | 1059 ++++++++++++++++ .../AssetSelectionBottomSheet.tsx | 381 +++--- .../CardWarningBox/CardWarningBox.tsx | 2 +- .../SpendingLimitProgressBar.styles.ts | 10 +- .../SpendingLimitProgressBar.test.tsx | 239 +--- .../SpendingLimitProgressBar.tsx | 100 +- .../UI/Card/hooks/useAssetBalance.test.ts | 1030 ---------------- .../UI/Card/hooks/useAssetBalance.tsx | 258 ---- .../UI/Card/hooks/useAssetBalances.test.ts | 740 ++++++++++++ .../UI/Card/hooks/useAssetBalances.tsx | 425 +++++++ app/components/UI/Card/hooks/useAssetsList.ts | 207 ---- .../UI/Card/hooks/useCardDelegation.test.ts | 1064 +++++++++++++++++ .../UI/Card/hooks/useCardDelegation.ts | 233 ++-- .../useGetCardExternalWalletDetails.test.ts | 931 +++++++++++++++ .../hooks/useGetCardExternalWalletDetails.ts | 49 +- .../hooks/useGetDelegationSettings.test.ts | 565 +++++++++ .../hooks/useGetPriorityCardToken.test.ts | 506 +++----- .../UI/Card/hooks/useGetPriorityCardToken.tsx | 3 +- .../UI/Card/hooks/useLoadCardData.test.ts | 758 ++++++++++++ .../UI/Card/hooks/useLoadCardData.ts | 27 +- .../UI/Card/hooks/useSupportedTokens.tsx | 57 - app/components/UI/Card/sdk/CardSDK.test.ts | 17 - app/components/UI/Card/sdk/CardSDK.ts | 17 +- app/components/UI/Card/sdk/index.test.tsx | 7 +- app/components/UI/Card/sdk/index.tsx | 4 + app/components/UI/Card/types.ts | 4 +- .../util/mapCaipChainIdToChainName.test.ts | 138 +++ .../UI/Card/util/mapCaipChainIdToChainName.ts | 10 + .../UI/Card/util/mapCountryToLocation.test.ts | 114 ++ .../Card/util/safeFormatChainIdToHex.test.ts | 209 ++++ locales/languages/en.json | 1 + 36 files changed, 7847 insertions(+), 3008 deletions(-) create mode 100644 app/components/UI/Card/components/AssetSelectionBottomSheet/AssetSelectionBottomSheet.test.tsx delete mode 100644 app/components/UI/Card/hooks/useAssetBalance.test.ts delete mode 100644 app/components/UI/Card/hooks/useAssetBalance.tsx create mode 100644 app/components/UI/Card/hooks/useAssetBalances.test.ts create mode 100644 app/components/UI/Card/hooks/useAssetBalances.tsx delete mode 100644 app/components/UI/Card/hooks/useAssetsList.ts create mode 100644 app/components/UI/Card/hooks/useCardDelegation.test.ts create mode 100644 app/components/UI/Card/hooks/useGetCardExternalWalletDetails.test.ts create mode 100644 app/components/UI/Card/hooks/useGetDelegationSettings.test.ts create mode 100644 app/components/UI/Card/hooks/useLoadCardData.test.ts delete mode 100644 app/components/UI/Card/hooks/useSupportedTokens.tsx create mode 100644 app/components/UI/Card/util/mapCaipChainIdToChainName.test.ts create mode 100644 app/components/UI/Card/util/mapCaipChainIdToChainName.ts create mode 100644 app/components/UI/Card/util/mapCountryToLocation.test.ts create mode 100644 app/components/UI/Card/util/safeFormatChainIdToHex.test.ts diff --git a/app/components/UI/Card/Views/CardHome/CardHome.test.tsx b/app/components/UI/Card/Views/CardHome/CardHome.test.tsx index 094f65268712..e08ee0372a07 100644 --- a/app/components/UI/Card/Views/CardHome/CardHome.test.tsx +++ b/app/components/UI/Card/Views/CardHome/CardHome.test.tsx @@ -90,8 +90,11 @@ const mockPriorityToken = { decimals: 6, balance: '1000000000', allowance: '500000000', + totalAllowance: '1000', name: 'USD Coin', chainId: 1, + caipChainId: 'eip155:1', + walletAddress: '0x789', allowanceState: AllowanceState.Enabled, }; @@ -118,26 +121,37 @@ const mockEventBuilder = { build: jest.fn().mockReturnValue({ event: 'built' }), }; -interface MockAssetBalanceReturn { +interface MockAssetBalanceInfo { balanceFiat: string | undefined; asset: { symbol: string; image: string }; - mainBalance: string | undefined; - secondaryBalance: string | undefined; + balanceFormatted: string | undefined; rawTokenBalance?: number; rawFiatNumber?: number; } -const mockUseAssetBalance = jest.fn(() => ({ - balanceFiat: '$1,000.00', - asset: { - symbol: 'USDC', - image: 'usdc-image-url', - }, - mainBalance: '$1,000.00', - secondaryBalance: '1000 USDC', - rawTokenBalance: 1000, - rawFiatNumber: 1000, -})); +const createMockAssetBalancesMap = ( + balanceInfo: MockAssetBalanceInfo, + token = mockPriorityToken, +): Map => { + const map = new Map(); + // Use the same key format as the component: `${address}-${caipChainId}-${walletAddress}` + const key = `${token.address?.toLowerCase()}-${token.caipChainId}-${token.walletAddress?.toLowerCase()}`; + map.set(key, balanceInfo); + return map; +}; + +const mockUseAssetBalances = jest.fn(() => + createMockAssetBalancesMap({ + balanceFiat: '$1,000.00', + asset: { + symbol: 'USDC', + image: 'usdc-image-url', + }, + balanceFormatted: '1000.000000 USDC', + rawTokenBalance: 1000, + rawFiatNumber: 1000, + }), +); const mockUseNavigateToCardPage = jest.fn(() => ({ navigateToCardPage: mockNavigateToCardPage, @@ -152,8 +166,8 @@ jest.mock('../../hooks/useLoadCardData', () => ({ default: jest.fn(), })); -jest.mock('../../hooks/useAssetBalance', () => ({ - useAssetBalance: () => mockUseAssetBalance(), +jest.mock('../../hooks/useAssetBalances', () => ({ + useAssetBalances: () => mockUseAssetBalances(), })); jest.mock('../../hooks/useNavigateToCardPage', () => ({ @@ -493,17 +507,18 @@ describe('CardHome Component', () => { isLoadingPollCardStatusUntilProvisioned: false, }); - mockUseAssetBalance.mockReturnValue({ - balanceFiat: '$1,000.00', - asset: { - symbol: 'USDC', - image: 'usdc-image-url', - }, - mainBalance: '$1,000.00', - secondaryBalance: '1000 USDC', - rawTokenBalance: 1000, - rawFiatNumber: 1000, - }); + mockUseAssetBalances.mockReturnValue( + createMockAssetBalancesMap({ + balanceFiat: '$1,000.00', + asset: { + symbol: 'USDC', + image: 'usdc-image-url', + }, + balanceFormatted: '1000.000000 USDC', + rawTokenBalance: 1000, + rawFiatNumber: 1000, + }), + ); mockUseNavigateToCardPage.mockReturnValue({ navigateToCardPage: mockNavigateToCardPage, @@ -791,46 +806,48 @@ describe('CardHome Component', () => { }); }); - it('falls back to mainBalance when balanceFiat is TOKEN_RATE_UNDEFINED', () => { + it('falls back to balanceFormatted when balanceFiat is TOKEN_RATE_UNDEFINED', () => { // Given: fiat rate is undefined - mockUseAssetBalance.mockReturnValue({ - balanceFiat: TOKEN_RATE_UNDEFINED, - asset: { - symbol: 'USDC', - image: 'usdc-image-url', - }, - mainBalance: '1000 USDC', - secondaryBalance: 'Unable to find conversion rate', - rawTokenBalance: 1000, - rawFiatNumber: 0, - }); + mockUseAssetBalances.mockReturnValue( + createMockAssetBalancesMap({ + balanceFiat: TOKEN_RATE_UNDEFINED, + asset: { + symbol: 'USDC', + image: 'usdc-image-url', + }, + balanceFormatted: '1000.000000 USDC', + rawTokenBalance: 1000, + rawFiatNumber: 0, + }), + ); // When: component renders render(); - // Then: should display main balance instead of fiat - expect(screen.getByText('1000 USDC')).toBeTruthy(); + // Then: should display formatted balance instead of fiat + expect(screen.getByText('1000.000000 USDC')).toBeTruthy(); }); - it('falls back to mainBalance when balanceFiat is not available', () => { + it('falls back to balanceFormatted when balanceFiat is not available', () => { // Given: fiat balance is empty - mockUseAssetBalance.mockReturnValue({ - balanceFiat: '', - asset: { - symbol: 'USDC', - image: 'usdc-image-url', - }, - mainBalance: '1000 USDC', - secondaryBalance: 'Unable to find conversion rate', - rawTokenBalance: 1000, - rawFiatNumber: 0, - }); + mockUseAssetBalances.mockReturnValue( + createMockAssetBalancesMap({ + balanceFiat: '', + asset: { + symbol: 'USDC', + image: 'usdc-image-url', + }, + balanceFormatted: '1000.000000 USDC', + rawTokenBalance: 1000, + rawFiatNumber: 0, + }), + ); // When: component renders render(); - // Then: should display main balance as fallback - expect(screen.getByText('1000 USDC')).toBeTruthy(); + // Then: should display formatted balance as fallback + expect(screen.getByText('1000.000000 USDC')).toBeTruthy(); }); it('fires CARD_HOME_VIEWED once when balances are loaded', async () => { @@ -861,14 +878,15 @@ describe('CardHome Component', () => { it('includes zero raw balances in metrics', async () => { // Given: zero balances - mockUseAssetBalance.mockReturnValueOnce({ - balanceFiat: '$0.00', - asset: { symbol: 'USDC', image: 'usdc-image-url' }, - mainBalance: '0 USDC', - secondaryBalance: '$0.00', - rawTokenBalance: 0, - rawFiatNumber: 0, - }); + mockUseAssetBalances.mockReturnValueOnce( + createMockAssetBalancesMap({ + balanceFiat: '$0.00', + asset: { symbol: 'USDC', image: 'usdc-image-url' }, + balanceFormatted: '0.000000 USDC', + rawTokenBalance: 0, + rawFiatNumber: 0, + }), + ); // When: component renders render(); @@ -884,15 +902,16 @@ describe('CardHome Component', () => { }); it('includes only rawTokenBalance when fiat is undefined', async () => { - // Given: only main balance is valid (fiat undefined) - mockUseAssetBalance.mockReturnValueOnce({ - balanceFiat: undefined as unknown as string, - asset: { symbol: 'USDC', image: 'usdc-image-url' }, - mainBalance: '1000 USDC', - secondaryBalance: '1000 USDC', - rawTokenBalance: 1000, - // rawFiatNumber intentionally omitted (undefined) - }); + // Given: only formatted balance is valid (fiat undefined) + mockUseAssetBalances.mockReturnValueOnce( + createMockAssetBalancesMap({ + balanceFiat: undefined as unknown as string, + asset: { symbol: 'USDC', image: 'usdc-image-url' }, + balanceFormatted: '1000.000000 USDC', + rawTokenBalance: 1000, + // rawFiatNumber intentionally omitted (undefined) + }), + ); // When: component renders render(); @@ -908,16 +927,17 @@ describe('CardHome Component', () => { ); }); - it('includes only rawFiatNumber when main balance is undefined', async () => { - // Given: only fiat balance is valid (main undefined) - mockUseAssetBalance.mockReturnValueOnce({ - balanceFiat: '$1,000.00', - asset: { symbol: 'USDC', image: 'usdc-image-url' }, - mainBalance: undefined as unknown as string, - secondaryBalance: '$1,000.00', - // rawTokenBalance omitted - rawFiatNumber: 1000, - }); + it('includes only rawFiatNumber when formatted balance is undefined', async () => { + // Given: only fiat balance is valid (formatted balance undefined) + mockUseAssetBalances.mockReturnValueOnce( + createMockAssetBalancesMap({ + balanceFiat: '$1,000.00', + asset: { symbol: 'USDC', image: 'usdc-image-url' }, + balanceFormatted: undefined as unknown as string, + // rawTokenBalance omitted + rawFiatNumber: 1000, + }), + ); // When: component renders render(); @@ -933,16 +953,17 @@ describe('CardHome Component', () => { ); }); - it('fires CARD_HOME_VIEWED once when only mainBalance is valid', async () => { - // Given: only main balance is available - mockUseAssetBalance.mockReturnValue({ - balanceFiat: undefined as unknown as string, - asset: { symbol: 'USDC', image: 'usdc-image-url' }, - mainBalance: '1000 USDC', - secondaryBalance: '1000 USDC', - rawTokenBalance: 1000, - // rawFiatNumber omitted - }); + it('fires CARD_HOME_VIEWED once when only balanceFormatted is valid', async () => { + // Given: only formatted balance is available + mockUseAssetBalances.mockReturnValue( + createMockAssetBalancesMap({ + balanceFiat: undefined as unknown as string, + asset: { symbol: 'USDC', image: 'usdc-image-url' }, + balanceFormatted: '1000.000000 USDC', + rawTokenBalance: 1000, + // rawFiatNumber omitted + }), + ); // When: component renders render(); @@ -958,14 +979,15 @@ describe('CardHome Component', () => { it('fires CARD_HOME_VIEWED once when only fiat balance is valid', async () => { // Given: only fiat balance is available - mockUseAssetBalance.mockReturnValue({ - balanceFiat: '$1,000.00', - asset: { symbol: 'USDC', image: 'usdc-image-url' }, - mainBalance: undefined as unknown as string, - secondaryBalance: '$1,000.00', - // rawTokenBalance omitted - rawFiatNumber: 1000, - }); + mockUseAssetBalances.mockReturnValue( + createMockAssetBalancesMap({ + balanceFiat: '$1,000.00', + asset: { symbol: 'USDC', image: 'usdc-image-url' }, + balanceFormatted: undefined as unknown as string, + // rawTokenBalance omitted + rawFiatNumber: 1000, + }), + ); // When: component renders render(); @@ -981,13 +1003,14 @@ describe('CardHome Component', () => { it('does not fire metrics when balances are still loading', async () => { // Given: balances show loading sentinels - mockUseAssetBalance.mockReturnValue({ - balanceFiat: 'tokenBalanceLoading', - asset: { symbol: 'USDC', image: 'usdc-image-url' }, - mainBalance: 'TOKENBALANCELOADING', - secondaryBalance: 'loading', - // raw values omitted - }); + mockUseAssetBalances.mockReturnValue( + createMockAssetBalancesMap({ + balanceFiat: 'tokenBalanceLoading', + asset: { symbol: 'USDC', image: 'usdc-image-url' }, + balanceFormatted: 'TOKENBALANCELOADING', + // raw values omitted + }), + ); // When: component renders render(); @@ -998,14 +1021,15 @@ describe('CardHome Component', () => { }); it('does not fire metrics when balances are unavailable', async () => { - // Given: fiat is undefined and main is also undefined - mockUseAssetBalance.mockReturnValue({ - balanceFiat: 'tokenRateUndefined', - asset: { symbol: 'USDC', image: 'usdc-image-url' }, - mainBalance: undefined as unknown as string, - secondaryBalance: 'n/a', - // raw values omitted - }); + // Given: fiat is undefined and formatted balance is also undefined + mockUseAssetBalances.mockReturnValue( + createMockAssetBalancesMap({ + balanceFiat: 'tokenRateUndefined', + asset: { symbol: 'USDC', image: 'usdc-image-url' }, + balanceFormatted: undefined as unknown as string, + // raw values omitted + }), + ); // When: component renders render(); @@ -1017,14 +1041,15 @@ describe('CardHome Component', () => { it('converts NaN rawTokenBalance to 0 in metrics', async () => { // Given: rawTokenBalance is NaN - mockUseAssetBalance.mockReturnValueOnce({ - balanceFiat: '$1,000.00', - asset: { symbol: 'USDC', image: 'usdc-image-url' }, - mainBalance: '1000 USDC', - secondaryBalance: '1000 USDC', - rawTokenBalance: NaN, - rawFiatNumber: 1000, - }); + mockUseAssetBalances.mockReturnValueOnce( + createMockAssetBalancesMap({ + balanceFiat: '$1,000.00', + asset: { symbol: 'USDC', image: 'usdc-image-url' }, + balanceFormatted: '1000.000000 USDC', + rawTokenBalance: NaN, + rawFiatNumber: 1000, + }), + ); // When: component renders and fires metrics render(); @@ -1042,14 +1067,15 @@ describe('CardHome Component', () => { it('converts NaN rawFiatNumber to 0 in metrics', async () => { // Given: rawFiatNumber is NaN - mockUseAssetBalance.mockReturnValueOnce({ - balanceFiat: '$1,000.00', - asset: { symbol: 'USDC', image: 'usdc-image-url' }, - mainBalance: '1000 USDC', - secondaryBalance: '1000 USDC', - rawTokenBalance: 1000, - rawFiatNumber: NaN, - }); + mockUseAssetBalances.mockReturnValueOnce( + createMockAssetBalancesMap({ + balanceFiat: '$1,000.00', + asset: { symbol: 'USDC', image: 'usdc-image-url' }, + balanceFormatted: '1000.000000 USDC', + rawTokenBalance: 1000, + rawFiatNumber: NaN, + }), + ); // When: component renders and fires metrics render(); @@ -1067,14 +1093,15 @@ describe('CardHome Component', () => { it('converts both NaN raw values to 0 in metrics', async () => { // Given: both raw values are NaN - mockUseAssetBalance.mockReturnValueOnce({ - balanceFiat: '$1,000.00', - asset: { symbol: 'USDC', image: 'usdc-image-url' }, - mainBalance: '1000 USDC', - secondaryBalance: '1000 USDC', - rawTokenBalance: NaN, - rawFiatNumber: NaN, - }); + mockUseAssetBalances.mockReturnValueOnce( + createMockAssetBalancesMap({ + balanceFiat: '$1,000.00', + asset: { symbol: 'USDC', image: 'usdc-image-url' }, + balanceFormatted: '1000.000000 USDC', + rawTokenBalance: NaN, + rawFiatNumber: NaN, + }), + ); // When: component renders and fires metrics render(); @@ -1092,13 +1119,14 @@ describe('CardHome Component', () => { it('preserves undefined raw values in metrics', async () => { // Given: raw values are undefined (not provided) - mockUseAssetBalance.mockReturnValueOnce({ - balanceFiat: '$1,000.00', - asset: { symbol: 'USDC', image: 'usdc-image-url' }, - mainBalance: '1000 USDC', - secondaryBalance: '1000 USDC', - // rawTokenBalance and rawFiatNumber intentionally omitted (undefined) - }); + mockUseAssetBalances.mockReturnValueOnce( + createMockAssetBalancesMap({ + balanceFiat: '$1,000.00', + asset: { symbol: 'USDC', image: 'usdc-image-url' }, + balanceFormatted: '1000.000000 USDC', + // rawTokenBalance and rawFiatNumber intentionally omitted (undefined) + }), + ); // When: component renders and fires metrics render(); @@ -1555,4 +1583,140 @@ describe('CardHome Component', () => { ).not.toBeOnTheScreen(); }); }); + + describe('SpendingLimitProgressBar', () => { + it('renders when authenticated and allowance is limited', () => { + // Given: authenticated with limited allowance + setupMockSelectors({ isAuthenticated: true }); + const limitedAllowanceToken = { + ...mockPriorityToken, + allowanceState: AllowanceState.Limited, + totalAllowance: '1000', + allowance: '500', + }; + setupLoadCardDataMock({ + priorityToken: limitedAllowanceToken, + allTokens: [limitedAllowanceToken], + isAuthenticated: true, + }); + + // When: component renders + render(); + + // Then: should display spending limit progress bar + expect(screen.getByText('Spending Limit')).toBeOnTheScreen(); + expect(screen.getByText('500/1000 USDC')).toBeOnTheScreen(); + }); + + it('does not render when not authenticated', () => { + // Given: not authenticated with limited allowance + setupMockSelectors({ isAuthenticated: false }); + const limitedAllowanceToken = { + ...mockPriorityToken, + allowanceState: AllowanceState.Limited, + totalAllowance: '1000', + allowance: '500', + }; + setupLoadCardDataMock({ + priorityToken: limitedAllowanceToken, + allTokens: [limitedAllowanceToken], + isAuthenticated: false, + }); + + // When: component renders + render(); + + // Then: should not display spending limit progress bar + expect(screen.queryByText('Spending Limit')).not.toBeOnTheScreen(); + }); + + it('does not render when allowance is enabled', () => { + // Given: authenticated with enabled allowance + setupMockSelectors({ isAuthenticated: true }); + const enabledAllowanceToken = { + ...mockPriorityToken, + allowanceState: AllowanceState.Enabled, + totalAllowance: '1000', + allowance: '500', + }; + setupLoadCardDataMock({ + priorityToken: enabledAllowanceToken, + allTokens: [enabledAllowanceToken], + isAuthenticated: true, + }); + + // When: component renders + render(); + + // Then: should not display spending limit progress bar + expect(screen.queryByText('Spending Limit')).not.toBeOnTheScreen(); + }); + + it('displays correct consumed and total amounts', () => { + // Given: authenticated with specific allowance values + setupMockSelectors({ isAuthenticated: true }); + const limitedAllowanceToken = { + ...mockPriorityToken, + allowanceState: AllowanceState.Limited, + totalAllowance: '200', + allowance: '150', + symbol: 'USDC', + }; + setupLoadCardDataMock({ + priorityToken: limitedAllowanceToken, + allTokens: [limitedAllowanceToken], + isAuthenticated: true, + }); + + // When: component renders + render(); + + // Then: should display correct consumed amount (50/200) + expect(screen.getByText('50/200 USDC')).toBeOnTheScreen(); + }); + + it('handles zero remaining allowance', () => { + // Given: authenticated with zero remaining allowance + setupMockSelectors({ isAuthenticated: true }); + const limitedAllowanceToken = { + ...mockPriorityToken, + allowanceState: AllowanceState.Limited, + totalAllowance: '1000', + allowance: '0', + }; + setupLoadCardDataMock({ + priorityToken: limitedAllowanceToken, + allTokens: [limitedAllowanceToken], + isAuthenticated: true, + }); + + // When: component renders + render(); + + // Then: should display fully consumed allowance + expect(screen.getByText('1000/1000 USDC')).toBeOnTheScreen(); + }); + + it('handles undefined allowance values', () => { + // Given: authenticated with undefined allowance values + setupMockSelectors({ isAuthenticated: true }); + const limitedAllowanceToken = { + ...mockPriorityToken, + allowanceState: AllowanceState.Limited, + totalAllowance: undefined as unknown as string, + allowance: undefined as unknown as string, + }; + setupLoadCardDataMock({ + priorityToken: limitedAllowanceToken, + allTokens: [limitedAllowanceToken], + isAuthenticated: true, + }); + + // When: component renders + render(); + + // Then: should display zero values as fallback + expect(screen.getByText('0/0 USDC')).toBeOnTheScreen(); + }); + }); }); diff --git a/app/components/UI/Card/Views/CardHome/CardHome.tsx b/app/components/UI/Card/Views/CardHome/CardHome.tsx index d3236708fb7b..b37f9732d901 100644 --- a/app/components/UI/Card/Views/CardHome/CardHome.tsx +++ b/app/components/UI/Card/Views/CardHome/CardHome.tsx @@ -1,11 +1,18 @@ import React, { useCallback, + useContext, useEffect, useMemo, useRef, useState, } from 'react'; -import { Alert, ScrollView, TouchableOpacity, View } from 'react-native'; +import { + Alert, + RefreshControl, + ScrollView, + TouchableOpacity, + View, +} from 'react-native'; import Icon, { IconName, @@ -30,7 +37,6 @@ import Button, { ButtonWidthTypes, } from '../../../../../component-library/components/Buttons/Button'; import { strings } from '../../../../../../locales/i18n'; -import { useAssetBalance } from '../../hooks/useAssetBalance'; import { useNavigateToCardPage } from '../../hooks/useNavigateToCardPage'; import { AllowanceState, CardStatus, CardType, CardWarning } from '../../types'; import CardAssetItem from '../../components/CardAssetItem'; @@ -55,6 +61,7 @@ import { setAuthenticatedPriorityToken, setAuthenticatedPriorityTokenLastFetched, setUserCardLocation, + clearAllCache, } from '../../../../../core/redux/slices/card'; import { useCardProvision } from '../../hooks/useCardProvision'; import CardWarningBox from '../../components/CardWarningBox/CardWarningBox'; @@ -65,6 +72,13 @@ import Logger from '../../../../../util/Logger'; import useLoadCardData from '../../hooks/useLoadCardData'; import AssetSelectionBottomSheet from '../../components/AssetSelectionBottomSheet/AssetSelectionBottomSheet'; import { CardActions } from '../../util/metrics'; +import { isSolanaChainId } from '@metamask/bridge-controller'; +import { useAssetBalances } from '../../hooks/useAssetBalances'; +import { + ToastContext, + ToastVariants, +} from '../../../../../component-library/components/Toast'; +import SpendingLimitProgressBar from '../../components/SpendingLimitProgressBar/SpendingLimitProgressBar'; /** * CardHome Component @@ -83,9 +97,15 @@ const CardHome = () => { const [openAssetSelectionBottomSheet, setOpenAssetSelectionBottomSheet] = useState(false); const [retries, setRetries] = useState(0); + const [isRefreshing, setIsRefreshing] = useState(false); const addFundsSheetRef = useRef(null); const assetSelectionSheetRef = useRef(null); + const { toastRef } = useContext(ToastContext); const { logoutFromProvider, isLoading: isSDKLoading } = useCardSDK(); + const [ + isCloseSpendingLimitWarningShown, + setIsCloseSpendingLimitWarningShown, + ] = useState(true); const { trackEvent, createEventBuilder } = useMetrics(); const navigation = useNavigation(); @@ -114,8 +134,19 @@ const CardHome = () => { externalWalletDetailsData, } = useLoadCardData(); - const { balanceFiat, mainBalance, rawFiatNumber, rawTokenBalance, asset } = - useAssetBalance(priorityToken); + const assetBalancesMap = useAssetBalances( + priorityToken ? [priorityToken] : [], + ); + const assetBalance = assetBalancesMap.get( + `${priorityToken?.address?.toLowerCase()}-${priorityToken?.caipChainId}-${priorityToken?.walletAddress?.toLowerCase()}`, + ); + const { + asset, + balanceFiat, + balanceFormatted, + rawFiatNumber, + rawTokenBalance, + } = assetBalance ?? {}; const { provisionCard, isLoading: isLoadingProvisionCard } = useCardProvision(); @@ -147,11 +178,11 @@ const CardHome = () => { const balanceAmount = useMemo(() => { if (!balanceFiat || balanceFiat === TOKEN_RATE_UNDEFINED) { - return mainBalance; + return balanceFormatted; } return balanceFiat; - }, [balanceFiat, mainBalance]); + }, [balanceFiat, balanceFormatted]); const renderAddFundsBottomSheet = useCallback( () => ( @@ -171,14 +202,22 @@ const CardHome = () => { sheetRef={assetSelectionSheetRef} setOpenAssetSelectionBottomSheet={setOpenAssetSelectionBottomSheet} tokensWithAllowances={allTokens} - priorityToken={priorityToken} delegationSettings={delegationSettings} cardExternalWalletDetails={externalWalletDetailsData} /> ), - [allTokens, priorityToken, delegationSettings, externalWalletDetailsData], + [allTokens, delegationSettings, externalWalletDetailsData], ); + const handleRefresh = useCallback(async () => { + setIsRefreshing(true); + try { + await fetchAllData(); + } finally { + setIsRefreshing(false); + } + }, [fetchAllData]); + // Track event only once after priorityToken and balances are loaded const hasTrackedCardHomeView = useRef(false); @@ -193,10 +232,10 @@ const CardHome = () => { return; } - const hasValidMainBalance = - mainBalance !== undefined && - mainBalance !== TOKEN_BALANCE_LOADING && - mainBalance !== TOKEN_BALANCE_LOADING_UPPERCASE; + const hasValidTokenBalance = + balanceFormatted !== undefined && + balanceFormatted !== TOKEN_BALANCE_LOADING && + balanceFormatted !== TOKEN_BALANCE_LOADING_UPPERCASE; const hasValidFiatBalance = balanceFiat !== undefined && @@ -205,7 +244,7 @@ const CardHome = () => { balanceFiat !== TOKEN_RATE_UNDEFINED; const isLoaded = - !!priorityToken && (hasValidMainBalance || hasValidFiatBalance); + !!priorityToken && (hasValidTokenBalance || hasValidFiatBalance); if (isLoaded) { // Set flag immediately to prevent race conditions @@ -229,7 +268,7 @@ const CardHome = () => { } }, [ priorityToken, - mainBalance, + balanceFormatted, balanceFiat, rawTokenBalance, rawFiatNumber, @@ -279,11 +318,26 @@ const CardHome = () => { ); if (isAuthenticated) { - navigation.navigate(Routes.CARD.SPENDING_LIMIT, { flow: 'manage' }); + navigation.navigate(Routes.CARD.SPENDING_LIMIT, { + flow: 'enable', + priorityToken, + allTokens, + delegationSettings, + externalWalletDetailsData, + }); } else { navigation.navigate(Routes.CARD.WELCOME); } - }, [isAuthenticated, navigation, trackEvent, createEventBuilder]); + }, [ + isAuthenticated, + navigation, + trackEvent, + createEventBuilder, + priorityToken, + allTokens, + delegationSettings, + externalWalletDetailsData, + ]); const logoutAction = useCallback(() => { Alert.alert( @@ -306,19 +360,41 @@ const CardHome = () => { ); }, [logoutFromProvider, navigation]); + const needToEnableCard = useMemo( + () => cardDetailsWarning === CardWarning.NoCard, + [cardDetailsWarning], + ); + const needToEnableAssets = useMemo( + () => priorityTokenWarning === CardWarning.NeedDelegation, + [priorityTokenWarning], + ); + const enableCardAction = useCallback(async () => { - await provisionCard(); - const isProvisioned = await pollCardStatusUntilProvisioned(); + try { + await provisionCard(); + const isProvisioned = await pollCardStatusUntilProvisioned(); - if (isProvisioned) { - fetchPriorityToken(); - changeAssetAction(); + if (isProvisioned) { + fetchPriorityToken(); + changeAssetAction(); + } + } catch (error) { + toastRef?.current?.showToast({ + variant: ToastVariants.Icon, + labelOptions: [{ label: strings('card.card_home.enable_card_error') }], + iconName: IconName.Danger, + iconColor: theme.colors.error.default, + backgroundColor: theme.colors.error.muted, + hasNoTimeout: false, + }); } }, [ provisionCard, pollCardStatusUntilProvisioned, fetchPriorityToken, changeAssetAction, + toastRef, + theme, ]); const ButtonsSection = useMemo(() => { @@ -334,7 +410,7 @@ const CardHome = () => { } if (isBaanxLoginEnabled) { - if (cardDetailsWarning === CardWarning.NoCard) { + if (needToEnableCard) { return (