Skip to content
2 changes: 2 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import HybridAppHandler from './HybridAppHandler';
import OnyxUpdateManager from './libs/actions/OnyxUpdateManager';
import './libs/HybridApp';
import {AttachmentModalContextProvider} from './pages/media/AttachmentModalScreen/AttachmentModalContext';
import ExpensifyCardContextProvider from './pages/settings/Wallet/ExpensifyCardPage/ExpensifyCardContextProvider';
import './setup/backgroundTask';
import './setup/fraudProtection';
import './setup/hybridApp';
Expand Down Expand Up @@ -121,6 +122,7 @@ function App() {
FullScreenBlockingViewContextProvider,
FullScreenLoaderContextProvider,
SidePanelContextProvider,
ExpensifyCardContextProvider,

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.

Previously, the card details were stored in the component state, so when the RHP was closed the user had to re-enter the magic code. Now, the details are stored in the app context, meaning they persist until a hard refresh. Are we okay with this change in behavior?

Would love your input here, @mountiny.

Screen.Recording.2025-09-19.at.2.07.29.AM.mov

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.

gentle bump @mountiny

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.

When the RHP is closed and reopened, is new code always sent?

If yes we should reset it

if no, if you start a different flow that will need the magic code, that reset the form, right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Common

  1. Go to Settings -> Wallet
  2. Press on one of the assigned cards
  3. Press on Reveal details
  4. Type in the code
  5. See the card A details
  6. Close the Card details RHP
  7. Click on the card A details

Old behaviour:
8. You need to go through 3 - 5 to see the card details (card details stored in state)

New behaviour:
8. You can see the card details (stored in context)

Common:
9. Close the Card details RHP
10. Click on card Bdetails
11. You need to go through 3 - 5 to see the card details

I'll reset it on closing to align with old behaviour

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.

Ooooh you mean card details like PAN and pic, ok I do think that was in state only on purpose. Why do you need to add it to the context to share it across screens? I believe we want this data to be as safe and secure as possible so the previous approach was safer imho

]}
>
<CustomStatusBarAndBackground />
Expand Down
4 changes: 4 additions & 0 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,10 @@ const ROUTES = {
route: 'settings/wallet/card/:cardID?',
getRoute: (cardID: string) => `settings/wallet/card/${cardID}` as const,
},
SETTINGS_WALLET_DOMAIN_CARD_CONFIRM_MAGIC_CODE: {
route: 'settings/wallet/card/:cardID/confirm-magic-code',
getRoute: (cardID: string) => `settings/wallet/card/${cardID}/confirm-magic-code` as const,
},
SETTINGS_DOMAIN_CARD_DETAIL: {
route: 'settings/card/:cardID?',
getRoute: (cardID: string) => `settings/card/${cardID}` as const,
Expand Down
1 change: 1 addition & 0 deletions src/SCREENS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ const SCREENS = {
ROOT: 'Settings_Wallet',
VERIFY_ACCOUNT: 'Settings_Wallet_VerifyAccount',
DOMAIN_CARD: 'Settings_Wallet_DomainCard',
DOMAIN_CARD_CONFIRM_MAGIC_CODE: 'Settings_Wallet_DomainCard_ConfirmMagicCode',
TRANSFER_BALANCE: 'Settings_Wallet_Transfer_Balance',
CHOOSE_TRANSFER_ACCOUNT: 'Settings_Wallet_Choose_Transfer_Account',
ENABLE_PAYMENTS: 'Settings_Wallet_EnablePayments',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ const ExpensifyCardModalStackNavigator = createModalStackNavigator({
});

const DomainCardModalStackNavigator = createModalStackNavigator({
[SCREENS.DOMAIN_CARD.DOMAIN_CARD_DETAIL]: () => require<ReactComponentModule>('../../../../pages/settings/Wallet/ExpensifyCardPage').default,
[SCREENS.DOMAIN_CARD.DOMAIN_CARD_DETAIL]: () => require<ReactComponentModule>('../../../../pages/settings/Wallet/ExpensifyCardPage/index').default,
[SCREENS.DOMAIN_CARD.DOMAIN_CARD_REPORT_FRAUD]: () => require<ReactComponentModule>('../../../../pages/settings/Wallet/ReportVirtualCardFraudPage').default,
});

Expand Down Expand Up @@ -351,7 +351,9 @@ const SettingsModalStackNavigator = createModalStackNavigator<SettingsNavigatorP
[SCREENS.SETTINGS.SHARE_LOG]: () => require<ReactComponentModule>('../../../../pages/settings/AboutPage/ShareLogPage').default,
[SCREENS.SETTINGS.WALLET.CARDS_DIGITAL_DETAILS_UPDATE_ADDRESS]: () => require<ReactComponentModule>('../../../../pages/settings/Profile/PersonalDetails/PersonalAddressPage').default,
[SCREENS.SETTINGS.WALLET.VERIFY_ACCOUNT]: () => require<ReactComponentModule>('../../../../pages/settings/Wallet/VerifyAccountPage').default,
[SCREENS.SETTINGS.WALLET.DOMAIN_CARD]: () => require<ReactComponentModule>('../../../../pages/settings/Wallet/ExpensifyCardPage').default,
[SCREENS.SETTINGS.WALLET.DOMAIN_CARD]: () => require<ReactComponentModule>('../../../../pages/settings/Wallet/ExpensifyCardPage/index').default,
[SCREENS.SETTINGS.WALLET.DOMAIN_CARD_CONFIRM_MAGIC_CODE]: () =>
require<ReactComponentModule>('../../../../pages/settings/Wallet/ExpensifyCardPage/ExpensifyCardVerifyAccountPage').default,
[SCREENS.SETTINGS.WALLET.REPORT_VIRTUAL_CARD_FRAUD]: () => require<ReactComponentModule>('../../../../pages/settings/Wallet/ReportVirtualCardFraudPage').default,
[SCREENS.SETTINGS.WALLET.REPORT_VIRTUAL_CARD_FRAUD_CONFIRM_MAGIC_CODE]: () =>
require<ReactComponentModule>('../../../../pages/settings/Wallet/ReportVirtualCardFraudVerifyAccountPage').default,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const SETTINGS_TO_RHP: Partial<Record<keyof SettingsSplitNavigatorParamList, str
],
[SCREENS.SETTINGS.WALLET.ROOT]: [
SCREENS.SETTINGS.WALLET.DOMAIN_CARD,
SCREENS.SETTINGS.WALLET.DOMAIN_CARD_CONFIRM_MAGIC_CODE,
SCREENS.SETTINGS.WALLET.VERIFY_ACCOUNT,
SCREENS.SETTINGS.WALLET.TRANSFER_BALANCE,
SCREENS.SETTINGS.WALLET.CHOOSE_TRANSFER_ACCOUNT,
Expand Down
4 changes: 4 additions & 0 deletions src/libs/Navigation/linkingConfig/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,10 @@ const config: LinkingOptions<RootNavigatorParamList>['config'] = {
path: ROUTES.SETTINGS_WALLET_DOMAIN_CARD.route,
exact: true,
},
[SCREENS.SETTINGS.WALLET.DOMAIN_CARD_CONFIRM_MAGIC_CODE]: {
path: ROUTES.SETTINGS_WALLET_DOMAIN_CARD_CONFIRM_MAGIC_CODE.route,
exact: true,
},
[SCREENS.SETTINGS.WALLET.VERIFY_ACCOUNT]: {
path: ROUTES.SETTINGS_WALLET_VERIFY_ACCOUNT,
exact: true,
Expand Down
4 changes: 4 additions & 0 deletions src/libs/Navigation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,10 @@ type SettingsNavigatorParamList = {
/** cardID of selected card */
cardID: string;
};
[SCREENS.SETTINGS.WALLET.DOMAIN_CARD_CONFIRM_MAGIC_CODE]: {
/** cardID of selected card */
cardID: string;
};
[SCREENS.SETTINGS.WALLET.REPORT_VIRTUAL_CARD_FRAUD]: {
/** cardID of selected card */
cardID: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import type {PropsWithChildren} from 'react';
import React, {createContext, useMemo, useState} from 'react';
import type {ExpensifyCardDetails} from '@src/types/onyx/Card';

type ExpensifyCardContextProviderProps = {
cardsDetails: Record<number, ExpensifyCardDetails | null>;
setCardsDetails: React.Dispatch<React.SetStateAction<Record<number, ExpensifyCardDetails | null>>>;
isCardDetailsLoading: Record<number, boolean>;
setIsCardDetailsLoading: React.Dispatch<React.SetStateAction<Record<number, boolean>>>;
cardsDetailsErrors: Record<number, string>;
setCardsDetailsErrors: React.Dispatch<React.SetStateAction<Record<number, string>>>;
};

const ExpensifyCardContext = createContext<ExpensifyCardContextProviderProps>({
cardsDetails: {},
setCardsDetails: () => {},
isCardDetailsLoading: {},
setIsCardDetailsLoading: () => {},
cardsDetailsErrors: {},
setCardsDetailsErrors: () => {},
});

/**
* Context to display revealed expensify card data and pass it between screens.
*/
function ExpensifyCardContextProvider({children}: PropsWithChildren) {
const [cardsDetails, setCardsDetails] = useState<Record<number, ExpensifyCardDetails | null>>({});
const [isCardDetailsLoading, setIsCardDetailsLoading] = useState<Record<number, boolean>>({});
const [cardsDetailsErrors, setCardsDetailsErrors] = useState<Record<number, string>>({});

const value = useMemo(
() => ({
cardsDetails,
setCardsDetails,
isCardDetailsLoading,
setIsCardDetailsLoading,
cardsDetailsErrors,
setCardsDetailsErrors,
}),
[cardsDetails, setCardsDetails, isCardDetailsLoading, setIsCardDetailsLoading, cardsDetailsErrors, setCardsDetailsErrors],
);

return <ExpensifyCardContext.Provider value={value}>{children}</ExpensifyCardContext.Provider>;
}

export default ExpensifyCardContextProvider;
export {ExpensifyCardContext};
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import React, {useState} from 'react';
import ValidateCodeActionContent from '@components/ValidateCodeActionModal/ValidateCodeActionContent';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
import {revealVirtualCardDetails} from '@libs/actions/Card';
import {requestValidateCodeAction, resetValidateActionCodeSent} from '@libs/actions/User';
import Navigation from '@libs/Navigation/Navigation';
import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
import type {SettingsNavigatorParamList} from '@libs/Navigation/types';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';
import type {ExpensifyCardDetails} from '@src/types/onyx/Card';
import type {Errors} from '@src/types/onyx/OnyxCommon';
import useExpensifyCardContext from './useExpensifyCardContext';

type ExpensifyCardVerifyAccountPageProps = PlatformStackScreenProps<SettingsNavigatorParamList, typeof SCREENS.SETTINGS.WALLET.DOMAIN_CARD_CONFIRM_MAGIC_CODE>;

function ExpensifyCardVerifyAccountPage({
route: {
params: {cardID = ''},
},
}: ExpensifyCardVerifyAccountPageProps) {
const {translate} = useLocalize();
const [validateError, setValidateError] = useState<Errors>({});
const [account] = useOnyx(ONYXKEYS.ACCOUNT, {canBeMissing: false});
const primaryLogin = account?.primaryLogin ?? '';
const {setIsCardDetailsLoading, setCardsDetails, setCardsDetailsErrors} = useExpensifyCardContext();

const handleRevealCardDetails = (validateCode: string) => {
setIsCardDetailsLoading((prevState: Record<number, boolean>) => ({
...prevState,
[cardID]: true,
}));
// We can't store the response in Onyx for security reasons.
// That is why this action is handled manually and the response is stored in a local state.
// Hence eslint disable here.
// eslint-disable-next-line rulesdir/no-thenable-actions-in-views
revealVirtualCardDetails(Number.parseInt(cardID, 10), validateCode)
.then((value) => {
setCardsDetails((prevState: Record<number, ExpensifyCardDetails | null>) => ({...prevState, [cardID]: value}));
setCardsDetailsErrors((prevState) => ({
...prevState,
[cardID]: '',
}));
Navigation.goBack(ROUTES.SETTINGS_WALLET_DOMAIN_CARD.getRoute(cardID));
})
.catch((error: string) => {
// Displaying magic code errors is handled in the modal, no need to set it on the card
setCardsDetailsErrors((prevState) => ({
...prevState,
[cardID]: error,
}));
})
.finally(() => {
setIsCardDetailsLoading((prevState: Record<number, boolean>) => ({...prevState, [cardID]: false}));
});
};

return (
<ValidateCodeActionContent
title={translate('cardPage.validateCardTitle')}
descriptionPrimary={translate('cardPage.enterMagicCode', {contactMethod: primaryLogin})}
sendValidateCode={() => requestValidateCodeAction()}
validateCodeActionErrorField="revealExpensifyCardDetails"
handleSubmitForm={handleRevealCardDetails}
validateError={validateError}
clearError={() => setValidateError({})}
onClose={() => {
resetValidateActionCodeSent();
Navigation.goBack(ROUTES.SETTINGS_WALLET_DOMAIN_CARD.getRoute(cardID));
}}
/>
);
}

ExpensifyCardVerifyAccountPage.displayName = 'ExpensifyCardVerifyAccountPage';

export default ExpensifyCardVerifyAccountPage;
Loading
Loading