Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6474,6 +6474,11 @@ const CONST = {
ERROR: 'error',
INFO: 'info',
},
FILE_DOWNLOAD_STATUS: {
GENERATING: 'generating',
READY: 'ready',
FAILED: 'failed',
},
REPORT_DETAILS_MENU_ITEM: {
GO_TO_ROOM: 'goToRoom',
MEMBERS: 'member',
Expand Down
34 changes: 18 additions & 16 deletions src/components/ExpensifyCardStatementPDFDownloadModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,24 @@ import useOnyx from '@hooks/useOnyx';
import {downloadExpensifyCardStatementPDF} from '@libs/ExpensifyCardStatementUtils';
import type {ExpensifyCardStatementParams} from '@libs/ExpensifyCardStatementUtils';

import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';

import React from 'react';

import PDFDownloadModal from './PDFDownloadModal';
import type {ModalProps} from './Modal/Global/ModalContext';

type ExpensifyCardStatementPDFDownloadModalProps = {
import FileDownloadModalWrapper from './Modal/Global/FileDownloadModalWrapper';

type ExpensifyCardStatementPDFDownloadModalProps = ModalProps & {
statementParams: ExpensifyCardStatementParams;
isVisible: boolean;
onClose: () => void;
onModalHide?: () => void;
};

function ExpensifyCardStatementPDFDownloadModal({statementParams, isVisible, onClose, onModalHide}: ExpensifyCardStatementPDFDownloadModalProps) {
/**
* Rendered by the global modal stack, so its Onyx subscription lives as long as the modal does. The caller that opened
* it can unmount while the statement is generating and the modal still picks up the file.
*/
function ExpensifyCardStatementPDFDownloadModal({statementParams, closeModal, resolveModal}: ExpensifyCardStatementPDFDownloadModalProps) {
const [expensifyCardStatement] = useOnyx(ONYXKEYS.EXPENSIFY_CARD_STATEMENT);
const [session] = useOnyx(ONYXKEYS.SESSION);
const {translate} = useLocalize();
Expand All @@ -32,18 +36,17 @@ function ExpensifyCardStatementPDFDownloadModal({statementParams, isVisible, onC
const isGeneratingPDF = expensifyCardStatement?.isGenerating === true;
const statementFileName = statementKey ? expensifyCardStatement?.[statementKey] : undefined;
const hasFinishedPDFDownload = !!statementKey && !isGeneratingPDF && typeof statementFileName === 'string' && statementFileName.length > 0;
const message = hasFinishedPDFDownload ? translate('reportDetailsPage.successPDF') : translate('reportDetailsPage.waitForPDF');

return (
<PDFDownloadModal
isVisible={isVisible}
onClose={onClose}
onModalHide={onModalHide}
hasFinishedPDFDownload={hasFinishedPDFDownload}
message={message}
<FileDownloadModalWrapper
closeModal={closeModal}
resolveModal={resolveModal}
title={translate('reportDetailsPage.generatingPDF')}
status={hasFinishedPDFDownload ? CONST.FILE_DOWNLOAD_STATUS.READY : CONST.FILE_DOWNLOAD_STATUS.GENERATING}
message={hasFinishedPDFDownload ? translate('reportDetailsPage.successPDF') : translate('reportDetailsPage.waitForPDF')}
shouldCloseOnDownload
shouldUseSuccessButton
onDownloadPDF={() => {
onDownload={() => {
if (!statementKey || typeof statementFileName !== 'string') {
return;
}
Expand All @@ -53,6 +56,5 @@ function ExpensifyCardStatementPDFDownloadModal({statementParams, isVisible, onC
);
}

ExpensifyCardStatementPDFDownloadModal.displayName = 'ExpensifyCardStatementPDFDownloadModal';

export default ExpensifyCardStatementPDFDownloadModal;
export type {ExpensifyCardStatementPDFDownloadModalProps};
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
import ActivityIndicator from '@components/ActivityIndicator';
import Button from '@components/Button';
import HeaderTitle from '@components/HeaderTitle';
import Icon from '@components/Icon';
import {PressableWithFeedback} from '@components/Pressable';
import Text from '@components/Text';

import useBottomSafeSafeAreaPaddingStyle from '@hooks/useBottomSafeSafeAreaPaddingStyle';
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
Expand All @@ -7,34 +14,32 @@ import useThemeStyles from '@hooks/useThemeStyles';

import CONST from '@src/CONST';

import type {ValueOf} from 'type-fest';

import React, {useEffect, useRef} from 'react';
import {View} from 'react-native';

import ActivityIndicator from './ActivityIndicator';
import Button from './Button';
import HeaderTitle from './HeaderTitle';
import Icon from './Icon';
import Modal from './Modal';
import {PressableWithFeedback} from './Pressable';
import Text from './Text';
import Modal from '.';

type PDFDownloadModalProps = {
/** Whether the PDF has finished generating and is ready to download */
hasFinishedPDFDownload: boolean;
type FileDownloadStatus = ValueOf<typeof CONST.FILE_DOWNLOAD_STATUS>;

/** Whether the PDF generation failed, so there is nothing left to wait for or download */
hasPDFDownloadFailed?: boolean;
type FileDownloadModalProps = {
/** Header text shown at the top of the modal */
title: string;

/** Whether the file is still generating, ready to download, or failed to generate */
status: FileDownloadStatus;

/** Message shown under the header (generating / ready / error copy) */
message: string;

/** Downloads the generated PDF; called on auto-download and on the download button press */
onDownloadPDF: () => void;
/** Downloads the generated file; called on auto-download and on the download button press */
onDownload: () => void;

/** Whether pressing the download button also closes the modal */
shouldCloseOnDownload?: boolean;

/** Whether the download button uses the success (green) style once the PDF is ready */
/** Whether the download button uses the success (green) style once the file is ready */
shouldUseSuccessButton?: boolean;

isVisible: boolean;
Expand All @@ -50,23 +55,23 @@ type PDFDownloadModalProps = {
};

/**
* Shared modal for OldDot-generated PDF downloads (report PDFs, Expensify Card statements, etc.). It shows a
* generating/ready state, auto-downloads once generation finishes while the modal is open, and offers a manual
* download button. Callers supply the status, copy, and download action for their specific PDF.
* Shared modal for server-generated file downloads (report PDFs, Expensify Card statements, etc.). It shows a
* generating/ready/failed state, auto-downloads once generation finishes while the modal is open, and offers a manual
* download button. Callers supply the status, copy, and download action for their specific file.
*/
function PDFDownloadModal({
hasFinishedPDFDownload,
hasPDFDownloadFailed = false,
function FileDownloadModal({
title,
status,
message,
onDownloadPDF,
onDownload,
shouldCloseOnDownload = false,
shouldUseSuccessButton = false,
isVisible,
shouldTreatModalAsCovering = false,
onClose,
onModalHide,
}: PDFDownloadModalProps) {
const shouldAutoDownloadPDF = useRef(false);
}: FileDownloadModalProps) {
const shouldAutoDownload = useRef(false);

const {translate} = useLocalize();
// We need to use isSmallScreenWidth here because the Modal breaks in RHP with shouldUseNarrowLayout.
Expand All @@ -76,17 +81,20 @@ function PDFDownloadModal({
const theme = useTheme();
const expensifyIcons = useMemoizedLazyExpensifyIcons(['Close']);

const isReady = status === CONST.FILE_DOWNLOAD_STATUS.READY;
const hasFailed = status === CONST.FILE_DOWNLOAD_STATUS.FAILED;

useEffect(() => {
shouldAutoDownloadPDF.current = isVisible;
shouldAutoDownload.current = isVisible;
}, [isVisible]);

useEffect(() => {
if (!isVisible || !shouldAutoDownloadPDF.current || !hasFinishedPDFDownload) {
if (!isVisible || !shouldAutoDownload.current || !isReady) {
return;
}
onDownloadPDF();
shouldAutoDownloadPDF.current = false;
}, [hasFinishedPDFDownload, isVisible, onDownloadPDF]);
onDownload();
shouldAutoDownload.current = false;
}, [isReady, isVisible, onDownload]);

const bottomSafeAreaPaddingStyle = useBottomSafeSafeAreaPaddingStyle({
addBottomSafeAreaPadding: isSmallScreenWidth,
Expand All @@ -110,13 +118,13 @@ function PDFDownloadModal({
<View style={[styles.flex1]}>
<View style={[styles.flexRow]}>
<HeaderTitle>
<HeaderTitle.Text>{translate('reportDetailsPage.generatingPDF')}</HeaderTitle.Text>
<HeaderTitle.Text>{title}</HeaderTitle.Text>
</HeaderTitle>
</View>
<Text style={[styles.mt5, styles.textAlignLeft]}>{message}</Text>
</View>

{!hasFinishedPDFDownload && !hasPDFDownloadFailed && (
{status === CONST.FILE_DOWNLOAD_STATUS.GENERATING && (
<View style={[styles.dFlex, styles.justifyContentEnd]}>
<ActivityIndicator
size={CONST.ACTIVITY_INDICATOR_SIZE.SMALL}
Expand All @@ -128,20 +136,20 @@ function PDFDownloadModal({
</View>
<Button
style={[styles.mt3, styles.noSelect]}
variant={shouldUseSuccessButton && hasFinishedPDFDownload ? CONST.BUTTON_VARIANT.SUCCESS : undefined}
variant={shouldUseSuccessButton && isReady ? CONST.BUTTON_VARIANT.SUCCESS : undefined}
onPress={() => {
if (!hasFinishedPDFDownload) {
if (!isReady) {
onClose();
return;
}

onDownloadPDF();
onDownload();
if (shouldCloseOnDownload) {
onClose();
}
}}
>
<Button.Text>{hasFinishedPDFDownload ? translate('common.download') : translate(hasPDFDownloadFailed ? 'common.close' : 'common.cancel')}</Button.Text>
<Button.Text>{isReady ? translate('common.download') : translate(hasFailed ? 'common.close' : 'common.cancel')}</Button.Text>
</Button>
</View>
<PressableWithFeedback
Expand All @@ -161,6 +169,7 @@ function PDFDownloadModal({
);
}

PDFDownloadModal.displayName = 'PDFDownloadModal';
FileDownloadModal.displayName = 'FileDownloadModal';

export default PDFDownloadModal;
export default FileDownloadModal;
export type {FileDownloadModalProps};

This file was deleted.

58 changes: 58 additions & 0 deletions src/components/Modal/Global/FileDownloadModalWrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import type {FileDownloadModalProps} from '@components/Modal/FileDownloadModal';
import FileDownloadModal from '@components/Modal/FileDownloadModal';

import CONST from '@src/CONST';

import type {ValueOf} from 'type-fest';

import React, {useRef, useState} from 'react';

import type {ModalProps} from './ModalContext';

import {ModalActions} from './ModalContext';

type FileDownloadModalWrapperProps = ModalProps & Omit<FileDownloadModalProps, 'isVisible' | 'onClose' | 'onModalHide'>;

function FileDownloadModalWrapper({closeModal, resolveModal, ...props}: FileDownloadModalWrapperProps) {
const [isVisible, setIsVisible] = useState(true);

// The props at the moment the user dismissed the modal. The props keep changing as the file generates until the
// modal has finished hiding, so render these instead to keep a dismissed modal from changing while it fades out.
const [dismissedProps, setDismissedProps] = useState<typeof props>();
const closeActionRef = useRef<ValueOf<typeof ModalActions>>(ModalActions.CLOSE);
const hasClosedModalRef = useRef(false);

const handleClose = () => {
if (!isVisible) {
return;
}

// Dismissing while the file is still generating cancels the download. Once it is ready or has failed, the
// flow has run its course.
closeActionRef.current = props.status === CONST.FILE_DOWNLOAD_STATUS.GENERATING ? ModalActions.CLOSE : ModalActions.CONFIRM;
setDismissedProps(props);
setIsVisible(false);
};

// Remove the modal from the stack only once it has finished hiding, so callers awaiting the show promise run
// their cleanup after the modal is gone.
const handleModalHide = () => {
if (isVisible || hasClosedModalRef.current) {
return;
}
hasClosedModalRef.current = true;
closeModal({action: closeActionRef.current});
};

return (
<FileDownloadModal
{...(dismissedProps ?? props)}
isVisible={isVisible}
onClose={handleClose}
onModalHide={handleModalHide}
/>
);
}

export default FileDownloadModalWrapper;
export type {FileDownloadModalWrapperProps};
2 changes: 1 addition & 1 deletion src/components/Modal/Global/ModalContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -173,5 +173,5 @@ function ModalProvider({children}: {children: React.ReactNode}) {
);
}

export type {ModalProps};
export type {ModalProps, ModalStateChangePayload};
export {ModalProvider, useModal, ModalActions};
32 changes: 0 additions & 32 deletions src/components/Modal/Global/ReportPDFDownloadModalWrapper.tsx

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,9 @@ function MoneyReportHeaderSecondaryActionsInner({reportID, primaryAction, isRepo
const {exportActionEntries} = useExportActions({
reportID,
policy,
onPDFModalOpen: openPDFDownload,
onPDFModalOpen: () => {
openPDFDownload();
},
});

const shareActions = useShareActions({reportID, policy});
Expand Down
Loading
Loading