From 333ac3e286989fe479952c0dc5b3a23665683dc5 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Fri, 9 Jan 2026 12:29:23 +0530 Subject: [PATCH 01/16] Update search total amount footer logic --- src/libs/SearchUIUtils.ts | 12 ++++++ src/pages/Search/SearchPage.tsx | 13 +++++- src/pages/Search/SearchPageNarrow.tsx | 15 +++++-- tests/unit/Search/SearchUIUtilsTest.ts | 57 ++++++++++++++++++++++++++ 4 files changed, 92 insertions(+), 5 deletions(-) diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index f29187b07395e..333023eeb3996 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -3403,6 +3403,17 @@ function getTableMinWidth(columns: SearchColumnType[]) { return minWidth; } +type ShouldShowSearchPageFooterParams = { + isSavedSearch: boolean; + resultsCount?: number; + isDefaultExpensesSearch: boolean; + selectedTransactionsCount: number; +}; + +function shouldShowSearchPageFooter({isSavedSearch, resultsCount, isDefaultExpensesSearch, selectedTransactionsCount}: ShouldShowSearchPageFooterParams) { + return isSavedSearch || (!!resultsCount && !isDefaultExpensesSearch) || selectedTransactionsCount > 0; +} + export { getSuggestedSearches, getDefaultActionableSearchMenuItem, @@ -3450,6 +3461,7 @@ export { getTransactionFromTransactionListItem, getSearchColumnTranslationKey, getTableMinWidth, + shouldShowSearchPageFooter, getCustomColumns, getCustomColumnDefault, }; diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index 69b0475351210..aaea2fa5ae8a9 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -78,7 +78,8 @@ import { isInvoiceReport, isIOUReport as isIOUReportUtil, } from '@libs/ReportUtils'; -import {buildSearchQueryJSON} from '@libs/SearchQueryUtils'; +import {buildSearchQueryJSON, isDefaultExpensesQuery} from '@libs/SearchQueryUtils'; +import {shouldShowSearchPageFooter} from '@libs/SearchUIUtils'; import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; import {hasTransactionBeenRejected} from '@libs/TransactionUtils'; import type {ReceiptFile} from '@pages/iou/request/step/IOURequestStepScan/types'; @@ -123,6 +124,7 @@ function SearchPage({route}: SearchPageProps) { const [personalPolicyID] = useOnyx(ONYXKEYS.PERSONAL_POLICY_ID, {canBeMissing: true}); const [personalPolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${personalPolicyID}`, {canBeMissing: true}); const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: true}); + const [savedSearches] = useOnyx(ONYXKEYS.SAVED_SEARCHES, {canBeMissing: true}); const [integrationsExportTemplates] = useOnyx(ONYXKEYS.NVP_INTEGRATION_SERVER_EXPORT_TEMPLATES, {canBeMissing: true}); const [csvExportLayouts] = useOnyx(ONYXKEYS.NVP_CSV_EXPORT_LAYOUTS, {canBeMissing: true}); @@ -1009,7 +1011,14 @@ function SearchPage({route}: SearchPageProps) { const {resetVideoPlayerData} = usePlaybackContext(); const metadata = searchResults?.search; - const shouldShowFooter = !!metadata?.count || selectedTransactionsKeys.length > 0; + const isDefaultExpensesSearch = queryJSON ? isDefaultExpensesQuery(queryJSON) : false; + const isSavedSearch = queryJSON?.hash !== undefined && Object.prototype.hasOwnProperty.call(savedSearches ?? {}, String(queryJSON.hash)); + const shouldShowFooter = shouldShowSearchPageFooter({ + isSavedSearch, + resultsCount: metadata?.count, + isDefaultExpensesSearch, + selectedTransactionsCount: selectedTransactionsKeys.length, + }); // Handles video player cleanup: // 1. On mount: Resets player if navigating from report screen diff --git a/src/pages/Search/SearchPageNarrow.tsx b/src/pages/Search/SearchPageNarrow.tsx index 5efc0be59900a..2d0ea4a8e0e83 100644 --- a/src/pages/Search/SearchPageNarrow.tsx +++ b/src/pages/Search/SearchPageNarrow.tsx @@ -31,8 +31,8 @@ import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import {turnOffMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; import Navigation from '@libs/Navigation/Navigation'; -import {buildCannedSearchQuery} from '@libs/SearchQueryUtils'; -import {isSearchDataLoaded} from '@libs/SearchUIUtils'; +import {buildCannedSearchQuery, isDefaultExpensesQuery} from '@libs/SearchQueryUtils'; +import {isSearchDataLoaded, shouldShowSearchPageFooter} from '@libs/SearchUIUtils'; import variables from '@styles/variables'; import {searchInServer} from '@userActions/Report'; import {search} from '@userActions/Search'; @@ -85,6 +85,7 @@ function SearchPageNarrow({ const {isOffline} = useNetwork(); const currentSearchResultsKey = queryJSON?.hash ?? CONST.DEFAULT_NUMBER_ID; const [currentSearchResults] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${currentSearchResultsKey}`, {canBeMissing: true}); + const [savedSearches] = useOnyx(ONYXKEYS.SAVED_SEARCHES, {canBeMissing: true}); // Controls the visibility of the educational tooltip based on user scrolling. // Hides the tooltip when the user is scrolling and displays it once scrolling stops. const triggerScrollEvent = useScrollEventEmitter(); @@ -181,7 +182,15 @@ function SearchPageNarrow({ ); } - const shouldShowFooter = !!metadata?.count || Object.keys(selectedTransactions).length > 0; + const isDefaultExpensesSearch = queryJSON ? isDefaultExpensesQuery(queryJSON) : false; + const isSavedSearch = queryJSON?.hash !== undefined && Object.prototype.hasOwnProperty.call(savedSearches ?? {}, String(queryJSON.hash)); + const shouldShowFooter = shouldShowSearchPageFooter({ + isSavedSearch, + resultsCount: metadata?.count, + isDefaultExpensesSearch, + selectedTransactionsCount: Object.keys(selectedTransactions).length, + }); + const isDataLoaded = isSearchDataLoaded(searchResults, queryJSON); const shouldShowLoadingState = !isOffline && (!isDataLoaded || !!currentSearchResults?.search?.isLoading); diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index 22961bd59d360..4321111a2a301 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -3211,4 +3211,61 @@ describe('SearchUIUtils', () => { expect(transactionThread).toBeTruthy(); }); }); + + describe('shouldShowSearchPageFooter', () => { + test('Should always show footer for saved searches', () => { + expect( + SearchUIUtils.shouldShowSearchPageFooter({ + isSavedSearch: true, + resultsCount: 0, + isDefaultExpensesSearch: true, + selectedTransactionsCount: 0, + }), + ).toBe(true); + }); + + test('Should show footer when there are results and not the default expenses search', () => { + expect( + SearchUIUtils.shouldShowSearchPageFooter({ + isSavedSearch: false, + resultsCount: 10, + isDefaultExpensesSearch: false, + selectedTransactionsCount: 0, + }), + ).toBe(true); + }); + + test('Should not show footer when results exist but it is the default expenses search', () => { + expect( + SearchUIUtils.shouldShowSearchPageFooter({ + isSavedSearch: false, + resultsCount: 10, + isDefaultExpensesSearch: true, + selectedTransactionsCount: 0, + }), + ).toBe(false); + }); + + test('Should show footer when there are selected transactions', () => { + expect( + SearchUIUtils.shouldShowSearchPageFooter({ + isSavedSearch: false, + resultsCount: 0, + isDefaultExpensesSearch: true, + selectedTransactionsCount: 1, + }), + ).toBe(true); + }); + + test('Should not show footer when there are no results and nothing is selected', () => { + expect( + SearchUIUtils.shouldShowSearchPageFooter({ + isSavedSearch: false, + resultsCount: 0, + isDefaultExpensesSearch: false, + selectedTransactionsCount: 0, + }), + ).toBe(false); + }); + }); }); From e38609a1602f65c948ed88916ee9ebfd5fc5d650 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Sat, 10 Jan 2026 01:59:17 +0530 Subject: [PATCH 02/16] Update isSavedSearch --- src/pages/Search/SearchPage.tsx | 2 +- src/pages/Search/SearchPageNarrow.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index aaea2fa5ae8a9..905d76ecaab38 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -1012,7 +1012,7 @@ function SearchPage({route}: SearchPageProps) { const metadata = searchResults?.search; const isDefaultExpensesSearch = queryJSON ? isDefaultExpensesQuery(queryJSON) : false; - const isSavedSearch = queryJSON?.hash !== undefined && Object.prototype.hasOwnProperty.call(savedSearches ?? {}, String(queryJSON.hash)); + const isSavedSearch = queryJSON?.hash != undefined && String(queryJSON.hash) in (savedSearches ?? {}); const shouldShowFooter = shouldShowSearchPageFooter({ isSavedSearch, resultsCount: metadata?.count, diff --git a/src/pages/Search/SearchPageNarrow.tsx b/src/pages/Search/SearchPageNarrow.tsx index 2d0ea4a8e0e83..ba2f2bb91adc0 100644 --- a/src/pages/Search/SearchPageNarrow.tsx +++ b/src/pages/Search/SearchPageNarrow.tsx @@ -183,7 +183,7 @@ function SearchPageNarrow({ } const isDefaultExpensesSearch = queryJSON ? isDefaultExpensesQuery(queryJSON) : false; - const isSavedSearch = queryJSON?.hash !== undefined && Object.prototype.hasOwnProperty.call(savedSearches ?? {}, String(queryJSON.hash)); + const isSavedSearch = queryJSON?.hash != undefined && String(queryJSON.hash) in (savedSearches ?? {}); const shouldShowFooter = shouldShowSearchPageFooter({ isSavedSearch, resultsCount: metadata?.count, From 9b313a5d9f44790dc187a4a7ca777f16221d69a4 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Tue, 13 Jan 2026 02:19:28 +0530 Subject: [PATCH 03/16] Use better logic --- src/components/Search/index.tsx | 2 +- src/hooks/useSearchShouldCalculateTotals.ts | 24 ++++++--------------- src/libs/SearchUIUtils.ts | 5 ++--- src/pages/Search/SearchPage.tsx | 4 +--- src/pages/Search/SearchPageNarrow.tsx | 4 +--- 5 files changed, 12 insertions(+), 27 deletions(-) diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index c02c8db81fb60..789a5e7a6fe02 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -302,7 +302,7 @@ function Search({ const searchKey = useMemo(() => Object.values(suggestedSearches).find((search) => search.similarSearchHash === similarSearchHash)?.key, [suggestedSearches, similarSearchHash]); - const shouldCalculateTotals = useSearchShouldCalculateTotals(searchKey, similarSearchHash, offset === 0); + const shouldCalculateTotals = useSearchShouldCalculateTotals(searchKey, queryJSON?.hash, offset === 0); const previousReportActions = usePrevious(reportActions); const {translate, localeCompare, formatPhoneNumber} = useLocalize(); diff --git a/src/hooks/useSearchShouldCalculateTotals.ts b/src/hooks/useSearchShouldCalculateTotals.ts index c7f10905f1d2c..655e2c431097a 100644 --- a/src/hooks/useSearchShouldCalculateTotals.ts +++ b/src/hooks/useSearchShouldCalculateTotals.ts @@ -1,11 +1,10 @@ import {useMemo} from 'react'; -import {buildSearchQueryJSON} from '@libs/SearchQueryUtils'; import type {SearchKey} from '@libs/SearchUIUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import useOnyx from './useOnyx'; -function useSearchShouldCalculateTotals(searchKey: SearchKey | undefined, similarSearchHash: number | undefined, enabled: boolean) { +function useSearchShouldCalculateTotals(searchKey: SearchKey | undefined, queryHash: number | undefined, enabled: boolean) { const [savedSearches] = useOnyx(ONYXKEYS.SAVED_SEARCHES, {canBeMissing: true}); const shouldCalculateTotals = useMemo(() => { @@ -13,9 +12,11 @@ function useSearchShouldCalculateTotals(searchKey: SearchKey | undefined, simila return false; } - const savedSearchValues = Object.values(savedSearches ?? {}); + if (queryHash != undefined && String(queryHash) in (savedSearches ?? {})) { + return true; + } - if (!savedSearchValues.length && !searchKey) { + if (!searchKey) { return false; } @@ -30,19 +31,8 @@ function useSearchShouldCalculateTotals(searchKey: SearchKey | undefined, simila CONST.SEARCH.SEARCH_KEYS.RECONCILIATION, ]; - if (eligibleSearchKeys.includes(searchKey)) { - return true; - } - - for (const savedSearch of savedSearchValues) { - const searchData = buildSearchQueryJSON(savedSearch.query); - if (searchData && searchData.similarSearchHash === similarSearchHash) { - return true; - } - } - - return false; - }, [enabled, savedSearches, searchKey, similarSearchHash]); + return eligibleSearchKeys.includes(searchKey); + }, [enabled, savedSearches, searchKey, queryHash]); return shouldCalculateTotals; } diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 333023eeb3996..5a4339a499562 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -3406,12 +3406,11 @@ function getTableMinWidth(columns: SearchColumnType[]) { type ShouldShowSearchPageFooterParams = { isSavedSearch: boolean; resultsCount?: number; - isDefaultExpensesSearch: boolean; selectedTransactionsCount: number; }; -function shouldShowSearchPageFooter({isSavedSearch, resultsCount, isDefaultExpensesSearch, selectedTransactionsCount}: ShouldShowSearchPageFooterParams) { - return isSavedSearch || (!!resultsCount && !isDefaultExpensesSearch) || selectedTransactionsCount > 0; +function shouldShowSearchPageFooter({isSavedSearch, resultsCount, selectedTransactionsCount}: ShouldShowSearchPageFooterParams) { + return isSavedSearch || !!resultsCount || selectedTransactionsCount > 0; } export { diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index 905d76ecaab38..54aa22436366a 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -78,7 +78,7 @@ import { isInvoiceReport, isIOUReport as isIOUReportUtil, } from '@libs/ReportUtils'; -import {buildSearchQueryJSON, isDefaultExpensesQuery} from '@libs/SearchQueryUtils'; +import {buildSearchQueryJSON} from '@libs/SearchQueryUtils'; import {shouldShowSearchPageFooter} from '@libs/SearchUIUtils'; import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; import {hasTransactionBeenRejected} from '@libs/TransactionUtils'; @@ -1011,12 +1011,10 @@ function SearchPage({route}: SearchPageProps) { const {resetVideoPlayerData} = usePlaybackContext(); const metadata = searchResults?.search; - const isDefaultExpensesSearch = queryJSON ? isDefaultExpensesQuery(queryJSON) : false; const isSavedSearch = queryJSON?.hash != undefined && String(queryJSON.hash) in (savedSearches ?? {}); const shouldShowFooter = shouldShowSearchPageFooter({ isSavedSearch, resultsCount: metadata?.count, - isDefaultExpensesSearch, selectedTransactionsCount: selectedTransactionsKeys.length, }); diff --git a/src/pages/Search/SearchPageNarrow.tsx b/src/pages/Search/SearchPageNarrow.tsx index ba2f2bb91adc0..397b94e862ba6 100644 --- a/src/pages/Search/SearchPageNarrow.tsx +++ b/src/pages/Search/SearchPageNarrow.tsx @@ -31,7 +31,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import {turnOffMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; import Navigation from '@libs/Navigation/Navigation'; -import {buildCannedSearchQuery, isDefaultExpensesQuery} from '@libs/SearchQueryUtils'; +import {buildCannedSearchQuery} from '@libs/SearchQueryUtils'; import {isSearchDataLoaded, shouldShowSearchPageFooter} from '@libs/SearchUIUtils'; import variables from '@styles/variables'; import {searchInServer} from '@userActions/Report'; @@ -182,12 +182,10 @@ function SearchPageNarrow({ ); } - const isDefaultExpensesSearch = queryJSON ? isDefaultExpensesQuery(queryJSON) : false; const isSavedSearch = queryJSON?.hash != undefined && String(queryJSON.hash) in (savedSearches ?? {}); const shouldShowFooter = shouldShowSearchPageFooter({ isSavedSearch, resultsCount: metadata?.count, - isDefaultExpensesSearch, selectedTransactionsCount: Object.keys(selectedTransactions).length, }); From 6e6a87e91a67d4048cf262c680048681359d528b Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Tue, 13 Jan 2026 09:45:06 +0530 Subject: [PATCH 04/16] Update tests --- tests/unit/Search/SearchUIUtilsTest.ts | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index 4321111a2a301..dec6a867ce909 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -3218,7 +3218,6 @@ describe('SearchUIUtils', () => { SearchUIUtils.shouldShowSearchPageFooter({ isSavedSearch: true, resultsCount: 0, - isDefaultExpensesSearch: true, selectedTransactionsCount: 0, }), ).toBe(true); @@ -3229,40 +3228,36 @@ describe('SearchUIUtils', () => { SearchUIUtils.shouldShowSearchPageFooter({ isSavedSearch: false, resultsCount: 10, - isDefaultExpensesSearch: false, selectedTransactionsCount: 0, }), ).toBe(true); }); - test('Should not show footer when results exist but it is the default expenses search', () => { + test('Should show footer when there are selected transactions', () => { expect( SearchUIUtils.shouldShowSearchPageFooter({ isSavedSearch: false, - resultsCount: 10, - isDefaultExpensesSearch: true, - selectedTransactionsCount: 0, + resultsCount: 0, + selectedTransactionsCount: 1, }), - ).toBe(false); + ).toBe(true); }); - test('Should show footer when there are selected transactions', () => { + test('Should not show footer when there are no results and nothing is selected', () => { expect( SearchUIUtils.shouldShowSearchPageFooter({ isSavedSearch: false, resultsCount: 0, - isDefaultExpensesSearch: true, - selectedTransactionsCount: 1, + selectedTransactionsCount: 0, }), - ).toBe(true); + ).toBe(false); }); - test('Should not show footer when there are no results and nothing is selected', () => { + test('Should not show footer for non-saved default expenses when there is no results count', () => { expect( SearchUIUtils.shouldShowSearchPageFooter({ isSavedSearch: false, - resultsCount: 0, - isDefaultExpensesSearch: false, + resultsCount: undefined, selectedTransactionsCount: 0, }), ).toBe(false); From 872ba8a31739c23af7dec1f04f8ca093c1dbbdef Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Tue, 13 Jan 2026 11:50:17 +0530 Subject: [PATCH 05/16] Fix lint --- src/hooks/useSearchShouldCalculateTotals.ts | 2 +- src/pages/Search/SearchPage.tsx | 2 +- src/pages/Search/SearchPageNarrow.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/hooks/useSearchShouldCalculateTotals.ts b/src/hooks/useSearchShouldCalculateTotals.ts index 655e2c431097a..63184e4bdf8f0 100644 --- a/src/hooks/useSearchShouldCalculateTotals.ts +++ b/src/hooks/useSearchShouldCalculateTotals.ts @@ -12,7 +12,7 @@ function useSearchShouldCalculateTotals(searchKey: SearchKey | undefined, queryH return false; } - if (queryHash != undefined && String(queryHash) in (savedSearches ?? {})) { + if (queryHash !== undefined && String(queryHash) in (savedSearches ?? {})) { return true; } diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index 54aa22436366a..c0d0b807d2393 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -1011,7 +1011,7 @@ function SearchPage({route}: SearchPageProps) { const {resetVideoPlayerData} = usePlaybackContext(); const metadata = searchResults?.search; - const isSavedSearch = queryJSON?.hash != undefined && String(queryJSON.hash) in (savedSearches ?? {}); + const isSavedSearch = queryJSON?.hash !== undefined && String(queryJSON.hash) in (savedSearches ?? {}); const shouldShowFooter = shouldShowSearchPageFooter({ isSavedSearch, resultsCount: metadata?.count, diff --git a/src/pages/Search/SearchPageNarrow.tsx b/src/pages/Search/SearchPageNarrow.tsx index 397b94e862ba6..c2fb6d96adaaa 100644 --- a/src/pages/Search/SearchPageNarrow.tsx +++ b/src/pages/Search/SearchPageNarrow.tsx @@ -182,7 +182,7 @@ function SearchPageNarrow({ ); } - const isSavedSearch = queryJSON?.hash != undefined && String(queryJSON.hash) in (savedSearches ?? {}); + const isSavedSearch = queryJSON?.hash !== undefined && String(queryJSON.hash) in (savedSearches ?? {}); const shouldShowFooter = shouldShowSearchPageFooter({ isSavedSearch, resultsCount: metadata?.count, From f4c1a3d13569bf6ee98d4e1b8765b4be358df1ca Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Tue, 13 Jan 2026 11:54:38 +0530 Subject: [PATCH 06/16] Update --- src/hooks/useSearchShouldCalculateTotals.ts | 2 +- src/pages/Search/SearchPage.tsx | 2 +- src/pages/Search/SearchPageNarrow.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/hooks/useSearchShouldCalculateTotals.ts b/src/hooks/useSearchShouldCalculateTotals.ts index 63184e4bdf8f0..58fb9c41228b8 100644 --- a/src/hooks/useSearchShouldCalculateTotals.ts +++ b/src/hooks/useSearchShouldCalculateTotals.ts @@ -12,7 +12,7 @@ function useSearchShouldCalculateTotals(searchKey: SearchKey | undefined, queryH return false; } - if (queryHash !== undefined && String(queryHash) in (savedSearches ?? {})) { + if (queryHash !== null && queryHash !== undefined && String(queryHash) in (savedSearches ?? {})) { return true; } diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index c0d0b807d2393..d0a23ae0ad7a3 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -1011,7 +1011,7 @@ function SearchPage({route}: SearchPageProps) { const {resetVideoPlayerData} = usePlaybackContext(); const metadata = searchResults?.search; - const isSavedSearch = queryJSON?.hash !== undefined && String(queryJSON.hash) in (savedSearches ?? {}); + const isSavedSearch = queryJSON?.hash !== null && queryJSON?.hash !== undefined && String(queryJSON.hash) in (savedSearches ?? {}); const shouldShowFooter = shouldShowSearchPageFooter({ isSavedSearch, resultsCount: metadata?.count, diff --git a/src/pages/Search/SearchPageNarrow.tsx b/src/pages/Search/SearchPageNarrow.tsx index c2fb6d96adaaa..cabd3936c6eea 100644 --- a/src/pages/Search/SearchPageNarrow.tsx +++ b/src/pages/Search/SearchPageNarrow.tsx @@ -182,7 +182,7 @@ function SearchPageNarrow({ ); } - const isSavedSearch = queryJSON?.hash !== undefined && String(queryJSON.hash) in (savedSearches ?? {}); + const isSavedSearch = queryJSON?.hash !== null && queryJSON?.hash !== undefined && String(queryJSON.hash) in (savedSearches ?? {}); const shouldShowFooter = shouldShowSearchPageFooter({ isSavedSearch, resultsCount: metadata?.count, From d67aee22847e02ba870e6b985a6a67292e777171 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Mon, 19 Jan 2026 00:08:41 +0530 Subject: [PATCH 07/16] Make branch match upstream/main --- .claude/agents/code-inline-reviewer.md | 392 +++++++ .claude/agents/deploy-blocker-investigator.md | 224 +++- .../skills/playwright-app-testing/SKILL.md | 71 ++ .github/CODEOWNERS | 3 + .../javascript/verifySignedCommits/index.js | 10 +- .../verifySignedCommits.ts | 30 +- .gitignore | 3 + CLAUDE.md | 29 +- Mobile-Expensify | 2 +- __mocks__/reportData/transactions.ts | 4 +- android/app/build.gradle | 4 +- assets/animations/Fingerprint.lottie | Bin 0 -> 10737 bytes .../multifactorAuthentication/fingerprint.svg | 1 + .../humpty-dumpty.svg | 1 + .../open-padlock.svg | 1 + .../running-out-of-time.svg | 1 + cspell.json | 5 + .../Using-Reports-in-New-Expensify.md | 14 +- .../Arrange-Travel-for-Another-User.md | 15 +- .../travel/booking-travel/Book-Rail-Travel.md | 13 +- .../booking-travel/Book-Travel-for-a-Guest.md | 32 +- .../travel/booking-travel/Book-a-Hotel.md | 25 +- .../booking-travel/Book-a-Rental-Car.md | 11 +- docs/assets/images/personal-card-01.png | Bin 0 -> 41434 bytes docs/assets/images/personal-card-02.png | Bin 0 -> 65006 bytes docs/redirects.csv | 1 + ios/NewExpensify/Info.plist | 4 +- ios/NotificationServiceExtension/Info.plist | 4 +- ios/ShareViewController/Info.plist | 4 +- jest.config.js | 4 +- jest/setup.ts | 4 + package-lock.json | 4 +- package.json | 4 +- src/CONST/index.ts | 341 +++--- src/ONYXKEYS.ts | 3 + src/ROUTES.ts | 28 +- src/SCREENS.ts | 6 +- src/components/ActivityIndicator.tsx | 2 + src/components/AddUnreportedExpenseFooter.tsx | 108 ++ src/components/ApprovalWorkflowSection.tsx | 4 + src/components/ArchivedReportFooter.tsx | 7 +- src/components/Domain/DomainMenuItem.tsx | 11 +- src/components/DraggableList/SortableItem.tsx | 2 + .../EmojiPicker/EmojiPickerButton.tsx | 23 +- src/components/FullscreenLoadingIndicator.tsx | 2 + .../HTMLRenderers/MentionUserRenderer.tsx | 5 + .../Icon/chunks/expensify-icons.chunk.ts | 2 + .../Icon/chunks/illustrations.chunk.ts | 9 + src/components/LottieAnimations/index.tsx | 5 + src/components/MenuItem.tsx | 11 +- src/components/MoneyReportHeader.tsx | 40 +- .../MoneyRequestConfirmationList.tsx | 45 +- .../MoneyRequestConfirmationListFooter.tsx | 16 +- src/components/MoneyRequestHeader.tsx | 11 +- .../MoneyRequestReportActionsList.tsx | 14 +- .../MoneyRequestReportGroupHeader.tsx | 4 +- .../MoneyRequestReportNavigation.tsx | 1 - .../MoneyRequestReportTransactionItem.tsx | 4 +- .../MoneyRequestReportTransactionList.tsx | 4 +- .../MoneyRequestReportView.tsx | 116 +- .../Navigation/NavigationTabBar/index.tsx | 3 +- src/components/Navigation/SearchSidebar.tsx | 20 +- src/components/ParentNavigationSubtitle.tsx | 65 +- src/components/ProcessMoneyReportHoldMenu.tsx | 12 +- src/components/PromotedActionsBar.tsx | 14 +- src/components/RenderHTML.tsx | 7 +- .../useReportPreviewSenderID.ts | 5 +- .../MoneyRequestReceiptView.tsx | 6 +- .../MoneyRequestReportPreviewContent.tsx | 14 +- .../MoneyRequestReportPreview/index.tsx | 15 +- .../ReportActionItem/MoneyRequestView.tsx | 94 +- .../TransactionPreview/index.tsx | 2 +- src/components/RoomHeaderAvatars.tsx | 10 +- .../ScrollOffsetContextProvider.tsx | 3 +- .../Search/SearchAutocompleteList.tsx | 24 +- src/components/Search/SearchContext.tsx | 8 + src/components/Search/SearchList/index.tsx | 8 +- .../SearchPageHeader/SearchFiltersBar.tsx | 8 +- .../Search/SearchRouter/SearchRouter.tsx | 19 +- src/components/Search/index.tsx | 157 ++- src/components/Search/types.ts | 6 +- .../SelectionList/BaseSelectionList.tsx | 2 - .../SelectionList/ListItem/BaseListItem.tsx | 7 +- .../ListItem/ListItemRenderer.tsx | 8 +- .../SelectionList/ListItem/SplitListItem.tsx | 18 +- .../SelectionList/ListItem/types.ts | 12 +- .../Search/CardListItemHeader.tsx | 4 +- .../Search/ExpenseReportListItem.tsx | 20 +- .../Search/ExpenseReportListItemRow.tsx | 11 +- .../Search/ExpensesCell.tsx | 25 - .../Search/ExportedIconCell.tsx | 23 +- .../Search/MemberListItemHeader.tsx | 4 +- .../Search/ReportListItemHeader.tsx | 3 +- .../Search/TransactionGroupListExpanded.tsx | 4 +- .../Search/TransactionListItem.tsx | 65 +- .../Search/WithdrawalIDListItemHeader.tsx | 4 +- .../TableListItem.tsx | 153 --- .../SelectionListWithSections/types.ts | 2 - src/components/SelectionScreen.tsx | 3 +- .../Share/ShareTabParticipantsSelector.tsx | 4 +- .../SidePanel/SidePanelModal/index.tsx | 7 +- src/components/TestDrive/TestDriveBanner.tsx | 2 +- src/components/TestDrive/TestDriveDemo.tsx | 6 + .../implementation/index.native.tsx | 9 +- .../BaseTextInput/implementation/index.tsx | 9 +- .../ReceiptPreview/index.tsx | 4 +- src/components/TransactionItemRow/index.tsx | 38 +- .../playbackContextReportIDUtils.ts | 8 +- .../WideRHPContextProvider/default.ts | 2 + .../getVisibleRHPRouteKeys.ts | 1 - .../WideRHPContextProvider/index.tsx | 17 +- .../WideRHPContextProvider/types.ts | 6 + src/hooks/useActivePolicy.ts | 8 + src/hooks/useAllTransactions.ts | 5 +- src/hooks/useDeleteTransactions.ts | 24 +- src/hooks/useMergeTransactions.ts | 7 +- src/hooks/useMultipleSnapshots.ts | 42 + .../index.native.ts | 13 + .../useResponsiveLayoutOnWideRHP/index.ts | 33 + .../useResponsiveLayoutOnWideRHP/types.ts | 8 + src/hooks/useSearchShouldCalculateTotals.ts | 24 +- src/hooks/useSelectedTransactionsActions.ts | 3 +- src/hooks/useTransactionViolations.ts | 4 +- src/hooks/useViolations.ts | 3 +- src/languages/de.ts | 69 +- src/languages/en.ts | 66 +- src/languages/es.ts | 68 +- src/languages/fr.ts | 70 +- src/languages/it.ts | 71 +- src/languages/ja.ts | 67 +- src/languages/nl.ts | 71 +- src/languages/params.ts | 9 + src/languages/pl.ts | 71 +- src/languages/pt-BR.ts | 69 +- src/languages/zh-hans.ts | 63 +- src/libs/API/index.ts | 32 +- src/libs/API/parameters/DeleteDomainParams.ts | 6 + .../GetDuplicateTransactionDetailsParams.ts | 5 + ...etPolicyCategoryAttendeesRequiredParams.ts | 7 + src/libs/API/parameters/TrackExpenseParams.ts | 1 + src/libs/API/parameters/index.ts | 3 + src/libs/API/types.ts | 6 + src/libs/AttendeeUtils.ts | 109 ++ src/libs/Authentication.ts | 140 ++- src/libs/DistanceRequestUtils.ts | 6 +- src/libs/LoginUtils.ts | 5 + src/libs/ModifiedExpenseMessage.ts | 15 +- src/libs/MoneyRequestReportUtils.ts | 6 +- .../Biometrics/ED25519/index.ts | 148 +++ .../Biometrics/ED25519/types.ts | 39 + .../Biometrics/SecureStore/index.ts | 67 ++ .../Biometrics/SecureStore/index.web.ts | 56 + .../Biometrics/SecureStore/types.ts | 61 + .../Biometrics/VALUES.ts | 13 + .../Navigation/AppNavigator/AuthScreens.tsx | 16 +- .../ModalStackNavigators/index.tsx | 3 + .../Navigators/RightModalNavigator.tsx | 33 +- .../Navigators/SearchFullscreenNavigator.tsx | 5 - src/libs/Navigation/Navigation.ts | 90 +- .../Navigation/helpers/isReportOpenInRHP.ts | 3 +- .../helpers/isReportOpenInSuperWideRHP.ts | 13 + .../linkingConfig/RELATIONS/DOMAIN_TO_RHP.ts | 2 +- .../linkingConfig/RELATIONS/SEARCH_TO_RHP.ts | 1 + .../RELATIONS/WORKSPACE_TO_RHP.ts | 1 + src/libs/Navigation/linkingConfig/config.ts | 12 +- src/libs/Navigation/types.ts | 34 +- src/libs/Network/SequentialQueue.ts | 4 +- src/libs/OptionsListUtils/index.ts | 29 +- src/libs/PersonalDetailsUtils.ts | 4 +- src/libs/PolicyDistanceRatesUtils.ts | 7 +- src/libs/ReportActionComposeFocusManager.ts | 2 +- src/libs/ReportActionsUtils.ts | 106 +- src/libs/ReportNameUtils.ts | 183 ++- src/libs/ReportPrimaryActionUtils.ts | 4 +- src/libs/ReportSecondaryActionUtils.ts | 4 + src/libs/ReportUtils.ts | 151 ++- src/libs/SearchParser/autocompleteParser.js | 814 ++++++++++--- src/libs/SearchParser/baseRules.peggy | 68 +- src/libs/SearchParser/searchParser.js | 814 ++++++++++--- src/libs/SearchUIUtils.ts | 50 +- src/libs/SettlementButtonUtils.ts | 8 + src/libs/SidebarUtils.ts | 6 + src/libs/StringUtils/index.ts | 2 +- src/libs/TransactionPreviewUtils.ts | 11 +- src/libs/TransactionUtils/index.ts | 99 +- src/libs/Violations/ViolationsUtils.ts | 66 +- src/libs/actions/App.ts | 12 +- src/libs/actions/BankAccounts.ts | 39 +- src/libs/actions/CompanyCards.ts | 8 +- src/libs/actions/Delegate.ts | 5 +- src/libs/actions/Domain.ts | 87 +- .../IOU/{DuplicateAction.ts => Duplicate.ts} | 0 src/libs/actions/IOU/Hold.ts | 524 +++++++++ src/libs/actions/IOU/SendInvoice.ts | 65 +- src/libs/actions/IOU/index.ts | 1034 ++++++----------- src/libs/actions/MergeTransaction.ts | 8 +- .../OnyxDerived/configs/reportAttributes.ts | 7 +- src/libs/actions/PaymentMethods.ts | 2 +- src/libs/actions/Policy/Category.ts | 102 +- src/libs/actions/Policy/DistanceRate.ts | 2 +- src/libs/actions/Policy/Member.ts | 5 +- src/libs/actions/Policy/PerDiem.ts | 14 +- src/libs/actions/Policy/Policy.ts | 72 +- src/libs/actions/Policy/ReportField.ts | 56 +- src/libs/actions/Policy/Tag.ts | 30 +- src/libs/actions/Policy/Travel.ts | 2 +- .../resetNonUSDBankAccount.ts | 4 +- .../resetUSDBankAccount.ts | 11 +- src/libs/actions/Report.ts | 353 +++--- src/libs/actions/Search.ts | 20 +- src/libs/actions/Session/index.ts | 19 +- src/libs/actions/Subscription.ts | 6 +- src/libs/actions/Task.ts | 11 +- src/libs/actions/TaxRate.ts | 18 +- src/libs/actions/Transaction.ts | 109 +- src/libs/actions/User.ts | 2 +- .../actions/connections/NetSuiteCommands.ts | 6 +- src/libs/telemetry/integrations.ts | 11 +- .../telemetry/trackAuthenticationError.ts | 2 +- src/pages/AddUnreportedExpense.tsx | 113 +- src/pages/EditReportFieldPage.tsx | 2 +- src/pages/NewReportWorkspaceSelectionPage.tsx | 24 +- src/pages/ProfilePage.tsx | 16 +- src/pages/ReportAddApproverPage.tsx | 2 +- src/pages/ReportChangeApproverPage.tsx | 4 +- src/pages/ReportDetailsPage.tsx | 81 +- src/pages/RoomDescriptionPage.tsx | 7 +- src/pages/RoomInvitePage.tsx | 22 +- src/pages/RoomMembersPage.tsx | 6 +- src/pages/Search/AdvancedSearchFilters.tsx | 9 +- .../SearchFiltersCategoryPage.tsx | 39 +- src/pages/Search/SearchHoldReasonPage.tsx | 2 +- .../Search/SearchMoneyRequestReportPage.tsx | 78 +- ...rchMoneyRequestReportVerifyAccountPage.tsx | 9 +- src/pages/Search/SearchPage.tsx | 28 +- src/pages/Search/SearchPageNarrow.tsx | 18 +- src/pages/Search/SearchRejectReasonPage.tsx | 2 +- .../Search/SearchReportVerifyAccountPage.tsx | 6 +- .../Search/SearchTransactionsChangeReport.tsx | 43 +- src/pages/Share/ShareDetailsPage.tsx | 5 +- src/pages/Share/ShareTab.tsx | 5 +- src/pages/Share/SubmitDetailsPage.tsx | 2 +- .../TransactionDuplicate/Confirmation.tsx | 15 +- src/pages/TransactionDuplicate/Review.tsx | 29 +- .../TransactionMerge/ConfirmationPage.tsx | 6 +- src/pages/Travel/PublicDomainErrorPage.tsx | 58 +- src/pages/Travel/VerifyAccountPage.tsx | 6 +- .../domain/Admins/DomainAdminDetailsPage.tsx | 9 + src/pages/domain/Admins/DomainAdminsPage.tsx | 4 +- src/pages/domain/BaseDomainMembersPage.tsx | 24 +- src/pages/domain/DomainAddedPage.tsx | 6 +- src/pages/domain/DomainInitialPage.tsx | 2 +- .../domain/DomainNotFoundPageWrapper.tsx | 4 +- src/pages/domain/DomainResetPage.tsx | 127 ++ src/pages/home/HeaderView.tsx | 4 +- src/pages/home/ReportScreen.tsx | 20 +- .../BaseReportActionContextMenu.tsx | 7 +- .../report/ContextMenu/ContextMenuActions.tsx | 81 +- .../PopoverReportActionContextMenu.tsx | 8 +- .../report/ExpenseReportVerifyAccountPage.tsx | 21 + .../home/report/PureReportActionItem.tsx | 66 +- .../ComposerWithSuggestions.tsx | 23 +- .../ReportActionCompose.tsx | 32 +- src/pages/home/report/ReportActionItem.tsx | 8 +- src/pages/home/report/ReportActionsList.tsx | 22 +- src/pages/home/report/ReportActionsView.tsx | 5 +- .../home/report/ReportDetailsExportPage.tsx | 2 +- src/pages/home/report/ReportFooter.tsx | 7 +- .../home/report/UserTypingEventListener.tsx | 6 +- .../shouldDisplayNewMarkerOnReportAction.ts | 6 +- src/pages/iou/HoldReasonPage.tsx | 2 +- src/pages/iou/MoneyRequestAmountForm.tsx | 32 +- src/pages/iou/RejectReasonPage.tsx | 14 +- src/pages/iou/SplitBillDetailsPage.tsx | 3 +- .../iou/SplitExpenseCreateDateRagePage.tsx | 5 +- src/pages/iou/SplitExpenseEditPage.tsx | 9 +- src/pages/iou/SplitExpensePage.tsx | 18 +- src/pages/iou/SplitList.tsx | 1 - .../iou/request/step/IOURequestEditReport.tsx | 30 +- .../step/IOURequestEditReportCommon.tsx | 6 +- .../iou/request/step/IOURequestStepAmount.tsx | 27 +- .../request/step/IOURequestStepAttendees.tsx | 27 +- .../request/step/IOURequestStepCategory.tsx | 2 + .../step/IOURequestStepConfirmation.tsx | 17 +- .../iou/request/step/IOURequestStepDate.tsx | 2 + .../step/IOURequestStepDescription.tsx | 20 +- .../request/step/IOURequestStepDistance.tsx | 4 +- .../step/IOURequestStepDistanceManual.tsx | 23 +- .../step/IOURequestStepDistanceMap.tsx | 4 +- .../step/IOURequestStepDistanceOdometer.tsx | 39 +- .../step/IOURequestStepDistanceRate.tsx | 2 + .../request/step/IOURequestStepMerchant.tsx | 19 +- .../step/IOURequestStepParticipants.tsx | 62 +- .../iou/request/step/IOURequestStepReport.tsx | 33 +- .../step/IOURequestStepScan/index.native.tsx | 2 +- .../request/step/IOURequestStepScan/index.tsx | 2 +- .../iou/request/step/IOURequestStepTag.tsx | 2 + .../step/IOURequestStepTaxAmountPage.tsx | 12 +- .../step/IOURequestStepTaxRatePage.tsx | 2 + src/pages/settings/InitialSettingsPage.tsx | 2 +- .../Report/NotificationPreferencePage.tsx | 6 +- .../settings/Security/CloseAccountPage.tsx | 124 +- .../Security/SecuritySettingsPage.tsx | 60 +- .../TwoFactorAuth/TwoFactorAuthWrapper.tsx | 42 +- .../SubscriptionPlanCardActionButton.tsx | 4 +- src/pages/settings/VerifyAccountPageBase.tsx | 15 +- .../workspace/AccessOrNotFoundWrapper.tsx | 6 +- src/pages/workspace/MemberRightIcon.tsx | 11 +- src/pages/workspace/WorkspaceMembersPage.tsx | 2 + src/pages/workspace/WorkspacesListPage.tsx | 5 +- .../SageIntacctPaymentAccountPage.tsx | 2 +- .../export/SageIntacctDefaultVendorPage.tsx | 2 +- ...ctNonReimbursableCreditCardAccountPage.tsx | 2 +- .../intacct/import/DimensionTypeSelector.tsx | 3 +- .../NetSuiteTokenInputPage.tsx | 6 +- .../categories/CategoryRequiredFieldsPage.tsx | 103 ++ .../categories/CategorySettingsPage.tsx | 104 +- .../categories/ImportCategoriesPage.tsx | 1 + ...kspaceCompanyCardAccountSelectCardPage.tsx | 2 +- ...ompanyCardEditTransactionStartDatePage.tsx | 2 +- .../index.tsx | 6 +- .../companyCards/addNew/AddNewCardPage.tsx | 9 +- .../assignCard/TransactionStartDateStep.tsx | 2 +- .../WorkspaceExpensifyCardPage.tsx | 11 +- .../workspace/perDiem/ImportPerDiemPage.tsx | 5 +- .../WorkspaceReceiptPartnersPage.tsx | 6 +- .../reports/WorkspaceReportsPage.tsx | 6 +- .../rules/IndividualExpenseRulesSection.tsx | 27 +- src/pages/workspace/rules/RulesCustomPage.tsx | 1 + .../tags/ImportMultiLevelTagsSettingsPage.tsx | 1 + .../workspace/tags/ImportTagsOptionsPage.tsx | 1 + src/pages/workspace/tags/ImportTagsPage.tsx | 1 + src/pages/workspace/tags/TagGLCodePage.tsx | 2 +- .../workspace/travel/PolicyTravelPage.tsx | 1 + .../workflows/WorkspaceWorkflowsPayerPage.tsx | 2 + src/setup/telemetry/index.ts | 4 +- src/stories/objects/Transaction.ts | 2 +- src/styles/index.ts | 2 +- src/styles/variables.ts | 9 + src/types/form/ResetDomainForm.ts | 18 + src/types/form/index.ts | 1 + src/types/onyx/DomainErrors.ts | 2 +- src/types/onyx/DomainPendingActions.ts | 5 + src/types/onyx/OriginalMessage.ts | 31 +- src/types/onyx/PolicyCategory.ts | 3 + src/types/onyx/Report.ts | 2 +- src/types/onyx/Request.ts | 30 +- src/types/onyx/Transaction.ts | 8 +- src/utils/Base64URL.ts | 44 + tests/actions/DomainTest.ts | 100 +- tests/actions/IOUTest.ts | 727 +++++++----- ...uplicateActionTest.ts => DuplicateTest.ts} | 4 +- tests/actions/IOUTest/HoldTest.ts | 356 ++++++ tests/actions/MergeTransactionTest.ts | 4 + tests/actions/ReportPreviewActionUtilsTest.ts | 12 + tests/actions/ReportTest.ts | 104 +- tests/actions/TaskTest.ts | 4 +- tests/actions/connections/QuickbooksOnline.ts | 4 +- .../ReportActionCompose.perf-test.tsx | 1 + tests/perf-test/ReportUtils.perf-test.ts | 2 +- tests/ui/PureReportActionItemTest.tsx | 6 + tests/ui/UnreadIndicatorsTest.tsx | 27 +- tests/ui/components/HeaderViewTest.tsx | 51 +- tests/unit/AddUnreportedExpenseSearchTest.ts | 2 +- tests/unit/Base64URL.test.ts | 155 +++ tests/unit/BaseSelectionListTest.tsx | 35 +- tests/unit/CloseAccountPageTest.ts | 4 +- tests/unit/DebugUtilsTest.ts | 15 +- tests/unit/LoginUtilsTest.ts | 46 +- tests/unit/MentionUserRendererTest.tsx | 11 + tests/unit/ModifiedExpenseMessageTest.ts | 4 +- .../MultifactorAuthentication/ED25519.test.ts | 77 ++ .../SecureStore.test.ts | 21 + tests/unit/OnyxDerivedTest.tsx | 4 +- tests/unit/OptionsListUtilsTest.tsx | 255 ++++ tests/unit/PersistedRequests.ts | 8 +- tests/unit/ReportActionsUtilsTest.ts | 150 +++ tests/unit/ReportNameUtilsTest.ts | 134 ++- tests/unit/ReportUtilsTest.ts | 223 +++- tests/unit/Search/SearchUIUtilsTest.ts | 173 +-- .../Search/handleActionButtonPressTest.ts | 4 +- tests/unit/SearchParserTest.ts | 25 + tests/unit/SequentialQueueTest.ts | 8 +- tests/unit/TransactionPreviewUtils.test.ts | 20 +- tests/unit/TransactionTest.ts | 412 ++++++- tests/unit/TransactionUtilsTest.ts | 3 +- tests/unit/ViolationUtilsTest.ts | 203 ++++ tests/unit/hooks/useAllTransactions.test.ts | 86 +- .../useSelectedTransactionsActions.test.ts | 6 +- tests/unit/useReportPreviewSenderIDTest.ts | 42 + tests/utils/TestNavigationContainer.tsx | 4 - tests/utils/collections/transaction.ts | 2 +- 392 files changed, 11169 insertions(+), 4272 deletions(-) create mode 100644 .claude/skills/playwright-app-testing/SKILL.md create mode 100644 assets/animations/Fingerprint.lottie create mode 100644 assets/images/multifactorAuthentication/fingerprint.svg create mode 100644 assets/images/multifactorAuthentication/humpty-dumpty.svg create mode 100644 assets/images/multifactorAuthentication/open-padlock.svg create mode 100644 assets/images/multifactorAuthentication/running-out-of-time.svg create mode 100644 docs/assets/images/personal-card-01.png create mode 100644 docs/assets/images/personal-card-02.png create mode 100644 src/components/AddUnreportedExpenseFooter.tsx delete mode 100644 src/components/SelectionListWithSections/Search/ExpensesCell.tsx delete mode 100644 src/components/SelectionListWithSections/TableListItem.tsx create mode 100644 src/hooks/useActivePolicy.ts create mode 100644 src/hooks/useMultipleSnapshots.ts create mode 100644 src/hooks/useResponsiveLayoutOnWideRHP/index.native.ts create mode 100644 src/hooks/useResponsiveLayoutOnWideRHP/index.ts create mode 100644 src/hooks/useResponsiveLayoutOnWideRHP/types.ts create mode 100644 src/libs/API/parameters/DeleteDomainParams.ts create mode 100644 src/libs/API/parameters/GetDuplicateTransactionDetailsParams.ts create mode 100644 src/libs/API/parameters/SetPolicyCategoryAttendeesRequiredParams.ts create mode 100644 src/libs/AttendeeUtils.ts create mode 100644 src/libs/MultifactorAuthentication/Biometrics/ED25519/index.ts create mode 100644 src/libs/MultifactorAuthentication/Biometrics/ED25519/types.ts create mode 100644 src/libs/MultifactorAuthentication/Biometrics/SecureStore/index.ts create mode 100644 src/libs/MultifactorAuthentication/Biometrics/SecureStore/index.web.ts create mode 100644 src/libs/MultifactorAuthentication/Biometrics/SecureStore/types.ts create mode 100644 src/libs/MultifactorAuthentication/Biometrics/VALUES.ts create mode 100644 src/libs/Navigation/helpers/isReportOpenInSuperWideRHP.ts rename src/libs/actions/IOU/{DuplicateAction.ts => Duplicate.ts} (100%) create mode 100644 src/libs/actions/IOU/Hold.ts create mode 100644 src/pages/domain/DomainResetPage.tsx create mode 100644 src/pages/home/report/ExpenseReportVerifyAccountPage.tsx create mode 100644 src/pages/workspace/categories/CategoryRequiredFieldsPage.tsx create mode 100644 src/types/form/ResetDomainForm.ts create mode 100644 src/utils/Base64URL.ts rename tests/actions/IOUTest/{DuplicateActionTest.ts => DuplicateTest.ts} (99%) create mode 100644 tests/actions/IOUTest/HoldTest.ts create mode 100644 tests/unit/Base64URL.test.ts create mode 100644 tests/unit/MultifactorAuthentication/ED25519.test.ts create mode 100644 tests/unit/MultifactorAuthentication/SecureStore.test.ts diff --git a/.claude/agents/code-inline-reviewer.md b/.claude/agents/code-inline-reviewer.md index c23f9a153127e..99356f1e76085 100644 --- a/.claude/agents/code-inline-reviewer.md +++ b/.claude/agents/code-inline-reviewer.md @@ -452,6 +452,398 @@ function Child({ onValueChange }) { --- +### [PERF-11] Optimize data selection and handling + +- **Search patterns**: `useOnyx`, `selector`, `.filter(`, `.map(` + +- **Condition**: Flag ONLY when ALL of these are true: + + - A component uses a broad data structure (e.g., entire object) without selecting specific fields + - This causes unnecessary re-renders when unrelated fields change + - OR unnecessary data filtering/fetching is performed (excluding necessary data, fetching already available data) + + **DO NOT flag if:** + + - Specific fields are already being selected or the data structure is static + - The filtering is necessary for correct functionality + - The fetched data is required and cannot be derived from existing data + - The function requires the entire object for valid operations + +- **Reasoning**: Using broad data structures or performing unnecessary data operations causes excessive re-renders and degrades performance. Selecting specific fields and avoiding redundant operations reduces render cycles and improves efficiency. + +Good: + +```tsx +function UserProfile({ userId }) { + const [user] = useOnyx(`${ONYXKEYS.USER}${userId}`, { + selector: (user) => ({ + name: user?.name, + avatar: user?.avatar, + }), + }); + return {user?.name}; +} +``` + +Bad: + +```tsx +function UserProfile({ userId }) { + const [user] = useOnyx(`${ONYXKEYS.USER}${userId}`); + // Component re-renders when any user field changes, even unused ones + return {user?.name}; +} +``` + +--- + +### [PERFORMANCE-12] Prevent memory leaks in components and plugins + +- **Search patterns**: `setInterval`, `setTimeout`, `addEventListener`, `subscribe`, `useEffect` with missing cleanup + +- **Condition**: Flag ONLY when ALL of these are true: + + - A resource (timeout, interval, event listener, subscription, etc.) is created in a component + - The resource is not cleared upon component unmount + - Asynchronous operations are initiated without a corresponding cleanup mechanism + + **DO NOT flag if:** + + - The resource is cleared properly in a cleanup function (e.g., inside `useEffect` return) + - The resource is not expected to persist beyond the component's lifecycle + - The resource is managed by a library that handles cleanup automatically + - The operation is guaranteed to complete before the component unmounts + +- **Reasoning**: Failing to clear resources causes memory leaks, leading to increased memory consumption and potential crashes, especially in long-lived components or components that mount/unmount frequently. + +Good: + +```tsx +function TimerComponent() { + useEffect(() => { + const intervalId = setInterval(() => { + updateTimer(); + }, 1000); + + return () => { + clearInterval(intervalId); + }; + }, []); + + return Timer; +} +``` + +Bad: + +```tsx +function TimerComponent() { + useEffect(() => { + const intervalId = setInterval(() => { + updateTimer(); + }, 1000); + // Missing cleanup - interval will continue after unmount + }, []); + + return Timer; +} +``` + +--- + +### [CONSISTENCY-1] Avoid platform-specific checks within components + +- **Search patterns**: `Platform.OS`, `isAndroid`, `isIOS`, `Platform\.select` + +- **Condition**: Flag ONLY when ALL of these are true: + + - Platform detection checks (e.g., `Platform.OS`, `isAndroid`, `isIOS`) are present within a component + - The checks lead to hardcoded values or styles specific to a platform + - The component is not structured to handle platform-specific logic through file extensions or separate components + + **DO NOT flag if:** + + - The logic is handled through platform-specific file extensions (e.g., `index.web.tsx`, `index.native.tsx`) + +- **Reasoning**: Mixing platform-specific logic within components increases maintenance overhead, complexity, and bug risk. Separating concerns through dedicated files or components improves maintainability and reduces platform-specific bugs. + +Good: + +```tsx +// Platform-specific file: Button.desktop.tsx +function Button() { + return