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