1/** 2 * Copyright (c) 650 Industries. 3 * Copyright (c) Meta Platforms, Inc. and affiliates. 4 * 5 * This source code is licensed under the MIT license found in the 6 * LICENSE file in the root directory of this source tree. 7 */ 8import React, { useCallback, useEffect, useState } from 'react'; 9import { Keyboard, ScrollView, View, StyleSheet } from 'react-native'; 10 11import * as LogBoxData from './Data/LogBoxData'; 12import { LogBoxLog, StackType } from './Data/LogBoxLog'; 13import { useLogs, useSelectedLog } from './Data/LogContext'; 14import * as LogBoxStyle from './UI/LogBoxStyle'; 15import { LogBoxInspectorCodeFrame } from './overlay/LogBoxInspectorCodeFrame'; 16import { LogBoxInspectorFooter as ErrorOverlayFooter } from './overlay/LogBoxInspectorFooter'; 17import { LogBoxInspectorHeader as ErrorOverlayHeader } from './overlay/LogBoxInspectorHeader'; 18import { LogBoxInspectorMessageHeader } from './overlay/LogBoxInspectorMessageHeader'; 19import { LogBoxInspectorStackFrames } from './overlay/LogBoxInspectorStackFrames'; 20 21const HEADER_TITLE_MAP = { 22 warn: 'Console Warning', 23 error: 'Console Error', 24 fatal: 'Uncaught Error', 25 syntax: 'Syntax Error', 26 static: 'Static Rendering Error (Node.js)', 27 component: 'Render Error', 28}; 29 30export function LogBoxInspectorContainer() { 31 const { selectedLogIndex, logs } = useLogs(); 32 const log = logs[selectedLogIndex]; 33 if (log == null) { 34 return null; 35 } 36 return <LogBoxInspector log={log} selectedLogIndex={selectedLogIndex} logs={logs} />; 37} 38 39export function LogBoxInspector({ 40 log, 41 selectedLogIndex, 42 logs, 43}: { 44 log: LogBoxLog; 45 selectedLogIndex: number; 46 logs: LogBoxLog[]; 47}) { 48 const onDismiss = useCallback((): void => { 49 // Here we handle the cases when the log is dismissed and it 50 // was either the last log, or when the current index 51 // is now outside the bounds of the log array. 52 const logsArray = Array.from(logs); 53 if (selectedLogIndex != null) { 54 if (logsArray.length - 1 <= 0) { 55 LogBoxData.setSelectedLog(-1); 56 } else if (selectedLogIndex >= logsArray.length - 1) { 57 LogBoxData.setSelectedLog(selectedLogIndex - 1); 58 } 59 60 LogBoxData.dismiss(logsArray[selectedLogIndex]); 61 } 62 }, [selectedLogIndex]); 63 64 const onMinimize = useCallback((): void => { 65 LogBoxData.setSelectedLog(-1); 66 }, []); 67 68 const onChangeSelectedIndex = useCallback((index: number): void => { 69 LogBoxData.setSelectedLog(index); 70 }, []); 71 72 useEffect(() => { 73 if (log) { 74 LogBoxData.symbolicateLogNow('stack', log); 75 LogBoxData.symbolicateLogNow('component', log); 76 } 77 }, [log]); 78 79 useEffect(() => { 80 // Optimistically symbolicate the last and next logs. 81 if (logs.length > 1) { 82 const selected = selectedLogIndex; 83 const lastIndex = logs.length - 1; 84 const prevIndex = selected - 1 < 0 ? lastIndex : selected - 1; 85 const nextIndex = selected + 1 > lastIndex ? 0 : selected + 1; 86 for (const type of ['component', 'stack'] as const) { 87 LogBoxData.symbolicateLogLazy(type, logs[prevIndex]); 88 LogBoxData.symbolicateLogLazy(type, logs[nextIndex]); 89 } 90 } 91 }, [logs, selectedLogIndex]); 92 93 useEffect(() => { 94 Keyboard.dismiss(); 95 }, []); 96 97 const _handleRetry = useCallback( 98 (type: StackType) => { 99 LogBoxData.retrySymbolicateLogNow(type, log); 100 }, 101 [log] 102 ); 103 104 return ( 105 <View style={styles.container}> 106 <ErrorOverlayHeader onSelectIndex={onChangeSelectedIndex} level={log.level} /> 107 <ErrorOverlayBody onRetry={_handleRetry} /> 108 <ErrorOverlayFooter onDismiss={onDismiss} onMinimize={onMinimize} /> 109 </View> 110 ); 111} 112 113export function ErrorOverlayBody({ onRetry }: { onRetry: (type: StackType) => void }) { 114 const log = useSelectedLog(); 115 return <ErrorOverlayBodyContents log={log} onRetry={onRetry} />; 116} 117 118export function ErrorOverlayBodyContents({ 119 log, 120 onRetry, 121}: { 122 log: LogBoxLog; 123 onRetry: (type: StackType) => void; 124}) { 125 const [collapsed, setCollapsed] = useState(true); 126 127 useEffect(() => { 128 setCollapsed(true); 129 }, [log]); 130 131 const headerTitle = HEADER_TITLE_MAP[log.isComponentError ? 'component' : log.level] ?? log.type; 132 133 const header = ( 134 <LogBoxInspectorMessageHeader 135 collapsed={collapsed} 136 onPress={() => setCollapsed(!collapsed)} 137 message={log.message} 138 level={log.level} 139 title={headerTitle} 140 /> 141 ); 142 143 return ( 144 <> 145 {collapsed && header} 146 <ScrollView style={styles.scrollBody}> 147 {!collapsed && header} 148 149 <LogBoxInspectorCodeFrame codeFrame={log.codeFrame} /> 150 <LogBoxInspectorStackFrames 151 type="stack" 152 // eslint-disable-next-line react/jsx-no-bind 153 onRetry={onRetry.bind(onRetry, 'stack')} 154 /> 155 {!!log?.componentStack?.length && ( 156 <LogBoxInspectorStackFrames 157 type="component" 158 // eslint-disable-next-line react/jsx-no-bind 159 onRetry={onRetry.bind(onRetry, 'component')} 160 /> 161 )} 162 </ScrollView> 163 </> 164 ); 165} 166 167const styles = StyleSheet.create({ 168 scrollBody: { 169 backgroundColor: LogBoxStyle.getBackgroundColor(1), 170 flex: 1, 171 }, 172 container: { 173 top: 0, 174 left: 0, 175 bottom: 0, 176 right: 0, 177 zIndex: 999, 178 flex: 1, 179 // @ts-expect-error: fixed is not in the RN types but it works on web 180 position: 'fixed', 181 }, 182}); 183 184export default LogBoxData.withSubscription(LogBoxInspectorContainer); 185