diff --git a/src/App.tsx b/src/App.tsx index 683f6772a9bb..abd15d47c8fe 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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'; @@ -121,6 +122,7 @@ function App() { FullScreenBlockingViewContextProvider, FullScreenLoaderContextProvider, SidePanelContextProvider, + ExpensifyCardContextProvider, ]} > diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 5c8496f59b69..8fc0ec871cbe 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -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, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index a4236feaed70..1ce7dd940c17 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -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', diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 302414f20a00..9ecf8b75ffbe 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -281,7 +281,7 @@ const ExpensifyCardModalStackNavigator = createModalStackNavigator({ }); const DomainCardModalStackNavigator = createModalStackNavigator({ - [SCREENS.DOMAIN_CARD.DOMAIN_CARD_DETAIL]: () => require('../../../../pages/settings/Wallet/ExpensifyCardPage').default, + [SCREENS.DOMAIN_CARD.DOMAIN_CARD_DETAIL]: () => require('../../../../pages/settings/Wallet/ExpensifyCardPage/index').default, [SCREENS.DOMAIN_CARD.DOMAIN_CARD_REPORT_FRAUD]: () => require('../../../../pages/settings/Wallet/ReportVirtualCardFraudPage').default, }); @@ -351,7 +351,9 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/settings/AboutPage/ShareLogPage').default, [SCREENS.SETTINGS.WALLET.CARDS_DIGITAL_DETAILS_UPDATE_ADDRESS]: () => require('../../../../pages/settings/Profile/PersonalDetails/PersonalAddressPage').default, [SCREENS.SETTINGS.WALLET.VERIFY_ACCOUNT]: () => require('../../../../pages/settings/Wallet/VerifyAccountPage').default, - [SCREENS.SETTINGS.WALLET.DOMAIN_CARD]: () => require('../../../../pages/settings/Wallet/ExpensifyCardPage').default, + [SCREENS.SETTINGS.WALLET.DOMAIN_CARD]: () => require('../../../../pages/settings/Wallet/ExpensifyCardPage/index').default, + [SCREENS.SETTINGS.WALLET.DOMAIN_CARD_CONFIRM_MAGIC_CODE]: () => + require('../../../../pages/settings/Wallet/ExpensifyCardPage/ExpensifyCardVerifyAccountPage').default, [SCREENS.SETTINGS.WALLET.REPORT_VIRTUAL_CARD_FRAUD]: () => require('../../../../pages/settings/Wallet/ReportVirtualCardFraudPage').default, [SCREENS.SETTINGS.WALLET.REPORT_VIRTUAL_CARD_FRAUD_CONFIRM_MAGIC_CODE]: () => require('../../../../pages/settings/Wallet/ReportVirtualCardFraudVerifyAccountPage').default, diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts b/src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts index 8c8a4930b017..9a841547c3ac 100755 --- a/src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts +++ b/src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts @@ -31,6 +31,7 @@ const SETTINGS_TO_RHP: Partial['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, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index c431b82e8eed..0bbe8d8af9fe 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -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; diff --git a/src/pages/settings/Wallet/ExpensifyCardPage/ExpensifyCardContextProvider.tsx b/src/pages/settings/Wallet/ExpensifyCardPage/ExpensifyCardContextProvider.tsx new file mode 100644 index 000000000000..22972219725c --- /dev/null +++ b/src/pages/settings/Wallet/ExpensifyCardPage/ExpensifyCardContextProvider.tsx @@ -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; + setCardsDetails: React.Dispatch>>; + isCardDetailsLoading: Record; + setIsCardDetailsLoading: React.Dispatch>>; + cardsDetailsErrors: Record; + setCardsDetailsErrors: React.Dispatch>>; +}; + +const ExpensifyCardContext = createContext({ + 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>({}); + const [isCardDetailsLoading, setIsCardDetailsLoading] = useState>({}); + const [cardsDetailsErrors, setCardsDetailsErrors] = useState>({}); + + const value = useMemo( + () => ({ + cardsDetails, + setCardsDetails, + isCardDetailsLoading, + setIsCardDetailsLoading, + cardsDetailsErrors, + setCardsDetailsErrors, + }), + [cardsDetails, setCardsDetails, isCardDetailsLoading, setIsCardDetailsLoading, cardsDetailsErrors, setCardsDetailsErrors], + ); + + return {children}; +} + +export default ExpensifyCardContextProvider; +export {ExpensifyCardContext}; diff --git a/src/pages/settings/Wallet/ExpensifyCardPage/ExpensifyCardVerifyAccountPage.tsx b/src/pages/settings/Wallet/ExpensifyCardPage/ExpensifyCardVerifyAccountPage.tsx new file mode 100644 index 000000000000..d5662c1a24e2 --- /dev/null +++ b/src/pages/settings/Wallet/ExpensifyCardPage/ExpensifyCardVerifyAccountPage.tsx @@ -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; + +function ExpensifyCardVerifyAccountPage({ + route: { + params: {cardID = ''}, + }, +}: ExpensifyCardVerifyAccountPageProps) { + const {translate} = useLocalize(); + const [validateError, setValidateError] = useState({}); + const [account] = useOnyx(ONYXKEYS.ACCOUNT, {canBeMissing: false}); + const primaryLogin = account?.primaryLogin ?? ''; + const {setIsCardDetailsLoading, setCardsDetails, setCardsDetailsErrors} = useExpensifyCardContext(); + + const handleRevealCardDetails = (validateCode: string) => { + setIsCardDetailsLoading((prevState: Record) => ({ + ...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) => ({...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) => ({...prevState, [cardID]: false})); + }); + }; + + return ( + 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; diff --git a/src/pages/settings/Wallet/ExpensifyCardPage.tsx b/src/pages/settings/Wallet/ExpensifyCardPage/index.tsx similarity index 82% rename from src/pages/settings/Wallet/ExpensifyCardPage.tsx rename to src/pages/settings/Wallet/ExpensifyCardPage/index.tsx index cfdf3695610d..af29c821243a 100644 --- a/src/pages/settings/Wallet/ExpensifyCardPage.tsx +++ b/src/pages/settings/Wallet/ExpensifyCardPage/index.tsx @@ -12,23 +12,23 @@ import MenuItem from '@components/MenuItem'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; -import ValidateCodeActionModal from '@components/ValidateCodeActionModal'; import useBeforeRemove from '@hooks/useBeforeRemove'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; -import {requestValidateCodeAction, resetValidateActionCodeSent} from '@libs/actions/User'; +import {resetValidateActionCodeSent} from '@libs/actions/User'; import {formatCardExpiration, getDomainCards, maskCard, maskPin} from '@libs/CardUtils'; import {convertToDisplayString, getCurrencyKeyByCountryCode} from '@libs/CurrencyUtils'; -import {getMicroSecondOnyxErrorWithTranslationKey} from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; import {buildCannedSearchQuery} from '@libs/SearchQueryUtils'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; -import {clearActivatedCardPin, revealVirtualCardDetails} from '@userActions/Card'; +import RedDotCardSection from '@pages/settings/Wallet/RedDotCardSection'; +import CardDetails from '@pages/settings/Wallet/WalletPage/CardDetails'; +import {clearActivatedCardPin} from '@userActions/Card'; import {openOldDotLink} from '@userActions/Link'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; @@ -36,11 +36,8 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import type {CurrencyList} from '@src/types/onyx'; -import type {ExpensifyCardDetails} from '@src/types/onyx/Card'; -import type {Errors} from '@src/types/onyx/OnyxCommon'; import {getEmptyObject} from '@src/types/utils/EmptyObject'; -import RedDotCardSection from './RedDotCardSection'; -import CardDetails from './WalletPage/CardDetails'; +import useExpensifyCardContext from './useExpensifyCardContext'; type ExpensifyCardPageProps = PlatformStackScreenProps; @@ -76,8 +73,6 @@ function ExpensifyCardPage({ const styles = useThemeStyles(); const {isOffline} = useNetwork(); const {translate} = useLocalize(); - const [isValidateCodeActionModalVisible, setIsValidateCodeActionModalVisible] = useState(false); - const [currentCardID, setCurrentCardID] = useState(-1); const isTravelCard = cardList?.[cardID]?.nameValuePairs?.isTravelCard; const shouldDisplayCardDomain = !isTravelCard && (!cardList?.[cardID]?.nameValuePairs?.issuedBy || !cardList?.[cardID]?.nameValuePairs?.isVirtual); const domain = cardList?.[cardID]?.domainName ?? ''; @@ -93,8 +88,6 @@ function ExpensifyCardPage({ return [cardList?.[cardID]]; }, [shouldDisplayCardDomain, cardList, cardID, domain]); - useBeforeRemove(() => setIsValidateCodeActionModalVisible(false)); - useEffect(() => { return () => { if (!pin) { @@ -118,52 +111,15 @@ function ExpensifyCardPage({ const cardToAdd = useMemo(() => { return virtualCards?.at(0); }, [virtualCards]); - const [cardsDetails, setCardsDetails] = useState>({}); - const [isCardDetailsLoading, setIsCardDetailsLoading] = useState>({}); - const [cardsDetailsErrors, setCardsDetailsErrors] = useState>({}); - const [validateError, setValidateError] = useState({}); - const {isAccountLocked, showLockedAccountModal} = useContext(LockedAccountContext); - const openValidateCodeModal = (revealedCardID: number) => { - setCurrentCardID(revealedCardID); - setIsValidateCodeActionModalVisible(true); - }; + const {cardsDetails, setCardsDetails, isCardDetailsLoading, cardsDetailsErrors} = useExpensifyCardContext(); - const handleRevealDetails = (validateCode: string) => { - setIsCardDetailsLoading((prevState: Record) => ({ - ...prevState, - [currentCardID]: 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(currentCardID, validateCode) - .then((value) => { - setCardsDetails((prevState: Record) => ({...prevState, [currentCardID]: value})); - setCardsDetailsErrors((prevState) => ({ - ...prevState, - [currentCardID]: '', - })); - setIsValidateCodeActionModalVisible(false); - }) - .catch((error: string) => { - // Displaying magic code errors is handled in the modal, no need to set it on the card - // TODO: remove setValidateError once backend deploys https://github.com/Expensify/Web-Expensify/pull/46007 - if (error === 'validateCodeForm.error.incorrectMagicCode') { - setValidateError(() => getMicroSecondOnyxErrorWithTranslationKey('validateCodeForm.error.incorrectMagicCode')); - return; - } - setCardsDetailsErrors((prevState) => ({ - ...prevState, - [currentCardID]: error, - })); - setIsValidateCodeActionModalVisible(false); - }) - .finally(() => { - setIsCardDetailsLoading((prevState: Record) => ({...prevState, [currentCardID]: false})); - }); - }; + // This resets card details when we exit the page. + useBeforeRemove(() => { + setCardsDetails((oldCardDetails) => ({...oldCardDetails, [cardID]: null})); + }); + + const {isAccountLocked, showLockedAccountModal} = useContext(LockedAccountContext); const hasDetectedDomainFraud = cardsToShow?.some((card) => card?.fraud === CONST.EXPENSIFY_CARD.FRAUD_TYPES.DOMAIN); const hasDetectedIndividualFraud = cardsToShow?.some((card) => card?.fraud === CONST.EXPENSIFY_CARD.FRAUD_TYPES.INDIVIDUAL); @@ -176,7 +132,6 @@ function ExpensifyCardPage({ const formattedAvailableSpendAmount = convertToDisplayString(cardsToShow?.at(0)?.availableSpend, currency); const {limitNameKey, limitTitleKey} = getLimitTypeTranslationKeys(cardsToShow?.at(0)?.nameValuePairs?.limitType); - const primaryLogin = account?.primaryLogin ?? ''; const isSignedInAsDelegate = !!account?.delegatedAccess?.delegate || false; if (isNotFound) { @@ -261,7 +216,7 @@ function ExpensifyCardPage({ showLockedAccountModal(); return; } - openValidateCodeModal(card.cardID); + Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAIN_CARD_CONFIRM_MAGIC_CODE.getRoute(cardID)); }} isDisabled={isCardDetailsLoading[card.cardID] || isOffline} isLoading={isCardDetailsLoading[card.cardID]} @@ -313,7 +268,7 @@ function ExpensifyCardPage({ !isSignedInAsDelegate ? (