Skip to content

Commit

Permalink
Merge pull request #51147 from Expensify/youssef_validateCode_replace…
Browse files Browse the repository at this point in the history
…ment_cards

Require validateCode when requesting replacement cards
  • Loading branch information
thienlnam authored Nov 7, 2024
2 parents 1cdd204 + cc4c172 commit 7a605ec
Show file tree
Hide file tree
Showing 6 changed files with 106 additions and 58 deletions.
4 changes: 3 additions & 1 deletion src/components/ValidateCodeActionModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton';
import Modal from '@components/Modal';
import ScreenWrapper from '@components/ScreenWrapper';
import Text from '@components/Text';
import useSafePaddingBottomStyle from '@hooks/useSafePaddingBottomStyle';
import useThemeStyles from '@hooks/useThemeStyles';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
Expand All @@ -27,6 +28,7 @@ function ValidateCodeActionModal({
hasMagicCodeBeenSent,
}: ValidateCodeActionModalProps) {
const themeStyles = useThemeStyles();
const safePaddingBottomStyle = useSafePaddingBottomStyle();
const firstRenderRef = useRef(true);
const validateCodeFormRef = useRef<ValidateCodeFormHandle>(null);

Expand Down Expand Up @@ -76,9 +78,9 @@ function ValidateCodeActionModal({
handleSubmitForm={handleSubmitForm}
sendValidateCode={sendValidateCode}
clearError={clearError}
buttonStyles={[themeStyles.justifyContentEnd, themeStyles.flex1, safePaddingBottomStyle]}
ref={validateCodeFormRef}
hasMagicCodeBeenSent={hasMagicCodeBeenSent}
buttonStyles={themeStyles.mtAuto}
/>
</View>
{footer?.()}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
type ReportVirtualExpensifyCardFraudParams = {
cardID: number;
validateCode: string;
};
export default ReportVirtualExpensifyCardFraudParams;
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
type RequestReplacementExpensifyCardParams = {
cardID: number;
reason: string;
validateCode: string;
};

export default RequestReplacementExpensifyCardParams;
7 changes: 5 additions & 2 deletions src/libs/actions/Card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,15 @@ type IssueNewCardFlowData = {
data?: Partial<IssueNewCardData>;
};

function reportVirtualExpensifyCardFraud(card?: Card) {
function reportVirtualExpensifyCardFraud(card: Card, validateCode: string) {
const cardID = card?.cardID ?? -1;
const optimisticData: OnyxUpdate[] = [
{
onyxMethod: Onyx.METHOD.MERGE,
key: ONYXKEYS.FORMS.REPORT_VIRTUAL_CARD_FRAUD,
value: {
isLoading: true,
errors: null,
},
},
{
Expand Down Expand Up @@ -105,6 +106,7 @@ function reportVirtualExpensifyCardFraud(card?: Card) {

const parameters: ReportVirtualExpensifyCardFraudParams = {
cardID,
validateCode,
};

API.write(WRITE_COMMANDS.REPORT_VIRTUAL_EXPENSIFY_CARD_FRAUD, parameters, {
Expand All @@ -119,7 +121,7 @@ function reportVirtualExpensifyCardFraud(card?: Card) {
* @param cardID - id of the card that is going to be replaced
* @param reason - reason for replacement
*/
function requestReplacementExpensifyCard(cardID: number, reason: ReplacementReason) {
function requestReplacementExpensifyCard(cardID: number, reason: ReplacementReason, validateCode: string) {
const optimisticData: OnyxUpdate[] = [
{
onyxMethod: Onyx.METHOD.MERGE,
Expand Down Expand Up @@ -154,6 +156,7 @@ function requestReplacementExpensifyCard(cardID: number, reason: ReplacementReas
const parameters: RequestReplacementExpensifyCardParams = {
cardID,
reason,
validateCode,
};

API.write(WRITE_COMMANDS.REQUEST_REPLACEMENT_EXPENSIFY_CARD, parameters, {
Expand Down
94 changes: 49 additions & 45 deletions src/pages/settings/Wallet/ReportCardLostPage.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import type {StackScreenProps} from '@react-navigation/stack';
import React, {useEffect, useState} from 'react';
import React, {useCallback, useEffect, useState} from 'react';
import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import {useOnyx} from 'react-native-onyx';
import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import ScreenWrapper from '@components/ScreenWrapper';
import SingleOptionSelector from '@components/SingleOptionSelector';
import Text from '@components/Text';
import ValidateCodeActionModal from '@components/ValidateCodeActionModal';
import useLocalize from '@hooks/useLocalize';
import usePrevious from '@hooks/usePrevious';
import useStyledSafeAreaInsets from '@hooks/useStyledSafeAreaInsets';
import useThemeStyles from '@hooks/useThemeStyles';
import {requestValidateCodeAction} from '@libs/actions/User';
import * as ErrorUtils from '@libs/ErrorUtils';
import Navigation from '@libs/Navigation/Navigation';
import type {SettingsNavigatorParamList} from '@libs/Navigation/types';
import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
Expand All @@ -24,8 +26,6 @@ import type {TranslationPaths} from '@src/languages/types';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';
import type {ReportPhysicalCardForm} from '@src/types/form';
import type {Card, PrivatePersonalDetails} from '@src/types/onyx';
import {isEmptyObject} from '@src/types/utils/EmptyObject';

const OPTIONS_KEYS = {
Expand All @@ -50,49 +50,32 @@ const OPTIONS: Option[] = [
},
];

type ReportCardLostPageOnyxProps = {
/** Onyx form data */
formData: OnyxEntry<ReportPhysicalCardForm>;

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

/** User's cards list */
cardList: OnyxEntry<Record<string, Card>>;
};

type ReportCardLostPageProps = ReportCardLostPageOnyxProps & StackScreenProps<SettingsNavigatorParamList, typeof SCREENS.SETTINGS.REPORT_CARD_LOST_OR_DAMAGED>;
type ReportCardLostPageProps = StackScreenProps<SettingsNavigatorParamList, typeof SCREENS.SETTINGS.REPORT_CARD_LOST_OR_DAMAGED>;

function ReportCardLostPage({
privatePersonalDetails = {
addresses: [
{
street: '',
street2: '',
city: '',
state: '',
zip: '',
country: '',
},
],
},
cardList = {},
route: {
params: {cardID = ''},
},
formData,
}: ReportCardLostPageProps) {
const styles = useThemeStyles();

const physicalCard = cardList?.[cardID];

const {translate} = useLocalize();

const [loginList] = useOnyx(ONYXKEYS.LOGIN_LIST);
const [account] = useOnyx(ONYXKEYS.ACCOUNT);
const [formData] = useOnyx(ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM);
const [cardList] = useOnyx(ONYXKEYS.CARD_LIST);
const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS);

const [reason, setReason] = useState<Option>();
const [isReasonConfirmed, setIsReasonConfirmed] = useState(false);
const [shouldShowAddressError, setShouldShowAddressError] = useState(false);
const [shouldShowReasonError, setShouldShowReasonError] = useState(false);

const physicalCard = cardList?.[cardID];
const validateError = ErrorUtils.getLatestErrorMessageField(physicalCard);
const [isValidateCodeActionModalVisible, setIsValidateCodeActionModalVisible] = useState(false);

const prevIsLoading = usePrevious(formData?.isLoading);

const {paddingBottom} = useStyledSafeAreaInsets();
Expand All @@ -115,6 +98,16 @@ function ReportCardLostPage({
FormActions.setErrors(ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM, physicalCard?.errors ?? {});
}, [formData?.isLoading, physicalCard?.errors]);

const handleValidateCodeEntered = useCallback(
(validateCode: string) => {
if (!physicalCard) {
return;
}
CardActions.requestReplacementExpensifyCard(physicalCard.cardID, reason?.key as ReplacementReason, validateCode);
},
[physicalCard, reason?.key],
);

if (isEmptyObject(physicalCard)) {
return <NotFoundPage />;
}
Expand All @@ -135,8 +128,17 @@ function ReportCardLostPage({
setShouldShowAddressError(true);
return;
}
setIsValidateCodeActionModalVisible(true);
};

const sendValidateCode = () => {
const primaryLogin = account?.primaryLogin ?? '';

if (loginList?.[primaryLogin]?.validateCodeSent) {
return;
}

CardActions.requestReplacementExpensifyCard(physicalCard.cardID, reason?.key as ReplacementReason);
requestValidateCodeAction();
};

const handleOptionSelect = (option: Option) => {
Expand Down Expand Up @@ -189,6 +191,18 @@ function ReportCardLostPage({
isLoading={formData?.isLoading}
buttonText={isDamaged ? translate('reportCardLostOrDamaged.shipNewCardButton') : translate('reportCardLostOrDamaged.deactivateCardButton')}
/>
<ValidateCodeActionModal
handleSubmitForm={handleValidateCodeEntered}
sendValidateCode={sendValidateCode}
validateError={validateError}
clearError={() => {
CardActions.clearCardListErrors(physicalCard.cardID);
}}
onClose={() => setIsValidateCodeActionModalVisible(false)}
isVisible={isValidateCodeActionModalVisible}
title={translate('cardPage.validateCardTitle')}
description={translate('cardPage.enterMagicCode', {contactMethod: account?.primaryLogin ?? ''})}
/>
</>
) : (
<>
Expand All @@ -215,14 +229,4 @@ function ReportCardLostPage({

ReportCardLostPage.displayName = 'ReportCardLostPage';

export default withOnyx<ReportCardLostPageProps, ReportCardLostPageOnyxProps>({
privatePersonalDetails: {
key: ONYXKEYS.PRIVATE_PERSONAL_DETAILS,
},
cardList: {
key: ONYXKEYS.CARD_LIST,
},
formData: {
key: ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM,
},
})(ReportCardLostPage);
export default ReportCardLostPage;
57 changes: 47 additions & 10 deletions src/pages/settings/Wallet/ReportVirtualCardFraudPage.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import type {StackScreenProps} from '@react-navigation/stack';
import React, {useCallback, useEffect} from 'react';
import {InteractionManager, View} from 'react-native';
import React, {useCallback, useEffect, useState} from 'react';
import {View} from 'react-native';
import {useOnyx} from 'react-native-onyx';
import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton';
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 usePrevious from '@hooks/usePrevious';
import useThemeStyles from '@hooks/useThemeStyles';
import {requestValidateCodeAction} from '@libs/actions/User';
import * as ErrorUtils from '@libs/ErrorUtils';
import Navigation from '@libs/Navigation/Navigation';
import type {SettingsNavigatorParamList} from '@libs/Navigation/types';
Expand All @@ -28,20 +30,20 @@ function ReportVirtualCardFraudPage({
}: ReportVirtualCardFraudPageProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const [loginList] = useOnyx(ONYXKEYS.LOGIN_LIST);
const [account] = useOnyx(ONYXKEYS.ACCOUNT);
const [cardList] = useOnyx(ONYXKEYS.CARD_LIST);
const [formData] = useOnyx(ONYXKEYS.FORMS.REPORT_VIRTUAL_CARD_FRAUD);
const primaryLogin = account?.primaryLogin ?? '';
const loginData = loginList?.[primaryLogin];

const virtualCard = cardList?.[cardID];
const virtualCardError = ErrorUtils.getLatestErrorMessage(virtualCard);
const validateError = ErrorUtils.getLatestErrorMessageField(virtualCard);

const prevIsLoading = usePrevious(formData?.isLoading);
const [isValidateCodeActionModalVisible, setIsValidateCodeActionModalVisible] = useState(false);

const submit = useCallback(() => {
Navigation.dismissModal();
InteractionManager.runAfterInteractions(() => {
Card.reportVirtualExpensifyCardFraud(virtualCard);
});
}, [virtualCard]);
const prevIsLoading = usePrevious(formData?.isLoading);

useEffect(() => {
if (!prevIsLoading || formData?.isLoading) {
Expand All @@ -54,6 +56,28 @@ function ReportVirtualCardFraudPage({
Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(cardID));
}, [cardID, formData?.isLoading, prevIsLoading, virtualCard?.errors]);

const handleValidateCodeEntered = useCallback(
(validateCode: string) => {
if (!virtualCard) {
return;
}
Card.reportVirtualExpensifyCardFraud(virtualCard, validateCode);
},
[virtualCard],
);

const sendValidateCode = () => {
if (loginData?.validateCodeSent) {
return;
}

requestValidateCodeAction();
};

const handleSubmit = useCallback(() => {
setIsValidateCodeActionModalVisible(true);
}, [setIsValidateCodeActionModalVisible]);

if (isEmptyObject(virtualCard)) {
return <NotFoundPage />;
}
Expand All @@ -68,12 +92,25 @@ function ReportVirtualCardFraudPage({
<Text style={[styles.webViewStyles.baseFontStyle, styles.mh5]}>{translate('reportFraudPage.description')}</Text>
<FormAlertWithSubmitButton
isAlertVisible={!!virtualCardError}
onSubmit={submit}
onSubmit={handleSubmit}
message={virtualCardError}
isLoading={formData?.isLoading}
buttonText={translate('reportFraudPage.deactivateCard')}
containerStyles={[styles.m5]}
/>
<ValidateCodeActionModal
handleSubmitForm={handleValidateCodeEntered}
sendValidateCode={sendValidateCode}
validateError={validateError}
clearError={() => {
Card.clearCardListErrors(virtualCard.cardID);
}}
onClose={() => setIsValidateCodeActionModalVisible(false)}
isVisible={isValidateCodeActionModalVisible}
title={translate('cardPage.validateCardTitle')}
description={translate('cardPage.enterMagicCode', {contactMethod: account?.primaryLogin ?? ''})}
hasMagicCodeBeenSent={!!loginData?.validateCodeSent}
/>
</View>
</ScreenWrapper>
);
Expand Down

0 comments on commit 7a605ec

Please sign in to comment.