Skip to content

Commit

Permalink
Merge pull request #51135 from hungvu193/feat-50967
Browse files Browse the repository at this point in the history
Feat: Add a step to to Request Physical Card form that collects a magic code
  • Loading branch information
NikkiWines authored Nov 22, 2024
2 parents 3797c31 + 4d96384 commit d2f86de
Show file tree
Hide file tree
Showing 6 changed files with 158 additions and 59 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

Expand Down
2 changes: 1 addition & 1 deletion src/components/ValidateCodeActionModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ function ValidateCodeActionModal({
<Text style={[themeStyles.mb3]}>{descriptionPrimary}</Text>
{!!descriptionSecondary && <Text style={[themeStyles.mb3]}>{descriptionSecondary}</Text>}
<ValidateCodeForm
isLoading={isLoading}
validateCodeAction={validateCodeAction}
validatePendingAction={validatePendingAction}
validateError={validateError}
Expand All @@ -86,7 +87,6 @@ function ValidateCodeActionModal({
buttonStyles={[themeStyles.justifyContentEnd, themeStyles.flex1, safePaddingBottomStyle]}
ref={validateCodeFormRef}
hasMagicCodeBeenSent={hasMagicCodeBeenSent}
isLoading={isLoading}
/>
</View>
{footer?.()}
Expand Down
2 changes: 1 addition & 1 deletion src/components/ValidateCodeActionModal/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ type RequestPhysicalExpensifyCardParams = {
addressState: string;
addressStreet: string;
addressZip: string;
validateCode: string;
};

export default RequestPhysicalExpensifyCardParams;
91 changes: 88 additions & 3 deletions src/libs/actions/Wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) ?? {};

Expand All @@ -271,6 +273,7 @@ function requestPhysicalExpensifyCard(cardID: number, authToken: string, private
addressState: state,
addressStreet: street,
addressZip: zip,
validateCode,
};

const optimisticData: OnyxUpdate[] = [
Expand All @@ -279,7 +282,7 @@ function requestPhysicalExpensifyCard(cardID: number, authToken: string, private
key: ONYXKEYS.CARD_LIST,
value: {
[cardID]: {
state: 4, // NOT_ACTIVATED
errors: null,
},
},
},
Expand All @@ -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,
Expand All @@ -311,4 +395,5 @@ export {
setKYCWallSource,
requestPhysicalExpensifyCard,
resetWalletAdditionalDetailsDraft,
clearPhysicalCardError,
};
119 changes: 66 additions & 53 deletions src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx
Original file line number Diff line number Diff line change
@@ -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<GetPhysicalCardForm>) => Errors;

Expand All @@ -28,24 +32,7 @@ type RenderContentProps = ChildrenProps & {
onValidate: OnValidate;
};

type BaseGetPhysicalCardOnyxProps = {
/** List of available assigned cards */
cardList: OnyxEntry<CardList>;

/** User's private personal details */
privatePersonalDetails: OnyxEntry<PrivatePersonalDetails>;

/** Draft values used by the get physical card form */
draftValues: OnyxEntry<GetPhysicalCardForm>;

/** Session info for the currently logged in user. */
session: OnyxEntry<Session>;

/** List of available login methods */
loginList: OnyxEntry<LoginList>;
};

type BaseGetPhysicalCardProps = BaseGetPhysicalCardOnyxProps & {
type BaseGetPhysicalCardProps = {
/** Text displayed below page title */
headline: string;

Expand Down Expand Up @@ -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<string | undefined>(cardToBeIssued?.cardID.toString());
const errorMessage = ErrorUtils.getLatestErrorMessageField(cardToBeIssued);

useEffect(() => {
if (isRouteSet.current || !privatePersonalDetails || !cardList) {
Expand Down Expand Up @@ -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 (
<ScreenWrapper
shouldEnablePickerAvoiding={false}
Expand All @@ -165,32 +177,33 @@ function BaseGetPhysicalCard({
>
<HeaderWithBackButton
title={title}
onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(cardID))}
onBackButtonPress={() => {
if (currentCardID) {
Navigation.goBack(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(currentCardID));
}
Navigation.goBack();
}}
/>
<Text style={[styles.textHeadline, styles.mh5, styles.mb5]}>{headline}</Text>
{renderContent({onSubmit, submitButtonText, children, onValidate})}
<ValidateCodeActionModal
isLoading={formData?.isLoading}
hasMagicCodeBeenSent={validateCodeAction?.validateCodeSent}
isVisible={isActionCodeModalVisible}
sendValidateCode={() => 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 ?? ''})}
/>
</ScreenWrapper>
);
}

BaseGetPhysicalCard.displayName = 'BaseGetPhysicalCard';

export default withOnyx<BaseGetPhysicalCardProps, BaseGetPhysicalCardOnyxProps>({
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};

0 comments on commit d2f86de

Please sign in to comment.