diff --git a/src/CONST.ts b/src/CONST.ts index c316a2327cc8..407b7dcd7608 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -74,6 +74,9 @@ const onboardingChoices = { type OnboardingPurposeType = ValueOf; const CONST = { + DEFAULT_DB_NAME: 'OnyxDB', + DEFAULT_TABLE_NAME: 'keyvaluepairs', + DEFAULT_ONYX_DUMP_FILE_NAME: 'onyx-state.txt', DEFAULT_POLICY_ROOM_CHAT_TYPES: [chatTypes.POLICY_ADMINS, chatTypes.POLICY_ANNOUNCE, chatTypes.DOMAIN_ALL], // Note: Group and Self-DM excluded as these are not tied to a Workspace diff --git a/src/languages/en.ts b/src/languages/en.ts index d72681701814..751c2095dede 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -958,6 +958,8 @@ export default { deviceCredentials: 'Device credentials', invalidate: 'Invalidate', destroy: 'Destroy', + maskExportOnyxStateData: 'Mask fragile user data while exporting Onyx state', + exportOnyxState: 'Export Onyx state', }, debugConsole: { saveLog: 'Save log', diff --git a/src/languages/es.ts b/src/languages/es.ts index a53f773795b1..15157b5a3524 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -955,6 +955,8 @@ export default { deviceCredentials: 'Credenciales del dispositivo', invalidate: 'Invalidar', destroy: 'Destruir', + maskExportOnyxStateData: 'Enmascare los datos frágiles del usuario mientras exporta el estado Onyx', + exportOnyxState: 'Exportar estado Onyx', }, debugConsole: { saveLog: 'Guardar registro', diff --git a/src/libs/ExportOnyxState/common.ts b/src/libs/ExportOnyxState/common.ts new file mode 100644 index 000000000000..ac2adf010eca --- /dev/null +++ b/src/libs/ExportOnyxState/common.ts @@ -0,0 +1,34 @@ +import {Str} from 'expensify-common'; +import ONYXKEYS from '@src/ONYXKEYS'; + +const maskFragileData = (data: Record, parentKey?: string): Record => { + const maskedData: Record = {}; + + if (!data) { + return maskedData; + } + + Object.keys(data).forEach((key) => { + if (!Object.prototype.hasOwnProperty.call(data, key)) { + return; + } + + const value = data[key]; + + if (typeof value === 'string' && (Str.isValidEmail(value) || key === 'authToken' || key === 'encryptedAuthToken')) { + maskedData[key] = '***'; + } else if (parentKey && parentKey.includes(ONYXKEYS.COLLECTION.REPORT_ACTIONS) && (key === 'text' || key === 'html')) { + maskedData[key] = '***'; + } else if (typeof value === 'object') { + maskedData[key] = maskFragileData(value as Record, key.includes(ONYXKEYS.COLLECTION.REPORT_ACTIONS) ? key : parentKey); + } else { + maskedData[key] = value; + } + }); + + return maskedData; +}; + +export default { + maskFragileData, +}; diff --git a/src/libs/ExportOnyxState/index.native.ts b/src/libs/ExportOnyxState/index.native.ts new file mode 100644 index 000000000000..778b7f9f9cb2 --- /dev/null +++ b/src/libs/ExportOnyxState/index.native.ts @@ -0,0 +1,42 @@ +import RNFS from 'react-native-fs'; +import {open} from 'react-native-quick-sqlite'; +import Share from 'react-native-share'; +import CONST from '@src/CONST'; +import common from './common'; + +const readFromOnyxDatabase = () => + new Promise((resolve) => { + const db = open({name: CONST.DEFAULT_DB_NAME}); + const query = `SELECT * FROM ${CONST.DEFAULT_TABLE_NAME}`; + + db.executeAsync(query, []).then(({rows}) => { + // eslint-disable-next-line no-underscore-dangle, @typescript-eslint/no-unsafe-member-access + const result = rows?._array.map((row) => ({[row?.record_key]: JSON.parse(row?.valueJSON as string)})); + + resolve(result); + }); + }); + +const shareAsFile = (value: string) => { + try { + // Define new filename and path for the app info file + const infoFileName = CONST.DEFAULT_ONYX_DUMP_FILE_NAME; + const infoFilePath = `${RNFS.DocumentDirectoryPath}/${infoFileName}`; + const actualInfoFile = `file://${infoFilePath}`; + + RNFS.writeFile(infoFilePath, value, 'utf8').then(() => { + Share.open({ + url: actualInfoFile, + failOnCancel: false, + }); + }); + } catch (error) { + console.error('Error renaming and sharing file:', error); + } +}; + +export default { + maskFragileData: common.maskFragileData, + readFromOnyxDatabase, + shareAsFile, +}; diff --git a/src/libs/ExportOnyxState/index.ts b/src/libs/ExportOnyxState/index.ts new file mode 100644 index 000000000000..148548ce5d1c --- /dev/null +++ b/src/libs/ExportOnyxState/index.ts @@ -0,0 +1,50 @@ +import CONST from '@src/CONST'; +import common from './common'; + +const readFromOnyxDatabase = () => + new Promise>((resolve) => { + let db: IDBDatabase; + const openRequest = indexedDB.open(CONST.DEFAULT_DB_NAME, 1); + openRequest.onsuccess = () => { + db = openRequest.result; + const transaction = db.transaction(CONST.DEFAULT_TABLE_NAME); + const objectStore = transaction.objectStore(CONST.DEFAULT_TABLE_NAME); + const cursor = objectStore.openCursor(); + + const queryResult: Record = {}; + + cursor.onerror = () => { + console.error('Error reading cursor'); + }; + + cursor.onsuccess = (event) => { + const {result} = event.target as IDBRequest; + if (result) { + queryResult[result.primaryKey as string] = result.value; + result.continue(); + } else { + // no results mean the cursor has reached the end of the data + resolve(queryResult); + } + }; + }; + }); + +const shareAsFile = (value: string) => { + const element = document.createElement('a'); + element.setAttribute('href', `data:text/plain;charset=utf-8,${encodeURIComponent(value)}`); + element.setAttribute('download', CONST.DEFAULT_ONYX_DUMP_FILE_NAME); + + element.style.display = 'none'; + document.body.appendChild(element); + + element.click(); + + document.body.removeChild(element); +}; + +export default { + maskFragileData: common.maskFragileData, + readFromOnyxDatabase, + shareAsFile, +}; diff --git a/src/pages/settings/Troubleshoot/TroubleshootPage.tsx b/src/pages/settings/Troubleshoot/TroubleshootPage.tsx index 0424682c7afb..54e2dae87a4e 100644 --- a/src/pages/settings/Troubleshoot/TroubleshootPage.tsx +++ b/src/pages/settings/Troubleshoot/TroubleshootPage.tsx @@ -1,4 +1,4 @@ -import React, {useMemo, useState} from 'react'; +import React, {useCallback, useMemo, useState} from 'react'; import {View} from 'react-native'; import Onyx, {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; @@ -13,7 +13,9 @@ import MenuItemList from '@components/MenuItemList'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; import Section from '@components/Section'; +import Switch from '@components/Switch'; import TestToolMenu from '@components/TestToolMenu'; +import TestToolRow from '@components/TestToolRow'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; import useEnvironment from '@hooks/useEnvironment'; @@ -22,6 +24,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWaitForNavigation from '@hooks/useWaitForNavigation'; import useWindowDimensions from '@hooks/useWindowDimensions'; +import ExportOnyxState from '@libs/ExportOnyxState'; import Navigation from '@libs/Navigation/Navigation'; import * as App from '@userActions/App'; import * as Report from '@userActions/Report'; @@ -52,6 +55,18 @@ function TroubleshootPage({shouldStoreLogs}: TroubleshootPageProps) { const waitForNavigate = useWaitForNavigation(); const {isSmallScreenWidth} = useWindowDimensions(); const illustrationStyle = getLightbulbIllustrationStyle(); + const [shouldMaskOnyxState, setShouldMaskOnyxState] = useState(true); + + const exportOnyxState = useCallback(() => { + ExportOnyxState.readFromOnyxDatabase().then((value: Record) => { + let dataToShare = value; + if (shouldMaskOnyxState) { + dataToShare = ExportOnyxState.maskFragileData(value); + } + + ExportOnyxState.shareAsFile(JSON.stringify(dataToShare)); + }); + }, [shouldMaskOnyxState]); const menuItems = useMemo(() => { const debugConsoleItem: BaseMenuItem = { @@ -66,6 +81,11 @@ function TroubleshootPage({shouldStoreLogs}: TroubleshootPageProps) { icon: Expensicons.RotateLeft, action: () => setIsConfirmationModalVisible(true), }, + { + translationKey: 'initialSettingsPage.troubleshoot.exportOnyxState', + icon: Expensicons.Download, + action: exportOnyxState, + }, ]; if (shouldStoreLogs) { @@ -81,7 +101,7 @@ function TroubleshootPage({shouldStoreLogs}: TroubleshootPageProps) { wrapperStyle: [styles.sectionMenuItemTopDescription], })) .reverse(); - }, [shouldStoreLogs, translate, waitForNavigate, styles.sectionMenuItemTopDescription]); + }, [waitForNavigate, exportOnyxState, shouldStoreLogs, translate, styles.sectionMenuItemTopDescription]); return ( + + +