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