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

Require validateCode when requesting replacement cards #51147

Merged
merged 29 commits into from
Nov 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
2df4f36
Sort command alphabetically
youssef-lr Oct 21, 2024
e0af600
Move commands to side effect commands list
youssef-lr Oct 21, 2024
4e26e41
Use a side effect request when report virtual card fraud
youssef-lr Oct 21, 2024
e631c0a
Navigate back to the card page on error
youssef-lr Oct 21, 2024
172e11f
Set description of ValidateCodeActionModal
youssef-lr Oct 21, 2024
85a9dd7
Cleanup code
youssef-lr Oct 21, 2024
f60a62f
Make sure all validateCode modals have confirmation button docked
youssef-lr Oct 22, 2024
f53f47f
Handle loading state and display validetCode error
youssef-lr Oct 23, 2024
5845c87
Navigate to new cardID page and fix brief display of 404 page
youssef-lr Oct 24, 2024
2c3e090
Cleanup unused stuff
youssef-lr Oct 25, 2024
d774a7f
Merge branch 'main' into youssef_validateCode_replacement_cards
youssef-lr Oct 25, 2024
00ae18e
Merge branch 'main' into youssef_validateCode_replacement_cards
youssef-lr Oct 25, 2024
de7bf8a
Show validateCode error in the valideCode modal
youssef-lr Oct 26, 2024
9feb1cd
Use makeRequestWithSideEffects when requesting physical replacement card
youssef-lr Oct 26, 2024
0bfa399
Require validate code when replacing physical cards
youssef-lr Oct 26, 2024
e777ee5
Lint
youssef-lr Oct 26, 2024
419167a
More lint
youssef-lr Oct 26, 2024
73e89c1
Don't break existing code
youssef-lr Oct 26, 2024
4cec967
Remove usage of withOnyx from ReportCardLostPage
youssef-lr Oct 27, 2024
37edf51
Merge branch 'main' into youssef_validateCode_replacement_cards
youssef-lr Oct 29, 2024
dc649ea
Fix spinner not showing
youssef-lr Oct 29, 2024
1d830fc
Use ACCOUNT key for handling loading state
youssef-lr Oct 29, 2024
c7b2059
Display validateCode error above submit button
youssef-lr Oct 29, 2024
62b7fea
Merge branch 'main' into youssef_validateCode_replacement_cards
youssef-lr Nov 3, 2024
7ed59ee
Merge branch 'main' into youssef_validateCode_replacement_cards
youssef-lr Nov 6, 2024
e4af3c8
Keep offline pattern
youssef-lr Nov 6, 2024
1868fcb
Remove usage of withOnyx and other lint fixes
youssef-lr Nov 7, 2024
ff17b0e
Merge branch 'main' into youssef_validateCode_replacement_cards
youssef-lr Nov 7, 2024
cc4c172
Lint
youssef-lr Nov 7, 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
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
Loading