Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: display all LHN options in Import state mode #53374

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);
});
});
Loading