From 2150208b14a9b11c3149ee680defa5fbe62c1e3e Mon Sep 17 00:00:00 2001 From: allgandaf Date: Mon, 27 Jul 2026 15:03:33 +0530 Subject: [PATCH 01/12] Add card reconciliation illustration for the CSV import empty state --- assets/images/companyCards/emptystate__card-reconciliation.svg | 1 + 1 file changed, 1 insertion(+) create mode 100644 assets/images/companyCards/emptystate__card-reconciliation.svg diff --git a/assets/images/companyCards/emptystate__card-reconciliation.svg b/assets/images/companyCards/emptystate__card-reconciliation.svg new file mode 100644 index 000000000000..95e38560ff05 --- /dev/null +++ b/assets/images/companyCards/emptystate__card-reconciliation.svg @@ -0,0 +1 @@ + \ No newline at end of file From 4bc9e220270429fd1e380d4b40d1bd0260434043 Mon Sep 17 00:00:00 2001 From: allgandaf Date: Mon, 27 Jul 2026 15:03:34 +0530 Subject: [PATCH 02/12] Register CardReconciliation in the illustrations chunk --- src/components/Icon/chunks/illustrations.chunk.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/Icon/chunks/illustrations.chunk.ts b/src/components/Icon/chunks/illustrations.chunk.ts index ccce57308da6..305e6e464757 100644 --- a/src/components/Icon/chunks/illustrations.chunk.ts +++ b/src/components/Icon/chunks/illustrations.chunk.ts @@ -12,6 +12,7 @@ import WellsFargoCompanyCardDetail from '@assets/images/companyCards/card-wellsf import CompanyCardsEmptyStateGeneric from '@assets/images/companyCards/companycards-generic.svg'; import CompanyCardsEmptyStateUKEU from '@assets/images/companyCards/companycards-uk_eu.svg'; import CompanyCardsEmptyStateUSCA from '@assets/images/companyCards/companycards-us_ca.svg'; +import CardReconciliation from '@assets/images/companyCards/emptystate__card-reconciliation.svg'; import LaptopAssignCard from '@assets/images/companyCards/emptystate__laptop-assign-card.svg'; import LaptopReviewCard from '@assets/images/companyCards/emptystate__laptop-review-card.svg'; import AmexCardCompanyCardDetailLarge from '@assets/images/companyCards/large/card-amex-large.svg'; @@ -251,6 +252,7 @@ const Illustrations = { CompanyCardsEmptyStateGeneric, CompanyCardsEmptyStateUKEU, CompanyCardsEmptyStateUSCA, + CardReconciliation, LaptopAssignCard, LaptopReviewCard, ExpensifyCardCoins, From 01cb6d4a90b4469c35255580117804d7d2c211cf Mon Sep 17 00:00:00 2001 From: allgandaf Date: Mon, 27 Jul 2026 15:03:35 +0530 Subject: [PATCH 03/12] Add sizing style for the card reconciliation illustration --- src/styles/index.ts | 5 +++++ src/styles/variables.ts | 2 ++ 2 files changed, 7 insertions(+) diff --git a/src/styles/index.ts b/src/styles/index.ts index 6bbaabe70df9..510c8d1ac7de 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -5730,6 +5730,11 @@ const staticStyles = (theme: ThemeColors) => height: 162, }, + cardReconciliationIllustration: { + width: variables.cardReconciliationIllustrationWidth, + height: variables.cardReconciliationIllustrationHeight, + }, + computerIllustrationContainer: { width: 272, height: 188, diff --git a/src/styles/variables.ts b/src/styles/variables.ts index 7b99d5d56264..0cf07c9ba410 100644 --- a/src/styles/variables.ts +++ b/src/styles/variables.ts @@ -327,6 +327,8 @@ export default { agentsRulesEmptyStateIllustrationWidth: 268, agentsRulesEmptyStateIllustrationHeight: 194, cardRulesEmptyStateMaxWidth: 496, + cardReconciliationIllustrationWidth: 168, + cardReconciliationIllustrationHeight: 156, expensifyCardEmptyIllustrationWidth: 280, expensifyCardEmptyIllustrationHeight: 172, rulesNewMenuItemMinHeight: 84, From fbcce62cc61b80152579d3269676de37af20a2a2 Mon Sep 17 00:00:00 2001 From: allgandaf Date: Mon, 27 Jul 2026 15:03:42 +0530 Subject: [PATCH 04/12] Add importStartedAt to the company card feed type --- src/types/onyx/CardFeeds.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/types/onyx/CardFeeds.ts b/src/types/onyx/CardFeeds.ts index ffeecb74d56b..da1d6219ff9d 100644 --- a/src/types/onyx/CardFeeds.ts +++ b/src/types/onyx/CardFeeds.ts @@ -84,6 +84,9 @@ type CustomCardFeedData = OnyxCommon.OnyxValueWithOfflineFeedback<{ /** Whether any actions are pending */ pending?: boolean; + /** Microsecond timestamp of when a CSV import for this feed started */ + importStartedAt?: number; + /** Determines if Automated Statement Reconciliation (ASR) is enabled for the cards */ asrEnabled?: boolean; From df7977cf336d7521436ae43a2be185c8f9eeb662 Mon Sep 17 00:00:00 2001 From: allgandaf Date: Mon, 27 Jul 2026 15:03:42 +0530 Subject: [PATCH 05/12] Add the CSV import processing window constant --- src/CONST/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 00de7b87ae51..0cf702ce56e6 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -4719,6 +4719,8 @@ const CONST = { // After a card connection has been broken and unresolved for this many days, stop actively // prompting the user: the time-sensitive home task and the RBR are removed (the error itself is kept). BROKEN_CONNECTION_DISMISS_AFTER_DAYS: 90, + // How long after a CSV import starts we keep showing the processing empty state before falling back to the regular one. + IMPORT_PROCESSING_WINDOW_MS: 10 * 60 * 1000, WORKSPACE_FEEDS_LOAD_ERROR: 'workspaceFeedsLoadError', FEED_LOAD_ERROR: 'feedLoadError', STEP: { From 18e9b5c050b70184cbcad01232498fde689897d0 Mon Sep 17 00:00:00 2001 From: allgandaf Date: Mon, 27 Jul 2026 15:03:43 +0530 Subject: [PATCH 06/12] Add isCompanyCardImportProcessing helper --- src/libs/CardUtils.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index a1cce79deae2..d4288145e00c 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -1031,6 +1031,15 @@ function isSelectedFeedExpired(cardFeed: CombinedCardFeed | undefined): boolean return cardFeed?.expiration ? isBefore(fromUnixTime(cardFeed.expiration), new Date()) : false; } +function isCompanyCardImportProcessing(feed: CombinedCardFeed | undefined): boolean { + if (!feed?.importStartedAt) { + return false; + } + // Auth stamps this in microseconds. + const elapsedMilliseconds = Date.now() - feed.importStartedAt / 1000; + return elapsedMilliseconds >= 0 && elapsedMilliseconds < CONST.COMPANY_CARDS.IMPORT_PROCESSING_WINDOW_MS; +} + /** * For Amex Direct (FDX) feeds, parent cards aggregate child accounts and should not be assignable. * Parent cards follow the format "CardType - Digits" (2 segments separated by " - "), @@ -2034,6 +2043,7 @@ export { getCardFeedIcon, getBankName, isSelectedFeedExpired, + isCompanyCardImportProcessing, isTravelCard, isTravelCardTransaction, getCompanyFeeds, From d2d094d51e3272781850d8ebbff1a9250f8470d2 Mon Sep 17 00:00:00 2001 From: allgandaf Date: Mon, 27 Jul 2026 15:03:43 +0530 Subject: [PATCH 07/12] Add importing empty state copy for the company cards table --- src/languages/de.ts | 2 ++ src/languages/en.ts | 2 ++ src/languages/es.ts | 2 ++ src/languages/fr.ts | 2 ++ src/languages/it.ts | 2 ++ src/languages/ja.ts | 2 ++ src/languages/nl.ts | 2 ++ src/languages/pl.ts | 2 ++ src/languages/pt-BR.ts | 2 ++ src/languages/zh-hans.ts | 2 ++ 10 files changed, 20 insertions(+) diff --git a/src/languages/de.ts b/src/languages/de.ts index 1e5a4131ca3a..5fce4f963483 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -6186,6 +6186,8 @@ _Für ausführlichere Anweisungen [besuchen Sie unsere Hilfeseite](${CONST.NETSU setTransactionLiabilityDescription: 'Wenn aktiviert, können Karteninhaber Kartentransaktionen löschen. Neue Transaktionen folgen dieser Regel.', emptyAddedFeedTitle: 'Keine Karten in diesem Feed', emptyAddedFeedDescription: 'Stelle sicher, dass sich Karten im Kartenfeed deiner Bank befinden.', + importingCardsTitle: 'Karten werden gleich angezeigt …', + importingCardsDescription: 'Bitte haben Sie etwas Geduld, neue Karten und Transaktionen können einige Minuten benötigen, bis sie erscheinen.', pendingFeedTitle: `Wir überprüfen Ihre Anfrage …`, pendingFeedDescription: `Wir überprüfen gerade Ihre Feed-Details. Sobald das erledigt ist, melden wir uns bei Ihnen über`, pendingBankTitle: 'Überprüfe dein Browserfenster', diff --git a/src/languages/en.ts b/src/languages/en.ts index 75bfadee2ca7..639194d3e8e3 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -6274,6 +6274,8 @@ const translations = { setTransactionLiabilityDescription: 'When enabled, cardholders can delete card transactions. New transactions will follow this rule.', emptyAddedFeedTitle: 'No cards in this feed', emptyAddedFeedDescription: "Make sure there are cards in your bank's card feed.", + importingCardsTitle: 'Cards will appear in a moment...', + importingCardsDescription: 'Hang tight, new cards and transactions may take a few minutes to appear.', pendingFeedTitle: `We're reviewing your request...`, pendingFeedDescription: `We're currently reviewing your feed details. Once that's done, we'll reach out to you via`, pendingBankTitle: 'Check your browser window', diff --git a/src/languages/es.ts b/src/languages/es.ts index 5e173ef30b78..25af9549a69a 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -6031,6 +6031,8 @@ ${amount} para ${merchant} - ${date}`, 'Cuando está habilitada, los titulares de tarjetas pueden eliminar transacciones con tarjeta. Las transacciones nuevas seguirán esta regla.', emptyAddedFeedTitle: 'Asignar tarjetas de empresa', emptyAddedFeedDescription: 'Comienza asignando tu primera tarjeta a un miembro.', + importingCardsTitle: 'Las tarjetas aparecerán en un momento...', + importingCardsDescription: 'Un momento, las nuevas tarjetas y transacciones pueden tardar unos minutos en aparecer.', pendingFeedTitle: `Estamos revisando tu solicitud...`, pendingFeedDescription: `Actualmente estamos revisando los detalles de tu feed. Una vez hecho esto, nos pondremos en contacto contigo a través de`, pendingBankTitle: 'Comprueba la ventana de tu navegador', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 664cee6663fb..bf0e4e1b83b8 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -6209,6 +6209,8 @@ _Pour des instructions plus détaillées, [visitez notre site d’aide](${CONST. 'Lorsque cette option est activée, les titulaires de carte peuvent supprimer les transactions de carte. Les nouvelles transactions suivront cette règle.', emptyAddedFeedTitle: 'Aucune carte dans ce flux', emptyAddedFeedDescription: 'Assurez-vous qu’il y a des cartes dans le flux de cartes de votre banque.', + importingCardsTitle: 'Les cartes vont apparaître dans un instant...', + importingCardsDescription: 'Veuillez patienter, l’apparition de nouvelles cartes et transactions peut prendre quelques minutes.', pendingFeedTitle: `Nous examinons votre demande...`, pendingFeedDescription: `Nous examinons actuellement les détails de votre flux. Une fois cela terminé, nous vous contacterons via`, pendingBankTitle: 'Vérifiez la fenêtre de votre navigateur', diff --git a/src/languages/it.ts b/src/languages/it.ts index 6dc456a27c0c..b2044f53c9f2 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -6169,6 +6169,8 @@ _Per istruzioni più dettagliate, [visita il nostro sito di assistenza](${CONST. setTransactionLiabilityDescription: 'Quando è abilitato, i titolari della carta possono eliminare le transazioni della carta. Le nuove transazioni seguiranno questa regola.', emptyAddedFeedTitle: 'Nessuna carta in questo feed', emptyAddedFeedDescription: 'Assicurati che ci siano carte nel flusso di carte della tua banca.', + importingCardsTitle: 'Le carte appariranno tra un momento...', + importingCardsDescription: 'Attendi, le nuove carte e transazioni potrebbero impiegare qualche minuto per apparire.', pendingFeedTitle: `Stiamo esaminando la tua richiesta...`, pendingFeedDescription: `Al momento stiamo esaminando i dettagli del tuo feed. Una volta terminato, ti contatteremo tramite`, pendingBankTitle: 'Controlla la finestra del browser', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 11db3c3def10..d3203820a06a 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -6092,6 +6092,8 @@ _詳しい手順については、[ヘルプサイトをご覧ください](${CO setTransactionLiabilityDescription: '有効にすると、カード保有者はカード取引を削除できるようになります。新しい取引にもこのルールが適用されます。', emptyAddedFeedTitle: 'このフィードにはカードがありません', emptyAddedFeedDescription: '銀行のカード明細フィードにカードが含まれていることを確認してください。', + importingCardsTitle: 'まもなくカードが表示されます…', + importingCardsDescription: 'しばらくお待ちください。新しいカードや取引が表示されるまでに数分かかる場合があります。', pendingFeedTitle: `リクエストを確認しています…`, pendingFeedDescription: `現在、お客さまのフィードの詳細を確認しています。完了し次第、次の方法でご連絡します:`, pendingBankTitle: 'ブラウザウィンドウを確認してください', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index c774982e5ffe..67cecdbd6c84 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -6156,6 +6156,8 @@ _Voor meer gedetailleerde instructies, [bezoek onze help-site](${CONST.NETSUITE_ setTransactionLiabilityDescription: 'Indien ingeschakeld kunnen kaarthouders kaarttransacties verwijderen. Nieuwe transacties zullen deze regel volgen.', emptyAddedFeedTitle: 'Geen kaarten in deze feed', emptyAddedFeedDescription: 'Controleer of er kaarten in de kaartfeed van je bank staan.', + importingCardsTitle: 'Kaarten verschijnen zo...', + importingCardsDescription: 'Even geduld, het kan een paar minuten duren voordat nieuwe kaarten en transacties verschijnen.', pendingFeedTitle: `We beoordelen je aanvraag...`, pendingFeedDescription: `We beoordelen momenteel je feedgegevens. Zodra dat klaar is, nemen we contact met je op via`, pendingBankTitle: 'Controleer je browservenster', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index ac30cc2c6efc..34fe20717441 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -6136,6 +6136,8 @@ _Aby uzyskać bardziej szczegółowe instrukcje, [odwiedź naszą stronę pomocy setTransactionLiabilityDescription: 'Po włączeniu posiadacze kart mogą usuwać transakcje kartą. Nowe transakcje będą podlegać tej zasadzie.', emptyAddedFeedTitle: 'Brak kart w tym kanale', emptyAddedFeedDescription: 'Upewnij się, że w strumieniu kart Twojego banku znajdują się karty.', + importingCardsTitle: 'Karty pojawią się za chwilę…', + importingCardsDescription: 'Prosimy o cierpliwość, nowe karty i transakcje mogą pojawić się dopiero za kilka minut.', pendingFeedTitle: `Przeglądamy Twoje zgłoszenie…`, pendingFeedDescription: `Aktualnie sprawdzamy szczegóły Twojego kanału. Gdy skończymy, skontaktujemy się z Tobą przez`, pendingBankTitle: 'Sprawdź okno przeglądarki', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index fe6a996bfccf..d996a2d4ad13 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -6149,6 +6149,8 @@ _Para instruções mais detalhadas, [visite nossa central de ajuda](${CONST.NETS setTransactionLiabilityDescription: 'Quando ativado, titulares de cartão podem excluir transações do cartão. Novas transações seguirão esta regra.', emptyAddedFeedTitle: 'Nenhum cartão neste feed', emptyAddedFeedDescription: 'Verifique se há cartões no feed de cartões do seu banco.', + importingCardsTitle: 'Os cartões aparecerão em instantes...', + importingCardsDescription: 'Aguarde um momento, novos cartões e transações podem levar alguns minutos para aparecer.', pendingFeedTitle: `Estamos analisando sua solicitação...`, pendingFeedDescription: `No momento, estamos analisando os detalhes do seu feed. Quando isso for concluído, entraremos em contato com você por meio de`, pendingBankTitle: 'Verifique a janela do seu navegador', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 487d47fe7611..bcd582233472 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -5956,6 +5956,8 @@ _如需更详细的说明,请[访问我们的帮助网站](${CONST.NETSUITE_IM setTransactionLiabilityDescription: '启用后,持卡人可以删除卡片交易。新的交易将遵循此规则。', emptyAddedFeedTitle: '此订阅源中没有卡片', emptyAddedFeedDescription: '请确保在您银行的卡片交易流水中有可用的卡片。', + importingCardsTitle: '卡片马上就会显示…', + importingCardsDescription: '请耐心等待,新卡片和交易可能需要几分钟才会显示。', pendingFeedTitle: `我们正在审核你的请求…`, pendingFeedDescription: `我们目前正在审核您的导入信息。完成后,我们将通过以下方式与您联系:`, pendingBankTitle: '检查你的浏览器窗口', From ce6075b8b29d5d3f798ac7bc500b79390677466d Mon Sep 17 00:00:00 2001 From: allgandaf Date: Mon, 27 Jul 2026 15:03:44 +0530 Subject: [PATCH 08/12] Show the importing empty state while a CSV import is processing --- .../Tables/WorkspaceCompanyCardsTable/index.tsx | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/components/Tables/WorkspaceCompanyCardsTable/index.tsx b/src/components/Tables/WorkspaceCompanyCardsTable/index.tsx index 9403670fddc4..5fb77914c168 100644 --- a/src/components/Tables/WorkspaceCompanyCardsTable/index.tsx +++ b/src/components/Tables/WorkspaceCompanyCardsTable/index.tsx @@ -17,7 +17,7 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import {resetFailedWorkspaceCompanyCardUnassignment} from '@libs/actions/CompanyCards'; -import {getCompanyCardCustomName, getDefaultCardName} from '@libs/CardUtils'; +import {getCompanyCardCustomName, getDefaultCardName, isCompanyCardImportProcessing} from '@libs/CardUtils'; import tokenizedSearch from '@libs/tokenizedSearch'; import WorkspaceCompanyCardPageEmptyState from '@pages/workspace/companyCards/WorkspaceCompanyCardPageEmptyState'; @@ -102,7 +102,7 @@ function WorkspaceCompanyCardsTable({ } = companyCards; const {cardFeedErrors} = useCardFeedErrors(); - const illustrations = useMemoizedLazyIllustrations(['LaptopAssignCard', 'BrokenMagnifyingGlass']); + const illustrations = useMemoizedLazyIllustrations(['LaptopAssignCard', 'BrokenMagnifyingGlass', 'CardReconciliation']); const isFeedConnectionBroken = feedName ? cardFeedErrors[feedName]?.isFeedConnectionBroken : false; const [countryByIp] = useOnyx(ONYXKEYS.COUNTRY); @@ -145,6 +145,8 @@ function WorkspaceCompanyCardsTable({ // If we already have fetched cards, then do not show a loading spinner (let the remaining updates refresh in the background), else show it const hasCards = (companyCardEntries ?? []).length > 0; + const isImportProcessing = !hasCards && isCompanyCardImportProcessing(selectedFeed); + const isLoadingOnyxCardList = !hasCards && isLoadingOnyxValue(cardListMetadata); const isLoadingOnyxPersonalDetails = isLoadingOnyxValue(personalDetailsMetadata); const isLoadingOnyxFeed = !isNoFeed && isLoadingOnyxValue(lastSelectedFeedMetadata) && !hasOnceLoadedSelectedFeed; @@ -406,12 +408,14 @@ function WorkspaceCompanyCardsTable({ <> {!!shouldShowGBDisclaimer && {translate('workspace.companyCards.ukRegulation')}} From 4113920162ad8216e52fb64d18a9f4a211864b29 Mon Sep 17 00:00:00 2001 From: allgandaf Date: Mon, 27 Jul 2026 15:03:44 +0530 Subject: [PATCH 09/12] Test the CSV import processing empty state --- tests/ui/WorkspaceCompanyCardsTableTest.tsx | 63 ++++++++++++++++++++- 1 file changed, 61 insertions(+), 2 deletions(-) diff --git a/tests/ui/WorkspaceCompanyCardsTableTest.tsx b/tests/ui/WorkspaceCompanyCardsTableTest.tsx index 985530d15503..34fb3387a960 100644 --- a/tests/ui/WorkspaceCompanyCardsTableTest.tsx +++ b/tests/ui/WorkspaceCompanyCardsTableTest.tsx @@ -25,6 +25,7 @@ const FEED_NAME = `${CONST.COMPANY_CARD.FEED_BANK_NAME.CHASE}#${DOMAIN_OR_WORKSP const BANK_NAME = CONST.COMPANY_CARD.FEED_BANK_NAME.CHASE as CompanyCardFeedWithNumber; const LOADED_METADATA = {status: 'loaded'} as const; +const MICROSECONDS_PER_MILLISECOND = 1000; jest.mock('@hooks/useLazyAsset', () => ({ useMemoizedLazyIllustrations: () => ({}), @@ -61,7 +62,7 @@ jest.mock('@components/Table', () => { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const ReactMock = require('react'); // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const {View} = require('react-native'); + const {Text, View} = require('react-native'); // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unused-vars const MockTable = ReactMock.forwardRef(({children}: {children?: React.ReactNode}, _ref: unknown) => {children}); @@ -73,7 +74,12 @@ jest.mock('@components/Table', () => { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access MockTable.Body = () => ; // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - MockTable.EmptyState = () => ; + MockTable.EmptyState = ({title, subtitle}: {title?: string; subtitle?: string}) => ( + + {title} + {subtitle} + + ); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access MockTable.NoResultsState = () => ; // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access @@ -212,3 +218,56 @@ describe('WorkspaceCompanyCardsTable loading suppression', () => { expect(screen.getByTestId('WorkspaceCompanyCardsTable')).toBeTruthy(); }); }); + +describe('WorkspaceCompanyCardsTable CSV import processing state', () => { + beforeEach(async () => { + await Onyx.clear(); + await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, {}); + await waitForBatchedUpdates(); + }); + + function buildLoadedFeedWithImportMarker(importStartedAt?: number): UseCompanyCardsResult { + return buildCompanyCards({ + workspaceCardFeedsStatus: { + [DOMAIN_OR_WORKSPACE_ACCOUNT_ID]: {isLoading: false}, + }, + isNoFeed: false, + feedName: FEED_NAME, + bankName: BANK_NAME, + selectedFeed: { + feed: BANK_NAME, + importStartedAt, + status: { + isLoading: false, + }, + }, + }); + } + + it('shows the importing empty state when the feed has a recent import marker and no cards', async () => { + renderTable(buildLoadedFeedWithImportMarker(Date.now() * MICROSECONDS_PER_MILLISECOND)); + + await waitForBatchedUpdates(); + + expect(screen.getByTestId('WorkspaceCompanyCardsTableEmptyStateTitle')).toHaveTextContent('Cards will appear in a moment...'); + expect(screen.getByTestId('WorkspaceCompanyCardsTableEmptyStateSubtitle')).toHaveTextContent('Hang tight, new cards and transactions may take a few minutes to appear.'); + }); + + it('shows the default empty state when the import marker is older than the processing window', async () => { + const expiredMarker = (Date.now() - CONST.COMPANY_CARDS.IMPORT_PROCESSING_WINDOW_MS - 1) * MICROSECONDS_PER_MILLISECOND; + + renderTable(buildLoadedFeedWithImportMarker(expiredMarker)); + + await waitForBatchedUpdates(); + + expect(screen.getByTestId('WorkspaceCompanyCardsTableEmptyStateTitle')).toHaveTextContent('No cards in this feed'); + }); + + it('shows the default empty state when the feed has no import marker', async () => { + renderTable(buildLoadedFeedWithImportMarker()); + + await waitForBatchedUpdates(); + + expect(screen.getByTestId('WorkspaceCompanyCardsTableEmptyStateTitle')).toHaveTextContent('No cards in this feed'); + }); +}); From a6a717c9c249a9f4e69db2dd4cd6625568f4b7a0 Mon Sep 17 00:00:00 2001 From: allgandaf Date: Mon, 27 Jul 2026 15:51:28 +0530 Subject: [PATCH 10/12] Drop the CSV import processing time window --- src/CONST/index.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 0cf702ce56e6..00de7b87ae51 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -4719,8 +4719,6 @@ const CONST = { // After a card connection has been broken and unresolved for this many days, stop actively // prompting the user: the time-sensitive home task and the RBR are removed (the error itself is kept). BROKEN_CONNECTION_DISMISS_AFTER_DAYS: 90, - // How long after a CSV import starts we keep showing the processing empty state before falling back to the regular one. - IMPORT_PROCESSING_WINDOW_MS: 10 * 60 * 1000, WORKSPACE_FEEDS_LOAD_ERROR: 'workspaceFeedsLoadError', FEED_LOAD_ERROR: 'feedLoadError', STEP: { From 1fad9047b557a07dc272124e9e75385097d60e91 Mon Sep 17 00:00:00 2001 From: allgandaf Date: Mon, 27 Jul 2026 15:51:28 +0530 Subject: [PATCH 11/12] Derive the importing state straight from the feed marker --- .../Tables/WorkspaceCompanyCardsTable/index.tsx | 4 ++-- src/libs/CardUtils.ts | 10 ---------- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/src/components/Tables/WorkspaceCompanyCardsTable/index.tsx b/src/components/Tables/WorkspaceCompanyCardsTable/index.tsx index 5fb77914c168..3e48825dd1e6 100644 --- a/src/components/Tables/WorkspaceCompanyCardsTable/index.tsx +++ b/src/components/Tables/WorkspaceCompanyCardsTable/index.tsx @@ -17,7 +17,7 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import {resetFailedWorkspaceCompanyCardUnassignment} from '@libs/actions/CompanyCards'; -import {getCompanyCardCustomName, getDefaultCardName, isCompanyCardImportProcessing} from '@libs/CardUtils'; +import {getCompanyCardCustomName, getDefaultCardName} from '@libs/CardUtils'; import tokenizedSearch from '@libs/tokenizedSearch'; import WorkspaceCompanyCardPageEmptyState from '@pages/workspace/companyCards/WorkspaceCompanyCardPageEmptyState'; @@ -145,7 +145,7 @@ function WorkspaceCompanyCardsTable({ // If we already have fetched cards, then do not show a loading spinner (let the remaining updates refresh in the background), else show it const hasCards = (companyCardEntries ?? []).length > 0; - const isImportProcessing = !hasCards && isCompanyCardImportProcessing(selectedFeed); + const isImportProcessing = !hasCards && !!selectedFeed?.importStartedAt; const isLoadingOnyxCardList = !hasCards && isLoadingOnyxValue(cardListMetadata); const isLoadingOnyxPersonalDetails = isLoadingOnyxValue(personalDetailsMetadata); diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index d4288145e00c..a1cce79deae2 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -1031,15 +1031,6 @@ function isSelectedFeedExpired(cardFeed: CombinedCardFeed | undefined): boolean return cardFeed?.expiration ? isBefore(fromUnixTime(cardFeed.expiration), new Date()) : false; } -function isCompanyCardImportProcessing(feed: CombinedCardFeed | undefined): boolean { - if (!feed?.importStartedAt) { - return false; - } - // Auth stamps this in microseconds. - const elapsedMilliseconds = Date.now() - feed.importStartedAt / 1000; - return elapsedMilliseconds >= 0 && elapsedMilliseconds < CONST.COMPANY_CARDS.IMPORT_PROCESSING_WINDOW_MS; -} - /** * For Amex Direct (FDX) feeds, parent cards aggregate child accounts and should not be assignable. * Parent cards follow the format "CardType - Digits" (2 segments separated by " - "), @@ -2043,7 +2034,6 @@ export { getCardFeedIcon, getBankName, isSelectedFeedExpired, - isCompanyCardImportProcessing, isTravelCard, isTravelCardTransaction, getCompanyFeeds, From 19fff96af60b3a6a2f555beddb42a891480b26ad Mon Sep 17 00:00:00 2001 From: allgandaf Date: Mon, 27 Jul 2026 15:51:29 +0530 Subject: [PATCH 12/12] Drop the expired-marker test case --- tests/ui/WorkspaceCompanyCardsTableTest.tsx | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/tests/ui/WorkspaceCompanyCardsTableTest.tsx b/tests/ui/WorkspaceCompanyCardsTableTest.tsx index 34fb3387a960..c89d8589fd34 100644 --- a/tests/ui/WorkspaceCompanyCardsTableTest.tsx +++ b/tests/ui/WorkspaceCompanyCardsTableTest.tsx @@ -244,7 +244,7 @@ describe('WorkspaceCompanyCardsTable CSV import processing state', () => { }); } - it('shows the importing empty state when the feed has a recent import marker and no cards', async () => { + it('shows the importing empty state when the feed has an import marker and no cards', async () => { renderTable(buildLoadedFeedWithImportMarker(Date.now() * MICROSECONDS_PER_MILLISECOND)); await waitForBatchedUpdates(); @@ -253,16 +253,6 @@ describe('WorkspaceCompanyCardsTable CSV import processing state', () => { expect(screen.getByTestId('WorkspaceCompanyCardsTableEmptyStateSubtitle')).toHaveTextContent('Hang tight, new cards and transactions may take a few minutes to appear.'); }); - it('shows the default empty state when the import marker is older than the processing window', async () => { - const expiredMarker = (Date.now() - CONST.COMPANY_CARDS.IMPORT_PROCESSING_WINDOW_MS - 1) * MICROSECONDS_PER_MILLISECOND; - - renderTable(buildLoadedFeedWithImportMarker(expiredMarker)); - - await waitForBatchedUpdates(); - - expect(screen.getByTestId('WorkspaceCompanyCardsTableEmptyStateTitle')).toHaveTextContent('No cards in this feed'); - }); - it('shows the default empty state when the feed has no import marker', async () => { renderTable(buildLoadedFeedWithImportMarker());