xref: /expo/home/menu/DevMenuView.tsx (revision 98ecfc87)
1import { MaterialCommunityIcons } from '@expo/vector-icons';
2import React from 'react';
3import { Clipboard, PixelRatio, StyleSheet } from 'react-native';
4
5import { StyledView } from '../components/Views';
6import DevMenuBottomSheetContext, { Context } from './DevMenuBottomSheetContext';
7import DevMenuButton from './DevMenuButton';
8import DevMenuCloseButton from './DevMenuCloseButton';
9import * as DevMenu from './DevMenuModule';
10import DevMenuOnboarding from './DevMenuOnboarding';
11import DevMenuTaskInfo from './DevMenuTaskInfo';
12
13type Props = {
14  task: { [key: string]: any };
15  uuid: string;
16};
17
18type State = {
19  enableDevMenuTools: boolean;
20  devMenuItems: { [key: string]: any };
21  isOnboardingFinished: boolean;
22  isLoaded: boolean;
23};
24
25// These are defined in EXVersionManager.m in a dictionary, ordering needs to be
26// done here.
27const DEV_MENU_ORDER = [
28  'dev-live-reload',
29  'dev-hmr',
30  'dev-remote-debug',
31  'dev-reload',
32  'dev-perf-monitor',
33  'dev-inspector',
34];
35
36const MENU_ITEMS_ICON_MAPPINGS: {
37  [key: string]: React.ComponentProps<typeof MaterialCommunityIcons>['name'];
38} = {
39  'dev-hmr': 'run-fast',
40  'dev-remote-debug': 'remote-desktop',
41  'dev-perf-monitor': 'speedometer',
42  'dev-inspector': 'border-style',
43};
44
45class DevMenuView extends React.PureComponent<Props, State> {
46  static contextType = DevMenuBottomSheetContext;
47
48  // @ts-expect-error - the provided solution (declare operator) conflicts with @babel/plugin-transform-flow-strip-types
49  context!: Context;
50
51  constructor(props: Props, context?: unknown) {
52    super(props, context);
53
54    this.state = {
55      enableDevMenuTools: false,
56      devMenuItems: {},
57      isOnboardingFinished: false,
58      isLoaded: false,
59    };
60  }
61
62  componentDidMount() {
63    this.loadStateAsync();
64  }
65
66  componentDidUpdate(prevProps: Props) {
67    if (this.props.uuid !== prevProps.uuid) {
68      this.loadStateAsync();
69    }
70  }
71
72  collapse = async () => {
73    if (this.context) {
74      await this.context.collapse();
75    }
76  };
77
78  collapseAndCloseDevMenuAsync = async () => {
79    await this.collapse();
80    await DevMenu.closeAsync();
81  };
82
83  loadStateAsync = async () => {
84    this.setState({ isLoaded: false });
85
86    const [enableDevMenuTools, devMenuItems, isOnboardingFinished] = await Promise.all([
87      DevMenu.doesCurrentTaskEnableDevtoolsAsync(),
88      DevMenu.getItemsToShowAsync(),
89      DevMenu.isOnboardingFinishedAsync(),
90    ]);
91
92    this.setState({
93      enableDevMenuTools,
94      devMenuItems,
95      isOnboardingFinished,
96      isLoaded: true,
97    });
98  };
99
100  onAppReload = () => {
101    this.collapse();
102    DevMenu.reloadAppAsync();
103  };
104
105  onCopyTaskUrl = async () => {
106    const { manifestUrl } = this.props.task;
107
108    await this.collapseAndCloseDevMenuAsync();
109    Clipboard.setString(manifestUrl);
110    alert(`Copied "${manifestUrl}" to the clipboard!`);
111  };
112
113  onGoToHome = () => {
114    this.collapse();
115    DevMenu.goToHomeAsync();
116  };
117
118  onPressDevMenuButton = (key: string) => {
119    DevMenu.selectItemWithKeyAsync(key);
120  };
121
122  onOnboardingFinished = () => {
123    DevMenu.setOnboardingFinishedAsync(true);
124    this.setState({ isOnboardingFinished: true });
125  };
126
127  maybeRenderDevMenuTools() {
128    const devMenuItems = Object.keys(this.state.devMenuItems).sort(
129      (a, b) => DEV_MENU_ORDER.indexOf(a) - DEV_MENU_ORDER.indexOf(b)
130    );
131
132    if (this.state.enableDevMenuTools && this.state.devMenuItems) {
133      return (
134        <>
135          <StyledView style={styles.separator} />
136          {devMenuItems.map((key) => {
137            return this.renderDevMenuItem(key, this.state.devMenuItems[key]);
138          })}
139        </>
140      );
141    }
142    return null;
143  }
144
145  renderDevMenuItem(key: string, item: any) {
146    const { label, isEnabled, detail } = item;
147
148    return (
149      <DevMenuButton
150        key={key}
151        buttonKey={key}
152        label={label}
153        onPress={this.onPressDevMenuButton}
154        icon={MENU_ITEMS_ICON_MAPPINGS[key]}
155        isEnabled={isEnabled}
156        detail={detail}
157      />
158    );
159  }
160
161  renderContent() {
162    const { task } = this.props;
163    const { isLoaded, isOnboardingFinished } = this.state;
164
165    if (!isLoaded) {
166      return null;
167    }
168
169    return (
170      <>
171        {!isOnboardingFinished && <DevMenuOnboarding onClose={this.onOnboardingFinished} />}
172
173        <DevMenuTaskInfo task={task} />
174
175        <StyledView style={styles.separator} />
176
177        <DevMenuButton buttonKey="reload" label="Reload" onPress={this.onAppReload} icon="reload" />
178        {task && task.manifestUrl && (
179          <DevMenuButton
180            buttonKey="copy"
181            label="Copy link to clipboard"
182            onPress={this.onCopyTaskUrl}
183            icon="clipboard-text"
184          />
185        )}
186        <DevMenuButton buttonKey="home" label="Go to Home" onPress={this.onGoToHome} icon="home" />
187
188        {this.maybeRenderDevMenuTools()}
189        <DevMenuCloseButton
190          style={styles.closeButton}
191          onPress={this.collapseAndCloseDevMenuAsync}
192        />
193      </>
194    );
195  }
196
197  render() {
198    return (
199      <StyledView style={styles.container} darkBackgroundColor="#000">
200        {this.renderContent()}
201      </StyledView>
202    );
203  }
204}
205
206const styles = StyleSheet.create({
207  container: {
208    flex: 1,
209    paddingTop: 10,
210    borderTopLeftRadius: 10,
211    borderTopRightRadius: 10,
212  },
213  scrollView: {
214    flex: 1,
215  },
216  buttonContainer: {
217    backgroundColor: 'transparent',
218  },
219  separator: {
220    borderTopWidth: 1 / PixelRatio.get(),
221    height: 12,
222    marginVertical: 4,
223    marginHorizontal: -1,
224  },
225  closeButton: {
226    position: 'absolute',
227    right: 12,
228    top: 12,
229    zIndex: 3, // should be higher than zIndex of onboarding container
230  },
231});
232
233export default DevMenuView;
234