From 609bfccd09a484c5ef619e7d225fa99183ad22c2 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Fri, 13 Mar 2026 17:36:05 +0100 Subject: [PATCH 1/9] Bump onyx to 3.0.46 --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index d72f893e94407..7e810d9ae4bdc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -117,7 +117,7 @@ "react-native-localize": "^3.5.4", "react-native-nitro-modules": "0.29.4", "react-native-nitro-sqlite": "9.2.0", - "react-native-onyx": "3.0.45", + "react-native-onyx": "3.0.46", "react-native-pager-view": "8.0.0", "react-native-pdf": "7.0.2", "react-native-permissions": "^5.4.0", @@ -34562,9 +34562,9 @@ } }, "node_modules/react-native-onyx": { - "version": "3.0.45", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-3.0.45.tgz", - "integrity": "sha512-oJyizoazptOzKfY8ENDu0Y+QcFvCvRFySIEgQGOMZ0SDvmuni9MJ57zPd6/C/3n84GzTCJ5yDLmOFQRiHuPoRQ==", + "version": "3.0.46", + "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-3.0.46.tgz", + "integrity": "sha512-/dB5PrK+AZ4QiaFdIdb2iC1q5tu7L3n6pxh+QZJiMhDORlQT6jK832b/6sqwgQ3qIZCrrsnqmyLVKCqEbYTrlQ==", "license": "MIT", "dependencies": { "ascii-table": "0.0.9", diff --git a/package.json b/package.json index a5a0097aa2a73..afd13d7e32757 100644 --- a/package.json +++ b/package.json @@ -181,7 +181,7 @@ "react-native-localize": "^3.5.4", "react-native-nitro-modules": "0.29.4", "react-native-nitro-sqlite": "9.2.0", - "react-native-onyx": "3.0.45", + "react-native-onyx": "3.0.46", "react-native-pager-view": "8.0.0", "react-native-pdf": "7.0.2", "react-native-permissions": "^5.4.0", From 32dbd5ed950d53290ff9512be9d01ce65d89d314 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Fri, 13 Mar 2026 17:36:12 +0100 Subject: [PATCH 2/9] Update tests --- tests/actions/ReportTest.ts | 8 +++++++ tests/unit/APITest.ts | 4 ++-- tests/unit/OptionsListUtilsTest.tsx | 25 ++++++++++---------- tests/unit/ReportSecondaryActionUtilsTest.ts | 6 +++-- tests/unit/SequentialQueueTest.ts | 21 ++++++++++------ 5 files changed, 40 insertions(+), 24 deletions(-) diff --git a/tests/actions/ReportTest.ts b/tests/actions/ReportTest.ts index 8cb7f83161cf2..91d5af02ac85f 100644 --- a/tests/actions/ReportTest.ts +++ b/tests/actions/ReportTest.ts @@ -1490,6 +1490,8 @@ describe('actions/Report', () => { currentUserAccountID: TEST_USER_ACCOUNT_ID, }); + await waitForBatchedUpdates(); + // Need the reportActionID to delete the comments const newComment = PersistedRequests.getAll().at(1); const reportActionID = newComment?.data?.reportActionID as string | undefined; @@ -1984,6 +1986,9 @@ describe('actions/Report', () => { const newComment = PersistedRequests.getAll().at(0); const reportActionID = newComment?.data?.reportActionID as string | undefined; const reportAction = TestHelper.buildTestReportComment(created, TEST_USER_ACCOUNT_ID, reportActionID); + + await waitForBatchedUpdates(); + await Onyx.set(ONYXKEYS.NETWORK, {isOffline: true}); // wait for Onyx.connect execute the callback and start processing the queue @@ -2177,6 +2182,7 @@ describe('actions/Report', () => { expect(requests?.at(0)?.data?.reportComment).toBe('value3'); await Onyx.set(ONYXKEYS.NETWORK, {isOffline: false}); + await waitForBatchedUpdates(); TestHelper.expectAPICommandToHaveBeenCalled(WRITE_COMMANDS.UPDATE_COMMENT, 1); }); @@ -2275,6 +2281,8 @@ describe('actions/Report', () => { expect(requests?.at(0)?.data?.reportComment).toBe('value3'); await Onyx.set(ONYXKEYS.NETWORK, {isOffline: false}); + await waitForBatchedUpdates(); + TestHelper.expectAPICommandToHaveBeenCalled(WRITE_COMMANDS.UPDATE_COMMENT, 1); }); diff --git a/tests/unit/APITest.ts b/tests/unit/APITest.ts index 8e81e4bfd42aa..f5c144464ac7e 100644 --- a/tests/unit/APITest.ts +++ b/tests/unit/APITest.ts @@ -436,7 +436,7 @@ describe('APITests', () => { }); Onyx.set(ONYXKEYS.NETWORK, {isOffline: true}); - expect(NetworkStore.isOffline()).toBe(false); + expect(NetworkStore.isOffline()).toBe(true); expect(NetworkStore.isAuthenticating()).toBe(false); return waitForBatchedUpdates(); }) @@ -551,7 +551,7 @@ describe('APITests', () => { API.write('MockCommandThree' as WriteCommand, {}); // THEN the retryable requests should immediately be added to the persisted requests - expect(PersistedRequests.getAll().length).toBe(2); + expect(PersistedRequests.getLength()).toBe(2); // WHEN we wait for the queue to run and finish processing return waitForBatchedUpdates(); diff --git a/tests/unit/OptionsListUtilsTest.tsx b/tests/unit/OptionsListUtilsTest.tsx index c475e57a27784..f2b1815aa912c 100644 --- a/tests/unit/OptionsListUtilsTest.tsx +++ b/tests/unit/OptionsListUtilsTest.tsx @@ -3526,7 +3526,7 @@ describe('OptionsListUtils', () => { expect(canCreate).toBe(false); }); - it('createOptionList() localization', () => { + it('createOptionList() localization', async () => { renderLocaleContextProvider(); // Given a set of reports and personal details // When we call createOptionList and extract the reports @@ -3535,18 +3535,15 @@ describe('OptionsListUtils', () => { // Then the returned reports should match the expected values expect(reports.at(10)?.subtitle).toBe(`Submits to Mister Fantastic`); - return ( - waitForBatchedUpdates() - // When we set the preferred locale to Spanish - .then(() => Onyx.set(ONYXKEYS.NVP_PREFERRED_LOCALE, CONST.LOCALES.ES)) - .then(() => { - // When we call createOptionList again - const newReports = createOptionList(PERSONAL_DETAILS, CURRENT_USER_ACCOUNT_ID, EMPTY_PRIVATE_IS_ARCHIVED_MAP, REPORTS).reports; - // Then the returned reports should change to Spanish - // cspell:disable-next-line - expect(newReports.at(10)?.subtitle).toBe('Se envía a Mister Fantastic'); - }) - ); + await Onyx.set(ONYXKEYS.NVP_PREFERRED_LOCALE, CONST.LOCALES.ES); + + await waitForBatchedUpdates(); + + // When we call createOptionList again + const newReports = createOptionList(PERSONAL_DETAILS, CURRENT_USER_ACCOUNT_ID, EMPTY_PRIVATE_IS_ARCHIVED_MAP, REPORTS).reports; + // Then the returned reports should change to Spanish + // cspell:disable-next-line + expect(newReports.at(10)?.subtitle).toBe('Se envía a Mister Fantastic'); }); }); @@ -3620,6 +3617,8 @@ describe('OptionsListUtils', () => { '1': getFakeAdvancedReportAction(CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT), }, }); + await waitForBatchedUpdates(); + // When we call createOptionList with report 10 marked as archived const archivedMap: PrivateIsArchivedMap = { [`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}10`]: reportNameValuePairs.private_isArchived, diff --git a/tests/unit/ReportSecondaryActionUtilsTest.ts b/tests/unit/ReportSecondaryActionUtilsTest.ts index fc15c9ae9c7ce..f7239ac2eef90 100644 --- a/tests/unit/ReportSecondaryActionUtilsTest.ts +++ b/tests/unit/ReportSecondaryActionUtilsTest.ts @@ -52,14 +52,16 @@ describe('getSecondaryAction', () => { beforeAll(() => { Onyx.init({ keys: ONYXKEYS, + initialKeyStates: { + [ONYXKEYS.SESSION]: SESSION, + [ONYXKEYS.PERSONAL_DETAILS_LIST]: {[EMPLOYEE_ACCOUNT_ID]: PERSONAL_DETAILS, [APPROVER_ACCOUNT_ID]: {accountID: APPROVER_ACCOUNT_ID, login: APPROVER_EMAIL}}, + }, }); }); beforeEach(async () => { jest.clearAllMocks(); Onyx.clear(); - await Onyx.merge(ONYXKEYS.SESSION, SESSION); - await Onyx.set(ONYXKEYS.PERSONAL_DETAILS_LIST, {[EMPLOYEE_ACCOUNT_ID]: PERSONAL_DETAILS, [APPROVER_ACCOUNT_ID]: {accountID: APPROVER_ACCOUNT_ID, login: APPROVER_EMAIL}}); }); it('should always return default options', () => { diff --git a/tests/unit/SequentialQueueTest.ts b/tests/unit/SequentialQueueTest.ts index 805fdb8f34126..3ea245876b820 100644 --- a/tests/unit/SequentialQueueTest.ts +++ b/tests/unit/SequentialQueueTest.ts @@ -53,10 +53,10 @@ describe('SequentialQueue', () => { }; SequentialQueue.push(requestWithConflictResolution); expect(getLength()).toBe(1); - // We know there is only one request in the queue, so we can get the first one and verify - // that the persisted request is the second one. - const persistedRequest = getAll().at(0); - expect(persistedRequest?.data?.accountID).toBe(56789); + // We know there is only one request and it's ongoing. + // We can get it and verify that the ongoing request is the second one. + const ongoingRequest = getOngoingRequest(); + expect(ongoingRequest?.data?.accountID).toBe(56789); }); it('should push two requests with conflict resolution and push', () => { @@ -109,7 +109,9 @@ describe('SequentialQueue', () => { }; SequentialQueue.push(requestWithConflictResolution); - expect(getLength()).toBe(2); + + const ongoingRequest = getOngoingRequest(); + expect(ongoingRequest?.data?.accountID).toBe(56789); }); it('should replace request request in queue while a similar one is ongoing', async () => { @@ -175,9 +177,14 @@ describe('SequentialQueue', () => { expect(getLength()).toBe(4); const persistedRequests = getAll(); - // We know ReconnectApp is at index 1 in the queue, so we can get it to verify + const ongoingRequest = getOngoingRequest(); + + // The first OpenReport call is ongoing + expect(ongoingRequest?.command).toBe('OpenReport'); + + // We know ReconnectApp is at index 0 in the queue now, so we can get it to verify // that was replaced by the new request. - expect(persistedRequests.at(1)?.data?.accountID).toBe(56789); + expect(persistedRequests.at(0)?.data?.accountID).toBe(56789); }); // need to test a rance condition between processing the next request and then pushing a new request with conflict resolver From b3ee3f9b7e2cd545f49ddf29c0345b09066f08e1 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Thu, 5 Mar 2026 14:26:23 +0100 Subject: [PATCH 3/9] Fix scrolling/highlighting of the new expense --- .../MoneyRequestReportPreviewContent.tsx | 38 +++++++++++-------- .../MoneyRequestReportPreview/index.tsx | 4 +- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx index 0a6f33c5630c6..a97e86aa796c0 100644 --- a/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx +++ b/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx @@ -1,4 +1,4 @@ -import {useFocusEffect} from '@react-navigation/native'; +import {useIsFocused} from '@react-navigation/native'; import {hasSeenTourSelector} from '@selectors/Onboarding'; import {FlashList} from '@shopify/flash-list'; import type {FlashListRef, ListRenderItemInfo} from '@shopify/flash-list'; @@ -551,20 +551,29 @@ function MoneyRequestReportPreviewContent({ carouselTransactionsRef.current = carouselTransactions; }, [carouselTransactions]); - useFocusEffect( - useCallback(() => { - const index = carouselTransactions.findIndex((transaction) => newTransactionIDs?.has(transaction.transactionID)); + const isFocused = useIsFocused(); + const isFocusedRef = useRef(isFocused); - if (index < 0) { + useEffect(() => { + isFocusedRef.current = isFocused; + }, [isFocused]); + + useEffect(() => { + const index = carouselTransactions.findIndex((transaction) => newTransactionIDs?.has(transaction.transactionID)); + + if (index < 0) { + return; + } + const newTransaction = carouselTransactions.at(index); + setTimeout(() => { + if (!isFocusedRef.current) { + return; + } + // If the new transaction is not available at the index it was on before the delay, avoid the scrolling + // because we are scrolling to either a wrong or unavailable transaction (which can cause crash). + if (newTransaction?.transactionID !== carouselTransactionsRef.current.at(index)?.transactionID) { return; } - const newTransaction = carouselTransactions.at(index); - setTimeout(() => { - // If the new transaction is not available at the index it was on before the delay, avoid the scrolling - // because we are scrolling to either a wrong or unavailable transaction (which can cause crash). - if (newTransaction?.transactionID !== carouselTransactionsRef.current.at(index)?.transactionID) { - return; - } carouselRef.current?.scrollToIndex({ index, @@ -573,9 +582,8 @@ function MoneyRequestReportPreviewContent({ }); }, CONST.ANIMATED_TRANSITION); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [newTransactionIDs]), - ); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [newTransactionIDs]); const onViewableItemsChanged = useRef(({viewableItems}: {viewableItems: ViewToken[]; changed: ViewToken[]}) => { const newIndex = viewableItems.at(0)?.index; diff --git a/src/components/ReportActionItem/MoneyRequestReportPreview/index.tsx b/src/components/ReportActionItem/MoneyRequestReportPreview/index.tsx index 9ce325e9dc7b8..08e953542f522 100644 --- a/src/components/ReportActionItem/MoneyRequestReportPreview/index.tsx +++ b/src/components/ReportActionItem/MoneyRequestReportPreview/index.tsx @@ -1,4 +1,3 @@ -import {useIsFocused} from '@react-navigation/native'; import type {ListRenderItem} from '@shopify/flash-list'; import React, {useCallback, useMemo, useRef, useState} from 'react'; import type {LayoutChangeEvent} from 'react-native'; @@ -122,9 +121,8 @@ function MoneyRequestReportPreview({ selector: hasOnceLoadedReportActionsSelector, }); const newTransactions = useNewTransactions(hasOnceLoadedReportActions, transactions); - const isFocused = useIsFocused(); // We only want to highlight the new expenses if the screen is focused. - const newTransactionIDs = isFocused ? new Set(newTransactions.map((transaction) => transaction.transactionID)) : undefined; + const newTransactionIDs = new Set(newTransactions.map((transaction) => transaction.transactionID)); const transactionPreviewContainerStyles = [styles.h100, reportPreviewStyles.transactionPreviewCarouselStyle]; From cbe0947f91345131cedd6300be01c74f7eed5f69 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Fri, 13 Mar 2026 17:43:35 +0100 Subject: [PATCH 4/9] Fix prettier --- .../MoneyRequestReportPreviewContent.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx index a97e86aa796c0..4f2405edca278 100644 --- a/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx +++ b/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx @@ -575,12 +575,12 @@ function MoneyRequestReportPreviewContent({ return; } - carouselRef.current?.scrollToIndex({ - index, - viewOffset: -2 * styles.gap2.gap, - animated: true, - }); - }, CONST.ANIMATED_TRANSITION); + carouselRef.current?.scrollToIndex({ + index, + viewOffset: -2 * styles.gap2.gap, + animated: true, + }); + }, CONST.ANIMATED_TRANSITION); // eslint-disable-next-line react-hooks/exhaustive-deps }, [newTransactionIDs]); From 3527401aba3fdeeab2d2363757e657cb213558be Mon Sep 17 00:00:00 2001 From: VickyStash Date: Fri, 13 Mar 2026 17:54:13 +0100 Subject: [PATCH 5/9] Fix SessionTest.ts --- tests/actions/SessionTest.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/actions/SessionTest.ts b/tests/actions/SessionTest.ts index 4c549912f09dc..fd46d90735ce3 100644 --- a/tests/actions/SessionTest.ts +++ b/tests/actions/SessionTest.ts @@ -189,6 +189,8 @@ describe('Session', () => { await Onyx.set(ONYXKEYS.NETWORK, {isOffline: false}); + await waitForBatchedUpdates(); + expect(getAllPersistedRequests().length).toBe(0); }); @@ -226,6 +228,8 @@ describe('Session', () => { await Onyx.set(ONYXKEYS.NETWORK, {isOffline: false}); + await waitForBatchedUpdates(); + expect(getAllPersistedRequests().length).toBe(0); }); From c3647c8f5505ac8ea65c7d2cb3b97ffa0e313aa2 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Mon, 16 Mar 2026 09:33:29 +0100 Subject: [PATCH 6/9] Fix side effect in withPolicy HOC --- src/pages/workspace/withPolicy.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/pages/workspace/withPolicy.tsx b/src/pages/workspace/withPolicy.tsx index f4ef4b7212a5b..751570ec2fdec 100644 --- a/src/pages/workspace/withPolicy.tsx +++ b/src/pages/workspace/withPolicy.tsx @@ -1,5 +1,5 @@ import type {ComponentType} from 'react'; -import React from 'react'; +import React, {useEffect} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import useOnyx from '@hooks/useOnyx'; import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types'; @@ -90,9 +90,12 @@ export default function (WrappedComponent: Compo /* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing */ const isLoadingPolicy = !hasLoadedApp || (!!policyID && isLoadingOnyxValue(policyResults, policyDraftResults)); - if (policyID && policyID.length > 0) { + useEffect(() => { + if (!policyID) { + return; + } updateLastAccessedWorkspace(policyID); - } + }, [policyID]); return ( Date: Mon, 16 Mar 2026 18:23:21 +0100 Subject: [PATCH 7/9] Add a comment --- .../MoneyRequestReportPreviewContent.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx index 4f2405edca278..6aaa16f249c27 100644 --- a/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx +++ b/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx @@ -582,6 +582,7 @@ function MoneyRequestReportPreviewContent({ }); }, CONST.ANIMATED_TRANSITION); + // We only want to scroll to a new transaction when the set of new transaction IDs changes. // eslint-disable-next-line react-hooks/exhaustive-deps }, [newTransactionIDs]); From 834a218c8ca493dda37a7639fd7a91be06630860 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Mon, 16 Mar 2026 18:27:11 +0100 Subject: [PATCH 8/9] Add clearTimeout --- .../MoneyRequestReportPreviewContent.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx index 6aaa16f249c27..a66729e65f0cf 100644 --- a/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx +++ b/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx @@ -565,7 +565,7 @@ function MoneyRequestReportPreviewContent({ return; } const newTransaction = carouselTransactions.at(index); - setTimeout(() => { + const timeoutID = setTimeout(() => { if (!isFocusedRef.current) { return; } @@ -582,6 +582,10 @@ function MoneyRequestReportPreviewContent({ }); }, CONST.ANIMATED_TRANSITION); + return () => { + clearTimeout(timeoutID); + }; + // We only want to scroll to a new transaction when the set of new transaction IDs changes. // eslint-disable-next-line react-hooks/exhaustive-deps }, [newTransactionIDs]); From 75145a7624a9e799b0b34b072c6438328ebccfe0 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Mon, 16 Mar 2026 18:48:05 +0100 Subject: [PATCH 9/9] Revert "Add clearTimeout" This reverts commit 834a218c8ca493dda37a7639fd7a91be06630860. --- .../MoneyRequestReportPreviewContent.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx index a66729e65f0cf..6aaa16f249c27 100644 --- a/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx +++ b/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx @@ -565,7 +565,7 @@ function MoneyRequestReportPreviewContent({ return; } const newTransaction = carouselTransactions.at(index); - const timeoutID = setTimeout(() => { + setTimeout(() => { if (!isFocusedRef.current) { return; } @@ -582,10 +582,6 @@ function MoneyRequestReportPreviewContent({ }); }, CONST.ANIMATED_TRANSITION); - return () => { - clearTimeout(timeoutID); - }; - // We only want to scroll to a new transaction when the set of new transaction IDs changes. // eslint-disable-next-line react-hooks/exhaustive-deps }, [newTransactionIDs]);