Skip to content
6 changes: 6 additions & 0 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2218,6 +2218,12 @@ const ROUTES = {
// eslint-disable-next-line no-restricted-syntax -- Legacy route generation
getRoute: (policyID: string, backTo?: string) => getUrlWithBackToParam(`workspaces/${policyID}/expensify-card/issue-new`, backTo),
},
WORKSPACE_EXPENSIFY_CARD_ISSUE_NEW_CONFIRM_MAGIC_CODE: {
route: 'workspaces/:policyID/expensify-card/issue-new/confirm-magic-code',

// eslint-disable-next-line no-restricted-syntax -- Legacy route generation
getRoute: (policyID: string, backTo?: string) => getUrlWithBackToParam(`workspaces/${policyID}/expensify-card/issue-new/confirm-magic-code`, backTo),
},
WORKSPACE_EXPENSIFY_CARD_BANK_ACCOUNT: {
route: 'workspaces/:policyID/expensify-card/choose-bank-account',
getRoute: (policyID: string | undefined) => {
Expand Down
1 change: 1 addition & 0 deletions src/SCREENS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,7 @@ const SCREENS = {
EXPENSIFY_CARD_DETAILS: 'Workspace_ExpensifyCard_Details',
EXPENSIFY_CARD_LIMIT: 'Workspace_ExpensifyCard_Limit',
EXPENSIFY_CARD_ISSUE_NEW: 'Workspace_ExpensifyCard_New',
EXPENSIFY_CARD_ISSUE_NEW_CONFIRM_MAGIC_CODE: 'Workspace_ExpensifyCard_New_Confirm_Magic_Code',
EXPENSIFY_CARD_NAME: 'Workspace_ExpensifyCard_Name',
EXPENSIFY_CARD_SELECT_FEED: 'Workspace_ExpensifyCard_Select_Feed',
EXPENSIFY_CARD_LIMIT_TYPE: 'Workspace_ExpensifyCard_LimitType',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -762,6 +762,8 @@ const SettingsModalStackNavigator = createModalStackNavigator<SettingsNavigatorP
[SCREENS.WORKSPACE.COMPANY_CARD_NAME]: () => require<ReactComponentModule>('../../../../pages/workspace/companyCards/WorkspaceCompanyCardEditCardNamePage').default,
[SCREENS.WORKSPACE.COMPANY_CARD_EXPORT]: () => require<ReactComponentModule>('../../../../pages/workspace/companyCards/WorkspaceCompanyCardAccountSelectCardPage').default,
[SCREENS.WORKSPACE.EXPENSIFY_CARD_ISSUE_NEW]: () => require<ReactComponentModule>('../../../../pages/workspace/expensifyCard/issueNew/IssueNewCardPage').default,
[SCREENS.WORKSPACE.EXPENSIFY_CARD_ISSUE_NEW_CONFIRM_MAGIC_CODE]: () =>
require<ReactComponentModule>('../../../../pages/workspace/expensifyCard/issueNew/IssueNewCardConfirmMagicCodePage').default,
[SCREENS.WORKSPACE.EXPENSIFY_CARD_SETTINGS]: () => require<ReactComponentModule>('../../../../pages/workspace/expensifyCard/WorkspaceCardSettingsPage').default,
[SCREENS.WORKSPACE.EXPENSIFY_CARD_SETTINGS_ACCOUNT]: () => require<ReactComponentModule>('../../../../pages/workspace/expensifyCard/WorkspaceSettlementAccountPage').default,
[SCREENS.WORKSPACE.EXPENSIFY_CARD_SETTINGS_FREQUENCY]: () => require<ReactComponentModule>('../../../../pages/workspace/expensifyCard/WorkspaceSettlementFrequencyPage').default,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ const WORKSPACE_TO_RHP: Partial<Record<keyof WorkspaceSplitNavigatorParamList, s
],
[SCREENS.WORKSPACE.EXPENSIFY_CARD]: [
SCREENS.WORKSPACE.EXPENSIFY_CARD_ISSUE_NEW,
SCREENS.WORKSPACE.EXPENSIFY_CARD_ISSUE_NEW_CONFIRM_MAGIC_CODE,
SCREENS.WORKSPACE.EXPENSIFY_CARD_BANK_ACCOUNT,
SCREENS.WORKSPACE.EXPENSIFY_CARD_SETTINGS,
SCREENS.WORKSPACE.EXPENSIFY_CARD_SETTINGS_ACCOUNT,
Expand Down
3 changes: 3 additions & 0 deletions src/libs/Navigation/linkingConfig/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -714,6 +714,9 @@ const config: LinkingOptions<RootNavigatorParamList>['config'] = {
[SCREENS.WORKSPACE.EXPENSIFY_CARD_ISSUE_NEW]: {
path: ROUTES.WORKSPACE_EXPENSIFY_CARD_ISSUE_NEW.route,
},
[SCREENS.WORKSPACE.EXPENSIFY_CARD_ISSUE_NEW_CONFIRM_MAGIC_CODE]: {
path: ROUTES.WORKSPACE_EXPENSIFY_CARD_ISSUE_NEW_CONFIRM_MAGIC_CODE.route,
},
[SCREENS.WORKSPACE.EXPENSIFY_CARD_NAME]: {
path: ROUTES.WORKSPACE_EXPENSIFY_CARD_NAME.route,
},
Expand Down
5 changes: 5 additions & 0 deletions src/libs/Navigation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1153,6 +1153,11 @@ type SettingsNavigatorParamList = {
// eslint-disable-next-line no-restricted-syntax -- `backTo` usages in this file are legacy. Do not add new `backTo` params to screens. See contributingGuides/NAVIGATION.md
backTo?: Routes;
};
[SCREENS.WORKSPACE.EXPENSIFY_CARD_ISSUE_NEW_CONFIRM_MAGIC_CODE]: {
policyID: string;
// eslint-disable-next-line no-restricted-syntax -- `backTo` usages in this file are legacy. Do not add new `backTo` params to screens. See contributingGuides/NAVIGATION.md
backTo?: Routes;
};
[SCREENS.WORKSPACE.EXPENSIFY_CARD_BANK_ACCOUNT]: {
policyID: string;
};
Expand Down
71 changes: 11 additions & 60 deletions src/pages/workspace/expensifyCard/issueNew/ConfirmationStep.tsx
Original file line number Diff line number Diff line change
@@ -1,57 +1,44 @@
import React, {useCallback, useEffect, useRef, useState} from 'react';
import React, {useEffect, useRef} from 'react';
import {View} from 'react-native';
import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton';
import InteractiveStepWrapper from '@components/InteractiveStepWrapper';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
import ValidateCodeActionModal from '@components/ValidateCodeActionModal';
import useDefaultFundID from '@hooks/useDefaultFundID';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useOnyx from '@hooks/useOnyx';
import usePermissions from '@hooks/usePermissions';
import useThemeStyles from '@hooks/useThemeStyles';
import {clearIssueNewCardError, clearIssueNewCardFlow, issueExpensifyCard, setIssueNewCardStepAndData} from '@libs/actions/Card';
import {requestValidateCodeAction, resetValidateActionCodeSent} from '@libs/actions/User';
import {setIssueNewCardStepAndData} from '@libs/actions/Card';
import {resetValidateActionCodeSent} from '@libs/actions/User';
import {getTranslationKeyForLimitType} from '@libs/CardUtils';
import {convertToShortDisplayString} from '@libs/CurrencyUtils';
import {getLatestErrorMessage, getLatestErrorMessageField} from '@libs/ErrorUtils';
import {getLatestErrorMessage} from '@libs/ErrorUtils';
import {getUserNameByEmail} from '@libs/PersonalDetailsUtils';
import Navigation from '@navigation/Navigation';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type {Route} from '@src/ROUTES';
import type {IssueNewCardStep} from '@src/types/onyx/Card';

type ConfirmationStepProps = {
/** ID of the policy that the card will be issued under */
policyID: string | undefined;

/** Route to navigate to */
backTo?: Route;

/** Array of step names */
stepNames: readonly string[];

/** Start from step index */
startStepIndex: number;
};

function ConfirmationStep({policyID, backTo, stepNames, startStepIndex}: ConfirmationStepProps) {
function ConfirmationStep({policyID, stepNames, startStepIndex}: ConfirmationStepProps) {
const {translate} = useLocalize();
const styles = useThemeStyles();
const {isOffline} = useNetwork();
const [account] = useOnyx(ONYXKEYS.ACCOUNT, {canBeMissing: true});
const [issueNewCard] = useOnyx(`${ONYXKEYS.COLLECTION.ISSUE_NEW_EXPENSIFY_CARD}${policyID}`, {canBeMissing: true});
const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {canBeMissing: true});
const validateError = getLatestErrorMessageField(issueNewCard);
const [isValidateCodeActionModalVisible, setIsValidateCodeActionModalVisible] = useState(false);
const data = issueNewCard?.data;
const isSuccessful = issueNewCard?.isSuccessful;
const defaultFundID = useDefaultFundID(policyID);
const {isBetaEnabled} = usePermissions();
const hasApprovalError = !!policy?.errorFields?.approvalMode;
const isAddApprovalEnabled = policy?.approvalMode !== CONST.POLICY.APPROVAL_MODE.OPTIONAL && !hasApprovalError;
const shouldDisableSubmitButton = !isAddApprovalEnabled && data?.limitType === CONST.EXPENSIFY_CARD.LIMIT_TYPES.SMART;
Expand All @@ -63,18 +50,6 @@ function ConfirmationStep({policyID, backTo, stepNames, startStepIndex}: Confirm
resetValidateActionCodeSent();
}, []);

useEffect(() => {
if (!isSuccessful) {
return;
}
setIsValidateCodeActionModalVisible(false);
}, [isSuccessful]);

const submit = (validateCode: string) => {
// NOTE: For Expensify Card UK/EU, the backend will automatically detect the correct feedCountry to use
issueExpensifyCard(defaultFundID, policyID, isBetaEnabled(CONST.BETAS.EXPENSIFY_CARD_EU_UK) ? '' : CONST.COUNTRY.US, validateCode, data);
};

const errorMessage = getLatestErrorMessage(issueNewCard) || (shouldDisableSubmitButton ? translate('workspace.card.issueNewCard.disabledApprovalForSmartLimitError') : '');

const editStep = (step: IssueNewCardStep) => {
Expand All @@ -87,19 +62,6 @@ function ConfirmationStep({policyID, backTo, stepNames, startStepIndex}: Confirm

const translationForLimitType = getTranslationKeyForLimitType(data?.limitType);

const onRedirect = useCallback(() => {
if (!isSuccessful) {
return;
}
if (backTo) {
Navigation.goBack(backTo);
} else {
Navigation.navigate(ROUTES.WORKSPACE_EXPENSIFY_CARD.getRoute(policyID));
}

clearIssueNewCardFlow(policyID);
}, [backTo, policyID, isSuccessful]);

return (
<InteractiveStepWrapper
wrapperID={ConfirmationStep.displayName}
Expand Down Expand Up @@ -158,27 +120,16 @@ function ConfirmationStep({policyID, backTo, stepNames, startStepIndex}: Confirm
isDisabled={isOffline || shouldDisableSubmitButton}
isMessageHtml={shouldDisableSubmitButton}
isLoading={issueNewCard?.isLoading}
onSubmit={() => setIsValidateCodeActionModalVisible(true)}
onSubmit={() => {
if (!policyID) {
return;
}
Navigation.navigate(ROUTES.WORKSPACE_EXPENSIFY_CARD_ISSUE_NEW_CONFIRM_MAGIC_CODE.getRoute(policyID, ROUTES.WORKSPACE_EXPENSIFY_CARD.getRoute(policyID)));
}}
buttonText={translate('workspace.card.issueCard')}
/>
</View>
</ScrollView>
{!!issueNewCard && (
<ValidateCodeActionModal
handleSubmitForm={submit}
isLoading={issueNewCard?.isLoading}
sendValidateCode={requestValidateCodeAction}
validateCodeActionErrorField={data?.cardType === CONST.EXPENSIFY_CARD.CARD_TYPE.PHYSICAL ? 'createExpensifyCard' : 'createAdminIssuedVirtualCard'}
validateError={validateError}
clearError={() => clearIssueNewCardError(policyID)}
onClose={() => setIsValidateCodeActionModalVisible(false)}
isVisible={isValidateCodeActionModalVisible}
title={translate('cardPage.validateCardTitle')}
descriptionPrimary={translate('cardPage.enterMagicCode', {contactMethod: account?.primaryLogin ?? ''})}
onModalHide={onRedirect}
disableAnimation
/>
)}
</InteractiveStepWrapper>
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import React, {useCallback, useEffect} from 'react';
import ValidateCodeActionContent from '@components/ValidateCodeActionModal/ValidateCodeActionContent';
import useDefaultFundID from '@hooks/useDefaultFundID';
import useInitial from '@hooks/useInitial';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
import usePermissions from '@hooks/usePermissions';
import {clearIssueNewCardError, clearIssueNewCardFlow, issueExpensifyCard} from '@libs/actions/Card';
import {requestValidateCodeAction, resetValidateActionCodeSent} from '@libs/actions/User';
import {getLatestErrorMessageField} from '@libs/ErrorUtils';
import Navigation from '@libs/Navigation/Navigation';
import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
import type {SettingsNavigatorParamList} from '@libs/Navigation/types';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';

type IssueNewCardConfirmMagicCodePageProps = PlatformStackScreenProps<SettingsNavigatorParamList, typeof SCREENS.WORKSPACE.EXPENSIFY_CARD_ISSUE_NEW_CONFIRM_MAGIC_CODE>;

function IssueNewCardConfirmMagicCodePage({route}: IssueNewCardConfirmMagicCodePageProps) {
const {translate} = useLocalize();
const policyID = route.params.policyID;
const backTo = route.params.backTo;
const [account] = useOnyx(ONYXKEYS.ACCOUNT, {canBeMissing: false});
const primaryLogin = account?.primaryLogin ?? '';
const [issueNewCard] = useOnyx(`${ONYXKEYS.COLLECTION.ISSUE_NEW_EXPENSIFY_CARD}${policyID}`, {canBeMissing: true});
const validateError = getLatestErrorMessageField(issueNewCard);
const data = issueNewCard?.data;
const isSuccessful = issueNewCard?.isSuccessful;
const defaultFundID = useDefaultFundID(policyID);
const {isBetaEnabled} = usePermissions();
const firstAssigneeEmail = useInitial(issueNewCard?.data?.assigneeEmail);
const shouldUseBackToParam = !firstAssigneeEmail || firstAssigneeEmail === issueNewCard?.data?.assigneeEmail;

useEffect(() => {
if (!isSuccessful) {
return;
}
if (backTo && shouldUseBackToParam) {
Navigation.goBack(backTo);
} else {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Came from this issue
We have a bit specific case when app reopens Expensify Card page after issuing card and swiping right on ios
More context here

Navigation.navigate(ROUTES.WORKSPACE_EXPENSIFY_CARD.getRoute(policyID), {forceReplace: true});
}
clearIssueNewCardFlow(policyID);
}, [backTo, isSuccessful, policyID, shouldUseBackToParam]);

const handleSubmit = useCallback(
(validateCode: string) => {
// NOTE: For Expensify Card UK/EU, the backend will automatically detect the correct feedCountry to use
issueExpensifyCard(defaultFundID, policyID, isBetaEnabled(CONST.BETAS.EXPENSIFY_CARD_EU_UK) ? '' : CONST.COUNTRY.US, validateCode, data);
},
[isBetaEnabled, data, defaultFundID, policyID],
);

const handleClose = useCallback(() => {
resetValidateActionCodeSent();
Navigation.navigate(ROUTES.WORKSPACE_EXPENSIFY_CARD_ISSUE_NEW.getRoute(policyID, backTo));
}, [policyID, backTo]);

return (
<ValidateCodeActionContent
isLoading={issueNewCard?.isLoading}
title={translate('cardPage.validateCardTitle')}
descriptionPrimary={translate('cardPage.enterMagicCode', {contactMethod: primaryLogin})}
sendValidateCode={() => requestValidateCodeAction()}
validateCodeActionErrorField={data?.cardType === CONST.EXPENSIFY_CARD.CARD_TYPE.PHYSICAL ? 'createExpensifyCard' : 'createAdminIssuedVirtualCard'}
handleSubmitForm={handleSubmit}
validateError={validateError}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ PERF-4 (docs)

The inline function () => setValidateError({}) creates a new function instance on every render, which causes child components to recreate their memoized callbacks that depend on clearError. This can trigger unnecessary re-renders.

Wrap this function with useCallback:

const clearError = useCallback(() => setValidateError({}), []);

Then pass it directly:

clearError={clearError}

clearError={() => clearIssueNewCardError(policyID)}
onClose={handleClose}
/>
);
}

IssueNewCardConfirmMagicCodePage.displayName = 'ExpensifyCardVerifyAccountPage';

export default IssueNewCardConfirmMagicCodePage;
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import React, {useEffect, useMemo} from 'react';
import type {OnyxEntry} from 'react-native-onyx';
import DelegateNoAccessWrapper from '@components/DelegateNoAccessWrapper';
import ScreenWrapper from '@components/ScreenWrapper';
import useInitial from '@hooks/useInitial';
import useOnyx from '@hooks/useOnyx';
import {startIssueNewCardFlow} from '@libs/actions/Card';
import Navigation from '@libs/Navigation/Navigation';
Expand Down Expand Up @@ -50,8 +49,6 @@ function IssueNewCardPage({policy, route}: IssueNewCardPageProps) {
const [issueNewCard] = useOnyx(`${ONYXKEYS.COLLECTION.ISSUE_NEW_EXPENSIFY_CARD}${policyID}`, {canBeMissing: true});
const {currentStep} = issueNewCard ?? {};
const backTo = route?.params?.backTo;
const firstAssigneeEmail = useInitial(issueNewCard?.data?.assigneeEmail);
const shouldUseBackToParam = !firstAssigneeEmail || firstAssigneeEmail === issueNewCard?.data?.assigneeEmail;
const [isActingAsDelegate] = useOnyx(ONYXKEYS.ACCOUNT, {selector: isActingAsDelegateSelector, canBeMissing: true});
const stepNames = issueNewCard?.isChangeAssigneeDisabled ? CONST.EXPENSIFY_CARD.ASSIGNEE_EXCLUDED_STEP_NAMES : CONST.EXPENSIFY_CARD.STEP_NAMES;
const startStepIndex = useMemo(() => getStartStepIndex(issueNewCard), [issueNewCard]);
Expand Down Expand Up @@ -107,7 +104,6 @@ function IssueNewCardPage({policy, route}: IssueNewCardPageProps) {
return (
<ConfirmationStep
policyID={policyID}
backTo={shouldUseBackToParam ? backTo : undefined}
stepNames={stepNames}
startStepIndex={startStepIndex}
/>
Expand Down
Loading