From 32deaef1081f242740ec20ef1092d7c9b509db0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Obudzi=C5=84ski?= Date: Wed, 2 Sep 2026 15:35:57 +0200 Subject: [PATCH 1/5] fix: rework the stale search empty state on Spend > Expenses --- .../simple-illustration__foldersync.svg | 1 + .../BlockingViews/FullPageErrorView.tsx | 26 ++++++++++++++++--- .../Icon/chunks/illustrations.chunk.ts | 2 ++ src/components/Search/index.tsx | 22 +++++++++++----- src/languages/en.ts | 5 ++++ 5 files changed, 46 insertions(+), 10 deletions(-) create mode 100644 assets/images/simple-illustrations/simple-illustration__foldersync.svg diff --git a/assets/images/simple-illustrations/simple-illustration__foldersync.svg b/assets/images/simple-illustrations/simple-illustration__foldersync.svg new file mode 100644 index 000000000000..ca3bfd59fd05 --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__foldersync.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/BlockingViews/FullPageErrorView.tsx b/src/components/BlockingViews/FullPageErrorView.tsx index 649a9e9444b7..31b4bad0e36f 100644 --- a/src/components/BlockingViews/FullPageErrorView.tsx +++ b/src/components/BlockingViews/FullPageErrorView.tsx @@ -13,6 +13,12 @@ import {View} from 'react-native'; import BlockingView from './BlockingView'; import ForceFullScreenView from './ForceFullScreenView'; +/** + * Illustrations this view can show. Both are requested up front, because the lazy illustrations hook + * only seeds its asset map once per mount, so a name it wasn't given at mount resolves to PlaceholderIcon. + */ +type FullPageErrorViewIllustration = 'BrokenMagnifyingGlass' | 'FolderSync'; + type FullPageErrorViewProps = { /** TestID for test */ testID?: string; @@ -42,6 +48,15 @@ type FullPageErrorViewProps = { /** Function to call when pressing the CTA button. The button only renders when this and `buttonTranslationKey` are both provided */ onButtonPress?: () => void; + + /** Illustration shown above the title */ + illustration?: FullPageErrorViewIllustration; + + /** Width of the illustration */ + illustrationWidth?: number; + + /** Height of the illustration */ + illustrationHeight?: number; }; function FullPageErrorView({ @@ -55,9 +70,12 @@ function FullPageErrorView({ containerStyle, buttonTranslationKey, onButtonPress, + illustration = 'BrokenMagnifyingGlass', + illustrationWidth = variables.errorPageIconWidth, + illustrationHeight = variables.errorPageIconHeight, }: FullPageErrorViewProps) { const styles = useThemeStyles(); - const illustrations = useMemoizedLazyIllustrations(['BrokenMagnifyingGlass']); + const illustrations = useMemoizedLazyIllustrations(['BrokenMagnifyingGlass', 'FolderSync']); if (shouldShow) { return ( @@ -67,9 +85,9 @@ function FullPageErrorView({ testID={testID} > { // A response replaces the snapshot's results rather than appending to them, so retrying at // the paginated offset would leave only that later page behind. Retry from the first page. diff --git a/src/languages/en.ts b/src/languages/en.ts index 62367aea9fc4..a7a71d3d8b94 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -9398,6 +9398,11 @@ const translations = { title: 'No expenses to approve', subtitle: 'Zero expenses. Maximum chill. Well done!', }, + staleResults: { + title: 'Refresh needed', + subtitle: 'This page is out of date, refresh to see the latest', + buttonText: 'Refresh', + }, }, columns: 'Columns', editColumns: 'Edit columns', From 2e4a14a0da112a601b1d411c80b111e29c826acd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Obudzi=C5=84ski?= Date: Mon, 7 Sep 2026 10:31:20 +0200 Subject: [PATCH 2/5] chore: add generated translations for the stale search empty state --- src/languages/de.ts | 1 + src/languages/el.ts | 1 + src/languages/es.ts | 1 + src/languages/fr.ts | 1 + src/languages/it.ts | 1 + src/languages/ja.ts | 1 + src/languages/nl.ts | 1 + src/languages/pl.ts | 1 + src/languages/pt-BR.ts | 1 + src/languages/zh-hans.ts | 1 + 10 files changed, 10 insertions(+) diff --git a/src/languages/de.ts b/src/languages/de.ts index 4ae8c1434ef0..10717fa28255 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -9289,6 +9289,7 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc title: 'Keine Ausgaben zum Genehmigen', subtitle: 'Null Ausgaben. Maximale Entspannung. Gut gemacht!', }, + staleResults: {title: 'Aktualisierung erforderlich', subtitle: 'Diese Seite ist veraltet, aktualisieren Sie sie, um die neuesten Inhalte zu sehen', buttonText: 'Aktualisieren'}, }, columns: 'Spalten', editColumns: 'Spalten bearbeiten', diff --git a/src/languages/el.ts b/src/languages/el.ts index d5fc56a94039..b671ca44a9f3 100644 --- a/src/languages/el.ts +++ b/src/languages/el.ts @@ -9499,6 +9499,7 @@ ${reportName}`, emptyViolationSnapshotResults: { subtitle: (formattedDate) => `Οι παραβάσεις παρακολουθούνται μόνο από ${formattedDate} και μετά. Δοκιμάστε να προσαρμόσετε τα φίλτρα ημερομηνίας σας.`, }, + staleResults: {title: 'Απαιτείται ανανέωση', subtitle: 'Αυτή η σελίδα είναι παρωχημένη, κάντε ανανέωση για να δείτε τα πιο πρόσφατα', buttonText: 'Ανανέωση'}, }, columns: 'Στήλες', editColumns: 'Επεξεργασία στηλών', diff --git a/src/languages/es.ts b/src/languages/es.ts index 848661311d71..20d6ec729908 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -9147,6 +9147,7 @@ ${reportName}`, title: 'No hay gastos para aprobar', subtitle: 'Cero gastos. Máxima relajación. ¡Bien hecho!', }, + staleResults: {title: 'Es necesario actualizar', subtitle: 'Esta página está desactualizada, actualiza para ver lo más reciente', buttonText: 'Actualizar'}, }, columns: 'Columnas', editColumns: 'Editar columnas', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index a70f4f104f95..be09a7e9a193 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -9319,6 +9319,7 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e title: 'Aucune dépense à approuver', subtitle: 'Zéro dépense. Détente maximale. Bien joué !', }, + staleResults: {title: 'Actualisation requise', subtitle: 'Cette page n’est plus à jour, actualisez-la pour voir les dernières informations', buttonText: 'Actualiser'}, }, columns: 'Colonnes', editColumns: 'Modifier les colonnes', diff --git a/src/languages/it.ts b/src/languages/it.ts index 31d957024b8e..c3f0f1a03299 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -9253,6 +9253,7 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`, title: 'Nessuna spesa da approvare', subtitle: 'Zero spese. Massimo relax. Ben fatto!', }, + staleResults: {title: 'Aggiornamento necessario', subtitle: 'Questa pagina non è aggiornata, ricarica per vedere le ultime novità', buttonText: 'Aggiorna'}, }, columns: 'Colonne', editColumns: 'Modifica colonne', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index d9c70fd9573b..74acf87f2bbc 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -9124,6 +9124,7 @@ ${reportName}`, title: '承認する経費はありません', subtitle: '経費はゼロ。リラックス度マックス。お見事です!', }, + staleResults: {title: '再読み込みが必要です', subtitle: 'このページは古くなっています。最新の情報を見るには再読み込みしてください。', buttonText: '更新'}, }, columns: '列', editColumns: '列の編集', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 13b91089acd7..6319c874819b 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -9216,6 +9216,7 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`, title: 'Geen declaraties om goed te keuren', subtitle: 'Nul uitgaven. Maximale chill. Goed gedaan!', }, + staleResults: {title: 'Vernieuwen nodig', subtitle: 'Deze pagina is verouderd, vernieuw om de laatste versie te zien', buttonText: 'Verversen'}, }, columns: 'Kolommen', editColumns: 'Kolommen bewerken', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 3d4a22425faa..5c0421a83a18 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -9237,6 +9237,7 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`, title: 'Brak wydatków do zatwierdzenia', subtitle: 'Zero wydatków. Maksymalny luz. Dobra robota!', }, + staleResults: {title: 'Wymagane odświeżenie', subtitle: 'Ta strona jest nieaktualna, odśwież ją, żeby zobaczyć najnowszą wersję', buttonText: 'Odśwież'}, }, columns: 'Kolumny', editColumns: 'Edytuj kolumny', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index c2bb06fd4366..14c647442eea 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -9220,6 +9220,7 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`, title: 'Nenhuma despesa para aprovar', subtitle: 'Zero despesas. Máximo sossego. Muito bem!', }, + staleResults: {title: 'É preciso atualizar', subtitle: 'Esta página está desatualizada, atualize para ver a versão mais recente', buttonText: 'Atualizar'}, }, columns: 'Colunas', editColumns: 'Editar colunas', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index b2fe6edc05a0..a63c59dca521 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -8879,6 +8879,7 @@ ${reportName}`, title: '没有报销可审批', subtitle: '零报销,最大轻松。干得好!', }, + staleResults: {title: '需要刷新', subtitle: '此页面已过期,刷新以查看最新内容', buttonText: '刷新'}, }, columns: '列', editColumns: '编辑列', From a03c9d61d58556f06100c91ca5e66fe1a4e50359 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Obudzi=C5=84ski?= Date: Mon, 7 Sep 2026 11:59:25 +0200 Subject: [PATCH 3/5] fix: only show the stale search empty state when the snapshot holds data --- src/components/Search/index.tsx | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index ef536cfd3147..686d9bb0525c 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -1095,6 +1095,15 @@ function Search({ if (hasErrors) { const isInvalidQuery = responseStatusCode === CONST.JSON_CODE.INVALID_SEARCH_QUERY; + // "Refresh needed" claims content went stale, but a failed first search never had any results to go + // stale, so only show it when the snapshot still holds data. A fresh failure keeps the generic error. + const isStaleResults = !isInvalidQuery && !isEmptyObject(searchResults?.data); + let subtitleKey: 'errorPage.subtitle' | 'errorPage.wrongTypeSubtitle' | 'search.searchResults.staleResults.subtitle' = 'errorPage.subtitle'; + if (isInvalidQuery) { + subtitleKey = 'errorPage.wrongTypeSubtitle'; + } else if (isStaleResults) { + subtitleKey = 'search.searchResults.staleResults.subtitle'; + } cancelNavigationSpans(); return ( @@ -1103,16 +1112,16 @@ function Search({ containerStyle={styles.searchBlockingErrorViewContainer} subtitleStyle={styles.textSupporting} title={ - isInvalidQuery - ? translate('errorPage.title', { + isStaleResults + ? translate('search.searchResults.staleResults.title') + : translate('errorPage.title', { isBreakLine: shouldUseNarrowLayout, }) - : translate('search.searchResults.staleResults.title') } - subtitle={translate(isInvalidQuery ? 'errorPage.wrongTypeSubtitle' : 'search.searchResults.staleResults.subtitle')} + subtitle={translate(subtitleKey)} // A failed request leaves results that are out of date rather than broken, so that case gets the // refresh copy and illustration. An invalid query keeps the error copy, since it really did fail. - {...(!isInvalidQuery && { + {...(isStaleResults && { illustration: 'FolderSync', illustrationWidth: variables.iconSizeUltraLarge, illustrationHeight: variables.iconSizeUltraLarge, From 54088bc5243982e554d4fbaa9019606afba8fd25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Obudzi=C5=84ski?= Date: Wed, 9 Sep 2026 15:19:15 +0200 Subject: [PATCH 4/5] revert: show the stale search empty state for every retryable failure --- src/components/Search/index.tsx | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 686d9bb0525c..ef536cfd3147 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -1095,15 +1095,6 @@ function Search({ if (hasErrors) { const isInvalidQuery = responseStatusCode === CONST.JSON_CODE.INVALID_SEARCH_QUERY; - // "Refresh needed" claims content went stale, but a failed first search never had any results to go - // stale, so only show it when the snapshot still holds data. A fresh failure keeps the generic error. - const isStaleResults = !isInvalidQuery && !isEmptyObject(searchResults?.data); - let subtitleKey: 'errorPage.subtitle' | 'errorPage.wrongTypeSubtitle' | 'search.searchResults.staleResults.subtitle' = 'errorPage.subtitle'; - if (isInvalidQuery) { - subtitleKey = 'errorPage.wrongTypeSubtitle'; - } else if (isStaleResults) { - subtitleKey = 'search.searchResults.staleResults.subtitle'; - } cancelNavigationSpans(); return ( @@ -1112,16 +1103,16 @@ function Search({ containerStyle={styles.searchBlockingErrorViewContainer} subtitleStyle={styles.textSupporting} title={ - isStaleResults - ? translate('search.searchResults.staleResults.title') - : translate('errorPage.title', { + isInvalidQuery + ? translate('errorPage.title', { isBreakLine: shouldUseNarrowLayout, }) + : translate('search.searchResults.staleResults.title') } - subtitle={translate(subtitleKey)} + subtitle={translate(isInvalidQuery ? 'errorPage.wrongTypeSubtitle' : 'search.searchResults.staleResults.subtitle')} // A failed request leaves results that are out of date rather than broken, so that case gets the // refresh copy and illustration. An invalid query keeps the error copy, since it really did fail. - {...(isStaleResults && { + {...(!isInvalidQuery && { illustration: 'FolderSync', illustrationWidth: variables.iconSizeUltraLarge, illustrationHeight: variables.iconSizeUltraLarge, From beec0b42fd9d365522796bfd0ae60138530699f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Obudzi=C5=84ski?= Date: Thu, 17 Sep 2026 15:37:13 +0200 Subject: [PATCH 5/5] chore: fix import formatting after the merge --- src/components/Search/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 1561115d15b3..c4e2829de15c 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -71,7 +71,6 @@ import type {SearchFullscreenNavigatorParamList} from '@navigation/types'; import EmptySearchView from '@pages/Search/EmptySearchView'; import type {GetReportTableColumnStylesParams} from '@styles/utils'; - import variables from '@styles/variables'; import CONST from '@src/CONST';