Skip to content

Commit

Permalink
Merge pull request #53374 from callstack-internal/fix/store-accountid…
Browse files Browse the repository at this point in the history
…-when-importing-state

fix: display all LHN options in Import state mode
  • Loading branch information
Gonals authored Dec 13, 2024
2 parents e27c5ea + 5265dc6 commit b2dca65
Show file tree
Hide file tree
Showing 8 changed files with 136 additions and 31 deletions.
4 changes: 4 additions & 0 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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_',
Expand Down Expand Up @@ -1014,6 +1017,7 @@ type OnyxValuesMapping = {
[ONYXKEYS.IS_USING_IMPORTED_STATE]: boolean;
[ONYXKEYS.NVP_EXPENSIFY_COMPANY_CARDS_CUSTOM_NAMES]: Record<string, string>;
[ONYXKEYS.CONCIERGE_REPORT_ID]: string;
[ONYXKEYS.PRESERVED_USER_SESSION]: OnyxTypes.Session;
[ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING]: OnyxTypes.DismissedProductTraining;
};
type OnyxValues = OnyxValuesMapping & OnyxCollectionValuesMapping & OnyxFormValuesMapping & OnyxFormDraftValuesMapping;
Expand Down
17 changes: 7 additions & 10 deletions src/components/ImportOnyxState/index.native.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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) {
Expand All @@ -57,6 +59,8 @@ export default function ImportOnyxState({setIsLoading, isLoading}: ImportOnyxSta
readOnyxFile(file.uri)
.then((fileContent: string) => {
const transformedState = cleanAndTransformState<OnyxValues>(fileContent);
const currentUserSessionCopy = {...session};
setPreservedUserSession(currentUserSessionCopy);
setShouldForceOffline(true);
Onyx.clear(KEYS_TO_PRESERVE).then(() => {
applyStateInChunks(transformedState).then(() => {
Expand All @@ -67,14 +71,7 @@ export default function ImportOnyxState({setIsLoading, isLoading}: ImportOnyxSta
})
.catch(() => {
setIsErrorModalVisible(true);
})
.finally(() => {
setIsLoading(false);
});

if (isLoading) {
setIsLoading(false);
}
};

return (
Expand Down
26 changes: 11 additions & 15 deletions src/components/ImportOnyxState/index.tsx
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -27,26 +29,20 @@ export default function ImportOnyxState({setIsLoading, isLoading}: ImportOnyxSta
.then((text) => {
const fileContent = text;
const transformedState = cleanAndTransformState<OnyxValues>(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 (
Expand Down
1 change: 0 additions & 1 deletion src/components/ImportOnyxState/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
type ImportOnyxStateProps = {
isLoading: boolean;
setIsLoading: (isLoading: boolean) => void;
};

Expand Down
2 changes: 1 addition & 1 deletion src/components/ImportOnyxState/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown> {
return typeof value === 'object' && !Array.isArray(value) && value !== null;
Expand Down
19 changes: 19 additions & 0 deletions src/libs/actions/App.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -102,6 +110,7 @@ const KEYS_TO_PRESERVE: OnyxKey[] = [
ONYXKEYS.PREFERRED_THEME,
ONYXKEYS.NVP_PREFERRED_LOCALE,
ONYXKEYS.CREDENTIALS,
ONYXKEYS.PRESERVED_USER_SESSION,
];

Onyx.connect({
Expand Down Expand Up @@ -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;
Expand All @@ -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.
Expand Down Expand Up @@ -574,5 +592,6 @@ export {
updateLastRoute,
setIsUsingImportedState,
clearOnyxAndResetApp,
setPreservedUserSession,
KEYS_TO_PRESERVE,
};
5 changes: 1 addition & 4 deletions src/pages/settings/Troubleshoot/TroubleshootPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,10 +144,7 @@ function TroubleshootPage() {
/>
</TestToolRow>
</View>
<ImportOnyxState
setIsLoading={setIsLoading}
isLoading={isLoading}
/>
<ImportOnyxState setIsLoading={setIsLoading} />
<MenuItemList
menuItems={menuItems}
shouldUseSingleExecution
Expand Down
93 changes: 93 additions & 0 deletions tests/unit/ImportOnyxStateTest.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});

0 comments on commit b2dca65

Please sign in to comment.