Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Add a step to to Request Physical Card form that collects a magic code #51135

Merged
merged 32 commits into from
Nov 22, 2024
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
f001f00
Merge remote-tracking branch 'getusha/reuse-validate-code-action-moda…
hungvu193 Oct 20, 2024
6aa9281
Add validatecode modal when issuing Physical card
hungvu193 Oct 20, 2024
b1e52cc
adjust logic
hungvu193 Oct 20, 2024
2d1a57b
address linting
hungvu193 Oct 20, 2024
91bc9b1
revert .ruby-version
hungvu193 Oct 20, 2024
6eeabd4
merge main
hungvu193 Oct 21, 2024
9890494
Onyx migration
hungvu193 Oct 23, 2024
60a5c2a
Merge remote-tracking branch 'origin/main' into feat-50967
hungvu193 Oct 30, 2024
55187f4
handle generic error
hungvu193 Oct 30, 2024
340d3b1
update the error field
hungvu193 Nov 1, 2024
d4eb736
Merge remote-tracking branch 'origin/main' into feat-50967
hungvu193 Nov 1, 2024
23c5163
add loading field and other stuffs
hungvu193 Nov 1, 2024
6ce22aa
remove cardState optimistic
hungvu193 Nov 1, 2024
2224d58
prettier
hungvu193 Nov 1, 2024
60d0fdc
fix lint
hungvu193 Nov 1, 2024
c2528e8
Merge remote-tracking branch 'origin/main' into feat-50967
hungvu193 Nov 5, 2024
cf938e0
update successful condition
hungvu193 Nov 6, 2024
c3e9d85
Merge remote-tracking branch 'origin/main' into feat-50967
hungvu193 Nov 8, 2024
9c81e75
Merge remote-tracking branch 'origin/main' into feat-50967
hungvu193 Nov 11, 2024
33c65fc
addressing comment
hungvu193 Nov 11, 2024
14652ea
add code request status
hungvu193 Nov 12, 2024
982abf0
Update src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx
hungvu193 Nov 12, 2024
55c4a0c
Update src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx
hungvu193 Nov 12, 2024
cbf6a2b
address comment
hungvu193 Nov 13, 2024
58c2b39
merge main
hungvu193 Nov 14, 2024
0855d78
remove dupe props
hungvu193 Nov 14, 2024
5c75531
deprecate description props
hungvu193 Nov 14, 2024
fb7ee30
address comment
hungvu193 Nov 14, 2024
ceca20c
fix -1 value appears inside cardList
hungvu193 Nov 15, 2024
7b876d3
address comment
hungvu193 Nov 19, 2024
6d5eaa9
Merge remote-tracking branch 'origin/main' into feat-50967
hungvu193 Nov 19, 2024
4d96384
remove -1
hungvu193 Nov 19, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -75,6 +75,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 @@ -84,7 +85,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);
hungvu193 marked this conversation as resolved.
Show resolved Hide resolved
Onyx.merge(ONYXKEYS.CARD_LIST, {
[cardID]: {
errors: null,
},
});
}

export {
openOnfidoFlow,
openInitialSettingsPage,
Expand All @@ -311,4 +395,5 @@ export {
setKYCWallSource,
requestPhysicalExpensifyCard,
resetWalletAdditionalDetailsDraft,
clearPhysicalCardError,
};
112 changes: 61 additions & 51 deletions src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx
hungvu193 marked this conversation as resolved.
Show resolved Hide resolved
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,33 @@ 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>(cardID);
hungvu193 marked this conversation as resolved.
Show resolved Hide resolved
const errorMessage = ErrorUtils.getLatestErrorMessageField(cardToBeIssued);

useEffect(() => {
if (isRouteSet.current || !privatePersonalDetails || !cardList) {
Expand Down Expand Up @@ -144,19 +137,40 @@ 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
// -1 is not a valid cardID, we don't need to clean up the form value in that case.
if (!isConfirmation || !!cardToBeIssued || !currentCardID || currentCardID === '-1') {
return;
hungvu193 marked this conversation as resolved.
Show resolved Hide resolved
}

// Form draft data needs to be erased when the flow is complete,
// so that no stale data is left on Onyx
hungvu193 marked this conversation as resolved.
Show resolved Hide resolved
FormActions.clearDraftValues(ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM);
Wallet.clearPhysicalCardError(currentCardID);
Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(currentCardID.toString()));
setCurrentCardID(undefined);
hungvu193 marked this conversation as resolved.
Show resolved Hide resolved
}, [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);
},
hungvu193 marked this conversation as resolved.
Show resolved Hide resolved
[cardToBeIssued?.cardID, draftValues, session?.authToken, privatePersonalDetails],
);

return (
<ScreenWrapper
shouldEnablePickerAvoiding={false}
Expand All @@ -169,28 +183,24 @@ function BaseGetPhysicalCard({
/>
<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};
Loading