Skip to content

Commit 7a605ec

Browse files
authored
Merge pull request #51147 from Expensify/youssef_validateCode_replacement_cards
Require validateCode when requesting replacement cards
2 parents 1cdd204 + cc4c172 commit 7a605ec

File tree

6 files changed

+106
-58
lines changed

6 files changed

+106
-58
lines changed

src/components/ValidateCodeActionModal/index.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton';
55
import Modal from '@components/Modal';
66
import ScreenWrapper from '@components/ScreenWrapper';
77
import Text from '@components/Text';
8+
import useSafePaddingBottomStyle from '@hooks/useSafePaddingBottomStyle';
89
import useThemeStyles from '@hooks/useThemeStyles';
910
import CONST from '@src/CONST';
1011
import ONYXKEYS from '@src/ONYXKEYS';
@@ -27,6 +28,7 @@ function ValidateCodeActionModal({
2728
hasMagicCodeBeenSent,
2829
}: ValidateCodeActionModalProps) {
2930
const themeStyles = useThemeStyles();
31+
const safePaddingBottomStyle = useSafePaddingBottomStyle();
3032
const firstRenderRef = useRef(true);
3133
const validateCodeFormRef = useRef<ValidateCodeFormHandle>(null);
3234

@@ -76,9 +78,9 @@ function ValidateCodeActionModal({
7678
handleSubmitForm={handleSubmitForm}
7779
sendValidateCode={sendValidateCode}
7880
clearError={clearError}
81+
buttonStyles={[themeStyles.justifyContentEnd, themeStyles.flex1, safePaddingBottomStyle]}
7982
ref={validateCodeFormRef}
8083
hasMagicCodeBeenSent={hasMagicCodeBeenSent}
81-
buttonStyles={themeStyles.mtAuto}
8284
/>
8385
</View>
8486
{footer?.()}
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
type ReportVirtualExpensifyCardFraudParams = {
22
cardID: number;
3+
validateCode: string;
34
};
45
export default ReportVirtualExpensifyCardFraudParams;
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
type RequestReplacementExpensifyCardParams = {
22
cardID: number;
33
reason: string;
4+
validateCode: string;
45
};
56

67
export default RequestReplacementExpensifyCardParams;

src/libs/actions/Card.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,15 @@ type IssueNewCardFlowData = {
3737
data?: Partial<IssueNewCardData>;
3838
};
3939

40-
function reportVirtualExpensifyCardFraud(card?: Card) {
40+
function reportVirtualExpensifyCardFraud(card: Card, validateCode: string) {
4141
const cardID = card?.cardID ?? -1;
4242
const optimisticData: OnyxUpdate[] = [
4343
{
4444
onyxMethod: Onyx.METHOD.MERGE,
4545
key: ONYXKEYS.FORMS.REPORT_VIRTUAL_CARD_FRAUD,
4646
value: {
4747
isLoading: true,
48+
errors: null,
4849
},
4950
},
5051
{
@@ -105,6 +106,7 @@ function reportVirtualExpensifyCardFraud(card?: Card) {
105106

106107
const parameters: ReportVirtualExpensifyCardFraudParams = {
107108
cardID,
109+
validateCode,
108110
};
109111

110112
API.write(WRITE_COMMANDS.REPORT_VIRTUAL_EXPENSIFY_CARD_FRAUD, parameters, {
@@ -119,7 +121,7 @@ function reportVirtualExpensifyCardFraud(card?: Card) {
119121
* @param cardID - id of the card that is going to be replaced
120122
* @param reason - reason for replacement
121123
*/
122-
function requestReplacementExpensifyCard(cardID: number, reason: ReplacementReason) {
124+
function requestReplacementExpensifyCard(cardID: number, reason: ReplacementReason, validateCode: string) {
123125
const optimisticData: OnyxUpdate[] = [
124126
{
125127
onyxMethod: Onyx.METHOD.MERGE,
@@ -154,6 +156,7 @@ function requestReplacementExpensifyCard(cardID: number, reason: ReplacementReas
154156
const parameters: RequestReplacementExpensifyCardParams = {
155157
cardID,
156158
reason,
159+
validateCode,
157160
};
158161

159162
API.write(WRITE_COMMANDS.REQUEST_REPLACEMENT_EXPENSIFY_CARD, parameters, {

src/pages/settings/Wallet/ReportCardLostPage.tsx

+49-45
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
11
import type {StackScreenProps} from '@react-navigation/stack';
2-
import React, {useEffect, useState} from 'react';
2+
import React, {useCallback, useEffect, useState} from 'react';
33
import {View} from 'react-native';
4-
import type {OnyxEntry} from 'react-native-onyx';
5-
import {withOnyx} from 'react-native-onyx';
4+
import {useOnyx} from 'react-native-onyx';
65
import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton';
76
import HeaderWithBackButton from '@components/HeaderWithBackButton';
87
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
98
import ScreenWrapper from '@components/ScreenWrapper';
109
import SingleOptionSelector from '@components/SingleOptionSelector';
1110
import Text from '@components/Text';
11+
import ValidateCodeActionModal from '@components/ValidateCodeActionModal';
1212
import useLocalize from '@hooks/useLocalize';
1313
import usePrevious from '@hooks/usePrevious';
1414
import useStyledSafeAreaInsets from '@hooks/useStyledSafeAreaInsets';
1515
import useThemeStyles from '@hooks/useThemeStyles';
16+
import {requestValidateCodeAction} from '@libs/actions/User';
17+
import * as ErrorUtils from '@libs/ErrorUtils';
1618
import Navigation from '@libs/Navigation/Navigation';
1719
import type {SettingsNavigatorParamList} from '@libs/Navigation/types';
1820
import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
@@ -24,8 +26,6 @@ import type {TranslationPaths} from '@src/languages/types';
2426
import ONYXKEYS from '@src/ONYXKEYS';
2527
import ROUTES from '@src/ROUTES';
2628
import type SCREENS from '@src/SCREENS';
27-
import type {ReportPhysicalCardForm} from '@src/types/form';
28-
import type {Card, PrivatePersonalDetails} from '@src/types/onyx';
2929
import {isEmptyObject} from '@src/types/utils/EmptyObject';
3030

3131
const OPTIONS_KEYS = {
@@ -50,49 +50,32 @@ const OPTIONS: Option[] = [
5050
},
5151
];
5252

53-
type ReportCardLostPageOnyxProps = {
54-
/** Onyx form data */
55-
formData: OnyxEntry<ReportPhysicalCardForm>;
56-
57-
/** User's private personal details */
58-
privatePersonalDetails: OnyxEntry<PrivatePersonalDetails>;
59-
60-
/** User's cards list */
61-
cardList: OnyxEntry<Record<string, Card>>;
62-
};
63-
64-
type ReportCardLostPageProps = ReportCardLostPageOnyxProps & StackScreenProps<SettingsNavigatorParamList, typeof SCREENS.SETTINGS.REPORT_CARD_LOST_OR_DAMAGED>;
53+
type ReportCardLostPageProps = StackScreenProps<SettingsNavigatorParamList, typeof SCREENS.SETTINGS.REPORT_CARD_LOST_OR_DAMAGED>;
6554

6655
function ReportCardLostPage({
67-
privatePersonalDetails = {
68-
addresses: [
69-
{
70-
street: '',
71-
street2: '',
72-
city: '',
73-
state: '',
74-
zip: '',
75-
country: '',
76-
},
77-
],
78-
},
79-
cardList = {},
8056
route: {
8157
params: {cardID = ''},
8258
},
83-
formData,
8459
}: ReportCardLostPageProps) {
8560
const styles = useThemeStyles();
8661

87-
const physicalCard = cardList?.[cardID];
88-
8962
const {translate} = useLocalize();
9063

64+
const [loginList] = useOnyx(ONYXKEYS.LOGIN_LIST);
65+
const [account] = useOnyx(ONYXKEYS.ACCOUNT);
66+
const [formData] = useOnyx(ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM);
67+
const [cardList] = useOnyx(ONYXKEYS.CARD_LIST);
68+
const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS);
69+
9170
const [reason, setReason] = useState<Option>();
9271
const [isReasonConfirmed, setIsReasonConfirmed] = useState(false);
9372
const [shouldShowAddressError, setShouldShowAddressError] = useState(false);
9473
const [shouldShowReasonError, setShouldShowReasonError] = useState(false);
9574

75+
const physicalCard = cardList?.[cardID];
76+
const validateError = ErrorUtils.getLatestErrorMessageField(physicalCard);
77+
const [isValidateCodeActionModalVisible, setIsValidateCodeActionModalVisible] = useState(false);
78+
9679
const prevIsLoading = usePrevious(formData?.isLoading);
9780

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

101+
const handleValidateCodeEntered = useCallback(
102+
(validateCode: string) => {
103+
if (!physicalCard) {
104+
return;
105+
}
106+
CardActions.requestReplacementExpensifyCard(physicalCard.cardID, reason?.key as ReplacementReason, validateCode);
107+
},
108+
[physicalCard, reason?.key],
109+
);
110+
118111
if (isEmptyObject(physicalCard)) {
119112
return <NotFoundPage />;
120113
}
@@ -135,8 +128,17 @@ function ReportCardLostPage({
135128
setShouldShowAddressError(true);
136129
return;
137130
}
131+
setIsValidateCodeActionModalVisible(true);
132+
};
133+
134+
const sendValidateCode = () => {
135+
const primaryLogin = account?.primaryLogin ?? '';
136+
137+
if (loginList?.[primaryLogin]?.validateCodeSent) {
138+
return;
139+
}
138140

139-
CardActions.requestReplacementExpensifyCard(physicalCard.cardID, reason?.key as ReplacementReason);
141+
requestValidateCodeAction();
140142
};
141143

142144
const handleOptionSelect = (option: Option) => {
@@ -189,6 +191,18 @@ function ReportCardLostPage({
189191
isLoading={formData?.isLoading}
190192
buttonText={isDamaged ? translate('reportCardLostOrDamaged.shipNewCardButton') : translate('reportCardLostOrDamaged.deactivateCardButton')}
191193
/>
194+
<ValidateCodeActionModal
195+
handleSubmitForm={handleValidateCodeEntered}
196+
sendValidateCode={sendValidateCode}
197+
validateError={validateError}
198+
clearError={() => {
199+
CardActions.clearCardListErrors(physicalCard.cardID);
200+
}}
201+
onClose={() => setIsValidateCodeActionModalVisible(false)}
202+
isVisible={isValidateCodeActionModalVisible}
203+
title={translate('cardPage.validateCardTitle')}
204+
description={translate('cardPage.enterMagicCode', {contactMethod: account?.primaryLogin ?? ''})}
205+
/>
192206
</>
193207
) : (
194208
<>
@@ -215,14 +229,4 @@ function ReportCardLostPage({
215229

216230
ReportCardLostPage.displayName = 'ReportCardLostPage';
217231

218-
export default withOnyx<ReportCardLostPageProps, ReportCardLostPageOnyxProps>({
219-
privatePersonalDetails: {
220-
key: ONYXKEYS.PRIVATE_PERSONAL_DETAILS,
221-
},
222-
cardList: {
223-
key: ONYXKEYS.CARD_LIST,
224-
},
225-
formData: {
226-
key: ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM,
227-
},
228-
})(ReportCardLostPage);
232+
export default ReportCardLostPage;

src/pages/settings/Wallet/ReportVirtualCardFraudPage.tsx

+47-10
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import type {StackScreenProps} from '@react-navigation/stack';
2-
import React, {useCallback, useEffect} from 'react';
3-
import {InteractionManager, View} from 'react-native';
2+
import React, {useCallback, useEffect, useState} from 'react';
3+
import {View} from 'react-native';
44
import {useOnyx} from 'react-native-onyx';
55
import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton';
66
import HeaderWithBackButton from '@components/HeaderWithBackButton';
77
import ScreenWrapper from '@components/ScreenWrapper';
88
import Text from '@components/Text';
9+
import ValidateCodeActionModal from '@components/ValidateCodeActionModal';
910
import useLocalize from '@hooks/useLocalize';
1011
import usePrevious from '@hooks/usePrevious';
1112
import useThemeStyles from '@hooks/useThemeStyles';
13+
import {requestValidateCodeAction} from '@libs/actions/User';
1214
import * as ErrorUtils from '@libs/ErrorUtils';
1315
import Navigation from '@libs/Navigation/Navigation';
1416
import type {SettingsNavigatorParamList} from '@libs/Navigation/types';
@@ -28,20 +30,20 @@ function ReportVirtualCardFraudPage({
2830
}: ReportVirtualCardFraudPageProps) {
2931
const styles = useThemeStyles();
3032
const {translate} = useLocalize();
33+
const [loginList] = useOnyx(ONYXKEYS.LOGIN_LIST);
34+
const [account] = useOnyx(ONYXKEYS.ACCOUNT);
3135
const [cardList] = useOnyx(ONYXKEYS.CARD_LIST);
3236
const [formData] = useOnyx(ONYXKEYS.FORMS.REPORT_VIRTUAL_CARD_FRAUD);
37+
const primaryLogin = account?.primaryLogin ?? '';
38+
const loginData = loginList?.[primaryLogin];
3339

3440
const virtualCard = cardList?.[cardID];
3541
const virtualCardError = ErrorUtils.getLatestErrorMessage(virtualCard);
42+
const validateError = ErrorUtils.getLatestErrorMessageField(virtualCard);
3643

37-
const prevIsLoading = usePrevious(formData?.isLoading);
44+
const [isValidateCodeActionModalVisible, setIsValidateCodeActionModalVisible] = useState(false);
3845

39-
const submit = useCallback(() => {
40-
Navigation.dismissModal();
41-
InteractionManager.runAfterInteractions(() => {
42-
Card.reportVirtualExpensifyCardFraud(virtualCard);
43-
});
44-
}, [virtualCard]);
46+
const prevIsLoading = usePrevious(formData?.isLoading);
4547

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

59+
const handleValidateCodeEntered = useCallback(
60+
(validateCode: string) => {
61+
if (!virtualCard) {
62+
return;
63+
}
64+
Card.reportVirtualExpensifyCardFraud(virtualCard, validateCode);
65+
},
66+
[virtualCard],
67+
);
68+
69+
const sendValidateCode = () => {
70+
if (loginData?.validateCodeSent) {
71+
return;
72+
}
73+
74+
requestValidateCodeAction();
75+
};
76+
77+
const handleSubmit = useCallback(() => {
78+
setIsValidateCodeActionModalVisible(true);
79+
}, [setIsValidateCodeActionModalVisible]);
80+
5781
if (isEmptyObject(virtualCard)) {
5882
return <NotFoundPage />;
5983
}
@@ -68,12 +92,25 @@ function ReportVirtualCardFraudPage({
6892
<Text style={[styles.webViewStyles.baseFontStyle, styles.mh5]}>{translate('reportFraudPage.description')}</Text>
6993
<FormAlertWithSubmitButton
7094
isAlertVisible={!!virtualCardError}
71-
onSubmit={submit}
95+
onSubmit={handleSubmit}
7296
message={virtualCardError}
7397
isLoading={formData?.isLoading}
7498
buttonText={translate('reportFraudPage.deactivateCard')}
7599
containerStyles={[styles.m5]}
76100
/>
101+
<ValidateCodeActionModal
102+
handleSubmitForm={handleValidateCodeEntered}
103+
sendValidateCode={sendValidateCode}
104+
validateError={validateError}
105+
clearError={() => {
106+
Card.clearCardListErrors(virtualCard.cardID);
107+
}}
108+
onClose={() => setIsValidateCodeActionModalVisible(false)}
109+
isVisible={isValidateCodeActionModalVisible}
110+
title={translate('cardPage.validateCardTitle')}
111+
description={translate('cardPage.enterMagicCode', {contactMethod: account?.primaryLogin ?? ''})}
112+
hasMagicCodeBeenSent={!!loginData?.validateCodeSent}
113+
/>
77114
</View>
78115
</ScreenWrapper>
79116
);

0 commit comments

Comments
 (0)