Skip to content
3 changes: 3 additions & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,7 @@ const CONST = {
TYPE: {
CSV: 'csv',
PDF: 'pdf',
RECEIPTS: 'receipts',
},
},

Expand Down Expand Up @@ -1464,6 +1465,7 @@ const CONST = {
CANCEL_PAYMENT: 'cancelPayment',
HOLD: 'hold',
DOWNLOAD_PDF: 'downloadPDF',
DOWNLOAD_RECEIPTS: 'downloadReceipts',
PRINT: 'print',
CHANGE_WORKSPACE: 'changeWorkspace',
CHANGE_APPROVER: 'changeApprover',
Expand Down Expand Up @@ -8550,6 +8552,7 @@ const CONST = {
EXPORT: 'MoreMenu-Export',
EXPORT_FILE: 'MoreMenu-ExportFile',
DOWNLOAD_PDF: 'MoreMenu-DownloadPDF',
DOWNLOAD_RECEIPTS: 'MoreMenu-DownloadReceipts',
PRINT: 'MoreMenu-Print',
CLOSE_PDF_MODAL: 'MoreMenu-ClosePDFModal',
SUBMIT: 'MoreMenu-Submit',
Expand Down
60 changes: 50 additions & 10 deletions src/components/ExportDownloadStatusModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,13 @@ function ExportDownloadStatusModal({exportID, isVisible, onClose, failedBody}: E
const exportType = displayedExport?.exportType;
const failedReportCount = displayedExport?.failedReportCount ?? 0;
const reportCount = displayedExport?.reportCount ?? 0;
const receiptCount = displayedExport?.receiptCount;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Handle partial failures for receipt zips

When ExportReceiptsToZip finishes with a usable zip but some receipts failed, the export status includes a failedReceiptCount alongside receiptCount; this modal only reads receiptCount and the existing partial-failure branch only checks failedReportCount, so that case is shown as a full success and auto-downloads without warning that receipts are missing. Please carry the receipt failure count through the Onyx type and use it to render receipt-specific partial-failure copy before users rely on an incomplete archive.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Resolved via ede5e7e

const failedReceiptCount = displayedExport?.failedReceiptCount ?? 0;
const isPreparing = state === CONST.EXPORT_DOWNLOAD.STATE.PREPARING && !shouldSendFromConcierge;
const isConcierge = !!shouldSendFromConcierge;
const isReady = state === CONST.EXPORT_DOWNLOAD.STATE.READY;
const isFailed = state === CONST.EXPORT_DOWNLOAD.STATE.FAILED;
const isPartialFailure = isReady && failedReportCount > 0;
const isEmptyReceipts = isReady && exportType === CONST.EXPORT_DOWNLOAD.TYPE.RECEIPTS && receiptCount === 0;

// Build the secure download URL the same way downloadReportPDF does, so the host always follows
// the app's current environment (instead of the env baked into a backend-built URL) and authenticates
Expand All @@ -82,12 +84,12 @@ function ExportDownloadStatusModal({exportID, isVisible, onClose, failedBody}: E
};

useEffect(() => {
if (!isReady || !fileName || shouldSendFromConcierge) {
if (!isReady || !fileName || shouldSendFromConcierge || isEmptyReceipts) {
return;
}
downloadFile();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isReady, fileName, shouldSendFromConcierge]);
}, [isReady, fileName, shouldSendFromConcierge, isEmptyReceipts]);

const handleSendFromConcierge = () => {
sendExportFileFromConcierge(exportID, displayedExport ?? undefined);
Expand Down Expand Up @@ -149,11 +151,34 @@ function ExportDownloadStatusModal({exportID, isVisible, onClose, failedBody}: E
);
}

if (isReady) {
if (isEmptyReceipts) {
return (
<>
<Text style={[styles.exportDownloadTitle, styles.mb2]}>{translate('exportDownload.readyTitle')}</Text>
{isPartialFailure ? (
<Text style={[styles.exportDownloadTitle, styles.mb2]}>{translate('exportDownload.noReceiptsTitle')}</Text>
<Text style={styles.mb5}>{translate('exportDownload.noReceiptsBody')}</Text>
<Button
text={translate('exportDownload.close')}
onPress={onClose}
style={styles.w100}
/>
</>
);
}

if (isReady) {
const renderPartialBody = () => {
if (exportType === CONST.EXPORT_DOWNLOAD.TYPE.RECEIPTS && failedReceiptCount > 0) {
return (
<Text style={styles.mb5}>
{translate('exportDownload.receiptsPartialBody', {
count: (receiptCount ?? 0) - failedReceiptCount,
total: receiptCount ?? 0,
})}
</Text>
);
}
if (failedReportCount > 0) {
return (
<View style={styles.mb5}>
<RenderHTML
html={translate('exportDownload.readyPartialBody', {
Expand All @@ -162,9 +187,15 @@ function ExportDownloadStatusModal({exportID, isVisible, onClose, failedBody}: E
})}
/>
</View>
) : (
<Text style={styles.mb5}>{translate('exportDownload.readyBody')}</Text>
)}
);
}
return <Text style={styles.mb5}>{translate('exportDownload.readyBody')}</Text>;
};

return (
<>
<Text style={[styles.exportDownloadTitle, styles.mb2]}>{translate('exportDownload.readyTitle')}</Text>
{renderPartialBody()}
<Button
success
text={translate('exportDownload.downloadFile')}
Expand All @@ -176,7 +207,16 @@ function ExportDownloadStatusModal({exportID, isVisible, onClose, failedBody}: E
}

if (isFailed) {
const resolvedFailedBody = failedBody ?? (exportType === CONST.EXPORT_DOWNLOAD.TYPE.CSV ? translate('exportDownload.csvFailedBody') : translate('exportDownload.pdfFailedBody'));
const getDefaultFailedBody = () => {
if (exportType === CONST.EXPORT_DOWNLOAD.TYPE.CSV) {
return translate('exportDownload.csvFailedBody');
}
if (exportType === CONST.EXPORT_DOWNLOAD.TYPE.RECEIPTS) {
return translate('exportDownload.receiptsFailedBody');
}
return translate('exportDownload.pdfFailedBody');
};
const resolvedFailedBody = failedBody ?? getDefaultFailedBody();
return (
<>
<Text style={[styles.exportDownloadTitle, styles.mb2]}>{translate('exportDownload.failedTitle')}</Text>
Expand Down
18 changes: 18 additions & 0 deletions src/hooks/useExportActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types';
import {useExportDownloadStatus} from '@components/MoneyReportHeaderActions/ExportDownloadStatusContext';
import type {PopoverMenuItem} from '@components/PopoverMenu';

import {exportReceiptsToZip} from '@libs/actions/Export';
import {openOldDotLink} from '@libs/actions/Link';
import {exportReportToCSV, exportReportToPDF, exportToIntegration, markAsManuallyExported} from '@libs/actions/Report';
import {getExportTemplates, queueExportSearchWithTemplate} from '@libs/actions/Search';
Expand Down Expand Up @@ -264,6 +265,23 @@ function useExportActions({reportID, policy, onPDFModalOpen}: UseExportActionsPa
exportReportToPDF({reportID: moneyRequestReport.reportID});
},
},
[CONST.REPORT.SECONDARY_ACTIONS.DOWNLOAD_RECEIPTS]: {
value: CONST.REPORT.SECONDARY_ACTIONS.DOWNLOAD_RECEIPTS,
text: translate('common.downloadReceipts'),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Update all generated locales for receipts copy

When the user’s locale is one of the other supported locales (de/fr/it/ja/nl/pl/pt-BR/zh-hans), the new common.downloadReceipts and exportDownload.receiptsFailedBody keys are absent from those generated language files (checked with rg "downloadReceipts|receiptsFailedBody" src/languages/{de,fr,it,ja,nl,pl,pt-BR,zh-hans}.ts). translate() treats missing keys as an error in development and as missing/raw text in production/staging, so opening this new menu item or seeing the receipts failure state is broken outside en/es. Please regenerate/update every locale for both new keys.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Resolved

icon: expensifyIcons.Download,
sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.DOWNLOAD_RECEIPTS,
onSelected: () => {
if (isOffline) {
showOfflineModal();
return;
}
if (!moneyRequestReport?.reportID) {
return;
}
const exportID = exportReceiptsToZip([moneyRequestReport.reportID]);
trackExport(exportID);
},
},
[CONST.REPORT.SECONDARY_ACTIONS.PRINT]: {
value: CONST.REPORT.SECONDARY_ACTIONS.PRINT,
text: translate('common.print'),
Expand Down
6 changes: 6 additions & 0 deletions src/languages/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,7 @@ const translations: TranslationDeepObject<typeof en> = {
perDiem: 'Tagegeld',
validate: 'Validieren',
downloadAsPDF: 'Als PDF herunterladen',
downloadReceipts: 'Belege herunterladen',
downloadAsCSV: 'Als CSV herunterladen',
submitViaPDF: 'Per PDF einreichen',
print: 'Drucken',
Expand Down Expand Up @@ -10355,6 +10356,11 @@ Hier ist ein *Testbeleg*, um dir zu zeigen, wie es funktioniert:`,
failedTitle: 'Export failed',
csvFailedBody: 'Your export could not be completed. Please try again later.',
pdfFailedBody: 'Your file could not be generated. Try again, or reach out to Concierge for help.',
receiptsFailedBody: 'Ihre Belege konnten nicht heruntergeladen werden. Bitte versuchen Sie es später erneut.',
noReceiptsTitle: 'Keine Belege zum Herunterladen',
noReceiptsBody: 'Keine der Ausgaben in diesem Bericht hat herunterladbare Belege.',
receiptsPartialBody: ({count, total}: {count: number; total: number}) =>
`${count} von ${total} Belegen wurden erfolgreich exportiert. Falls der Download nicht automatisch gestartet wurde, verwenden Sie die Schaltfläche unten.`,
readyPartialBody: ({count, total}: {count: number; total: number}) =>
`${count} of ${total} reports exported. If it didn't automatically download, use the button below. See which reports failed in <concierge-link>Concierge</concierge-link>.`,
close: 'Close',
Expand Down
6 changes: 6 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,7 @@ const translations = {
perDiem: 'Per diem',
validate: 'Validate',
downloadAsPDF: 'Download as PDF',
downloadReceipts: 'Download receipts',
downloadAsCSV: 'Download as CSV',
submitViaPDF: 'Submit via PDF',
print: 'Print',
Expand Down Expand Up @@ -10508,6 +10509,11 @@ const translations = {
failedTitle: 'Export failed',
csvFailedBody: 'Your export could not be completed. Please try again later.',
pdfFailedBody: 'Your file could not be generated. Try again, or reach out to Concierge for help.',
receiptsFailedBody: 'Your receipts could not be downloaded. Please try again later.',
noReceiptsTitle: 'No receipts to download',
noReceiptsBody: 'None of the expenses on this report have downloadable receipts.',
receiptsPartialBody: ({count, total}: {count: number; total: number}) =>
`${count} of ${total} receipts were exported successfully. If it didn't automatically download, use the button below.`,
readyPartialBody: ({count, total}: {count: number; total: number}) =>
`${count} of ${total} reports exported. If it didn't automatically download, use the button below. See which reports failed in <concierge-link>Concierge</concierge-link>.`,
close: 'Close',
Expand Down
6 changes: 6 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,7 @@ const translations: TranslationDeepObject<typeof en> = {
perDiem: 'Per diem',
validate: 'Validar',
downloadAsPDF: 'Descargar como PDF',
downloadReceipts: 'Descargar recibos',
downloadAsCSV: 'Descargar como CSV',
submitViaPDF: 'Enviar por PDF',
print: 'Imprimir',
Expand Down Expand Up @@ -10514,6 +10515,11 @@ El plan Controlar empieza en 9 $ por miembro activo al mes.`,
failedTitle: 'Exportación fallida',
csvFailedBody: 'No se pudo completar la exportación. Inténtalo de nuevo más tarde.',
pdfFailedBody: 'Your file could not be generated. Try again, or reach out to Concierge for help.',
receiptsFailedBody: 'No se pudieron descargar los recibos. Inténtalo de nuevo más tarde.',
noReceiptsTitle: 'No hay recibos para descargar',
noReceiptsBody: 'Ninguno de los gastos en este informe tiene recibos descargables.',
receiptsPartialBody: ({count, total}: {count: number; total: number}) =>
`${count} de ${total} recibos se exportaron correctamente. Si no se descargó automáticamente, usa el botón de abajo.`,
readyPartialBody: ({count, total}: {count: number; total: number}) =>
`${count} of ${total} reports exported. If it didn't automatically download, use the button below. See which reports failed in <concierge-link>Concierge</concierge-link>.`,
close: 'Cerrar',
Expand Down
6 changes: 6 additions & 0 deletions src/languages/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,7 @@ const translations: TranslationDeepObject<typeof en> = {
perDiem: 'Indemnité journalière',
validate: 'Valider',
downloadAsPDF: 'Télécharger en PDF',
downloadReceipts: 'Télécharger les reçus',
downloadAsCSV: 'Télécharger au format CSV',
submitViaPDF: 'Soumettre via PDF',
print: 'Imprimer',
Expand Down Expand Up @@ -10389,6 +10390,11 @@ Voici un *reçu test* pour vous montrer comment ça fonctionne :`,
failedTitle: 'Export failed',
csvFailedBody: 'Your export could not be completed. Please try again later.',
pdfFailedBody: 'Your file could not be generated. Try again, or reach out to Concierge for help.',
receiptsFailedBody: "Vos reçus n'ont pas pu être téléchargés. Veuillez réessayer plus tard.",
noReceiptsTitle: 'Aucun reçu à télécharger',
noReceiptsBody: "Aucune des dépenses de ce rapport n'a de reçus téléchargeables.",
receiptsPartialBody: ({count, total}: {count: number; total: number}) =>
`${count} reçus sur ${total} ont été exportés avec succès. Si le téléchargement ne s'est pas lancé automatiquement, utilisez le bouton ci-dessous.`,
readyPartialBody: ({count, total}: {count: number; total: number}) =>
`${count} of ${total} reports exported. If it didn't automatically download, use the button below. See which reports failed in <concierge-link>Concierge</concierge-link>.`,
close: 'Close',
Expand Down
6 changes: 6 additions & 0 deletions src/languages/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,7 @@ const translations: TranslationDeepObject<typeof en> = {
perDiem: 'Diaria',
validate: 'Convalida',
downloadAsPDF: 'Scarica come PDF',
downloadReceipts: 'Scarica ricevute',
downloadAsCSV: 'Scarica come CSV',
submitViaPDF: 'Invia tramite PDF',
print: 'Stampa',
Expand Down Expand Up @@ -10331,6 +10332,11 @@ Ecco una *ricevuta di prova* per mostrarti come funziona:`,
failedTitle: 'Export failed',
csvFailedBody: 'Your export could not be completed. Please try again later.',
pdfFailedBody: 'Your file could not be generated. Try again, or reach out to Concierge for help.',
receiptsFailedBody: 'Non è stato possibile scaricare le ricevute. Riprova più tardi.',
noReceiptsTitle: 'Nessuna ricevuta da scaricare',
noReceiptsBody: 'Nessuna delle spese in questo report ha ricevute scaricabili.',
receiptsPartialBody: ({count, total}: {count: number; total: number}) =>
`${count} ricevute su ${total} sono state esportate con successo. Se il download non è partito automaticamente, usa il pulsante qui sotto.`,
readyPartialBody: ({count, total}: {count: number; total: number}) =>
`${count} of ${total} reports exported. If it didn't automatically download, use the button below. See which reports failed in <concierge-link>Concierge</concierge-link>.`,
close: 'Close',
Expand Down
6 changes: 6 additions & 0 deletions src/languages/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,7 @@ const translations: TranslationDeepObject<typeof en> = {
perDiem: '日当',
validate: '検証',
downloadAsPDF: 'PDFとしてダウンロード',
downloadReceipts: '領収書をダウンロード',
downloadAsCSV: 'CSVとしてダウンロード',
submitViaPDF: 'PDFで提出',
print: '印刷',
Expand Down Expand Up @@ -10193,6 +10194,11 @@ ${reportName}`,
failedTitle: 'Export failed',
csvFailedBody: 'Your export could not be completed. Please try again later.',
pdfFailedBody: 'Your file could not be generated. Try again, or reach out to Concierge for help.',
receiptsFailedBody: '領収書をダウンロードできませんでした。後でもう一度お試しください。',
noReceiptsTitle: 'ダウンロード可能な領収書がありません',
noReceiptsBody: 'このレポートの経費にはダウンロード可能な領収書がありません。',
receiptsPartialBody: ({count, total}: {count: number; total: number}) =>
`${total}件中${count}件の領収書がエクスポートされました。自動的にダウンロードされなかった場合は、下のボタンを使用してください。`,
readyPartialBody: ({count, total}: {count: number; total: number}) =>
`${count} of ${total} reports exported. If it didn't automatically download, use the button below. See which reports failed in <concierge-link>Concierge</concierge-link>.`,
close: 'Close',
Expand Down
6 changes: 6 additions & 0 deletions src/languages/nl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,7 @@ const translations: TranslationDeepObject<typeof en> = {
perDiem: 'Dagvergoeding',
validate: 'Valideren',
downloadAsPDF: 'Downloaden als PDF',
downloadReceipts: 'Bonnetjes downloaden',
downloadAsCSV: 'Downloaden als CSV',
submitViaPDF: 'Indienen via pdf',
print: 'Afdrukken',
Expand Down Expand Up @@ -10301,6 +10302,11 @@ Hier is een *proefbon* om je te laten zien hoe het werkt:`,
failedTitle: 'Export failed',
csvFailedBody: 'Your export could not be completed. Please try again later.',
pdfFailedBody: 'Your file could not be generated. Try again, or reach out to Concierge for help.',
receiptsFailedBody: 'Uw bonnetjes konden niet worden gedownload. Probeer het later opnieuw.',
noReceiptsTitle: 'Geen bonnetjes om te downloaden',
noReceiptsBody: 'Geen van de uitgaven in dit rapport heeft downloadbare bonnetjes.',
receiptsPartialBody: ({count, total}: {count: number; total: number}) =>
`${count} van ${total} bonnetjes zijn succesvol geëxporteerd. Als de download niet automatisch is gestart, gebruik dan de knop hieronder.`,
readyPartialBody: ({count, total}: {count: number; total: number}) =>
`${count} of ${total} reports exported. If it didn't automatically download, use the button below. See which reports failed in <concierge-link>Concierge</concierge-link>.`,
close: 'Close',
Expand Down
6 changes: 6 additions & 0 deletions src/languages/pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,7 @@ const translations: TranslationDeepObject<typeof en> = {
perDiem: 'Dieta',
validate: 'Zatwierdź',
downloadAsPDF: 'Pobierz jako PDF',
downloadReceipts: 'Pobierz paragony',
downloadAsCSV: 'Pobierz jako CSV',
submitViaPDF: 'Prześlij przez PDF',
print: 'Drukuj',
Expand Down Expand Up @@ -10271,6 +10272,11 @@ Oto *paragon testowy*, żeby pokazać Ci, jak to działa:`,
failedTitle: 'Export failed',
csvFailedBody: 'Your export could not be completed. Please try again later.',
pdfFailedBody: 'Your file could not be generated. Try again, or reach out to Concierge for help.',
receiptsFailedBody: 'Nie udało się pobrać paragonów. Spróbuj ponownie później.',
noReceiptsTitle: 'Brak paragonów do pobrania',
noReceiptsBody: 'Żaden z wydatków w tym raporcie nie ma paragonów do pobrania.',
receiptsPartialBody: ({count, total}: {count: number; total: number}) =>
`${count} z ${total} paragonów zostało wyeksportowanych pomyślnie. Jeśli pobieranie nie rozpoczęło się automatycznie, użyj przycisku poniżej.`,
readyPartialBody: ({count, total}: {count: number; total: number}) =>
`${count} of ${total} reports exported. If it didn't automatically download, use the button below. See which reports failed in <concierge-link>Concierge</concierge-link>.`,
close: 'Close',
Expand Down
Loading
Loading