Skip to content

Commit a587e8d

Browse files
authored
Merge pull request Expensify#86939 from callstack-internal/perf/decompose-decision-modal-moneyreportheader
Decompose DecisionModal inline state into useDecisionModal hook in MoneyReportHeader
2 parents ca2cdf6 + 8529261 commit a587e8d

4 files changed

Lines changed: 121 additions & 37 deletions

File tree

src/components/DecisionModal.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ type DecisionModalProps = {
5050

5151
/** Whether modal is visible */
5252
isVisible: boolean;
53+
54+
/** Whether to handle browser navigation back to close the modal */
55+
shouldHandleNavigationBack?: boolean;
5356
};
5457

5558
function DecisionModal({
@@ -67,6 +70,7 @@ function DecisionModal({
6770
isFirstOptionSuccess = true,
6871
isSecondOptionSuccess = false,
6972
isSecondOptionDanger = false,
73+
shouldHandleNavigationBack,
7074
}: DecisionModalProps) {
7175
const styles = useThemeStyles();
7276

@@ -77,6 +81,7 @@ function DecisionModal({
7781
type={isSmallScreenWidth ? CONST.MODAL.MODAL_TYPE.BOTTOM_DOCKED : CONST.MODAL.MODAL_TYPE.CONFIRM}
7882
innerContainerStyle={styles.pv0}
7983
onModalHide={onModalHide}
84+
shouldHandleNavigationBack={shouldHandleNavigationBack}
8085
>
8186
<ScrollView contentContainerStyle={styles.m5}>
8287
<View>
@@ -112,4 +117,5 @@ function DecisionModal({
112117
);
113118
}
114119

120+
export type {DecisionModalProps};
115121
export default DecisionModal;
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import React, {useState} from 'react';
2+
import type {DecisionModalProps} from '@components/DecisionModal';
3+
import DecisionModal from '@components/DecisionModal';
4+
import useResponsiveLayout from '@hooks/useResponsiveLayout';
5+
import {ModalActions} from './ModalContext';
6+
import type {ModalProps} from './ModalContext';
7+
8+
type DecisionModalWrapperProps = ModalProps & Omit<DecisionModalProps, 'onClose' | 'onSecondOptionSubmit' | 'onFirstOptionSubmit' | 'isVisible' | 'isSmallScreenWidth'>;
9+
10+
function DecisionModalWrapper({closeModal, onModalHide, ...props}: DecisionModalWrapperProps) {
11+
const [isVisible, setIsVisible] = useState(true);
12+
const [closeAction, setCloseAction] = useState<typeof ModalActions.CONFIRM | typeof ModalActions.CLOSE>(ModalActions.CLOSE);
13+
// We need to use isSmallScreenWidth here because the DecisionModal breaks in RHP with shouldUseNarrowLayout.
14+
// eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
15+
const {isSmallScreenWidth} = useResponsiveLayout();
16+
17+
const handleFirstOption = () => {
18+
setCloseAction(ModalActions.CONFIRM);
19+
setIsVisible(false);
20+
};
21+
22+
const handleSecondOption = () => {
23+
setCloseAction(ModalActions.CLOSE);
24+
setIsVisible(false);
25+
};
26+
27+
const handleModalHide = () => {
28+
if (isVisible) {
29+
return;
30+
}
31+
closeModal({action: closeAction});
32+
onModalHide?.();
33+
};
34+
35+
return (
36+
<DecisionModal
37+
// Spreading is needed to forward all modal configuration props from the wrapper to the underlying DecisionModal.
38+
// eslint-disable-next-line react/jsx-props-no-spreading
39+
{...props}
40+
isVisible={isVisible}
41+
isSmallScreenWidth={isSmallScreenWidth}
42+
onFirstOptionSubmit={handleFirstOption}
43+
onSecondOptionSubmit={handleSecondOption}
44+
onClose={handleSecondOption}
45+
onModalHide={handleModalHide}
46+
/>
47+
);
48+
}
49+
50+
export default DecisionModalWrapper;

src/components/MoneyReportHeader.tsx

Lines changed: 38 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import useConfirmModal from '@hooks/useConfirmModal';
1414
import useConfirmPendingRTERAndProceed from '@hooks/useConfirmPendingRTERAndProceed';
1515
import {useCurrencyListActions} from '@hooks/useCurrencyList';
1616
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
17+
import useDecisionModal from '@hooks/useDecisionModal';
1718
import useDefaultExpensePolicy from '@hooks/useDefaultExpensePolicy';
1819
import useDeleteTransactions from '@hooks/useDeleteTransactions';
1920
import useDuplicateTransactionsAndViolations from '@hooks/useDuplicateTransactionsAndViolations';
@@ -158,7 +159,6 @@ import ActivityIndicator from './ActivityIndicator';
158159
import Button from './Button';
159160
import ButtonWithDropdownMenu from './ButtonWithDropdownMenu';
160161
import type {ButtonWithDropdownMenuRef, DropdownOption} from './ButtonWithDropdownMenu/types';
161-
import DecisionModal from './DecisionModal';
162162
import {useDelegateNoAccessActions, useDelegateNoAccessState} from './DelegateNoAccessModalProvider';
163163
import Header from './Header';
164164
import HeaderLoadingBar from './HeaderLoadingBar';
@@ -363,7 +363,6 @@ function MoneyReportHeader({reportID: reportIDProp, shouldDisplayBackButton = fa
363363
}, [isExported, reportActions]);
364364

365365
const transactionViolations = useTransactionViolations(transaction?.transactionID);
366-
const [downloadErrorModalVisible, setDownloadErrorModalVisible] = useState(false);
367366
const [isPDFModalVisible, setIsPDFModalVisible] = useState(false);
368367
const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED);
369368
const [isTrackIntentUser] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {selector: isTrackIntentUserSelector});
@@ -384,6 +383,24 @@ function MoneyReportHeader({reportID: reportIDProp, shouldDisplayBackButton = fa
384383

385384
const [exportModalStatus, setExportModalStatus] = useState<ExportType | null>(null);
386385
const {showConfirmModal} = useConfirmModal();
386+
const {showDecisionModal} = useDecisionModal();
387+
388+
const showOfflineModal = () => {
389+
showDecisionModal({
390+
title: translate('common.youAppearToBeOffline'),
391+
prompt: translate('common.offlinePrompt'),
392+
secondOptionText: translate('common.buttonConfirm'),
393+
});
394+
};
395+
396+
const showDownloadErrorModal = () => {
397+
showDecisionModal({
398+
title: translate('common.downloadFailedTitle'),
399+
prompt: translate('common.downloadFailedDescription'),
400+
secondOptionText: translate('common.buttonConfirm'),
401+
});
402+
};
403+
387404
const {isPaidAnimationRunning, isApprovedAnimationRunning, isSubmittingAnimationRunning, startAnimation, stopAnimation, startApprovedAnimation, startSubmittingAnimation} =
388405
usePaymentAnimations();
389406
const styles = useThemeStyles();
@@ -516,7 +533,6 @@ function MoneyReportHeader({reportID: reportIDProp, shouldDisplayBackButton = fa
516533

517534
const [isDuplicateActive, temporarilyDisableDuplicateAction] = useThrottledButtonState(handleDuplicateReset);
518535

519-
const [isDownloadErrorModalVisible, setIsDownloadErrorModalVisible] = useState(false);
520536
const [isHoldEducationalModalVisible, setIsHoldEducationalModalVisible] = useState(false);
521537
const [rejectModalAction, setRejectModalAction] = useState<ValueOf<
522538
typeof CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.HOLD | typeof CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.REJECT | typeof CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.REJECT_BULK
@@ -532,7 +548,6 @@ function MoneyReportHeader({reportID: reportIDProp, shouldDisplayBackButton = fa
532548

533549
const shouldDisplayNarrowMoreButton = !shouldDisplayNarrowVersion || isWideRHPDisplayedOnWideLayout || isSuperWideRHPDisplayedOnWideLayout;
534550

535-
const [offlineModalVisible, setOfflineModalVisible] = useState(false);
536551
const {showNonReimbursablePaymentErrorModal, shouldBlockDirectPayment, nonReimbursablePaymentErrorDecisionModal} = useNonReimbursablePaymentModal(moneyRequestReport, transactions);
537552

538553
const showExportProgressModal = useCallback(() => {
@@ -547,7 +562,11 @@ function MoneyReportHeader({reportID: reportIDProp, shouldDisplayBackButton = fa
547562
const beginExportWithTemplate = useCallback(
548563
(templateName: string, templateType: string, transactionIDList: string[], policyID?: string) => {
549564
if (isOffline) {
550-
setOfflineModalVisible(true);
565+
showDecisionModal({
566+
title: translate('common.youAppearToBeOffline'),
567+
prompt: translate('common.offlinePrompt'),
568+
secondOptionText: translate('common.buttonConfirm'),
569+
});
551570
return;
552571
}
553572

@@ -570,7 +589,7 @@ function MoneyReportHeader({reportID: reportIDProp, shouldDisplayBackButton = fa
570589
policyID,
571590
});
572591
},
573-
[isOffline, moneyRequestReport, showExportProgressModal, clearSelectedTransactions],
592+
[isOffline, moneyRequestReport, showExportProgressModal, clearSelectedTransactions, showDecisionModal, translate],
574593
);
575594

576595
const isOnSearch = route.name.toLowerCase().startsWith('search');
@@ -583,8 +602,8 @@ function MoneyReportHeader({reportID: reportIDProp, shouldDisplayBackButton = fa
583602
reportActions,
584603
allTransactionsLength: transactions.length,
585604
session,
586-
onExportFailed: () => setIsDownloadErrorModalVisible(true),
587-
onExportOffline: () => setOfflineModalVisible(true),
605+
onExportFailed: showDownloadErrorModal,
606+
onExportOffline: showOfflineModal,
588607
policy,
589608
beginExportWithTemplate: (templateName, templateType, transactionIDList, policyID) => beginExportWithTemplate(templateName, templateType, transactionIDList, policyID),
590609
isOnSearch,
@@ -1152,7 +1171,11 @@ function MoneyReportHeader({reportID: reportIDProp, shouldDisplayBackButton = fa
11521171
return;
11531172
}
11541173
if (isOffline) {
1155-
setOfflineModalVisible(true);
1174+
showDecisionModal({
1175+
title: translate('common.youAppearToBeOffline'),
1176+
prompt: translate('common.offlinePrompt'),
1177+
secondOptionText: translate('common.buttonConfirm'),
1178+
});
11561179
return;
11571180
}
11581181
exportReportToCSV(
@@ -1161,7 +1184,11 @@ function MoneyReportHeader({reportID: reportIDProp, shouldDisplayBackButton = fa
11611184
transactionIDList: transactionIDs,
11621185
},
11631186
() => {
1164-
setDownloadErrorModalVisible(true);
1187+
showDecisionModal({
1188+
title: translate('common.downloadFailedTitle'),
1189+
prompt: translate('common.downloadFailedDescription'),
1190+
secondOptionText: translate('common.buttonConfirm'),
1191+
});
11651192
},
11661193
translate,
11671194
);
@@ -1236,6 +1263,7 @@ function MoneyReportHeader({reportID: reportIDProp, shouldDisplayBackButton = fa
12361263
isExported,
12371264
exportTemplates,
12381265
beginExportWithTemplate,
1266+
showDecisionModal,
12391267
]);
12401268

12411269
const primaryActionComponent = (
@@ -2334,24 +2362,6 @@ function MoneyReportHeader({reportID: reportIDProp, shouldDisplayBackButton = fa
23342362
onNonReimbursablePaymentError={showNonReimbursablePaymentErrorModal}
23352363
/>
23362364
)}
2337-
<DecisionModal
2338-
title={translate('common.downloadFailedTitle')}
2339-
prompt={translate('common.downloadFailedDescription')}
2340-
isSmallScreenWidth={isSmallScreenWidth}
2341-
onSecondOptionSubmit={() => setDownloadErrorModalVisible(false)}
2342-
secondOptionText={translate('common.buttonConfirm')}
2343-
isVisible={downloadErrorModalVisible}
2344-
onClose={() => setDownloadErrorModalVisible(false)}
2345-
/>
2346-
<DecisionModal
2347-
title={translate('common.downloadFailedTitle')}
2348-
prompt={translate('common.downloadFailedDescription')}
2349-
isSmallScreenWidth={isSmallScreenWidth}
2350-
onSecondOptionSubmit={() => setIsDownloadErrorModalVisible(false)}
2351-
secondOptionText={translate('common.buttonConfirm')}
2352-
isVisible={isDownloadErrorModalVisible}
2353-
onClose={() => setIsDownloadErrorModalVisible(false)}
2354-
/>
23552365
{!!rejectModalAction && (
23562366
<HoldOrRejectEducationalModal
23572367
onClose={dismissRejectModalBasedOnAction}
@@ -2364,15 +2374,6 @@ function MoneyReportHeader({reportID: reportIDProp, shouldDisplayBackButton = fa
23642374
onConfirm={dismissModalAndUpdateUseHold}
23652375
/>
23662376
)}
2367-
<DecisionModal
2368-
title={translate('common.youAppearToBeOffline')}
2369-
prompt={translate('common.offlinePrompt')}
2370-
isSmallScreenWidth={isSmallScreenWidth}
2371-
onSecondOptionSubmit={() => setOfflineModalVisible(false)}
2372-
secondOptionText={translate('common.buttonConfirm')}
2373-
isVisible={offlineModalVisible}
2374-
onClose={() => setOfflineModalVisible(false)}
2375-
/>
23762377
{nonReimbursablePaymentErrorDecisionModal}
23772378
<Modal
23782379
onClose={() => {

src/hooks/useDecisionModal.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import DecisionModalWrapper from '@components/Modal/Global/DecisionModalWrapper';
2+
import type {ModalProps} from '@components/Modal/Global/ModalContext';
3+
import {useModal} from '@components/Modal/Global/ModalContext';
4+
5+
type DecisionModalOptions = Omit<React.ComponentProps<typeof DecisionModalWrapper>, keyof ModalProps>;
6+
7+
const useDecisionModal = () => {
8+
const context = useModal();
9+
10+
const showDecisionModal = (options: DecisionModalOptions) => {
11+
return context.showModal({
12+
component: DecisionModalWrapper,
13+
props: {
14+
shouldHandleNavigationBack: true,
15+
...options,
16+
},
17+
});
18+
};
19+
20+
return {
21+
...context,
22+
closeModal: () => context.closeModal(),
23+
showDecisionModal,
24+
};
25+
};
26+
27+
export default useDecisionModal;

0 commit comments

Comments
 (0)