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};