diff --git a/Mobile-Expensify b/Mobile-Expensify
index 00ea0c661cfd..6d9ea25d509d 160000
--- a/Mobile-Expensify
+++ b/Mobile-Expensify
@@ -1 +1 @@
-Subproject commit 00ea0c661cfdddeb4d7b5d34d25298a0fa422bcf
+Subproject commit 6d9ea25d509d12843bb9c49a7befb62309d5da5a
diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index 2edd44ea9550..8e7f0c696566 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -800,6 +800,8 @@ const ONYXKEYS = {
WORKSPACE_COMPANY_CARD_FEED_NAME_DRAFT: 'workspaceCompanyCardFeedNameDraft',
EDIT_WORKSPACE_COMPANY_CARD_NAME_FORM: 'editCompanyCardName',
EDIT_WORKSPACE_COMPANY_CARD_NAME_DRAFT_FORM: 'editCompanyCardNameDraft',
+ IMPORT_TRANSACTIONS_FORM: 'importTransactionsForm',
+ IMPORT_TRANSACTIONS_FORM_DRAFT: 'importTransactionsFormDraft',
EDIT_PERSONAL_CARD_NAME_FORM: 'editPersonalCardName',
EDIT_PERSONAL_CARD_NAME_FORM_DRAFT: 'editPersonalCardNameDraft',
WORKSPACE_REPORT_FIELDS_FORM: 'workspaceReportFieldForm',
@@ -1018,6 +1020,7 @@ type OnyxFormValuesMapping = {
[ONYXKEYS.FORMS.WORKSPACE_TAX_CUSTOM_NAME]: FormTypes.WorkspaceTaxCustomName;
[ONYXKEYS.FORMS.WORKSPACE_COMPANY_CARD_FEED_NAME]: FormTypes.WorkspaceCompanyCardFeedName;
[ONYXKEYS.FORMS.EDIT_WORKSPACE_COMPANY_CARD_NAME_FORM]: FormTypes.WorkspaceCompanyCardEditName;
+ [ONYXKEYS.FORMS.IMPORT_TRANSACTIONS_FORM]: FormTypes.ImportTransactionsForm;
[ONYXKEYS.FORMS.EDIT_PERSONAL_CARD_NAME_FORM]: FormTypes.EditPersonalCardNameForm;
[ONYXKEYS.FORMS.WORKSPACE_REPORT_FIELDS_FORM]: FormTypes.WorkspaceReportFieldForm;
[ONYXKEYS.FORMS.WORKSPACE_CATEGORY_DESCRIPTION_HINT_FORM]: FormTypes.WorkspaceCategoryDescriptionHintForm;
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index 070e28a21958..ebb0766c6484 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -393,6 +393,27 @@ const ROUTES = {
},
SETTINGS_WALLET_TRANSFER_BALANCE: 'settings/wallet/transfer-balance',
SETTINGS_WALLET_CHOOSE_TRANSFER_ACCOUNT: 'settings/wallet/choose-transfer-account',
+ SETTINGS_WALLET_IMPORT_TRANSACTIONS: 'settings/wallet/import-transactions',
+ SETTINGS_WALLET_IMPORT_TRANSACTIONS_CARD_NAME: 'settings/wallet/import-transactions/card-name',
+ SETTINGS_WALLET_IMPORT_TRANSACTIONS_CURRENCY: 'settings/wallet/import-transactions/currency',
+ SETTINGS_WALLET_IMPORT_TRANSACTIONS_SPREADSHEET: {
+ route: 'settings/wallet/import-transactions-spreadsheet/:cardID?',
+ getRoute: (cardID?: number) => {
+ if (cardID) {
+ return `settings/wallet/import-transactions-spreadsheet/${cardID}` as const;
+ }
+ return 'settings/wallet/import-transactions-spreadsheet' as const;
+ },
+ },
+ SETTINGS_WALLET_TRANSACTIONS_IMPORTED: {
+ route: 'settings/wallet/transactions-imported/:cardID?',
+ getRoute: (cardID?: number) => {
+ if (cardID) {
+ return `settings/wallet/transactions-imported/${cardID}` as const;
+ }
+ return 'settings/wallet/transactions-imported' as const;
+ },
+ },
SETTINGS_WALLET_REPORT_CARD_LOST_OR_DAMAGED: {
route: 'settings/wallet/card/:cardID/report-card-lost-or-damaged',
getRoute: (cardID: string) => `settings/wallet/card/${cardID}/report-card-lost-or-damaged` as const,
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index 2b3fd09c3008..1871a47bc9c1 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -184,6 +184,11 @@ const SCREENS = {
CARD_MISSING_DETAILS_CONFIRM_MAGIC_CODE: 'Settings_Wallet_Card_MissingDetails_ConfirmMagicCode',
TRANSFER_BALANCE: 'Settings_Wallet_Transfer_Balance',
CHOOSE_TRANSFER_ACCOUNT: 'Settings_Wallet_Choose_Transfer_Account',
+ IMPORT_TRANSACTIONS: 'Settings_Wallet_Import_Transactions',
+ IMPORT_TRANSACTIONS_CARD_NAME: 'Settings_Wallet_Import_Transactions_Card_Name',
+ IMPORT_TRANSACTIONS_CURRENCY: 'Settings_Wallet_Import_Transactions_Currency',
+ IMPORT_TRANSACTIONS_SPREADSHEET: 'Settings_Wallet_Import_Transactions_Spreadsheet',
+ TRANSACTIONS_IMPORTED: 'Settings_Wallet_Transactions_Imported',
ENABLE_PAYMENTS: 'Settings_Wallet_EnablePayments',
CARD_ACTIVATE: 'Settings_Wallet_Card_Activate',
REPORT_VIRTUAL_CARD_FRAUD: 'Settings_Wallet_ReportVirtualCardFraud',
diff --git a/src/components/ImportSpreadsheet.tsx b/src/components/ImportSpreadsheet.tsx
index 084e08ef47a4..b9206f72eb5a 100644
--- a/src/components/ImportSpreadsheet.tsx
+++ b/src/components/ImportSpreadsheet.tsx
@@ -4,6 +4,7 @@ import RNFetchBlob from 'react-native-blob-util';
import type {TupleToUnion} from 'type-fest';
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
+import useOnyx from '@hooks/useOnyx';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';
import {setSpreadsheetData} from '@libs/actions/ImportSpreadsheet';
@@ -13,6 +14,7 @@ import {splitExtensionFromFileName} from '@libs/fileDownload/FileUtils';
import Navigation from '@libs/Navigation/Navigation';
import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
+import ONYXKEYS from '@src/ONYXKEYS';
import type {Route as Routes} from '@src/ROUTES';
import type {FileObject} from '@src/types/utils/Attachment';
import Button from './Button';
@@ -38,6 +40,7 @@ type ImportSpreadsheetProps = {
};
function ImportSpreadsheet({backTo, goTo, isImportingMultiLevelTags}: ImportSpreadsheetProps) {
+ const [importedSpreadsheet] = useOnyx(ONYXKEYS.IMPORTED_SPREADSHEET, {canBeMissing: true});
const icons = useMemoizedLazyExpensifyIcons(['SpreadsheetComputer']);
const styles = useThemeStyles();
const {translate} = useLocalize();
@@ -104,20 +107,33 @@ function ImportSpreadsheet({backTo, goTo, isImportingMultiLevelTags}: ImportSpre
.then((data) => {
return data.text();
})
- .then((text) => XLSX.read(text, {type: 'string'}));
+ .then((text) => XLSX.read(text, {type: 'string', raw: true}));
}
return fetch(fileURI)
.then((data) => {
return data.arrayBuffer();
})
- .then((arrayBuffer) => XLSX.read(new Uint8Array(arrayBuffer), {type: 'buffer'}));
+ .then((arrayBuffer) => XLSX.read(new Uint8Array(arrayBuffer), {type: 'buffer', raw: true}));
};
readWorkbook()
.then((workbook) => {
const worksheet = workbook.Sheets[workbook.SheetNames[0]];
- const data = XLSX.utils.sheet_to_json(worksheet, {header: 1, blankrows: false}) as string[][] | unknown[][];
- const formattedSpreadsheetData = data.map((row) => row.map((cell) => String(cell)));
- setSpreadsheetData(formattedSpreadsheetData, fileURI, file.type, file.name, isImportingMultiLevelTags ?? false)
+ // Use raw: true to preserve original string values from CSV (especially dates)
+ const data = XLSX.utils.sheet_to_json(worksheet, {header: 1, blankrows: false, raw: true}) as string[][] | unknown[][];
+ const formattedSpreadsheetData = data.map((row) =>
+ row.map((cell) => {
+ if (cell == null) {
+ return '';
+ }
+ // Handle primitives (string, number, boolean) directly
+ if (typeof cell === 'string' || typeof cell === 'number' || typeof cell === 'boolean') {
+ return String(cell);
+ }
+ // For objects (Date, arrays, etc.), serialize to JSON
+ return JSON.stringify(cell);
+ }),
+ );
+ setSpreadsheetData(formattedSpreadsheetData, fileURI, file.type, file.name, isImportingMultiLevelTags ?? false, importedSpreadsheet?.importTransactionSettings)
.then(() => {
Navigation.navigate(goTo);
})
diff --git a/src/components/PopoverMenu.tsx b/src/components/PopoverMenu.tsx
index 701e604c9dfd..515879d47305 100644
--- a/src/components/PopoverMenu.tsx
+++ b/src/components/PopoverMenu.tsx
@@ -430,14 +430,7 @@ function BasePopoverMenu({
if (!headerText || enteredSubMenuIndexes.length !== 0) {
return;
}
- return (
-
- {headerText}
-
- );
+ return {headerText};
};
useKeyboardShortcut(
diff --git a/src/languages/de.ts b/src/languages/de.ts
index 8da341287e34..931d672eef17 100644
--- a/src/languages/de.ts
+++ b/src/languages/de.ts
@@ -1057,6 +1057,8 @@ const translations: TranslationDeepObject = {
other: (count: number) =>
`Bitte bestätige die folgenden Details für die ${count} neuen Workspace-Mitglieder, die im Rahmen dieses Uploads hinzugefügt werden. Bestehende Mitglieder erhalten keine Rollenaktualisierungen oder Einladung Nachrichten.`,
}),
+ importTransactionsSuccessfulDescription: ({transactions}: {transactions: number}) =>
+ transactions > 1 ? `${transactions} Transaktionen wurden importiert.` : '1 Transaktion wurde importiert.',
},
receipt: {
upload: 'Beleg hochladen',
@@ -4963,6 +4965,15 @@ _Für ausführlichere Anweisungen [besuchen Sie unsere Hilfeseite](${CONST.NETSU
editStartDateDescription:
'Wählen Sie ein neues Startdatum für Transaktionen. Wir synchronisieren alle Transaktionen ab diesem Datum, ausgenommen diejenigen, die wir bereits importiert haben.',
unassignCardFailedError: 'Aufhebung der Kartenzuweisung fehlgeschlagen.',
+ importTransactions: {
+ title: 'Transaktionen aus Datei importieren',
+ description: 'Bitte passen Sie die Einstellungen für Ihre Datei an, die beim Import angewendet werden.',
+ cardDisplayName: 'Name der Karte',
+ currency: 'Währung',
+ transactionsAreReimbursable: 'Transaktionen sind erstattungsfähig',
+ flipAmountSign: 'Vorzeichen des Betrags umkehren',
+ importButton: 'Transaktionen importieren',
+ },
error: {
workspaceFeedsCouldNotBeLoadedTitle: 'Kartenfeeds konnten nicht geladen werden',
workspaceFeedsCouldNotBeLoadedMessage:
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 62a48239b88b..e637bf914204 100644
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -1073,6 +1073,8 @@ const translations = {
importTagsSuccessfulDescription: ({tags}: {tags: number}) => (tags > 1 ? `${tags} tags have been added.` : '1 tag has been added.'),
importMultiLevelTagsSuccessfulDescription: 'Multi-level tags have been added.',
importPerDiemRatesSuccessfulDescription: ({rates}: {rates: number}) => (rates > 1 ? `${rates} per diem rates have been added.` : '1 per diem rate has been added.'),
+ importTransactionsSuccessfulDescription: ({transactions}: {transactions: number}) =>
+ transactions > 1 ? `${transactions} transactions have been imported.` : '1 transaction has been imported.',
importFailedTitle: 'Import failed',
importFailedDescription: 'Please ensure all fields are filled out correctly and try again. If the problem persists, please reach out to Concierge.',
importDescription: 'Choose which fields to map from your spreadsheet by clicking the dropdown next to each imported column below.',
@@ -4921,6 +4923,15 @@ const translations = {
assignCardFailedError: 'Card assignment failed.',
unassignCardFailedError: 'Card unassignment failed.',
cardAlreadyAssignedError: 'This card is already assigned to a user in another workspace.',
+ importTransactions: {
+ title: 'Import transactions from file',
+ description: 'Please adjust the settings for your file that will be applied on import.',
+ cardDisplayName: 'Card display name',
+ currency: 'Currency',
+ transactionsAreReimbursable: 'Transactions are reimbursable',
+ flipAmountSign: 'Flip amount sign',
+ importButton: 'Import transactions',
+ },
},
expensifyCard: {
issueAndManageCards: 'Issue and manage your Expensify Cards',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 550115b5b902..a04778b2b85b 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -820,6 +820,7 @@ const translations: TranslationDeepObject = {
importTagsSuccessfulDescription: ({tags}) => (tags > 1 ? `Se han agregado ${tags} etiquetas.` : 'Se ha agregado 1 etiqueta.'),
importMultiLevelTagsSuccessfulDescription: 'Etiquetas de nivel múltiple han sido agregadas.',
importPerDiemRatesSuccessfulDescription: ({rates}) => (rates > 1 ? `Se han añadido ${rates} tasas de per diem.` : 'Se ha añadido 1 tasa de per diem.'),
+ importTransactionsSuccessfulDescription: ({transactions}) => (transactions > 1 ? `Se han importado ${transactions} transacciones.` : 'Se ha importado 1 transacción.'),
importSuccessfulTitle: 'Importar categorías',
importDescription: 'Elige qué campos mapear desde tu hoja de cálculo haciendo clic en el menú desplegable junto a cada columna importada a continuación.',
sizeNotMet: 'El archivo adjunto debe ser más grande que 0 bytes.',
@@ -4672,6 +4673,15 @@ ${amount} para ${merchant} - ${date}`,
assignCardFailedError: 'Error al asignar la tarjeta.',
unassignCardFailedError: 'Error al desasignar la tarjeta.',
cardAlreadyAssignedError: 'Esta tarjeta ya está asignada a un usuario en otro espacio de trabajo.',
+ importTransactions: {
+ title: 'Importar transacciones desde archivo',
+ description: 'Por favor, ajusta la configuración de archivo que se aplicará al importar.',
+ cardDisplayName: 'Nombre de la tarjeta',
+ currency: 'Moneda',
+ transactionsAreReimbursable: 'Las transacciones son reembolsables',
+ flipAmountSign: 'Invertir el signo del monto',
+ importButton: 'Importar transacciones',
+ },
},
expensifyCard: {
issueAndManageCards: 'Emitir y gestionar Tarjetas Expensify',
diff --git a/src/languages/fr.ts b/src/languages/fr.ts
index 726c88e995a4..09c6b7a548ce 100644
--- a/src/languages/fr.ts
+++ b/src/languages/fr.ts
@@ -1059,6 +1059,8 @@ const translations: TranslationDeepObject = {
other: (count: number) =>
`Veuillez confirmer les détails ci-dessous pour les ${count} nouveaux membres d’espace de travail qui seront ajoutés dans le cadre de cet import. Les membres existants ne recevront aucune mise à jour de rôle ni message d’invitation.`,
}),
+ importTransactionsSuccessfulDescription: ({transactions}: {transactions: number}) =>
+ transactions > 1 ? `${transactions} transactions ont été importées.` : '1 transaction a été importée.',
},
receipt: {
upload: 'Téléverser un reçu',
@@ -4970,6 +4972,15 @@ _Pour des instructions plus détaillées, [visitez notre site d’aide](${CONST.
editStartDateDescription:
'Choisissez une nouvelle date de début des transactions. Nous synchroniserons toutes les transactions à partir de cette date, en excluant celles que nous avons déjà importées.',
unassignCardFailedError: 'Échec de la désaffectation de la carte.',
+ importTransactions: {
+ title: 'Importer des transactions depuis un fichier',
+ description: 'Veuillez ajuster les paramètres de votre fichier qui seront appliqués lors de l’importation.',
+ cardDisplayName: 'Nom d’affichage de la carte',
+ currency: 'Devise',
+ transactionsAreReimbursable: 'Les transactions sont remboursables',
+ flipAmountSign: 'Inverser le signe du montant',
+ importButton: 'Importer des transactions',
+ },
error: {
workspaceFeedsCouldNotBeLoadedTitle: 'Impossible de charger les flux de carte',
workspaceFeedsCouldNotBeLoadedMessage:
diff --git a/src/languages/it.ts b/src/languages/it.ts
index 33c0428bfdf4..b01a9a9bcef1 100644
--- a/src/languages/it.ts
+++ b/src/languages/it.ts
@@ -1055,6 +1055,8 @@ const translations: TranslationDeepObject = {
other: (count: number) =>
`Conferma i dettagli qui sotto per i ${count} nuovi membri dello spazio di lavoro che verranno aggiunti come parte di questo caricamento. I membri esistenti non riceveranno aggiornamenti del ruolo né messaggi di invito.`,
}),
+ importTransactionsSuccessfulDescription: ({transactions}: {transactions: number}) =>
+ transactions > 1 ? `${transactions} transazioni sono state importate.` : '1 transazione è stata importata.',
},
receipt: {
upload: 'Carica ricevuta',
@@ -4948,6 +4950,15 @@ _Per istruzioni più dettagliate, [visita il nostro sito di assistenza](${CONST.
cardAlreadyAssignedError: 'This card is already assigned to a user in another workspace.',
editStartDateDescription: 'Scegli una nuova data di inizio per le transazioni. Sincronizzeremo tutte le transazioni da quella data in poi, escludendo quelle già importate.',
unassignCardFailedError: 'Rimozione della carta non riuscita.',
+ importTransactions: {
+ title: 'Importa transazioni da file',
+ description: 'Regola le impostazioni per il tuo file che verranno applicate all’importazione.',
+ cardDisplayName: 'Nome visualizzato carta',
+ currency: 'Valuta',
+ transactionsAreReimbursable: 'Le transazioni sono rimborsabili',
+ flipAmountSign: 'Inverti segno importo',
+ importButton: 'Importa transazioni',
+ },
error: {
workspaceFeedsCouldNotBeLoadedTitle: 'Impossibile caricare i feed delle carte',
workspaceFeedsCouldNotBeLoadedMessage:
diff --git a/src/languages/ja.ts b/src/languages/ja.ts
index 761da2729ec5..bd9cd07e34b8 100644
--- a/src/languages/ja.ts
+++ b/src/languages/ja.ts
@@ -1053,6 +1053,8 @@ const translations: TranslationDeepObject = {
other: (count: number) =>
`このアップロードで追加される新しいワークスペースメンバー${count}人分の詳細を、以下で確認してください。既存のメンバーには、ロールの更新や招待メッセージは送信されません。`,
}),
+ importTransactionsSuccessfulDescription: ({transactions}: {transactions: number}) =>
+ transactions > 1 ? `${transactions}件の取引がインポートされました。` : '1件の取引がインポートされました。',
},
receipt: {
upload: '領収書をアップロード',
@@ -4919,6 +4921,15 @@ _より詳しい手順については、[ヘルプサイトをご覧ください
cardAlreadyAssignedError: 'This card is already assigned to a user in another workspace.',
editStartDateDescription: '新しい取引の開始日を選択してください。その日以降のすべての取引を、すでに取り込んだものを除いて同期します。',
unassignCardFailedError: 'カードの割り当て解除に失敗しました。',
+ importTransactions: {
+ title: 'ファイルから取引をインポート',
+ description: 'インポート時に適用されるファイルの設定を調整してください。',
+ cardDisplayName: 'カード表示名',
+ currency: '通貨',
+ transactionsAreReimbursable: '取扱明細は精算対象です',
+ flipAmountSign: '金額の符号を反転',
+ importButton: '取引をインポート',
+ },
error: {
workspaceFeedsCouldNotBeLoadedTitle: 'カードフィードを読み込めませんでした',
workspaceFeedsCouldNotBeLoadedMessage: 'ワークスペースのカードフィードを読み込む際にエラーが発生しました。もう一度お試しいただくか、管理者に連絡してください。',
diff --git a/src/languages/nl.ts b/src/languages/nl.ts
index f4f2720e3872..f61d9a624a28 100644
--- a/src/languages/nl.ts
+++ b/src/languages/nl.ts
@@ -1055,6 +1055,8 @@ const translations: TranslationDeepObject = {
other: (count: number) =>
`Bevestig hieronder de details voor de ${count} nieuwe werkruimteleden die als onderdeel van deze upload zullen worden toegevoegd. Bestaande leden ontvangen geen rolupdates of uitnodigingsberichten.`,
}),
+ importTransactionsSuccessfulDescription: ({transactions}: {transactions: number}) =>
+ transactions > 1 ? `${transactions} transacties zijn geïmporteerd.` : '1 transactie is geïmporteerd.',
},
receipt: {
upload: 'Bon uploaden',
@@ -4945,6 +4947,15 @@ _Voor gedetailleerdere instructies, [bezoek onze helpsite](${CONST.NETSUITE_IMPO
editStartDateDescription:
'Kies een nieuwe startdatum voor transacties. We synchroniseren alle transacties vanaf die datum, met uitzondering van de transacties die we al hebben geïmporteerd.',
unassignCardFailedError: 'Kaartontkoppeling mislukt.',
+ importTransactions: {
+ title: 'Transacties importeren uit bestand',
+ description: 'Pas de instellingen voor je bestand aan die bij het importeren worden toegepast.',
+ cardDisplayName: 'Kaartweergavenaam',
+ currency: 'Valuta',
+ transactionsAreReimbursable: 'Transacties zijn terugbetaalbaar',
+ flipAmountSign: 'Teken van bedrag omdraaien',
+ importButton: 'Transacties importeren',
+ },
error: {
workspaceFeedsCouldNotBeLoadedTitle: 'Kan kaartfeeds niet laden',
workspaceFeedsCouldNotBeLoadedMessage:
diff --git a/src/languages/pl.ts b/src/languages/pl.ts
index 7c790e3888ce..fd02dd603cd0 100644
--- a/src/languages/pl.ts
+++ b/src/languages/pl.ts
@@ -1056,6 +1056,7 @@ const translations: TranslationDeepObject = {
other: (count: number) =>
`Potwierdź poniższe szczegóły dla ${count} nowych członków przestrzeni roboczej, którzy zostaną dodani w ramach tego przesyłania. Istniejący członkowie nie otrzymają żadnych aktualizacji ról ani wiadomości z zaproszeniem.`,
}),
+ importTransactionsSuccessfulDescription: ({transactions}: {transactions: number}) => (transactions > 1 ? `Zaimportowano ${transactions} transakcji.` : 'Zaimportowano 1 transakcję.'),
},
receipt: {
upload: 'Prześlij paragon',
@@ -4934,6 +4935,15 @@ _Aby uzyskać bardziej szczegółowe instrukcje, [odwiedź naszą stronę pomocy
cardAlreadyAssignedError: 'This card is already assigned to a user in another workspace.',
editStartDateDescription: 'Wybierz nową datę początkową transakcji. Zsynchronizujemy wszystkie transakcje od tej daty, z wyłączeniem tych, które już zostały zaimportowane.',
unassignCardFailedError: 'Nie udało się odłączyć karty.',
+ importTransactions: {
+ title: 'Importuj transakcje z pliku',
+ description: 'Dostosuj ustawienia dla swojego pliku, które zostaną zastosowane podczas importu.',
+ cardDisplayName: 'Wyświetlana nazwa karty',
+ currency: 'Waluta',
+ transactionsAreReimbursable: 'Transakcje podlegają zwrotowi',
+ flipAmountSign: 'Odwróć znak kwoty',
+ importButton: 'Importuj transakcje',
+ },
error: {
workspaceFeedsCouldNotBeLoadedTitle: 'Nie można było wczytać kanałów kart',
workspaceFeedsCouldNotBeLoadedMessage: 'Wystąpił błąd podczas ładowania kanałów kart w przestrzeni roboczej. Spróbuj ponownie lub skontaktuj się z administratorem.',
diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts
index 84ec54156619..57aacb48ca59 100644
--- a/src/languages/pt-BR.ts
+++ b/src/languages/pt-BR.ts
@@ -1054,6 +1054,8 @@ const translations: TranslationDeepObject = {
other: (count: number) =>
`Confirme os detalhes abaixo para os ${count} novos membros do workspace que serão adicionados como parte deste upload. Membros existentes não receberão nenhuma atualização de função nem mensagens de convite.`,
}),
+ importTransactionsSuccessfulDescription: ({transactions}: {transactions: number}) =>
+ transactions > 1 ? `${transactions} transações foram importadas.` : '1 transação foi importada.',
},
receipt: {
upload: 'Enviar recibo',
@@ -4933,6 +4935,15 @@ _Para instruções mais detalhadas, [visite nosso site de ajuda](${CONST.NETSUIT
cardAlreadyAssignedError: 'This card is already assigned to a user in another workspace.',
editStartDateDescription: 'Escolha uma nova data de início das transações. Vamos sincronizar todas as transações a partir dessa data, excluindo aquelas que já importamos.',
unassignCardFailedError: 'Falha ao desatribuir o cartão.',
+ importTransactions: {
+ title: 'Importar transações do arquivo',
+ description: 'Ajuste as configurações do seu arquivo que serão aplicadas na importação.',
+ cardDisplayName: 'Nome exibido no cartão',
+ currency: 'Moeda',
+ transactionsAreReimbursable: 'As transações são reembolsáveis',
+ flipAmountSign: 'Inverter o sinal do valor',
+ importButton: 'Importar transações',
+ },
error: {
workspaceFeedsCouldNotBeLoadedTitle: 'Não foi possível carregar os feeds do cartão',
workspaceFeedsCouldNotBeLoadedMessage: 'Ocorreu um erro ao carregar os feeds de cartões do workspace. Tente novamente ou entre em contato com o administrador.',
diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts
index 7630d11c09fb..5b6ba2ed8f7b 100644
--- a/src/languages/zh-hans.ts
+++ b/src/languages/zh-hans.ts
@@ -1039,6 +1039,7 @@ const translations: TranslationDeepObject = {
one: `请确认以下将通过本次上传添加的新工作区成员的详细信息。现有成员不会收到任何角色更新或邀请消息。`,
other: (count: number) => `请确认以下有关将在此次上传中添加的 ${count} 位新的工作区成员的详细信息。现有成员不会收到任何角色更新或邀请消息。`,
}),
+ importTransactionsSuccessfulDescription: ({transactions}: {transactions: number}) => (transactions > 1 ? `已导入 ${transactions} 笔交易。` : '已导入 1 笔交易。'),
},
receipt: {
upload: '上传收据',
@@ -4840,6 +4841,15 @@ _如需更详细的说明,请[访问我们的帮助网站](${CONST.NETSUITE_IM
cardAlreadyAssignedError: 'This card is already assigned to a user in another workspace.',
editStartDateDescription: '选择一个新的交易起始日期。我们将从该日期起同步所有交易,但不包括已经导入的交易。',
unassignCardFailedError: '卡片取消分配失败。',
+ importTransactions: {
+ title: '从文件导入交易',
+ description: '请调整文件的设置,这些设置将在导入时应用。',
+ cardDisplayName: '卡片显示名称',
+ currency: '货币',
+ transactionsAreReimbursable: '交易可报销',
+ flipAmountSign: '翻转金额符号',
+ importButton: '导入交易',
+ },
error: {
workspaceFeedsCouldNotBeLoadedTitle: '无法加载卡片信息流',
workspaceFeedsCouldNotBeLoadedMessage: '加载工作区卡片动态时出错。请重试或联系您的管理员。',
diff --git a/src/libs/API/parameters/AddCSVTransactionsParams.ts b/src/libs/API/parameters/AddCSVTransactionsParams.ts
new file mode 100644
index 000000000000..62947a824243
--- /dev/null
+++ b/src/libs/API/parameters/AddCSVTransactionsParams.ts
@@ -0,0 +1,18 @@
+type ImportCSVTransactionsParams = {
+ /** JSON string of the transaction list */
+ transactionList: string;
+
+ /** Card ID (0 for new card) */
+ cardID: number;
+
+ /** Display name for the card */
+ cardName: string;
+
+ /** Currency code (e.g., 'USD') */
+ currency: string;
+
+ /** Whether transactions are reimbursable */
+ reimbursable: boolean;
+};
+
+export default ImportCSVTransactionsParams;
diff --git a/src/libs/API/parameters/ImportCSVTransactionsParams.ts b/src/libs/API/parameters/ImportCSVTransactionsParams.ts
new file mode 100644
index 000000000000..62947a824243
--- /dev/null
+++ b/src/libs/API/parameters/ImportCSVTransactionsParams.ts
@@ -0,0 +1,18 @@
+type ImportCSVTransactionsParams = {
+ /** JSON string of the transaction list */
+ transactionList: string;
+
+ /** Card ID (0 for new card) */
+ cardID: number;
+
+ /** Display name for the card */
+ cardName: string;
+
+ /** Currency code (e.g., 'USD') */
+ currency: string;
+
+ /** Whether transactions are reimbursable */
+ reimbursable: boolean;
+};
+
+export default ImportCSVTransactionsParams;
diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts
index ce301711406c..5cdbc9202dc4 100644
--- a/src/libs/API/parameters/index.ts
+++ b/src/libs/API/parameters/index.ts
@@ -1,4 +1,5 @@
export type {default as GetTransactionsForMergingParams} from './GetTransactionsForMergingParams';
+export type {default as ImportCSVTransactionsParams} from './ImportCSVTransactionsParams';
export type {default as ImportMultiLevelTagsParams} from './ImportMultiLevelTagsParams';
export type {default as CleanPolicyTagsParams} from './CleanPolicyTagsParams';
export type {default as ActivatePhysicalExpensifyCardParams} from './ActivatePhysicalExpensifyCardParams';
diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts
index f32a18795889..4bc6c6027aae 100644
--- a/src/libs/API/types.ts
+++ b/src/libs/API/types.ts
@@ -11,6 +11,7 @@ import type UpdateBeneficialOwnersForBankAccountParams from './parameters/Update
type ApiRequestType = ValueOf;
const WRITE_COMMANDS = {
+ IMPORT_CSV_TRANSACTIONS: 'ImportCSVTransactions',
CLEAN_POLICY_TAGS: 'ClearPolicyTags',
IMPORT_MULTI_LEVEL_TAGS: 'ImportMultiLevelTags',
SET_WORKSPACE_AUTO_HARVESTING: 'SetWorkspaceAutoHarvesting',
@@ -551,6 +552,7 @@ const WRITE_COMMANDS = {
type WriteCommand = ValueOf;
type WriteCommandParameters = {
+ [WRITE_COMMANDS.IMPORT_CSV_TRANSACTIONS]: Parameters.ImportCSVTransactionsParams;
[WRITE_COMMANDS.CLEAN_POLICY_TAGS]: Parameters.CleanPolicyTagsParams;
[WRITE_COMMANDS.IMPORT_MULTI_LEVEL_TAGS]: Parameters.ImportMultiLevelTagsParams;
[WRITE_COMMANDS.DISMISS_REFERRAL_BANNER]: Parameters.DismissReferralBannerParams;
diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts
index 64e153733290..f452722fa372 100644
--- a/src/libs/CardUtils.ts
+++ b/src/libs/CardUtils.ts
@@ -887,6 +887,16 @@ function isCardAlreadyAssigned(cardNumberToCheck: string, workspaceCardFeeds: On
});
}
+/**
+ * Generate a random cardID up to 53 bits aka 9,007,199,254,740,991 (Number.MAX_SAFE_INTEGER).
+ * There were approximately 24,000,000 reports with sequential IDs generated before we started using this approach, those make up roughly 0.25 billionth of the space for these numbers,
+ * so we live with the 1 in 4 billion chance of a collision with an older ID until we can switch to 64-bit IDs.
+ *
+ */
+function generateCardID(): number {
+ return Math.floor(Math.random() * 2 ** 21) * 2 ** 32 + Math.floor(Math.random() * 2 ** 32);
+}
+
/**
* Check if there are any assigned cards that should be displayed in the wallet page.
* This includes active Expensify cards, company cards (domain), and personal cards.
@@ -971,6 +981,7 @@ export {
COMPANY_CARD_BANK_ICON_NAMES,
splitMaskedCardNumber,
isCardAlreadyAssigned,
+ generateCardID,
hasDisplayableAssignedCards,
};
diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
index 96863596ff1e..9ed54065bb21 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
@@ -396,6 +396,11 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/settings/Wallet/ActivatePhysicalCardPage').default,
[SCREENS.SETTINGS.WALLET.TRANSFER_BALANCE]: () => require('../../../../pages/settings/Wallet/TransferBalancePage').default,
[SCREENS.SETTINGS.WALLET.CHOOSE_TRANSFER_ACCOUNT]: () => require('../../../../pages/settings/Wallet/ChooseTransferAccountPage').default,
+ [SCREENS.SETTINGS.WALLET.IMPORT_TRANSACTIONS]: () => require('../../../../pages/settings/Wallet/ImportTransactionsPage').default,
+ [SCREENS.SETTINGS.WALLET.IMPORT_TRANSACTIONS_CARD_NAME]: () => require('../../../../pages/settings/Wallet/ImportTransactionsCardNamePage').default,
+ [SCREENS.SETTINGS.WALLET.IMPORT_TRANSACTIONS_CURRENCY]: () => require('../../../../pages/settings/Wallet/ImportTransactionsCurrencyPage').default,
+ [SCREENS.SETTINGS.WALLET.IMPORT_TRANSACTIONS_SPREADSHEET]: () => require('../../../../pages/settings/Wallet/ImportTransactionsSpreadsheetPage').default,
+ [SCREENS.SETTINGS.WALLET.TRANSACTIONS_IMPORTED]: () => require('../../../../pages/settings/Wallet/TransactionsImportedPage').default,
[SCREENS.SETTINGS.WALLET.ENABLE_PAYMENTS]: () => require('../../../../pages/EnablePayments/EnablePayments').default,
[SCREENS.SETTINGS.WALLET.UNSHARE_BANK_ACCOUNT]: () => require('../../../../pages/settings/Wallet/UnshareBankAccount/UnshareBankAccount').default,
[SCREENS.SETTINGS.WALLET.ENABLE_GLOBAL_REIMBURSEMENTS]: () => require('../../../../pages/settings/Wallet/EnableGlobalReimbursements').default,
diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts b/src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts
index 5c679d406e89..e50234509d29 100755
--- a/src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts
+++ b/src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts
@@ -40,6 +40,11 @@ const SETTINGS_TO_RHP: Partial['config'] = {
path: ROUTES.SETTINGS_WALLET_CHOOSE_TRANSFER_ACCOUNT,
exact: true,
},
+ [SCREENS.SETTINGS.WALLET.IMPORT_TRANSACTIONS]: {
+ path: ROUTES.SETTINGS_WALLET_IMPORT_TRANSACTIONS,
+ exact: true,
+ },
+ [SCREENS.SETTINGS.WALLET.IMPORT_TRANSACTIONS_CARD_NAME]: {
+ path: ROUTES.SETTINGS_WALLET_IMPORT_TRANSACTIONS_CARD_NAME,
+ exact: true,
+ },
+ [SCREENS.SETTINGS.WALLET.IMPORT_TRANSACTIONS_CURRENCY]: {
+ path: ROUTES.SETTINGS_WALLET_IMPORT_TRANSACTIONS_CURRENCY,
+ exact: true,
+ },
+ [SCREENS.SETTINGS.WALLET.IMPORT_TRANSACTIONS_SPREADSHEET]: {
+ path: ROUTES.SETTINGS_WALLET_IMPORT_TRANSACTIONS_SPREADSHEET.route,
+ exact: true,
+ },
+ [SCREENS.SETTINGS.WALLET.TRANSACTIONS_IMPORTED]: {
+ path: ROUTES.SETTINGS_WALLET_TRANSACTIONS_IMPORTED.route,
+ exact: true,
+ },
[SCREENS.SETTINGS.REPORT_CARD_LOST_OR_DAMAGED]: {
path: ROUTES.SETTINGS_WALLET_REPORT_CARD_LOST_OR_DAMAGED.route,
exact: true,
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index d4ae33a8b318..68f0d2de9fb9 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -215,6 +215,11 @@ type SettingsNavigatorParamList = {
[SCREENS.SETTINGS.WALLET.VERIFY_ACCOUNT]: undefined;
[SCREENS.SETTINGS.WALLET.TRANSFER_BALANCE]: undefined;
[SCREENS.SETTINGS.WALLET.CHOOSE_TRANSFER_ACCOUNT]: undefined;
+ [SCREENS.SETTINGS.WALLET.IMPORT_TRANSACTIONS]: undefined;
+ [SCREENS.SETTINGS.WALLET.IMPORT_TRANSACTIONS_CARD_NAME]: undefined;
+ [SCREENS.SETTINGS.WALLET.IMPORT_TRANSACTIONS_CURRENCY]: undefined;
+ [SCREENS.SETTINGS.WALLET.IMPORT_TRANSACTIONS_SPREADSHEET]: {cardID?: string};
+ [SCREENS.SETTINGS.WALLET.TRANSACTIONS_IMPORTED]: {cardID?: string};
[SCREENS.SETTINGS.WALLET.ENABLE_PAYMENTS]: undefined;
[SCREENS.SETTINGS.WALLET.UNSHARE_BANK_ACCOUNT]: {
bankAccountID: string;
diff --git a/src/libs/actions/ImportSpreadsheet.ts b/src/libs/actions/ImportSpreadsheet.ts
index 980030b60690..3b2b71c4b1e2 100644
--- a/src/libs/actions/ImportSpreadsheet.ts
+++ b/src/libs/actions/ImportSpreadsheet.ts
@@ -1,19 +1,60 @@
import Onyx from 'react-native-onyx';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
+import type {ImportTransactionSettings} from '@src/types/onyx/ImportedSpreadsheet';
-function setSpreadsheetData(data: string[][], fileURI: string, fileType: string, fileName: string, isImportingMultiLevelTags: boolean): Promise {
- if (!Array.isArray(data) || !Array.isArray(data.at(0))) {
- return Promise.reject(new Error('Invalid data format'));
+function setSpreadsheetData(
+ data: string[][],
+ fileURI: string,
+ fileType: string,
+ fileName: string,
+ isImportingMultiLevelTags: boolean,
+ importTransactionSettings?: ImportTransactionSettings,
+): Promise {
+ // Validate that data is a non-empty array
+ if (!Array.isArray(data) || data.length === 0) {
+ return Promise.reject(new Error('Invalid data format: data is empty or not an array'));
}
- const transposedData = data.at(0)?.map((_, colIndex) => data.map((row) => row.at(colIndex) ?? ''));
- const columnNames: Record =
- data.at(0)?.reduce((acc: Record, _, colIndex) => {
- acc[colIndex] = CONST.CSV_IMPORT_COLUMNS.IGNORE;
- return acc;
- }, {}) ?? {};
- return Onyx.set(ONYXKEYS.IMPORTED_SPREADSHEET, {data: transposedData, columns: columnNames, fileURI, fileType, fileName, isImportingMultiLevelTags});
+ // Validate that we have at least one row with data
+ const firstRow = data.at(0);
+ if (!Array.isArray(firstRow) || firstRow.length === 0) {
+ return Promise.reject(new Error('Invalid data format: first row is empty or not an array'));
+ }
+
+ // Require at least 2 rows (header + data) for most imports
+ if (data.length < 2) {
+ return Promise.reject(new Error('Invalid data format: file must contain at least 2 rows'));
+ }
+
+ const numColumns = firstRow.length;
+
+ // Transpose data from row-major to column-major format
+ const transposedData: string[][] = firstRow.map((_, colIndex) => data.map((row) => String(row.at(colIndex) ?? '')));
+
+ const columnNames: Record = {};
+ for (let colIndex = 0; colIndex < numColumns; colIndex++) {
+ columnNames[colIndex] = CONST.CSV_IMPORT_COLUMNS.IGNORE;
+ }
+
+ // Use Onyx.set to replace the entire object (avoiding stale column data from previous files)
+ // but include the preserved settings passed from the caller
+ return Onyx.set(ONYXKEYS.IMPORTED_SPREADSHEET, {
+ data: transposedData,
+ columns: columnNames,
+ fileURI,
+ fileType,
+ fileName,
+ isImportingMultiLevelTags,
+ // Preserve transaction import settings that were configured before file upload
+ importTransactionSettings,
+ // Reset modal state for new import
+ shouldFinalModalBeOpened: false,
+ importFinalModal: undefined,
+ containsHeader: true,
+ isImportingIndependentMultiLevelTags: false,
+ isGLAdjacent: false,
+ });
}
function setColumnName(columnIndex: number, columnName: string): Promise {
@@ -25,7 +66,41 @@ function setContainsHeader(containsHeader: boolean): Promise {
}
function closeImportPage(): Promise {
- return Onyx.merge(ONYXKEYS.IMPORTED_SPREADSHEET, {data: null, columns: null, shouldFinalModalBeOpened: false, importFinalModal: null});
+ return Onyx.merge(ONYXKEYS.IMPORTED_SPREADSHEET, {
+ data: null,
+ columns: null,
+ shouldFinalModalBeOpened: false,
+ importFinalModal: null,
+ // Clear the import settings so the next import starts fresh
+ importTransactionSettings: null,
+ });
+}
+
+function setImportTransactionCardName(cardDisplayName: string): Promise {
+ return Onyx.merge(ONYXKEYS.IMPORTED_SPREADSHEET, {
+ importTransactionSettings: {
+ cardDisplayName,
+ },
+ });
+}
+
+function setImportTransactionCurrency(currency: string): Promise {
+ return Onyx.merge(ONYXKEYS.IMPORTED_SPREADSHEET, {
+ importTransactionSettings: {
+ currency,
+ },
+ });
+}
+
+function setImportTransactionSettings(cardDisplayName: string, currency: string, isReimbursable: boolean, flipAmountSign: boolean): Promise {
+ return Onyx.merge(ONYXKEYS.IMPORTED_SPREADSHEET, {
+ importTransactionSettings: {
+ cardDisplayName,
+ currency,
+ isReimbursable,
+ flipAmountSign,
+ },
+ });
}
-export {setSpreadsheetData, setColumnName, closeImportPage, setContainsHeader};
+export {setSpreadsheetData, setColumnName, closeImportPage, setContainsHeader, setImportTransactionCardName, setImportTransactionCurrency, setImportTransactionSettings};
diff --git a/src/libs/actions/ImportTransactions.ts b/src/libs/actions/ImportTransactions.ts
new file mode 100644
index 000000000000..007542451407
--- /dev/null
+++ b/src/libs/actions/ImportTransactions.ts
@@ -0,0 +1,343 @@
+import {addDays, format, isValid, parse} from 'date-fns';
+import type {OnyxUpdate} from 'react-native-onyx';
+import Onyx from 'react-native-onyx';
+import * as API from '@libs/API';
+import type {ImportCSVTransactionsParams} from '@libs/API/parameters';
+import {WRITE_COMMANDS} from '@libs/API/types';
+import {generateCardID} from '@libs/CardUtils';
+import DateUtils from '@libs/DateUtils';
+import {rand64} from '@libs/NumberUtils';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type {Card, CardList} from '@src/types/onyx';
+import type ImportedSpreadsheet from '@src/types/onyx/ImportedSpreadsheet';
+import type {ImportTransactionSettings} from '@src/types/onyx/ImportedSpreadsheet';
+import type Transaction from '@src/types/onyx/Transaction';
+
+type TransactionFromCSV = {
+ transactionID: string;
+ created: string;
+ merchant: string;
+ amount: number;
+ category?: string;
+};
+
+// Common date formats to try when parsing CSV dates
+// Order matters - more specific/common formats first
+const CSV_DATE_FORMATS = [
+ 'yyyy-MM-dd', // ISO format: 2025-11-02
+ 'MM/dd/yyyy', // US format: 11/02/2025
+ 'dd/MM/yyyy', // European format: 02/11/2025
+ 'M/d/yyyy', // US short: 1/2/2025
+ 'd/M/yyyy', // European short: 2/1/2025
+ 'MM-dd-yyyy', // US with dashes: 11-02-2025
+ 'dd-MM-yyyy', // European with dashes: 02-11-2025
+ 'yyyy/MM/dd', // Alternative ISO: 2025/11/02
+ 'MMM d, yyyy', // Month name: Nov 2, 2025
+ 'MMMM d, yyyy', // Full month: November 2, 2025
+ 'd MMM yyyy', // European with month name: 2 Nov 2025
+ 'dd MMM yyyy', // European with month name: 02 Nov 2025
+ 'yyyyMMdd', // Compact: 20251102
+];
+
+/**
+ * Parses a date string from various formats and returns it in yyyy-MM-dd format
+ */
+function parseCSVDate(input: string): string | null {
+ if (!input || typeof input !== 'string') {
+ return null;
+ }
+
+ const trimmedInput = input.trim();
+
+ // Try native Date parsing first (handles ISO and some other formats)
+ let date = new Date(trimmedInput);
+ if (isValid(date) && !Number.isNaN(date.getTime())) {
+ return format(date, CONST.DATE.FNS_FORMAT_STRING);
+ }
+
+ // Try parsing with common date formats using date-fns
+ for (const dateFormat of CSV_DATE_FORMATS) {
+ const parsedDate = parse(trimmedInput, dateFormat, new Date());
+ if (isValid(parsedDate)) {
+ return format(parsedDate, CONST.DATE.FNS_FORMAT_STRING);
+ }
+ }
+
+ // If the date didn't parse, try taking just the first 10 characters
+ if (trimmedInput.length > 10) {
+ const shortInput = trimmedInput.substring(0, 10);
+ date = new Date(shortInput);
+ if (isValid(date) && !Number.isNaN(date.getTime())) {
+ return format(date, CONST.DATE.FNS_FORMAT_STRING);
+ }
+ // Also try format parsing on the shortened input
+ for (const dateFormat of CSV_DATE_FORMATS) {
+ const parsedDate = parse(shortInput, dateFormat, new Date());
+ if (isValid(parsedDate)) {
+ return format(parsedDate, CONST.DATE.FNS_FORMAT_STRING);
+ }
+ }
+ }
+
+ // If it didn't parse, maybe it's an Excel date number
+ // Excel stores dates serialized from January 1st, 1900 (with 1/1/1900 being 1)
+ // Excel thinks that 1900 was a leap year and adds an extra day to account for that
+ if (/^\d+$/.test(trimmedInput)) {
+ const inputInt = parseInt(trimmedInput, 10);
+ if (inputInt > 0 && inputInt < 100000) {
+ const excelEpoch = new Date(1900, 0, 1); // January 1, 1900
+ const parsedDate = addDays(excelEpoch, inputInt - 2);
+ if (isValid(parsedDate)) {
+ return format(parsedDate, CONST.DATE.FNS_FORMAT_STRING);
+ }
+ }
+ }
+
+ return null;
+}
+
+/**
+ * Converts spreadsheet data to transaction objects based on column mapping
+ */
+function buildTransactionListFromSpreadsheet(spreadsheet: ImportedSpreadsheet, settings: ImportTransactionSettings): TransactionFromCSV[] {
+ const {data, columns, containsHeader = true} = spreadsheet;
+ const {flipAmountSign = false} = settings;
+
+ // Find the column indexes for each field
+ let dateColumnIndex = -1;
+ let merchantColumnIndex = -1;
+ let amountColumnIndex = -1;
+ let categoryColumnIndex = -1;
+
+ if (columns) {
+ for (const [indexStr, role] of Object.entries(columns)) {
+ const index = Number(indexStr);
+ switch (role) {
+ case 'date':
+ dateColumnIndex = index;
+ break;
+ case 'merchant':
+ merchantColumnIndex = index;
+ break;
+ case 'amount':
+ amountColumnIndex = index;
+ break;
+ case 'category':
+ categoryColumnIndex = index;
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ const transactions: TransactionFromCSV[] = [];
+ const startIndex = containsHeader ? 1 : 0;
+
+ // Data is in column-major format, so we need to iterate through columns
+ if (!data || data.length === 0) {
+ return transactions;
+ }
+
+ // Get the number of rows (length of first column)
+ const numRows = data.at(0)?.length ?? 0;
+
+ for (let rowIndex = startIndex; rowIndex < numRows; rowIndex++) {
+ const dateValue = dateColumnIndex >= 0 ? data.at(dateColumnIndex)?.at(rowIndex) : undefined;
+ const merchantValue = merchantColumnIndex >= 0 ? data.at(merchantColumnIndex)?.at(rowIndex) : undefined;
+ const amountValue = amountColumnIndex >= 0 ? data.at(amountColumnIndex)?.at(rowIndex) : undefined;
+ const categoryValue = categoryColumnIndex >= 0 ? data.at(categoryColumnIndex)?.at(rowIndex) : undefined;
+
+ // Skip rows with missing required fields
+ if (!dateValue || !amountValue) {
+ continue;
+ }
+
+ // Parse the date using our multi-format parser
+ const parsedDate = parseCSVDate(dateValue);
+
+ // Skip rows with invalid dates
+ if (!parsedDate) {
+ continue;
+ }
+
+ // Parse amount - remove non-numeric characters except decimal and minus
+ let parsedAmount = Math.round(Number(String(amountValue).replaceAll(/[^\d.-]/g, '')) * 100);
+
+ // Flip sign if needed
+ if (flipAmountSign) {
+ parsedAmount *= -1;
+ }
+
+ const transaction: TransactionFromCSV = {
+ transactionID: rand64(),
+ created: parsedDate,
+ merchant: merchantValue ?? '',
+ amount: parsedAmount,
+ };
+
+ if (categoryValue) {
+ transaction.category = categoryValue;
+ }
+
+ transactions.push(transaction);
+ }
+
+ return transactions;
+}
+
+/**
+ * Creates an optimistic card object for the imported transactions
+ */
+function buildOptimisticCard(cardDisplayName: string): {card: Card; cardID: number} {
+ const cardID = generateCardID();
+ return {
+ cardID,
+ card: {
+ cardID,
+ state: CONST.EXPENSIFY_CARD.STATE.OPEN,
+ // Use the CSV bank name constant so the card shows up in the Assigned Cards section
+ bank: CONST.PERSONAL_CARD.BANK_NAME.CSV,
+ domainName: '',
+ lastFourPAN: '',
+ availableSpend: 0,
+ scrapeMinDate: '',
+ fraud: CONST.EXPENSIFY_CARD.FRAUD_TYPES.NONE,
+ lastUpdated: DateUtils.getDBTime(),
+ nameValuePairs: {
+ cardTitle: cardDisplayName,
+ } as Card['nameValuePairs'],
+ },
+ };
+}
+
+/**
+ * Creates optimistic transaction objects from the CSV data
+ */
+function buildOptimisticTransactions(transactionList: TransactionFromCSV[], cardID: number, currency: string, isReimbursable: boolean): Transaction[] {
+ return transactionList.map(
+ (csvTransaction) =>
+ ({
+ transactionID: csvTransaction.transactionID,
+ cardID,
+ created: csvTransaction.created,
+ merchant: csvTransaction.merchant,
+ amount: csvTransaction.amount,
+ currency,
+ category: csvTransaction.category ?? '',
+ reimbursable: isReimbursable,
+ pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
+ comment: {
+ comment: '',
+ },
+ reportID: '0',
+ }) as Transaction,
+ );
+}
+
+/**
+ * Import transactions from a CSV spreadsheet
+ */
+function importTransactionsFromCSV(spreadsheet: ImportedSpreadsheet) {
+ const settings = spreadsheet.importTransactionSettings ?? {};
+ const {cardDisplayName = 'Imported Card', currency = CONST.CURRENCY.USD, isReimbursable = true} = settings;
+
+ // Build transaction list from spreadsheet
+ const transactionList = buildTransactionListFromSpreadsheet(spreadsheet, settings);
+
+ if (transactionList.length === 0) {
+ Onyx.merge(ONYXKEYS.IMPORTED_SPREADSHEET, {
+ shouldFinalModalBeOpened: true,
+ importFinalModal: {
+ titleKey: 'spreadsheet.importFailedTitle' as const,
+ promptKey: 'spreadsheet.invalidFileMessage' as const,
+ promptKeyParams: undefined,
+ },
+ });
+ return;
+ }
+
+ // Create optimistic card
+ const {card: optimisticCard, cardID} = buildOptimisticCard(cardDisplayName);
+
+ // Create optimistic transactions
+ const optimisticTransactions = buildOptimisticTransactions(transactionList, cardID, currency, isReimbursable);
+
+ // Build optimistic card list update
+ const optimisticCardList: CardList = {
+ [cardID]: optimisticCard,
+ };
+
+ const params: ImportCSVTransactionsParams = {
+ transactionList: JSON.stringify(transactionList),
+ cardID,
+ cardName: cardDisplayName,
+ currency,
+ reimbursable: isReimbursable,
+ };
+
+ const optimisticData = [] as OnyxUpdate[];
+ const failureData = [] as OnyxUpdate[];
+
+ optimisticData.push({
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.CARD_LIST,
+ value: optimisticCardList,
+ });
+
+ for (const transaction of optimisticTransactions) {
+ optimisticData.push({
+ onyxMethod: Onyx.METHOD.SET,
+ key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`,
+ value: transaction,
+ });
+ failureData.push({
+ onyxMethod: Onyx.METHOD.SET,
+ key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`,
+ value: null,
+ });
+ }
+
+ optimisticData.push({
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.IMPORTED_SPREADSHEET,
+ value: {
+ shouldFinalModalBeOpened: true,
+ importFinalModal: {
+ titleKey: 'spreadsheet.importSuccessfulTitle' as const,
+ promptKey: 'spreadsheet.importTransactionsSuccessfulDescription' as const,
+ promptKeyParams: {transactions: transactionList.length},
+ },
+ },
+ });
+
+ failureData.push(
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.CARD_LIST,
+ value: {
+ [cardID]: null,
+ },
+ },
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.IMPORTED_SPREADSHEET,
+ value: {
+ shouldFinalModalBeOpened: true,
+ importFinalModal: {
+ titleKey: 'spreadsheet.importFailedTitle' as const,
+ promptKey: 'spreadsheet.importFailedDescription' as const,
+ promptKeyParams: undefined,
+ },
+ },
+ },
+ );
+
+ API.write(WRITE_COMMANDS.IMPORT_CSV_TRANSACTIONS, params, {
+ optimisticData,
+ failureData,
+ });
+}
+
+export default importTransactionsFromCSV;
diff --git a/src/pages/settings/Wallet/ImportTransactionsCardNamePage.tsx b/src/pages/settings/Wallet/ImportTransactionsCardNamePage.tsx
new file mode 100644
index 000000000000..496e9a1984e9
--- /dev/null
+++ b/src/pages/settings/Wallet/ImportTransactionsCardNamePage.tsx
@@ -0,0 +1,79 @@
+import React, {useCallback} from 'react';
+import {Keyboard} from 'react-native';
+import FormProvider from '@components/Form/FormProvider';
+import InputWrapper from '@components/Form/InputWrapper';
+import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import ScreenWrapper from '@components/ScreenWrapper';
+import TextInput from '@components/TextInput';
+import useAutoFocusInput from '@hooks/useAutoFocusInput';
+import useLocalize from '@hooks/useLocalize';
+import useOnyx from '@hooks/useOnyx';
+import useThemeStyles from '@hooks/useThemeStyles';
+import {setImportTransactionCardName} from '@libs/actions/ImportSpreadsheet';
+import {addErrorMessage} from '@libs/ErrorUtils';
+import Navigation from '@libs/Navigation/Navigation';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import INPUT_IDS from '@src/types/form/ImportTransactionsForm';
+
+function ImportTransactionsCardNamePage() {
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+ const {inputCallbackRef} = useAutoFocusInput();
+ const [importedSpreadsheet] = useOnyx(ONYXKEYS.IMPORTED_SPREADSHEET, {canBeMissing: true});
+
+ const submit = useCallback((values: FormOnyxValues) => {
+ setImportTransactionCardName(values.cardDisplayName.trim());
+ Keyboard.dismiss();
+ Navigation.setNavigationActionToMicrotaskQueue(() => Navigation.goBack());
+ }, []);
+
+ const validate = useCallback(
+ (values: FormOnyxValues) => {
+ const errors: FormInputErrors = {};
+ const name = values.cardDisplayName.trim();
+
+ if ([...name].length > CONST.TITLE_CHARACTER_LIMIT) {
+ addErrorMessage(errors, 'cardDisplayName', translate('common.error.characterLimitExceedCounter', [...name].length, CONST.TITLE_CHARACTER_LIMIT));
+ }
+
+ return errors;
+ },
+ [translate],
+ );
+
+ return (
+
+ Navigation.goBack()}
+ />
+
+
+
+
+ );
+}
+
+export default ImportTransactionsCardNamePage;
diff --git a/src/pages/settings/Wallet/ImportTransactionsCurrencyPage.tsx b/src/pages/settings/Wallet/ImportTransactionsCurrencyPage.tsx
new file mode 100644
index 000000000000..fc914cdfc28f
--- /dev/null
+++ b/src/pages/settings/Wallet/ImportTransactionsCurrencyPage.tsx
@@ -0,0 +1,42 @@
+import React from 'react';
+import CurrencySelectionList from '@components/CurrencySelectionList';
+import type {CurrencyListItem} from '@components/CurrencySelectionList/types';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import ScreenWrapper from '@components/ScreenWrapper';
+import useLocalize from '@hooks/useLocalize';
+import useOnyx from '@hooks/useOnyx';
+import {setImportTransactionCurrency} from '@libs/actions/ImportSpreadsheet';
+import Navigation from '@libs/Navigation/Navigation';
+import ONYXKEYS from '@src/ONYXKEYS';
+
+function ImportTransactionsCurrencyPage() {
+ const {translate} = useLocalize();
+ const [importedSpreadsheet] = useOnyx(ONYXKEYS.IMPORTED_SPREADSHEET, {canBeMissing: true});
+
+ const onSelectCurrency = (item: CurrencyListItem) => {
+ setImportTransactionCurrency(item.currencyCode);
+ Navigation.setNavigationActionToMicrotaskQueue(Navigation.goBack);
+ };
+
+ return (
+
+ Navigation.goBack()}
+ />
+
+
+
+ );
+}
+
+export default ImportTransactionsCurrencyPage;
diff --git a/src/pages/settings/Wallet/ImportTransactionsPage.tsx b/src/pages/settings/Wallet/ImportTransactionsPage.tsx
new file mode 100644
index 000000000000..a3c77260a1a4
--- /dev/null
+++ b/src/pages/settings/Wallet/ImportTransactionsPage.tsx
@@ -0,0 +1,107 @@
+import React, {useCallback, useState} from 'react';
+import {View} from 'react-native';
+import Button from '@components/Button';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
+import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
+import Text from '@components/Text';
+import useLocalize from '@hooks/useLocalize';
+import useOnyx from '@hooks/useOnyx';
+import useThemeStyles from '@hooks/useThemeStyles';
+import {setImportTransactionSettings} from '@libs/actions/ImportSpreadsheet';
+import Navigation from '@libs/Navigation/Navigation';
+import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+
+function ImportTransactionsPage() {
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+ const [importedSpreadsheet] = useOnyx(ONYXKEYS.IMPORTED_SPREADSHEET, {canBeMissing: true});
+
+ const cardDisplayName = importedSpreadsheet?.importTransactionSettings?.cardDisplayName ?? '';
+ const currency = importedSpreadsheet?.importTransactionSettings?.currency ?? 'USD';
+ const [isReimbursable, setIsReimbursable] = useState(importedSpreadsheet?.importTransactionSettings?.isReimbursable ?? true);
+ const [flipAmountSign, setFlipAmountSign] = useState(importedSpreadsheet?.importTransactionSettings?.flipAmountSign ?? false);
+ const [shouldShowError, setShouldShowError] = useState(false);
+
+ const navigateToCardNameSelection = useCallback(() => {
+ Navigation.navigate(ROUTES.SETTINGS_WALLET_IMPORT_TRANSACTIONS_CARD_NAME);
+ }, []);
+
+ const navigateToCurrencySelection = useCallback(() => {
+ Navigation.navigate(ROUTES.SETTINGS_WALLET_IMPORT_TRANSACTIONS_CURRENCY);
+ }, []);
+
+ const handleNext = useCallback(() => {
+ if (!cardDisplayName) {
+ setShouldShowError(true);
+ return;
+ }
+ // Store import settings in Onyx so they're available when importing
+ setImportTransactionSettings(cardDisplayName, currency, isReimbursable, flipAmountSign);
+ // No cardID = creating a new card
+ Navigation.navigate(ROUTES.SETTINGS_WALLET_IMPORT_TRANSACTIONS_SPREADSHEET.getRoute());
+ }, [cardDisplayName, currency, isReimbursable, flipAmountSign]);
+
+ return (
+
+
+
+
+ {translate('workspace.companyCards.importTransactions.description')}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default ImportTransactionsPage;
diff --git a/src/pages/settings/Wallet/ImportTransactionsSpreadsheetPage.tsx b/src/pages/settings/Wallet/ImportTransactionsSpreadsheetPage.tsx
new file mode 100644
index 000000000000..4c9087c7e21d
--- /dev/null
+++ b/src/pages/settings/Wallet/ImportTransactionsSpreadsheetPage.tsx
@@ -0,0 +1,14 @@
+import React from 'react';
+import ImportSpreadsheet from '@components/ImportSpreadsheet';
+import ROUTES from '@src/ROUTES';
+
+function ImportTransactionsSpreadsheetPage() {
+ return (
+
+ );
+}
+
+export default ImportTransactionsSpreadsheetPage;
diff --git a/src/pages/settings/Wallet/PaymentMethodList.tsx b/src/pages/settings/Wallet/PaymentMethodList.tsx
index 36ca84de3227..7b04a142ee1b 100644
--- a/src/pages/settings/Wallet/PaymentMethodList.tsx
+++ b/src/pages/settings/Wallet/PaymentMethodList.tsx
@@ -223,7 +223,8 @@ function PaymentMethodList({
if (card.fundID) {
const feedNameWithDomainID = getCompanyCardFeedWithDomainID(card.bank as CompanyCardFeed, card.fundID);
shouldShowRBR = shouldShowRbrForFeedNameWithDomainID[feedNameWithDomainID];
- } else {
+ } else if (card.bank !== CONST.PERSONAL_CARD.BANK_NAME.CSV) {
+ // Don't show red dot for CSV imported cards without fundID
shouldShowRBR = true;
}
@@ -242,9 +243,8 @@ function PaymentMethodList({
const lastFourPAN = lastFourNumbersFromCardName(card.cardName);
const plaidUrl = getPlaidInstitutionIconUrl(card.bank);
const isCSVImportCard = card.bank === CONST.COMPANY_CARDS.BANK_NAME.UPLOAD;
- const cardDisplayName = maskCardNumber(card.cardName, card.bank);
+ const cardTitle = isCSVImportCard ? (card.nameValuePairs?.cardTitle ?? card.cardName) : maskCardNumber(card.cardName, card.bank);
const pressHandler = onPress as CardPressHandler;
-
let cardDescription;
if (isUserPersonalCard) {
cardDescription = lastFourPAN;
@@ -274,7 +274,7 @@ function PaymentMethodList({
assignedCardsGrouped.push({
key: card.cardID.toString(),
plaidUrl: isUserPersonalCard ? undefined : plaidUrl,
- title: cardDisplayName,
+ title: cardTitle,
description: isCSVImportCard ? translate('cardPage.csvCardDescription') : cardDescription,
interactive: !isDisabled,
disabled: isDisabled,
diff --git a/src/pages/settings/Wallet/TransactionsImportedPage.tsx b/src/pages/settings/Wallet/TransactionsImportedPage.tsx
new file mode 100644
index 000000000000..86d825880a24
--- /dev/null
+++ b/src/pages/settings/Wallet/TransactionsImportedPage.tsx
@@ -0,0 +1,122 @@
+import React, {useCallback, useMemo, useState} from 'react';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import type {ColumnRole} from '@components/ImportColumn';
+import ImportSpreadsheetColumns from '@components/ImportSpreadsheetColumns';
+import ImportSpreadsheetConfirmModal from '@components/ImportSpreadsheetConfirmModal';
+import ScreenWrapper from '@components/ScreenWrapper';
+import useCloseImportPage from '@hooks/useCloseImportPage';
+import useLocalize from '@hooks/useLocalize';
+import useOnyx from '@hooks/useOnyx';
+import importTransactionsFromCSV from '@libs/actions/ImportTransactions';
+import {findDuplicate, generateColumnNames} from '@libs/importSpreadsheetUtils';
+import Navigation from '@libs/Navigation/Navigation';
+import NotFoundPage from '@pages/ErrorPage/NotFoundPage';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+import type {Errors} from '@src/types/onyx/OnyxCommon';
+import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue';
+
+function TransactionsImportedPage() {
+ const {translate} = useLocalize();
+ const [spreadsheet, spreadsheetMetadata] = useOnyx(ONYXKEYS.IMPORTED_SPREADSHEET, {canBeMissing: true});
+ const [isImporting, setIsImporting] = useState(false);
+ const [isValidationEnabled, setIsValidationEnabled] = useState(false);
+
+ const {setIsClosing} = useCloseImportPage();
+
+ const columnNames = generateColumnNames(spreadsheet?.data?.length ?? 0);
+
+ const columnRoles: ColumnRole[] = useMemo(
+ () => [
+ {text: translate('common.ignore'), value: CONST.CSV_IMPORT_COLUMNS.IGNORE},
+ {text: translate('common.date'), value: 'date', isRequired: true},
+ {text: translate('common.merchant'), value: 'merchant', isRequired: true},
+ {text: translate('common.category'), value: 'category'},
+ {text: translate('iou.amount'), value: 'amount', isRequired: true},
+ ],
+ [translate],
+ );
+
+ const requiredColumns = useMemo(() => columnRoles.filter((role) => role.isRequired), [columnRoles]);
+
+ const validate = useCallback(() => {
+ const columns = Object.values(spreadsheet?.columns ?? {});
+ let errors: Errors = {};
+
+ const missingRequiredColumns = requiredColumns.find((requiredColumn) => !columns.includes(requiredColumn.value));
+ if (missingRequiredColumns) {
+ errors.required = translate('spreadsheet.fieldNotMapped', missingRequiredColumns.text);
+ } else {
+ const duplicate = findDuplicate(columns);
+ const duplicateColumn = columnRoles.find((role) => role.value === duplicate);
+
+ if (duplicateColumn) {
+ errors.duplicates = translate('spreadsheet.singleFieldMultipleColumns', duplicateColumn.text);
+ } else {
+ errors = {};
+ }
+ }
+ return errors;
+ }, [spreadsheet?.columns, requiredColumns, translate, columnRoles]);
+
+ const importTransactions = useCallback(() => {
+ setIsValidationEnabled(true);
+ const errors = validate();
+ if (Object.keys(errors).length > 0) {
+ return;
+ }
+
+ if (!spreadsheet) {
+ return;
+ }
+
+ setIsImporting(true);
+ importTransactionsFromCSV(spreadsheet);
+ }, [validate, spreadsheet]);
+
+ if (!spreadsheet && isLoadingOnyxValue(spreadsheetMetadata)) {
+ return null;
+ }
+
+ const spreadsheetColumns = spreadsheet?.data;
+
+ if (!spreadsheetColumns) {
+ return ;
+ }
+
+ const closeImportPageAndModal = () => {
+ setIsClosing(true);
+ setIsImporting(false);
+ Navigation.dismissModal();
+ };
+
+ return (
+
+ Navigation.goBack(ROUTES.SETTINGS_WALLET_IMPORT_TRANSACTIONS_SPREADSHEET.getRoute())}
+ />
+
+
+
+
+ );
+}
+
+export default TransactionsImportedPage;
diff --git a/src/pages/settings/Wallet/WalletPage/index.tsx b/src/pages/settings/Wallet/WalletPage/index.tsx
index 9728427f6c5c..35fd85f8c1ee 100644
--- a/src/pages/settings/Wallet/WalletPage/index.tsx
+++ b/src/pages/settings/Wallet/WalletPage/index.tsx
@@ -78,7 +78,7 @@ function WalletPage() {
const {isAccountLocked, showLockedAccountModal} = useContext(LockedAccountContext);
const {login: currentUserLogin} = useCurrentUserPersonalDetails();
- const icons = useMemoizedLazyExpensifyIcons(['MoneySearch', 'Wallet', 'Transfer', 'Hourglass', 'Exclamation', 'Star', 'Trashcan', 'Globe', 'UserPlus', 'UserMinus']);
+ const icons = useMemoizedLazyExpensifyIcons(['MoneySearch', 'Wallet', 'Transfer', 'Hourglass', 'Exclamation', 'Star', 'Trashcan', 'Globe', 'UserPlus', 'UserMinus', 'Table']);
const illustrations = useMemoizedLazyIllustrations(['MoneyIntoWallet']);
const walletIllustration = useWalletSectionIllustration();
@@ -528,14 +528,14 @@ function WalletPage() {
/>
- {hasAssignedCard ? (
-
+
+ {hasAssignedCard && (
-
- ) : null}
+ )}
+ {isBetaEnabled(CONST.BETAS.PERSONAL_CARD_IMPORT) && (
+
+
+ )}
+
{hasWallet && (
{contentArea}
diff --git a/src/types/form/ImportTransactionsForm.ts b/src/types/form/ImportTransactionsForm.ts
new file mode 100644
index 000000000000..e7d39e992bb2
--- /dev/null
+++ b/src/types/form/ImportTransactionsForm.ts
@@ -0,0 +1,18 @@
+import type {ValueOf} from 'type-fest';
+import type Form from './Form';
+
+const INPUT_IDS = {
+ CARD_DISPLAY_NAME: 'cardDisplayName',
+} as const;
+
+type InputID = ValueOf;
+
+type ImportTransactionsForm = Form<
+ InputID,
+ {
+ [INPUT_IDS.CARD_DISPLAY_NAME]: string;
+ }
+>;
+
+export type {ImportTransactionsForm};
+export default INPUT_IDS;
diff --git a/src/types/form/index.ts b/src/types/form/index.ts
index c17648c836a1..3ff86d448309 100644
--- a/src/types/form/index.ts
+++ b/src/types/form/index.ts
@@ -8,6 +8,7 @@ export type {ExitSurveyResponseForm} from './ExitSurveyResponseForm';
export type {FeedbackSurveyForm} from './FeedbackSurveyForm';
export type {HomeAddressForm} from './HomeAddressForm';
export type {IKnowTeacherForm} from './IKnowTeacherForm';
+export type {ImportTransactionsForm} from './ImportTransactionsForm';
export type {IntroSchoolPrincipalForm} from './IntroSchoolPrincipalForm';
export type {IssueNewExpensifyCardForm} from './IssueNewExpensifyCardForm';
export type {AddNewCardFeedForm} from './AddNewCardFeedForm';
diff --git a/src/types/onyx/ImportedSpreadsheet.ts b/src/types/onyx/ImportedSpreadsheet.ts
index 393109dbab58..841cef9a17df 100644
--- a/src/types/onyx/ImportedSpreadsheet.ts
+++ b/src/types/onyx/ImportedSpreadsheet.ts
@@ -25,6 +25,21 @@ type ImportFinalModalUnion = {
[K in SpreadsheetTranslationPaths]: ImportFinalModal;
}[SpreadsheetTranslationPaths];
+/** Settings for importing transactions */
+type ImportTransactionSettings = {
+ /** Display name for the card */
+ cardDisplayName?: string;
+
+ /** Currency for the imported transactions */
+ currency?: string;
+
+ /** Whether the transactions are reimbursable */
+ isReimbursable?: boolean;
+
+ /** Whether to flip the amount sign */
+ flipAmountSign?: boolean;
+};
+
/** Model of imported spreadsheet data */
type ImportedSpreadsheet = {
/** Data of the imported spreadsheet */
@@ -59,6 +74,10 @@ type ImportedSpreadsheet = {
/** The file name of the imported file */
fileName?: string;
+
+ /** Settings for importing transactions from the spreadsheet */
+ importTransactionSettings?: ImportTransactionSettings;
};
export default ImportedSpreadsheet;
+export type {ImportTransactionSettings};