From 7fa7e1f00f5602d1ebed96a651ecc02043744b38 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Thu, 21 Nov 2024 15:20:16 +0100 Subject: [PATCH 1/4] override session of the user --- src/ONYXKEYS.ts | 4 ++++ src/components/ImportOnyxState/index.tsx | 8 ++++++-- src/components/ImportOnyxState/utils.ts | 2 +- src/libs/actions/App.ts | 19 +++++++++++++++++++ 4 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index b4510a2faeed..85b53ea8a7e8 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -458,6 +458,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_', @@ -1029,6 +1032,7 @@ type OnyxValuesMapping = { [ONYXKEYS.SHOULD_SHOW_SAVED_SEARCH_RENAME_TOOLTIP]: boolean; [ONYXKEYS.NVP_EXPENSIFY_COMPANY_CARDS_CUSTOM_NAMES]: Record; [ONYXKEYS.CONCIERGE_REPORT_ID]: string; + [ONYXKEYS.PRESERVED_USER_SESSION]: OnyxTypes.Session; }; type OnyxValues = OnyxValuesMapping & OnyxCollectionValuesMapping & OnyxFormValuesMapping & OnyxFormDraftValuesMapping; diff --git a/src/components/ImportOnyxState/index.tsx b/src/components/ImportOnyxState/index.tsx index 8add2d9172fd..e3c4fc6109f7 100644 --- a/src/components/ImportOnyxState/index.tsx +++ b/src/components/ImportOnyxState/index.tsx @@ -1,10 +1,11 @@ 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'; @@ -12,6 +13,7 @@ import {cleanAndTransformState} from './utils'; export default function ImportOnyxState({setIsLoading, isLoading}: ImportOnyxStateProps) { const [isErrorModalVisible, setIsErrorModalVisible] = useState(false); + const [session] = useOnyx(ONYXKEYS.SESSION); const handleFileRead = (file: FileObject) => { if (!file.uri) { @@ -27,6 +29,8 @@ 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) 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 f1f46aee0a93..71697d293b05 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({ @@ -521,6 +530,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; @@ -535,6 +548,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. @@ -571,5 +589,6 @@ export { updateLastRoute, setIsUsingImportedState, clearOnyxAndResetApp, + setPreservedUserSession, KEYS_TO_PRESERVE, }; From 531b937689191297786acc38330ed81f9c2e6d7a Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Fri, 13 Dec 2024 07:37:06 +0100 Subject: [PATCH 2/4] add unit tests for import onyx state --- tests/unit/ImportOnyxStateTest.ts | 93 +++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 tests/unit/ImportOnyxStateTest.ts diff --git a/tests/unit/ImportOnyxStateTest.ts b/tests/unit/ImportOnyxStateTest.ts new file mode 100644 index 000000000000..4a62a1e1a7e1 --- /dev/null +++ b/tests/unit/ImportOnyxStateTest.ts @@ -0,0 +1,93 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import {cleanAndTransformState, transformNumericKeysToArray} from '@components/ImportOnyxState/utils'; +import ONYXKEYS from '@src/ONYXKEYS'; + +describe('transformNumericKeysToArray', () => { + 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); + }); +}); From 47aae42dbc30845839aacfbaccd4f6e10ec8818c Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Fri, 13 Dec 2024 09:05:23 +0100 Subject: [PATCH 3/4] preserve user session on mobile --- src/components/ImportOnyxState/index.native.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/ImportOnyxState/index.native.tsx b/src/components/ImportOnyxState/index.native.tsx index 2258da4c8f6c..d4b2dcfed446 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'; @@ -47,6 +48,7 @@ function applyStateInChunks(state: OnyxValues) { export default function ImportOnyxState({setIsLoading, isLoading}: 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(() => { From 981ee0d446d6b76f77a371fc0054c1b5454d580e Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Fri, 13 Dec 2024 09:31:44 +0100 Subject: [PATCH 4/4] navigate to homescreen when onyx state is fully set --- .../ImportOnyxState/index.native.tsx | 9 +-------- src/components/ImportOnyxState/index.tsx | 18 +++++------------- src/components/ImportOnyxState/types.ts | 1 - .../settings/Troubleshoot/TroubleshootPage.tsx | 5 +---- 4 files changed, 7 insertions(+), 26 deletions(-) diff --git a/src/components/ImportOnyxState/index.native.tsx b/src/components/ImportOnyxState/index.native.tsx index d4b2dcfed446..bdd805241c55 100644 --- a/src/components/ImportOnyxState/index.native.tsx +++ b/src/components/ImportOnyxState/index.native.tsx @@ -46,7 +46,7 @@ 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); @@ -71,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 e3c4fc6109f7..2f9a2b70b65b 100644 --- a/src/components/ImportOnyxState/index.tsx +++ b/src/components/ImportOnyxState/index.tsx @@ -11,7 +11,7 @@ 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); @@ -33,24 +33,16 @@ export default function ImportOnyxState({setIsLoading, isLoading}: ImportOnyxSta 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/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() { /> - +