diff --git a/app/components/UI/Card/Views/CardHome/CardHome.test.tsx b/app/components/UI/Card/Views/CardHome/CardHome.test.tsx index 094f6526871..e08ee0372a0 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 d3236708fb7..b37f9732d90 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 (