diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e919961dbf..97424431f97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # edge-react-gui +## Unreleased (develop) + +- added: Debug settings scene (Developer Mode only) with nodes/servers inspection, engine `dataDump` viewer, and log viewer + ## 4.45.0 (staging) - fixed: Fixed Zano token minting transaction detection issues. diff --git a/src/components/Main.tsx b/src/components/Main.tsx index ac7c71d8729..71de1794e5d 100644 --- a/src/components/Main.tsx +++ b/src/components/Main.tsx @@ -71,6 +71,7 @@ import { CreateWalletImportScene as CreateWalletImportSceneComponent } from './s import { CreateWalletSelectCryptoScene as CreateWalletSelectCryptoSceneComponent } from './scenes/CreateWalletSelectCryptoScene' import { CurrencyNotificationScene as CurrencyNotificationSceneComponent } from './scenes/CurrencyNotificationScene' import { CurrencySettingsScene as CurrencySettingsSceneComponent } from './scenes/CurrencySettingsScene' +import { DebugScene as DebugSceneComponent } from './scenes/DebugScene' import { DefaultFiatSettingScene as DefaultFiatSettingSceneComponent } from './scenes/DefaultFiatSettingScene' import { DevTestScene } from './scenes/DevTestScene' import { DuressModeHowToScene as DuressModeHowToSceneComponent } from './scenes/DuressModeHowToScene' @@ -209,6 +210,7 @@ const CreateWalletSelectFiatScene = ifLoggedIn( ) const CurrencyNotificationScene = ifLoggedIn(CurrencyNotificationSceneComponent) const CurrencySettingsScene = ifLoggedIn(CurrencySettingsSceneComponent) +const DebugScene = ifLoggedIn(DebugSceneComponent) const DefaultFiatSettingScene = ifLoggedIn(DefaultFiatSettingSceneComponent) const EarnScene = ifLoggedIn(EarnSceneComponent) const EdgeLoginScene = ifLoggedIn(EdgeLoginSceneComponent) @@ -1105,6 +1107,13 @@ const EdgeAppStack: React.FC = () => { options={{ headerShown: false }} /> + + +interface DumpResult { + dump?: EdgeDataDump + error?: string +} + +interface NodesWalletSectionProps { + wallet: EdgeCurrencyWallet + dumpResult: DumpResult | undefined + isExpanded: boolean + isLoading: boolean + onToggle: (walletId: string) => void + onLongPress: (walletId: string) => void +} + +interface DumpWalletRowProps { + wallet: EdgeCurrencyWallet + dumpResult: DumpResult | undefined + isExpanded: boolean + isLoading: boolean + onPress: (walletId: string) => void + onLongPress: (walletId: string) => void +} + +// --------------------------------------------------------------------------- +// Main scene +// --------------------------------------------------------------------------- + +export const DebugScene: React.FC = () => { + const theme = useTheme() + const styles = getStyles(theme) + const account = useSelector(state => state.core.account) + const currencyWallets = useWatch(account, 'currencyWallets') + const wallets = React.useMemo( + () => Object.values(currencyWallets), + [currencyWallets] + ) + + const [showNodesAndServers, setShowNodesAndServers] = React.useState(false) + const [showDataDump, setShowDataDump] = React.useState(false) + const [showLogs, setShowLogs] = React.useState(false) + const [walletDumpMap, setWalletDumpMap] = React.useState< + Record + >({}) + const [walletExpandedMap, setWalletExpandedMap] = React.useState< + Record + >({}) + const [loadingWallets, setLoadingWallets] = React.useState< + Record + >({}) + const [logsInfo, setLogsInfo] = React.useState('') + const [logsActivity, setLogsActivity] = React.useState('') + const [showInfoLog, setShowInfoLog] = React.useState(false) + const [showActivityLog, setShowActivityLog] = React.useState(false) + + const logsLoadedRef = React.useRef(false) + + // --- Core async operations --- + + const loadWalletDump = useHandler((walletId: string): void => { + const wallet = currencyWallets[walletId] + if (wallet == null) return + setLoadingWallets(prev => ({ ...prev, [walletId]: true })) + wallet + .dumpData() + .then(dump => { + setWalletDumpMap(prev => ({ ...prev, [walletId]: { dump } })) + }) + .catch((error: unknown) => { + setWalletDumpMap(prev => ({ + ...prev, + [walletId]: { + error: error instanceof Error ? error.message : String(error) + } + })) + showError(error) + }) + .finally(() => { + setLoadingWallets(prev => ({ ...prev, [walletId]: false })) + }) + }) + + const handleRefreshLogs = useHandler(async (): Promise => { + const [info, activity] = await Promise.all([ + readLogs('info'), + readLogs('activity') + ]) + setLogsInfo(info ?? '') + setLogsActivity(activity ?? '') + }) + + // --- Section toggle handlers --- + + const handleToggleNodesAndServers = useHandler(() => { + setShowNodesAndServers(prev => !prev) + }) + + const handleToggleDataDump = useHandler(() => { + setShowDataDump(prev => !prev) + }) + + const handleToggleLogs = useHandler(() => { + setShowLogs(prev => !prev) + }) + + // --- Long press (copy) handlers for top-level sections --- + + const handleCopyJson = useHandler((json: unknown, label: string): void => { + try { + Clipboard.setString(JSON.stringify(json, null, 2)) + showToast(sprintf(lstrings.settings_debug_copied_1s, label)) + } catch (error: unknown) { + showError(error) + } + }) + + const handleLongPressNodesAndServers = useHandler(() => { + const nodesData: Record = {} + for (const wallet of wallets) { + const data = walletDumpMap[wallet.id]?.dump?.data + if (data != null) { + nodesData[wallet.id] = { + label: getWalletLabel(wallet), + defaultServers: data.defaultServers ?? {}, + infoServerServers: data.infoServerServers ?? {}, + customServers: data.customServers ?? {}, + userSettings: wallet.currencyConfig.userSettings ?? {}, + ...(data.networkConfig != null + ? { networkConfig: data.networkConfig } + : {}) + } + } + } + handleCopyJson(nodesData, lstrings.settings_debug_nodes_servers) + }) + + const handleLongPressDataDump = useHandler(() => { + const dumpData: Record = {} + for (const wallet of wallets) { + const dump = walletDumpMap[wallet.id]?.dump + if (dump != null) { + dumpData[wallet.id] = { + label: getWalletLabel(wallet), + dump + } + } + } + handleCopyJson(dumpData, lstrings.settings_debug_engine_dump) + }) + + const handleLongPressLogs = useHandler(() => { + const allLogs = `=== Info Log ===\n${logsInfo}\n\n=== Activity Log ===\n${logsActivity}` + Clipboard.setString(allLogs) + showToast( + sprintf(lstrings.settings_debug_copied_1s, lstrings.settings_debug_logs) + ) + }) + + // --- Long press (copy) handlers for log sub-sections --- + + const handleLongPressInfoLog = useHandler(() => { + Clipboard.setString(logsInfo) + showToast( + sprintf( + lstrings.settings_debug_copied_1s, + lstrings.settings_debug_info_log + ) + ) + }) + + const handleLongPressActivityLog = useHandler(() => { + Clipboard.setString(logsActivity) + showToast( + sprintf( + lstrings.settings_debug_copied_1s, + lstrings.settings_debug_activity_log + ) + ) + }) + + // --- Per-wallet handlers --- + + const handleNodesWalletToggle = useHandler((walletId: string): void => { + setWalletExpandedMap(prev => ({ + ...prev, + [`nodes:${walletId}`]: !(prev[`nodes:${walletId}`] ?? false) + })) + }) + + const handleNodesWalletLongPress = useHandler((walletId: string): void => { + const wallet = account.currencyWallets[walletId] + const data = walletDumpMap[walletId]?.dump?.data + if (data != null && wallet != null) { + const content = { + defaultServers: data.defaultServers ?? {}, + infoServerServers: data.infoServerServers ?? {}, + customServers: data.customServers ?? {}, + userSettings: wallet.currencyConfig.userSettings ?? {}, + ...(data.networkConfig != null + ? { networkConfig: data.networkConfig } + : {}) + } + const label = getWalletLabel(wallet) + handleCopyJson(content, label) + } + }) + + const handleDumpWalletPress = useHandler((walletId: string): void => { + // `walletDumpMap` is shared with Nodes & Servers; that section may have + // already loaded the dump for this wallet. + const dumpResult = walletDumpMap[walletId] + if (dumpResult?.dump == null && !(loadingWallets[walletId] ?? false)) { + loadWalletDump(walletId) + setWalletExpandedMap(prev => ({ + ...prev, + [`dump:${walletId}`]: true + })) + } else { + setWalletExpandedMap(prev => ({ + ...prev, + [`dump:${walletId}`]: !(prev[`dump:${walletId}`] ?? false) + })) + } + }) + + const handleDumpWalletLongPress = useHandler((walletId: string): void => { + const dump = walletDumpMap[walletId]?.dump + if (dump != null) { + const wallet = account.currencyWallets[walletId] + const label = wallet != null ? getWalletLabel(wallet) : walletId + handleCopyJson(dump, label) + } + }) + + const handleToggleInfoLog = useHandler(() => { + setShowInfoLog(prev => !prev) + }) + + const handleToggleActivityLog = useHandler(() => { + setShowActivityLog(prev => !prev) + }) + + // --- Auto-load effects --- + + React.useEffect(() => { + if (!showNodesAndServers) return + for (const wallet of wallets) { + if ( + walletDumpMap[wallet.id] == null && + !(loadingWallets[wallet.id] ?? false) + ) { + loadWalletDump(wallet.id) + } + } + }, [ + loadingWallets, + loadWalletDump, + showNodesAndServers, + walletDumpMap, + wallets + ]) + + React.useEffect(() => { + if (showLogs && !logsLoadedRef.current) { + logsLoadedRef.current = true + handleRefreshLogs().catch((error: unknown) => { + showError(error) + }) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [showLogs]) + + // --- Render --- + + return ( + + + {lstrings.settings_debug_long_press_hint} + + + + {/* Nodes & Servers */} + + + + {lstrings.settings_debug_nodes_servers} + + + + {showNodesAndServers && wallets.length === 0 ? ( + + {lstrings.settings_debug_no_wallets} + + ) : null} + {showNodesAndServers + ? wallets.map(wallet => ( + + )) + : null} + + + {/* Engine dataDump */} + + + + {lstrings.settings_debug_engine_dump} + + + + {showDataDump && wallets.length === 0 ? ( + + {lstrings.settings_debug_no_wallets} + + ) : null} + {showDataDump + ? wallets.map(wallet => ( + + )) + : null} + + + {/* Log Viewer */} + + + + {lstrings.settings_debug_logs} + + + + {showLogs ? ( + <> + + + + + {lstrings.settings_debug_info_log} + + + + {showInfoLog ? ( + + {__DEV__ ? ( + + {lstrings.settings_debug_info_log_dev} + + ) : logsInfo.trim() !== '' ? ( + + {logsInfo} + + ) : ( + + {lstrings.settings_debug_no_logs} + + )} + + ) : null} + + + + {lstrings.settings_debug_activity_log} + + + + {showActivityLog ? ( + + {logsActivity.trim() !== '' ? ( + + {logsActivity} + + ) : ( + + {lstrings.settings_debug_no_logs} + + )} + + ) : null} + + ) : null} + + + + ) +} + +// --------------------------------------------------------------------------- +// Sub-components (avoid inline arrow fns in JSX handlers) +// --------------------------------------------------------------------------- + +const NodesWalletSection: React.FC = props => { + const { wallet, dumpResult, isExpanded, isLoading, onToggle, onLongPress } = + props + const theme = useTheme() + const styles = getStyles(theme) + + const handleToggle = useHandler(() => { + onToggle(wallet.id) + }) + + const handleLongPress = useHandler(() => { + onLongPress(wallet.id) + }) + + const walletLabel = getWalletLabel(wallet) + + const data = dumpResult?.dump?.data + + return ( + + + + {walletLabel} + + {isLoading ? ( + + ) : ( + + )} + + {isExpanded && data != null ? ( + + + {lstrings.settings_debug_defaults} + + + + {JSON.stringify(data.defaultServers ?? {}, null, 2)} + + + + + {lstrings.settings_debug_info_servers} + + + + {JSON.stringify(data.infoServerServers ?? {}, null, 2)} + + + + + {lstrings.settings_debug_custom_servers} + + + + {JSON.stringify(data.customServers ?? {}, null, 2)} + + + + + {lstrings.settings_debug_user_settings} + + + + {JSON.stringify( + wallet.currencyConfig.userSettings ?? {}, + null, + 2 + )} + + + + {data.networkConfig != null ? ( + <> + + {lstrings.settings_debug_network_config} + + + + {JSON.stringify(data.networkConfig, null, 2)} + + + + ) : null} + + ) : null} + {isExpanded && dumpResult?.error != null ? ( + + {dumpResult.error} + + ) : null} + + ) +} + +const DumpWalletRow: React.FC = props => { + const { wallet, dumpResult, isExpanded, isLoading, onPress, onLongPress } = + props + const theme = useTheme() + const styles = getStyles(theme) + + const handlePress = useHandler(() => { + onPress(wallet.id) + }) + + const handleLongPress = useHandler(() => { + onLongPress(wallet.id) + }) + + const walletLabel = getWalletLabel(wallet) + + return ( + + + + {walletLabel} + + {isLoading ? ( + + ) : ( + + )} + + {isExpanded && dumpResult?.dump != null ? ( + + + {JSON.stringify(dumpResult.dump, null, 2)} + + + ) : null} + {isExpanded && dumpResult?.error != null ? ( + + {dumpResult.error} + + ) : null} + + ) +} + +// --------------------------------------------------------------------------- +// Utilities +// --------------------------------------------------------------------------- + +const getWalletLabel = (wallet: EdgeCurrencyWallet): string => + `${wallet.name ?? wallet.currencyInfo.currencyCode} (${ + wallet.currencyInfo.pluginId + })` + +// --------------------------------------------------------------------------- +// Styles +// --------------------------------------------------------------------------- + +const getStyles = cacheStyles((theme: Theme) => ({ + hintText: { + fontSize: theme.rem(0.7), + color: theme.deactivatedText, + textAlign: 'center', + paddingVertical: theme.rem(0.5) + }, + sectionHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + padding: theme.rem(0.75) + }, + sectionTitle: { + fontSize: theme.rem(1), + fontFamily: theme.fontFaceMedium + }, + walletHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingVertical: theme.rem(0.5), + paddingHorizontal: theme.rem(0.75) + }, + walletTitle: { + fontSize: theme.rem(0.875), + fontFamily: theme.fontFaceMedium, + flexShrink: 1, + marginRight: theme.rem(0.5) + }, + walletContent: { + paddingHorizontal: theme.rem(0.75), + paddingBottom: theme.rem(0.5) + }, + subLabel: { + fontSize: theme.rem(0.8), + fontFamily: theme.fontFaceBold, + marginTop: theme.rem(0.5), + marginBottom: theme.rem(0.25) + }, + jsonBox: { + maxHeight: theme.rem(12), + marginBottom: theme.rem(0.25), + padding: theme.rem(0.5), + backgroundColor: theme.tileBackground, + borderRadius: theme.rem(0.5), + borderWidth: 1, + borderColor: theme.lineDivider + }, + logText: { + fontSize: theme.rem(0.5), + fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace', + color: theme.primaryText + }, + logSubHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingVertical: theme.rem(0.5), + paddingHorizontal: theme.rem(0.75) + }, + logSectionLabel: { + fontSize: theme.rem(0.9), + fontFamily: theme.fontFaceBold + }, + logBox: { + maxHeight: theme.rem(20), + marginHorizontal: theme.rem(0.5), + marginBottom: theme.rem(0.5), + padding: theme.rem(0.5), + backgroundColor: theme.tileBackground, + borderRadius: theme.rem(0.5), + borderWidth: 1, + borderColor: theme.lineDivider + }, + logEmptyText: { + fontSize: theme.rem(0.75), + color: theme.deactivatedText + }, + errorText: { + fontSize: theme.rem(0.65), + fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace', + color: theme.dangerText, + padding: theme.rem(0.5) + }, + emptyText: { + padding: theme.rem(0.75) + } +})) diff --git a/src/components/scenes/SettingsScene.tsx b/src/components/scenes/SettingsScene.tsx index 6f87e29bd85..fe8fb3f3540 100644 --- a/src/components/scenes/SettingsScene.tsx +++ b/src/components/scenes/SettingsScene.tsx @@ -416,6 +416,10 @@ export const SettingsScene: React.FC = props => { navigation.navigate('swapSettings') }) + const handleOpenDebugSettings = useHandler((): void => { + navigation.navigate('debugSettings') + }) + const handleSpendingLimits = useHandler(async (): Promise => { if (await hasLock()) return navigation.navigate('spendingLimits') @@ -732,6 +736,12 @@ export const SettingsScene: React.FC = props => { }} /> )} + {developerModeOn && ( + + )} )} diff --git a/src/locales/en_US.ts b/src/locales/en_US.ts index 5f6b1d5a827..9b1c1693cbf 100644 --- a/src/locales/en_US.ts +++ b/src/locales/en_US.ts @@ -513,6 +513,23 @@ const strings = { settings_button_change_password: 'Change Password', settings_button_change_username: 'Change Username', settings_developer_mode: 'Developer Mode', + settings_debug_title: 'Debug', + settings_debug_nodes_servers: 'Nodes & Servers', + settings_debug_engine_dump: 'Engine dataDump', + settings_debug_logs: 'Info/Activity Logs', + settings_debug_refresh_logs: 'Refresh', + settings_debug_defaults: 'Defaults', + settings_debug_info_servers: 'Info-Server Added', + settings_debug_custom_servers: 'User Added', + settings_debug_user_settings: 'User Settings', + settings_debug_network_config: 'Network Config', + settings_debug_no_wallets: 'No wallets available', + settings_debug_info_log: 'Info Log', + settings_debug_activity_log: 'Activity Log', + settings_debug_no_logs: 'No logs available', + settings_debug_info_log_dev: 'Not available in __DEV__', + settings_debug_long_press_hint: 'Long press any header to copy', + settings_debug_copied_1s: 'Copied: %1$s', settings_verbose_logging: 'Verbose Logging', settings_theme: 'Theme', settings_theme_light: 'Light', diff --git a/src/locales/strings/enUS.json b/src/locales/strings/enUS.json index 2ce36adcf86..5aabb0ecdb6 100644 --- a/src/locales/strings/enUS.json +++ b/src/locales/strings/enUS.json @@ -367,6 +367,23 @@ "settings_button_change_password": "Change Password", "settings_button_change_username": "Change Username", "settings_developer_mode": "Developer Mode", + "settings_debug_title": "Debug", + "settings_debug_nodes_servers": "Nodes & Servers", + "settings_debug_engine_dump": "Engine dataDump", + "settings_debug_logs": "Info/Activity Logs", + "settings_debug_refresh_logs": "Refresh", + "settings_debug_defaults": "Defaults", + "settings_debug_info_servers": "Info-Server Added", + "settings_debug_custom_servers": "User Added", + "settings_debug_user_settings": "User Settings", + "settings_debug_network_config": "Network Config", + "settings_debug_no_wallets": "No wallets available", + "settings_debug_info_log": "Info Log", + "settings_debug_activity_log": "Activity Log", + "settings_debug_no_logs": "No logs available", + "settings_debug_info_log_dev": "Not available in __DEV__", + "settings_debug_long_press_hint": "Long press any header to copy", + "settings_debug_copied_1s": "Copied: %1$s", "settings_verbose_logging": "Verbose Logging", "settings_theme": "Theme", "settings_theme_light": "Light", diff --git a/src/types/routerTypes.tsx b/src/types/routerTypes.tsx index 9c53eddc19f..549cd757894 100644 --- a/src/types/routerTypes.tsx +++ b/src/types/routerTypes.tsx @@ -181,6 +181,7 @@ export type EdgeAppStackParamList = {} & { createWalletSelectCryptoNewAccount: CreateWalletSelectCryptoParams currencyNotificationSettings: CurrencyNotificationParams currencySettings: CurrencySettingsParams + debugSettings: undefined defaultFiatSetting: undefined duressModeHowTo: undefined duressModeSetting: undefined