diff --git a/src/pages/settings/Wallet/ExpensifyCardPage/index.tsx b/src/pages/settings/Wallet/ExpensifyCardPage/index.tsx index 5ea4af7906b2..8ab33a35173c 100644 --- a/src/pages/settings/Wallet/ExpensifyCardPage/index.tsx +++ b/src/pages/settings/Wallet/ExpensifyCardPage/index.tsx @@ -453,13 +453,20 @@ function ExpensifyCardPage({route}: ExpensifyCardPageProps) { } Navigation.navigate(ROUTES.SETTINGS_WALLET_CARD_DIGITAL_DETAILS_UPDATE_ADDRESS.getRoute(domain)); }} - limitType={card?.nameValuePairs?.limitType} - cardHintText={getCardHintText( - card?.nameValuePairs?.validFrom, - card?.nameValuePairs?.validThru, - personalDetails?.[card?.accountID ?? CONST.DEFAULT_NUMBER_ID]?.timezone?.selected, - translate, - )} + // The top-level "Limit type" row already shows the current card's limit. On combo card pages the + // revealed virtual card differs from the current (physical) card, so render its own limit here to + // avoid losing it; otherwise omit it to prevent a duplicate row for a single card. + limitType={card.cardID === currentCard.cardID ? undefined : card?.nameValuePairs?.limitType} + cardHintText={ + card.cardID === currentCard.cardID + ? undefined + : getCardHintText( + card?.nameValuePairs?.validFrom, + card?.nameValuePairs?.validThru, + personalDetails?.[card?.accountID ?? CONST.DEFAULT_NUMBER_ID]?.timezone?.selected, + translate, + ) + } /> ) : ( <> diff --git a/tests/ui/WalletExpensifyCardPageTest.tsx b/tests/ui/WalletExpensifyCardPageTest.tsx index 608c050ebc9c..9849f91ee8bf 100644 --- a/tests/ui/WalletExpensifyCardPageTest.tsx +++ b/tests/ui/WalletExpensifyCardPageTest.tsx @@ -12,6 +12,7 @@ import {CurrentReportIDContextProvider} from '@hooks/useCurrentReportID'; import * as useResponsiveLayoutModule from '@hooks/useResponsiveLayout'; import type ResponsiveLayoutResult from '@hooks/useResponsiveLayout/types'; import createPlatformStackNavigator from '@libs/Navigation/PlatformStackNavigation/createPlatformStackNavigator'; +import {clearRevealedVirtualCardDetails, setRevealedVirtualCardDetails} from '@libs/RevealedCardSecretsStore'; import type {SettingsNavigatorParamList} from '@navigation/types'; import ExpensifyCardPage from '@pages/settings/Wallet/ExpensifyCardPage'; import CONST from '@src/CONST'; @@ -71,6 +72,8 @@ describe('ExpensifyCardPage', () => { // Clear Onyx data and reset all mocks after each test to ensure a clean state. await act(async () => { await Onyx.clear(); + // Clear the in-memory revealed card secrets so state doesn't leak between tests. + clearRevealedVirtualCardDetails(); }); jest.clearAllMocks(); }); @@ -330,4 +333,60 @@ describe('ExpensifyCardPage', () => { unmount(); await waitForBatchedUpdatesWithAct(); }); + + it('should show the Limit type row only once after revealing a single virtual card', async () => { + // Sign in as a test user before running the test. + await TestHelper.signInWithTestUser(); + + // Add a single virtual card with a limit type set. The page renders a canonical top-level + // "Limit type" row for the current card; before this fix the revealed details section also + // rendered its own "Limit type" row, duplicating it for a single card. + await act(async () => { + await Onyx.merge(ONYXKEYS.CARD_LIST, { + [userCardID]: { + cardID: 1234, + state: CONST.EXPENSIFY_CARD.STATE.OPEN, + fundID: '12345', + domainName: 'xyz', + nameValuePairs: { + isVirtual: true, + cardTitle: 'Test Virtual Card', + limitType: CONST.EXPENSIFY_CARD.LIMIT_TYPES.SMART, + }, + availableSpend: 50000, + fraud: null, + }, + }); + }); + + // Render the page with the specified card ID. + const {unmount} = renderPage(SCREENS.SETTINGS.WALLET.DOMAIN_CARD, {cardID: '1234'}); + + await waitForBatchedUpdatesWithAct(); + + // Simulate the card details being revealed, as the SCA reveal flow would, which mounts the + // revealed card details section (CardDetails). + act(() => { + setRevealedVirtualCardDetails(userCardID, { + pan: '4111111111111111', + expiration: '1225', + cvv: '123', + }); + }); + + await waitForBatchedUpdatesWithAct(); + + // The revealed card number row confirms the details section is now rendered. + await waitFor(() => { + expect(screen.getByText(TestHelper.translateLocal('cardPage.cardDetails.cardNumber'))).toBeOnTheScreen(); + }); + + // The "Limit type" row should appear exactly once — only the canonical top-level row — and + // not be duplicated by the revealed card details section for a single card. + expect(screen.getAllByText(TestHelper.translateLocal('workspace.card.issueNewCard.limitType'))).toHaveLength(1); + + // Unmount the component after assertions to clean up. + unmount(); + await waitForBatchedUpdatesWithAct(); + }); });