diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 2e65b5f372b4..e19aa71cce52 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -452,6 +452,9 @@ const ONYXKEYS = { /** The user's Concierge reportID */ CONCIERGE_REPORT_ID: 'conciergeReportID', + /** The user's session that will be preserved when using imported state */ + PRESERVED_USER_SESSION: 'preservedUserSession', + /** Collection Keys */ COLLECTION: { DOWNLOAD: 'download_', @@ -1014,6 +1017,7 @@ type OnyxValuesMapping = { [ONYXKEYS.IS_USING_IMPORTED_STATE]: boolean; [ONYXKEYS.NVP_EXPENSIFY_COMPANY_CARDS_CUSTOM_NAMES]: Record; [ONYXKEYS.CONCIERGE_REPORT_ID]: string; + [ONYXKEYS.PRESERVED_USER_SESSION]: OnyxTypes.Session; [ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING]: OnyxTypes.DismissedProductTraining; }; type OnyxValues = OnyxValuesMapping & OnyxCollectionValuesMapping & OnyxFormValuesMapping & OnyxFormDraftValuesMapping; diff --git a/src/components/ImportOnyxState/index.native.tsx b/src/components/ImportOnyxState/index.native.tsx index 2258da4c8f6c..bdd805241c55 100644 --- a/src/components/ImportOnyxState/index.native.tsx +++ b/src/components/ImportOnyxState/index.native.tsx @@ -1,11 +1,12 @@ import React, {useState} from 'react'; import ReactNativeBlobUtil from 'react-native-blob-util'; -import Onyx from 'react-native-onyx'; +import Onyx, {useOnyx} from 'react-native-onyx'; import type {FileObject} from '@components/AttachmentModal'; -import {KEYS_TO_PRESERVE, setIsUsingImportedState} from '@libs/actions/App'; +import {KEYS_TO_PRESERVE, setIsUsingImportedState, setPreservedUserSession} from '@libs/actions/App'; import {setShouldForceOffline} from '@libs/actions/Network'; import Navigation from '@libs/Navigation/Navigation'; import type {OnyxValues} from '@src/ONYXKEYS'; +import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import BaseImportOnyxState from './BaseImportOnyxState'; import type ImportOnyxStateProps from './types'; @@ -45,8 +46,9 @@ function applyStateInChunks(state: OnyxValues) { return promise; } -export default function ImportOnyxState({setIsLoading, isLoading}: ImportOnyxStateProps) { +export default function ImportOnyxState({setIsLoading}: ImportOnyxStateProps) { const [isErrorModalVisible, setIsErrorModalVisible] = useState(false); + const [session] = useOnyx(ONYXKEYS.SESSION); const handleFileRead = (file: FileObject) => { if (!file.uri) { @@ -57,6 +59,8 @@ export default function ImportOnyxState({setIsLoading, isLoading}: ImportOnyxSta readOnyxFile(file.uri) .then((fileContent: string) => { const transformedState = cleanAndTransformState(fileContent); + const currentUserSessionCopy = {...session}; + setPreservedUserSession(currentUserSessionCopy); setShouldForceOffline(true); Onyx.clear(KEYS_TO_PRESERVE).then(() => { applyStateInChunks(transformedState).then(() => { @@ -67,14 +71,7 @@ export default function ImportOnyxState({setIsLoading, isLoading}: ImportOnyxSta }) .catch(() => { setIsErrorModalVisible(true); - }) - .finally(() => { - setIsLoading(false); }); - - if (isLoading) { - setIsLoading(false); - } }; return ( diff --git a/src/components/ImportOnyxState/index.tsx b/src/components/ImportOnyxState/index.tsx index 8add2d9172fd..2f9a2b70b65b 100644 --- a/src/components/ImportOnyxState/index.tsx +++ b/src/components/ImportOnyxState/index.tsx @@ -1,17 +1,19 @@ import React, {useState} from 'react'; -import Onyx from 'react-native-onyx'; +import Onyx, {useOnyx} from 'react-native-onyx'; import type {FileObject} from '@components/AttachmentModal'; -import {KEYS_TO_PRESERVE, setIsUsingImportedState} from '@libs/actions/App'; +import {KEYS_TO_PRESERVE, setIsUsingImportedState, setPreservedUserSession} from '@libs/actions/App'; import {setShouldForceOffline} from '@libs/actions/Network'; import Navigation from '@libs/Navigation/Navigation'; import type {OnyxValues} from '@src/ONYXKEYS'; +import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import BaseImportOnyxState from './BaseImportOnyxState'; import type ImportOnyxStateProps from './types'; import {cleanAndTransformState} from './utils'; -export default function ImportOnyxState({setIsLoading, isLoading}: ImportOnyxStateProps) { +export default function ImportOnyxState({setIsLoading}: ImportOnyxStateProps) { const [isErrorModalVisible, setIsErrorModalVisible] = useState(false); + const [session] = useOnyx(ONYXKEYS.SESSION); const handleFileRead = (file: FileObject) => { if (!file.uri) { @@ -27,26 +29,20 @@ export default function ImportOnyxState({setIsLoading, isLoading}: ImportOnyxSta .then((text) => { const fileContent = text; const transformedState = cleanAndTransformState(fileContent); + const currentUserSessionCopy = {...session}; + setPreservedUserSession(currentUserSessionCopy); setShouldForceOffline(true); Onyx.clear(KEYS_TO_PRESERVE).then(() => { - Onyx.multiSet(transformedState) - .then(() => { - setIsUsingImportedState(true); - Navigation.navigate(ROUTES.HOME); - }) - .finally(() => { - setIsLoading(false); - }); + Onyx.multiSet(transformedState).then(() => { + setIsUsingImportedState(true); + Navigation.navigate(ROUTES.HOME); + }); }); }) .catch(() => { setIsErrorModalVisible(true); setIsLoading(false); }); - - if (isLoading) { - setIsLoading(false); - } }; return ( diff --git a/src/components/ImportOnyxState/types.ts b/src/components/ImportOnyxState/types.ts index 8e504c493529..2b4b56a3b20c 100644 --- a/src/components/ImportOnyxState/types.ts +++ b/src/components/ImportOnyxState/types.ts @@ -1,5 +1,4 @@ type ImportOnyxStateProps = { - isLoading: boolean; setIsLoading: (isLoading: boolean) => void; }; diff --git a/src/components/ImportOnyxState/utils.ts b/src/components/ImportOnyxState/utils.ts index a5f24fa80714..94779868384d 100644 --- a/src/components/ImportOnyxState/utils.ts +++ b/src/components/ImportOnyxState/utils.ts @@ -3,7 +3,7 @@ import type {UnknownRecord} from 'type-fest'; import ONYXKEYS from '@src/ONYXKEYS'; // List of Onyx keys from the .txt file we want to keep for the local override -const keysToOmit = [ONYXKEYS.ACTIVE_CLIENTS, ONYXKEYS.FREQUENTLY_USED_EMOJIS, ONYXKEYS.NETWORK, ONYXKEYS.CREDENTIALS, ONYXKEYS.SESSION, ONYXKEYS.PREFERRED_THEME]; +const keysToOmit = [ONYXKEYS.ACTIVE_CLIENTS, ONYXKEYS.FREQUENTLY_USED_EMOJIS, ONYXKEYS.NETWORK, ONYXKEYS.CREDENTIALS, ONYXKEYS.PREFERRED_THEME]; function isRecord(value: unknown): value is Record { return typeof value === 'object' && !Array.isArray(value) && value !== null; diff --git a/src/libs/actions/App.ts b/src/libs/actions/App.ts index 61ce04655ae5..931f9e226995 100644 --- a/src/libs/actions/App.ts +++ b/src/libs/actions/App.ts @@ -89,6 +89,14 @@ Onyx.connect({ }, }); +let preservedUserSession: OnyxTypes.Session | undefined; +Onyx.connect({ + key: ONYXKEYS.PRESERVED_USER_SESSION, + callback: (value) => { + preservedUserSession = value; + }, +}); + const KEYS_TO_PRESERVE: OnyxKey[] = [ ONYXKEYS.ACCOUNT, ONYXKEYS.IS_CHECKING_PUBLIC_ROOM, @@ -102,6 +110,7 @@ const KEYS_TO_PRESERVE: OnyxKey[] = [ ONYXKEYS.PREFERRED_THEME, ONYXKEYS.NVP_PREFERRED_LOCALE, ONYXKEYS.CREDENTIALS, + ONYXKEYS.PRESERVED_USER_SESSION, ]; Onyx.connect({ @@ -524,6 +533,10 @@ function setIsUsingImportedState(usingImportedState: boolean) { Onyx.set(ONYXKEYS.IS_USING_IMPORTED_STATE, usingImportedState); } +function setPreservedUserSession(session: OnyxTypes.Session) { + Onyx.set(ONYXKEYS.PRESERVED_USER_SESSION, session); +} + function clearOnyxAndResetApp(shouldNavigateToHomepage?: boolean) { // The value of isUsingImportedState will be lost once Onyx is cleared, so we need to store it const isStateImported = isUsingImportedState; @@ -538,6 +551,11 @@ function clearOnyxAndResetApp(shouldNavigateToHomepage?: boolean) { Navigation.navigate(ROUTES.HOME); } + if (preservedUserSession) { + Onyx.set(ONYXKEYS.SESSION, preservedUserSession); + Onyx.set(ONYXKEYS.PRESERVED_USER_SESSION, null); + } + // Requests in a sequential queue should be called even if the Onyx state is reset, so we do not lose any pending data. // However, the OpenApp request must be called before any other request in a queue to ensure data consistency. // To do that, sequential queue is cleared together with other keys, and then it's restored once the OpenApp request is resolved. @@ -574,5 +592,6 @@ export { updateLastRoute, setIsUsingImportedState, clearOnyxAndResetApp, + setPreservedUserSession, KEYS_TO_PRESERVE, }; diff --git a/src/pages/settings/Troubleshoot/TroubleshootPage.tsx b/src/pages/settings/Troubleshoot/TroubleshootPage.tsx index bd0ce596c733..defc5eb941ac 100644 --- a/src/pages/settings/Troubleshoot/TroubleshootPage.tsx +++ b/src/pages/settings/Troubleshoot/TroubleshootPage.tsx @@ -144,10 +144,7 @@ function TroubleshootPage() { /> - + { + it('converts object with numeric keys to array', () => { + const input = {'0': 'a', '1': 'b', '2': 'c'}; + expect(transformNumericKeysToArray(input)).toEqual(['a', 'b', 'c']); + }); + + it('handles nested numeric objects', () => { + const input = { + '0': {'0': 'a', '1': 'b'}, + '1': {'0': 'c', '1': 'd'}, + }; + expect(transformNumericKeysToArray(input)).toEqual([ + ['a', 'b'], + ['c', 'd'], + ]); + }); + + it('preserves non-numeric keys', () => { + const input = {foo: 'bar', baz: {'0': 'qux'}}; + expect(transformNumericKeysToArray(input)).toEqual({foo: 'bar', baz: ['qux']}); + }); + + it('handles empty objects', () => { + expect(transformNumericKeysToArray({})).toEqual({}); + }); + + it('handles non-sequential numeric keys', () => { + const input = {'0': 'a', '2': 'b', '5': 'c'}; + expect(transformNumericKeysToArray(input)).toEqual({'0': 'a', '2': 'b', '5': 'c'}); + }); +}); + +describe('cleanAndTransformState', () => { + it('removes omitted keys and transforms numeric objects', () => { + const input = JSON.stringify({ + [ONYXKEYS.NETWORK]: 'should be removed', + someKey: {'0': 'a', '1': 'b'}, + otherKey: 'value', + }); + + expect(cleanAndTransformState(input)).toEqual({ + someKey: ['a', 'b'], + otherKey: 'value', + }); + }); + + it('handles empty state', () => { + expect(cleanAndTransformState('{}')).toEqual({}); + }); + + it('removes keys that start with omitted keys', () => { + const input = JSON.stringify({ + [`${ONYXKEYS.NETWORK}_something`]: 'should be removed', + validKey: 'keep this', + }); + + expect(cleanAndTransformState(input)).toEqual({ + validKey: 'keep this', + }); + }); + + it('throws on invalid JSON', () => { + expect(() => cleanAndTransformState('invalid json')).toThrow(); + }); + + it('removes all specified ONYXKEYS', () => { + const input = JSON.stringify({ + [ONYXKEYS.ACTIVE_CLIENTS]: 'remove1', + [ONYXKEYS.FREQUENTLY_USED_EMOJIS]: 'remove2', + [ONYXKEYS.NETWORK]: 'remove3', + [ONYXKEYS.CREDENTIALS]: 'remove4', + [ONYXKEYS.PREFERRED_THEME]: 'remove5', + keepThis: 'value', + }); + + const result = cleanAndTransformState(input); + + expect(result).toEqual({ + keepThis: 'value', + }); + + // Verify each key is removed + expect(result).not.toHaveProperty(ONYXKEYS.ACTIVE_CLIENTS); + expect(result).not.toHaveProperty(ONYXKEYS.FREQUENTLY_USED_EMOJIS); + expect(result).not.toHaveProperty(ONYXKEYS.NETWORK); + expect(result).not.toHaveProperty(ONYXKEYS.CREDENTIALS); + expect(result).not.toHaveProperty(ONYXKEYS.PREFERRED_THEME); + }); +});