diff --git a/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx b/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx index cd80330b08ef..dda879f3ab2b 100644 --- a/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx +++ b/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx @@ -67,7 +67,7 @@ type ValidateCodeFormProps = { /** Function is called when validate code modal is mounted and on magic code resend */ sendValidateCode: () => void; - /** Wheather the form is loading or not */ + /** Whether the form is loading or not */ isLoading?: boolean; }; diff --git a/src/components/ValidateCodeActionModal/index.tsx b/src/components/ValidateCodeActionModal/index.tsx index f715fd8ef136..323e22c9e1a9 100644 --- a/src/components/ValidateCodeActionModal/index.tsx +++ b/src/components/ValidateCodeActionModal/index.tsx @@ -75,6 +75,7 @@ function ValidateCodeActionModal({ {descriptionPrimary} {!!descriptionSecondary && {descriptionSecondary}} {footer?.()} diff --git a/src/components/ValidateCodeActionModal/type.ts b/src/components/ValidateCodeActionModal/type.ts index 2fbf88768e62..5537af67b89d 100644 --- a/src/components/ValidateCodeActionModal/type.ts +++ b/src/components/ValidateCodeActionModal/type.ts @@ -41,7 +41,7 @@ type ValidateCodeActionModalProps = { /** If the magic code has been resent previously */ hasMagicCodeBeenSent?: boolean; - /** Wheather the form is loading or not */ + /** Whether the form is loading or not */ isLoading?: boolean; }; diff --git a/src/libs/API/parameters/RequestPhysicalExpensifyCardParams.ts b/src/libs/API/parameters/RequestPhysicalExpensifyCardParams.ts index 91995b6e37aa..94e45a29b728 100644 --- a/src/libs/API/parameters/RequestPhysicalExpensifyCardParams.ts +++ b/src/libs/API/parameters/RequestPhysicalExpensifyCardParams.ts @@ -8,6 +8,7 @@ type RequestPhysicalExpensifyCardParams = { addressState: string; addressStreet: string; addressZip: string; + validateCode: string; }; export default RequestPhysicalExpensifyCardParams; diff --git a/src/libs/actions/Wallet.ts b/src/libs/actions/Wallet.ts index b1f97421eea0..ea7e86ef49b7 100644 --- a/src/libs/actions/Wallet.ts +++ b/src/libs/actions/Wallet.ts @@ -10,12 +10,14 @@ import type { VerifyIdentityParams, } from '@libs/API/parameters'; import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; +import * as ErrorUtils from '@libs/ErrorUtils'; import type {PrivatePersonalDetails} from '@libs/GetPhysicalCardUtils'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import type CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {WalletAdditionalQuestionDetails} from '@src/types/onyx'; import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; +import * as FormActions from './FormActions'; type WalletQuestionAnswer = { question: string; @@ -257,7 +259,7 @@ function answerQuestionsForWallet(answers: WalletQuestionAnswer[], idNumber: str }); } -function requestPhysicalExpensifyCard(cardID: number, authToken: string, privatePersonalDetails: PrivatePersonalDetails) { +function requestPhysicalExpensifyCard(cardID: number, authToken: string, privatePersonalDetails: PrivatePersonalDetails, validateCode: string) { const {legalFirstName = '', legalLastName = '', phoneNumber = ''} = privatePersonalDetails; const {city = '', country = '', state = '', street = '', zip = ''} = PersonalDetailsUtils.getCurrentAddress(privatePersonalDetails) ?? {}; @@ -271,6 +273,7 @@ function requestPhysicalExpensifyCard(cardID: number, authToken: string, private addressState: state, addressStreet: street, addressZip: zip, + validateCode, }; const optimisticData: OnyxUpdate[] = [ @@ -279,7 +282,7 @@ function requestPhysicalExpensifyCard(cardID: number, authToken: string, private key: ONYXKEYS.CARD_LIST, value: { [cardID]: { - state: 4, // NOT_ACTIVATED + errors: null, }, }, }, @@ -288,15 +291,96 @@ function requestPhysicalExpensifyCard(cardID: number, authToken: string, private key: ONYXKEYS.PRIVATE_PERSONAL_DETAILS, value: privatePersonalDetails, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM, + value: { + isLoading: true, + errors: null, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.VALIDATE_ACTION_CODE, + value: { + validateCodeSent: false, + }, + }, + ]; + + const successData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.CARD_LIST, + value: { + [cardID]: { + state: 4, // NOT_ACTIVATED + errors: null, + }, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM, + value: { + isLoading: false, + errors: null, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.VALIDATE_ACTION_CODE, + value: { + validateCodeSent: false, + }, + }, + ]; + + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.CARD_LIST, + value: { + [cardID]: { + state: 2, + isLoading: false, + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), + }, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM, + value: { + isLoading: false, + }, + }, ]; - API.write(WRITE_COMMANDS.REQUEST_PHYSICAL_EXPENSIFY_CARD, requestParams, {optimisticData}); + API.write(WRITE_COMMANDS.REQUEST_PHYSICAL_EXPENSIFY_CARD, requestParams, {optimisticData, failureData, successData}); } function resetWalletAdditionalDetailsDraft() { Onyx.set(ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS_DRAFT, null); } +/** + * Clear the error of specific card + * @param cardID The card id of the card that you want to clear the errors. + */ +function clearPhysicalCardError(cardID?: string) { + if (!cardID) { + return; + } + + FormActions.clearErrors(ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM); + Onyx.merge(ONYXKEYS.CARD_LIST, { + [cardID]: { + errors: null, + }, + }); +} + export { openOnfidoFlow, openInitialSettingsPage, @@ -311,4 +395,5 @@ export { setKYCWallSource, requestPhysicalExpensifyCard, resetWalletAdditionalDetailsDraft, + clearPhysicalCardError, }; diff --git a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx index ae003c4afbe2..eef5024180e7 100644 --- a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx +++ b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx @@ -1,24 +1,28 @@ -import React, {useCallback, useEffect, useRef} from 'react'; +import React, {useCallback, useEffect, useRef, useState} from 'react'; import type {ReactNode} from 'react'; -import {withOnyx} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import FormProvider from '@components/Form/FormProvider'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; +import ValidateCodeActionModal from '@components/ValidateCodeActionModal'; +import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as FormActions from '@libs/actions/FormActions'; +import * as User from '@libs/actions/User'; import * as Wallet from '@libs/actions/Wallet'; import * as CardUtils from '@libs/CardUtils'; +import * as ErrorUtils from '@libs/ErrorUtils'; import * as GetPhysicalCardUtils from '@libs/GetPhysicalCardUtils'; import Navigation from '@libs/Navigation/Navigation'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {GetPhysicalCardForm} from '@src/types/form'; -import type {CardList, LoginList, PrivatePersonalDetails, Session} from '@src/types/onyx'; import type {Errors} from '@src/types/onyx/OnyxCommon'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; type OnValidate = (values: OnyxEntry) => Errors; @@ -28,24 +32,7 @@ type RenderContentProps = ChildrenProps & { onValidate: OnValidate; }; -type BaseGetPhysicalCardOnyxProps = { - /** List of available assigned cards */ - cardList: OnyxEntry; - - /** User's private personal details */ - privatePersonalDetails: OnyxEntry; - - /** Draft values used by the get physical card form */ - draftValues: OnyxEntry; - - /** Session info for the currently logged in user. */ - session: OnyxEntry; - - /** List of available login methods */ - loginList: OnyxEntry; -}; - -type BaseGetPhysicalCardProps = BaseGetPhysicalCardOnyxProps & { +type BaseGetPhysicalCardProps = { /** Text displayed below page title */ headline: string; @@ -91,27 +78,32 @@ function DefaultRenderContent({onSubmit, submitButtonText, children, onValidate} } function BaseGetPhysicalCard({ - cardList, children, currentRoute, domain, - draftValues, - privatePersonalDetails, headline, isConfirmation = false, - loginList, renderContent = DefaultRenderContent, - session, submitButtonText, title, onValidate = () => ({}), }: BaseGetPhysicalCardProps) { const styles = useThemeStyles(); const isRouteSet = useRef(false); - + const {translate} = useLocalize(); + const [cardList] = useOnyx(ONYXKEYS.CARD_LIST); + const [loginList] = useOnyx(ONYXKEYS.LOGIN_LIST); + const [session] = useOnyx(ONYXKEYS.SESSION); + const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS); + const [validateCodeAction] = useOnyx(ONYXKEYS.VALIDATE_ACTION_CODE); + const [draftValues] = useOnyx(ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM_DRAFT); + const [account] = useOnyx(ONYXKEYS.ACCOUNT); + const [isActionCodeModalVisible, setActionCodeModalVisible] = useState(false); + const [formData] = useOnyx(ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM); const domainCards = CardUtils.getDomainCards(cardList)[domain] || []; const cardToBeIssued = domainCards.find((card) => !card?.nameValuePairs?.isVirtual && card?.state === CONST.EXPENSIFY_CARD.STATE.STATE_NOT_ISSUED); - const cardID = cardToBeIssued?.cardID.toString() ?? '-1'; + const [currentCardID, setCurrentCardID] = useState(cardToBeIssued?.cardID.toString()); + const errorMessage = ErrorUtils.getLatestErrorMessageField(cardToBeIssued); useEffect(() => { if (isRouteSet.current || !privatePersonalDetails || !cardList) { @@ -144,19 +136,39 @@ function BaseGetPhysicalCard({ isRouteSet.current = true; }, [cardList, currentRoute, domain, domainCards.length, draftValues, loginList, cardToBeIssued, privatePersonalDetails]); + useEffect(() => { + // Current step of the get physical card flow should be the confirmation page; and + // Card has NOT_ACTIVATED state when successfully being issued so cardToBeIssued should be undefined + if (!isConfirmation || !!cardToBeIssued || !currentCardID) { + return; + } + + // Form draft data needs to be erased when the flow is complete, + // so that no stale data is left on Onyx + FormActions.clearDraftValues(ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM); + Wallet.clearPhysicalCardError(currentCardID); + Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(currentCardID)); + setCurrentCardID(undefined); + }, [currentCardID, isConfirmation, cardToBeIssued]); + const onSubmit = useCallback(() => { const updatedPrivatePersonalDetails = GetPhysicalCardUtils.getUpdatedPrivatePersonalDetails(draftValues, privatePersonalDetails); - // If the current step of the get physical card flow is the confirmation page if (isConfirmation) { - Wallet.requestPhysicalExpensifyCard(cardToBeIssued?.cardID ?? -1, session?.authToken ?? '', updatedPrivatePersonalDetails); - // Form draft data needs to be erased when the flow is complete, - // so that no stale data is left on Onyx - FormActions.clearDraftValues(ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM); - Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(cardID.toString())); + setActionCodeModalVisible(true); return; } GetPhysicalCardUtils.goToNextPhysicalCardRoute(domain, updatedPrivatePersonalDetails); - }, [cardID, cardToBeIssued?.cardID, domain, draftValues, isConfirmation, session?.authToken, privatePersonalDetails]); + }, [isConfirmation, domain, draftValues, privatePersonalDetails]); + + const handleIssuePhysicalCard = useCallback( + (validateCode: string) => { + setCurrentCardID(cardToBeIssued?.cardID.toString()); + const updatedPrivatePersonalDetails = GetPhysicalCardUtils.getUpdatedPrivatePersonalDetails(draftValues, privatePersonalDetails); + Wallet.requestPhysicalExpensifyCard(cardToBeIssued?.cardID ?? -1, session?.authToken ?? '', updatedPrivatePersonalDetails, validateCode); + }, + [cardToBeIssued?.cardID, draftValues, session?.authToken, privatePersonalDetails], + ); + return ( Navigation.goBack(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(cardID))} + onBackButtonPress={() => { + if (currentCardID) { + Navigation.goBack(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(currentCardID)); + } + Navigation.goBack(); + }} /> {headline} {renderContent({onSubmit, submitButtonText, children, onValidate})} + User.requestValidateCodeAction()} + clearError={() => Wallet.clearPhysicalCardError(currentCardID)} + validateError={!isEmptyObject(formData?.errors) ? formData?.errors : errorMessage} + handleSubmitForm={handleIssuePhysicalCard} + title={translate('cardPage.validateCardTitle')} + onClose={() => setActionCodeModalVisible(false)} + descriptionPrimary={translate('cardPage.enterMagicCode', {contactMethod: account?.primaryLogin ?? ''})} + /> ); } BaseGetPhysicalCard.displayName = 'BaseGetPhysicalCard'; -export default withOnyx({ - cardList: { - key: ONYXKEYS.CARD_LIST, - }, - loginList: { - key: ONYXKEYS.LOGIN_LIST, - }, - session: { - key: ONYXKEYS.SESSION, - }, - privatePersonalDetails: { - key: ONYXKEYS.PRIVATE_PERSONAL_DETAILS, - }, - draftValues: { - key: ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM_DRAFT, - }, -})(BaseGetPhysicalCard); +export default BaseGetPhysicalCard; export type {RenderContentProps};