-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #49255 from callstack-internal/feat/import-onyx-state
feat: Import onyx state
- Loading branch information
Showing
20 changed files
with
470 additions
and
89 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import React from 'react'; | ||
import type {FileObject} from '@components/AttachmentModal'; | ||
import AttachmentPicker from '@components/AttachmentPicker'; | ||
import DecisionModal from '@components/DecisionModal'; | ||
import * as Expensicons from '@components/Icon/Expensicons'; | ||
import MenuItem from '@components/MenuItem'; | ||
import useLocalize from '@hooks/useLocalize'; | ||
import useResponsiveLayout from '@hooks/useResponsiveLayout'; | ||
import useThemeStyles from '@hooks/useThemeStyles'; | ||
|
||
function BaseImportOnyxState({ | ||
onFileRead, | ||
isErrorModalVisible, | ||
setIsErrorModalVisible, | ||
}: { | ||
onFileRead: (file: FileObject) => void; | ||
isErrorModalVisible: boolean; | ||
setIsErrorModalVisible: (value: boolean) => void; | ||
}) { | ||
const {translate} = useLocalize(); | ||
const styles = useThemeStyles(); | ||
const {isSmallScreenWidth} = useResponsiveLayout(); | ||
|
||
return ( | ||
<> | ||
<AttachmentPicker | ||
acceptedFileTypes={['text']} | ||
shouldHideCameraOption | ||
shouldHideGalleryOption | ||
> | ||
{({openPicker}) => { | ||
return ( | ||
<MenuItem | ||
icon={Expensicons.Upload} | ||
title={translate('initialSettingsPage.troubleshoot.importOnyxState')} | ||
wrapperStyle={[styles.sectionMenuItemTopDescription]} | ||
onPress={() => { | ||
openPicker({ | ||
onPicked: onFileRead, | ||
}); | ||
}} | ||
/> | ||
); | ||
}} | ||
</AttachmentPicker> | ||
<DecisionModal | ||
title={translate('initialSettingsPage.troubleshoot.invalidFile')} | ||
prompt={translate('initialSettingsPage.troubleshoot.invalidFileDescription')} | ||
isSmallScreenWidth={isSmallScreenWidth} | ||
onSecondOptionSubmit={() => setIsErrorModalVisible(false)} | ||
secondOptionText={translate('common.ok')} | ||
isVisible={isErrorModalVisible} | ||
onClose={() => setIsErrorModalVisible(false)} | ||
/> | ||
</> | ||
); | ||
} | ||
|
||
export default BaseImportOnyxState; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
import React, {useState} from 'react'; | ||
import RNFS from 'react-native-fs'; | ||
import Onyx from 'react-native-onyx'; | ||
import type {FileObject} from '@components/AttachmentModal'; | ||
import {KEYS_TO_PRESERVE, setIsUsingImportedState} from '@libs/actions/App'; | ||
import {setShouldForceOffline} from '@libs/actions/Network'; | ||
import Navigation from '@libs/Navigation/Navigation'; | ||
import type {OnyxValues} from '@src/ONYXKEYS'; | ||
import ROUTES from '@src/ROUTES'; | ||
import BaseImportOnyxState from './BaseImportOnyxState'; | ||
import type ImportOnyxStateProps from './types'; | ||
import {cleanAndTransformState} from './utils'; | ||
|
||
const CHUNK_SIZE = 100; | ||
|
||
function readFileInChunks(fileUri: string, chunkSize = 1024 * 1024) { | ||
const filePath = decodeURIComponent(fileUri.replace('file://', '')); | ||
|
||
return RNFS.exists(filePath) | ||
.then((exists) => { | ||
if (!exists) { | ||
throw new Error('File does not exist'); | ||
} | ||
return RNFS.stat(filePath); | ||
}) | ||
.then((fileStats) => { | ||
const fileSize = fileStats.size; | ||
let fileContent = ''; | ||
const promises = []; | ||
|
||
// Chunk the file into smaller parts to avoid memory issues | ||
for (let i = 0; i < fileSize; i += chunkSize) { | ||
promises.push(RNFS.read(filePath, chunkSize, i, 'utf8').then((chunk) => chunk)); | ||
} | ||
|
||
// After all chunks have been read, join them together | ||
return Promise.all(promises).then((chunks) => { | ||
fileContent = chunks.join(''); | ||
|
||
return fileContent; | ||
}); | ||
}); | ||
} | ||
|
||
function chunkArray<T>(array: T[], size: number): T[][] { | ||
const result = []; | ||
for (let i = 0; i < array.length; i += size) { | ||
result.push(array.slice(i, i + size)); | ||
} | ||
return result; | ||
} | ||
|
||
function applyStateInChunks(state: OnyxValues) { | ||
const entries = Object.entries(state); | ||
const chunks = chunkArray(entries, CHUNK_SIZE); | ||
|
||
let promise = Promise.resolve(); | ||
chunks.forEach((chunk) => { | ||
const partialOnyxState = Object.fromEntries(chunk) as Partial<OnyxValues>; | ||
promise = promise.then(() => Onyx.multiSet(partialOnyxState)); | ||
}); | ||
|
||
return promise; | ||
} | ||
|
||
export default function ImportOnyxState({setIsLoading, isLoading}: ImportOnyxStateProps) { | ||
const [isErrorModalVisible, setIsErrorModalVisible] = useState(false); | ||
|
||
const handleFileRead = (file: FileObject) => { | ||
if (!file.uri) { | ||
return; | ||
} | ||
|
||
setIsLoading(true); | ||
readFileInChunks(file.uri) | ||
.then((fileContent) => { | ||
const transformedState = cleanAndTransformState<OnyxValues>(fileContent); | ||
setShouldForceOffline(true); | ||
Onyx.clear(KEYS_TO_PRESERVE).then(() => { | ||
applyStateInChunks(transformedState).then(() => { | ||
setIsUsingImportedState(true); | ||
Navigation.navigate(ROUTES.HOME); | ||
}); | ||
}); | ||
}) | ||
.catch(() => { | ||
setIsErrorModalVisible(true); | ||
}) | ||
.finally(() => { | ||
setIsLoading(false); | ||
}); | ||
|
||
if (isLoading) { | ||
setIsLoading(false); | ||
} | ||
}; | ||
|
||
return ( | ||
<BaseImportOnyxState | ||
onFileRead={handleFileRead} | ||
isErrorModalVisible={isErrorModalVisible} | ||
setIsErrorModalVisible={setIsErrorModalVisible} | ||
/> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import React, {useState} from 'react'; | ||
import Onyx from 'react-native-onyx'; | ||
import type {FileObject} from '@components/AttachmentModal'; | ||
import {KEYS_TO_PRESERVE, setIsUsingImportedState} from '@libs/actions/App'; | ||
import {setShouldForceOffline} from '@libs/actions/Network'; | ||
import Navigation from '@libs/Navigation/Navigation'; | ||
import type {OnyxValues} 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) { | ||
const [isErrorModalVisible, setIsErrorModalVisible] = useState(false); | ||
|
||
const handleFileRead = (file: FileObject) => { | ||
if (!file.uri) { | ||
return; | ||
} | ||
|
||
setIsLoading(true); | ||
const blob = new Blob([file as BlobPart]); | ||
const response = new Response(blob); | ||
|
||
response | ||
.text() | ||
.then((text) => { | ||
const fileContent = text; | ||
const transformedState = cleanAndTransformState<OnyxValues>(fileContent); | ||
setShouldForceOffline(true); | ||
Onyx.clear(KEYS_TO_PRESERVE).then(() => { | ||
Onyx.multiSet(transformedState) | ||
.then(() => { | ||
setIsUsingImportedState(true); | ||
Navigation.navigate(ROUTES.HOME); | ||
}) | ||
.finally(() => { | ||
setIsLoading(false); | ||
}); | ||
}); | ||
}) | ||
.catch(() => { | ||
setIsErrorModalVisible(true); | ||
setIsLoading(false); | ||
}); | ||
|
||
if (isLoading) { | ||
setIsLoading(false); | ||
} | ||
}; | ||
|
||
return ( | ||
<BaseImportOnyxState | ||
onFileRead={handleFileRead} | ||
isErrorModalVisible={isErrorModalVisible} | ||
setIsErrorModalVisible={setIsErrorModalVisible} | ||
/> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
type ImportOnyxStateProps = { | ||
isLoading: boolean; | ||
setIsLoading: (isLoading: boolean) => void; | ||
}; | ||
|
||
export default ImportOnyxStateProps; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import cloneDeep from 'lodash/cloneDeep'; | ||
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]; | ||
|
||
function isRecord(value: unknown): value is Record<string, unknown> { | ||
return typeof value === 'object' && !Array.isArray(value) && value !== null; | ||
} | ||
|
||
function transformNumericKeysToArray(data: UnknownRecord): UnknownRecord | unknown[] { | ||
const dataCopy = cloneDeep(data); | ||
if (!isRecord(dataCopy)) { | ||
return Array.isArray(dataCopy) ? (dataCopy as UnknownRecord[]).map(transformNumericKeysToArray) : (dataCopy as UnknownRecord); | ||
} | ||
|
||
const keys = Object.keys(dataCopy); | ||
|
||
if (keys.length === 0) { | ||
return dataCopy; | ||
} | ||
const allKeysAreNumeric = keys.every((key) => !Number.isNaN(Number(key))); | ||
const keysAreSequential = keys.every((key, index) => parseInt(key, 10) === index); | ||
if (allKeysAreNumeric && keysAreSequential) { | ||
return keys.map((key) => transformNumericKeysToArray(dataCopy[key] as UnknownRecord)); | ||
} | ||
|
||
for (const key in dataCopy) { | ||
if (key in dataCopy) { | ||
dataCopy[key] = transformNumericKeysToArray(dataCopy[key] as UnknownRecord); | ||
} | ||
} | ||
|
||
return dataCopy; | ||
} | ||
|
||
function cleanAndTransformState<T>(state: string): T { | ||
const parsedState = JSON.parse(state) as UnknownRecord; | ||
|
||
Object.keys(parsedState).forEach((key) => { | ||
const shouldOmit = keysToOmit.some((onyxKey) => key.startsWith(onyxKey)); | ||
|
||
if (shouldOmit) { | ||
delete parsedState[key]; | ||
} | ||
}); | ||
|
||
const transformedState = transformNumericKeysToArray(parsedState) as T; | ||
return transformedState; | ||
} | ||
|
||
export {transformNumericKeysToArray, cleanAndTransformState}; |
Oops, something went wrong.