1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 | #import <React/RCTDevMenu.h>
|
9 |
|
10 | #import <FBReactNativeSpec/FBReactNativeSpec.h>
|
11 | #import <React/RCTBridge+Private.h>
|
12 | #import <React/RCTBundleURLProvider.h>
|
13 | #import <React/RCTDefines.h>
|
14 | #import <React/RCTDevSettings.h>
|
15 | #import <React/RCTKeyCommands.h>
|
16 | #import <React/RCTLog.h>
|
17 | #import <React/RCTReloadCommand.h>
|
18 | #import <React/RCTUtils.h>
|
19 |
|
20 | #import "CoreModulesPlugins.h"
|
21 |
|
22 | #if RCT_DEV_MENU
|
23 |
|
24 | #if RCT_ENABLE_INSPECTOR
|
25 | #import <React/RCTInspectorDevServerHelper.h>
|
26 | #endif
|
27 |
|
28 | NSString *const RCTShowDevMenuNotification = @"RCTShowDevMenuNotification";
|
29 |
|
30 | @implementation UIWindow (RCTDevMenu)
|
31 |
|
32 | - (void)RCT_motionEnded:(__unused UIEventSubtype)motion withEvent:(UIEvent *)event
|
33 | {
|
34 | if (event.subtype == UIEventSubtypeMotionShake) {
|
35 | [[NSNotificationCenter defaultCenter] postNotificationName:RCTShowDevMenuNotification object:nil];
|
36 | }
|
37 | }
|
38 |
|
39 | @end
|
40 |
|
41 | @implementation RCTDevMenuItem {
|
42 | RCTDevMenuItemTitleBlock _titleBlock;
|
43 | dispatch_block_t _handler;
|
44 | }
|
45 |
|
46 | - (instancetype)initWithTitleBlock:(RCTDevMenuItemTitleBlock)titleBlock handler:(dispatch_block_t)handler
|
47 | {
|
48 | if ((self = [super init])) {
|
49 | _titleBlock = [titleBlock copy];
|
50 | _handler = [handler copy];
|
51 | }
|
52 | return self;
|
53 | }
|
54 |
|
55 | RCT_NOT_IMPLEMENTED(-(instancetype)init)
|
56 |
|
57 | + (instancetype)buttonItemWithTitleBlock:(NSString * (^)(void))titleBlock handler:(dispatch_block_t)handler
|
58 | {
|
59 | return [[self alloc] initWithTitleBlock:titleBlock handler:handler];
|
60 | }
|
61 |
|
62 | + (instancetype)buttonItemWithTitle:(NSString *)title handler:(dispatch_block_t)handler
|
63 | {
|
64 | return [[self alloc]
|
65 | initWithTitleBlock:^NSString * {
|
66 | return title;
|
67 | }
|
68 | handler:handler];
|
69 | }
|
70 |
|
71 | - (void)callHandler
|
72 | {
|
73 | if (_handler) {
|
74 | _handler();
|
75 | }
|
76 | }
|
77 |
|
78 | - (NSString *)title
|
79 | {
|
80 | if (_titleBlock) {
|
81 | return _titleBlock();
|
82 | }
|
83 | return nil;
|
84 | }
|
85 |
|
86 | @end
|
87 |
|
88 | typedef void (^RCTDevMenuAlertActionHandler)(UIAlertAction *action);
|
89 |
|
90 | @interface RCTDevMenu () <RCTBridgeModule, RCTInvalidating, NativeDevMenuSpec>
|
91 |
|
92 | @end
|
93 |
|
94 | @implementation RCTDevMenu {
|
95 | UIAlertController *_actionSheet;
|
96 | NSMutableArray<RCTDevMenuItem *> *_extraMenuItems;
|
97 | }
|
98 |
|
99 | @synthesize bridge = _bridge;
|
100 |
|
101 | RCT_EXPORT_MODULE()
|
102 |
|
103 | + (void)initialize
|
104 | {
|
105 |
|
106 |
|
107 |
|
108 | RCTSwapInstanceMethods([UIWindow class], @selector(motionEnded:withEvent:), @selector(RCT_motionEnded:withEvent:));
|
109 | }
|
110 |
|
111 | + (BOOL)requiresMainQueueSetup
|
112 | {
|
113 | return YES;
|
114 | }
|
115 |
|
116 | - (instancetype)init
|
117 | {
|
118 | if ((self = [super init])) {
|
119 | [[NSNotificationCenter defaultCenter] addObserver:self
|
120 | selector:@selector(showOnShake)
|
121 | name:RCTShowDevMenuNotification
|
122 | object:nil];
|
123 | _extraMenuItems = [NSMutableArray new];
|
124 |
|
125 | #if TARGET_OS_SIMULATOR
|
126 | RCTKeyCommands *commands = [RCTKeyCommands sharedInstance];
|
127 | __weak __typeof(self) weakSelf = self;
|
128 |
|
129 |
|
130 | [commands registerKeyCommandWithInput:@"d"
|
131 | modifierFlags:UIKeyModifierCommand
|
132 | action:^(__unused UIKeyCommand *command) {
|
133 | [weakSelf toggle];
|
134 | }];
|
135 |
|
136 |
|
137 | [commands registerKeyCommandWithInput:@"i"
|
138 | modifierFlags:UIKeyModifierCommand
|
139 | action:^(__unused UIKeyCommand *command) {
|
140 | [weakSelf.bridge.devSettings toggleElementInspector];
|
141 | }];
|
142 |
|
143 |
|
144 | [commands registerKeyCommandWithInput:@"n"
|
145 | modifierFlags:UIKeyModifierCommand
|
146 | action:^(__unused UIKeyCommand *command) {
|
147 | [weakSelf.bridge.devSettings setIsDebuggingRemotely:NO];
|
148 | }];
|
149 | #endif
|
150 | }
|
151 | return self;
|
152 | }
|
153 |
|
154 | - (dispatch_queue_t)methodQueue
|
155 | {
|
156 | return dispatch_get_main_queue();
|
157 | }
|
158 |
|
159 | - (void)invalidate
|
160 | {
|
161 | _presentedItems = nil;
|
162 | [_actionSheet dismissViewControllerAnimated:YES
|
163 | completion:^(void){}];
|
164 | }
|
165 |
|
166 | - (void)showOnShake
|
167 | {
|
168 | if ([_bridge.devSettings isShakeToShowDevMenuEnabled]) {
|
169 | [self show];
|
170 | }
|
171 | }
|
172 |
|
173 | - (void)toggle
|
174 | {
|
175 | if (_actionSheet) {
|
176 | [_actionSheet dismissViewControllerAnimated:YES
|
177 | completion:^(void){
|
178 | }];
|
179 | _actionSheet = nil;
|
180 | } else {
|
181 | [self show];
|
182 | }
|
183 | }
|
184 |
|
185 | - (BOOL)isActionSheetShown
|
186 | {
|
187 | return _actionSheet != nil;
|
188 | }
|
189 |
|
190 | - (void)addItem:(NSString *)title handler:(void (^)(void))handler
|
191 | {
|
192 | [self addItem:[RCTDevMenuItem buttonItemWithTitle:title handler:handler]];
|
193 | }
|
194 |
|
195 | - (void)addItem:(RCTDevMenuItem *)item
|
196 | {
|
197 | [_extraMenuItems addObject:item];
|
198 | }
|
199 |
|
200 | - (void)setDefaultJSBundle
|
201 | {
|
202 | [[RCTBundleURLProvider sharedSettings] resetToDefaults];
|
203 | self->_bridge.bundleURL = [[RCTBundleURLProvider sharedSettings] jsBundleURLForFallbackResource:nil
|
204 | fallbackExtension:nil];
|
205 | RCTTriggerReloadCommandListeners(@"Dev menu - reset to default");
|
206 | }
|
207 |
|
208 | - (NSArray<RCTDevMenuItem *> *)_menuItemsToPresent
|
209 | {
|
210 | NSMutableArray<RCTDevMenuItem *> *items = [NSMutableArray new];
|
211 |
|
212 |
|
213 | __weak RCTBridge *bridge = _bridge;
|
214 | __weak RCTDevSettings *devSettings = _bridge.devSettings;
|
215 | __weak RCTDevMenu *weakSelf = self;
|
216 |
|
217 | [items addObject:[RCTDevMenuItem buttonItemWithTitle:@"Reload"
|
218 | handler:^{
|
219 | RCTTriggerReloadCommandListeners(@"Dev menu - reload");
|
220 | }]];
|
221 |
|
222 | if (!devSettings.isProfilingEnabled) {
|
223 | if (!devSettings.isRemoteDebuggingAvailable) {
|
224 | [items
|
225 | addObject:[RCTDevMenuItem
|
226 | buttonItemWithTitle:@"Debugger Unavailable"
|
227 | handler:^{
|
228 | NSString *message = RCTTurboModuleEnabled()
|
229 | ? @"Debugging is not currently supported when TurboModule is enabled."
|
230 | : @"Include the RCTWebSocket library to enable JavaScript debugging.";
|
231 | UIAlertController *alertController =
|
232 | [UIAlertController alertControllerWithTitle:@"Debugger Unavailable"
|
233 | message:message
|
234 | preferredStyle:UIAlertControllerStyleAlert];
|
235 | __weak __typeof__(alertController) weakAlertController = alertController;
|
236 | [alertController
|
237 | addAction:[UIAlertAction actionWithTitle:@"OK"
|
238 | style:UIAlertActionStyleDefault
|
239 | handler:^(__unused UIAlertAction *action) {
|
240 | [weakAlertController
|
241 | dismissViewControllerAnimated:YES
|
242 | completion:nil];
|
243 | }]];
|
244 | [RCTPresentedViewController() presentViewController:alertController
|
245 | animated:YES
|
246 | completion:NULL];
|
247 | }]];
|
248 | } else {
|
249 | [items addObject:[RCTDevMenuItem
|
250 | buttonItemWithTitleBlock:^NSString * {
|
251 | if (devSettings.isNuclideDebuggingAvailable) {
|
252 | return devSettings.isDebuggingRemotely ? @"Stop Chrome Debugger" : @"Debug with Chrome";
|
253 | } else {
|
254 | return devSettings.isDebuggingRemotely ? @"Stop Debugging" : @"Debug";
|
255 | }
|
256 | }
|
257 | handler:^{
|
258 | devSettings.isDebuggingRemotely = !devSettings.isDebuggingRemotely;
|
259 | }]];
|
260 | }
|
261 |
|
262 | if (devSettings.isNuclideDebuggingAvailable && !devSettings.isDebuggingRemotely) {
|
263 | [items addObject:[RCTDevMenuItem buttonItemWithTitle:@"Debug with Nuclide"
|
264 | handler:^{
|
265 | #if RCT_ENABLE_INSPECTOR
|
266 | [RCTInspectorDevServerHelper
|
267 | attachDebugger:@"ReactNative"
|
268 | withBundleURL:bridge.bundleURL
|
269 | withView:RCTPresentedViewController()];
|
270 | #endif
|
271 | }]];
|
272 | }
|
273 | }
|
274 |
|
275 | [items addObject:[RCTDevMenuItem
|
276 | buttonItemWithTitleBlock:^NSString * {
|
277 | return devSettings.isElementInspectorShown ? @"Hide Inspector" : @"Show Inspector";
|
278 | }
|
279 | handler:^{
|
280 | [devSettings toggleElementInspector];
|
281 | }]];
|
282 |
|
283 | if (devSettings.isHotLoadingAvailable) {
|
284 | [items addObject:[RCTDevMenuItem
|
285 | buttonItemWithTitleBlock:^NSString * {
|
286 |
|
287 | return devSettings.isHotLoadingEnabled ? @"Disable Fast Refresh" : @"Enable Fast Refresh";
|
288 | }
|
289 | handler:^{
|
290 | devSettings.isHotLoadingEnabled = !devSettings.isHotLoadingEnabled;
|
291 | }]];
|
292 | }
|
293 |
|
294 | if (devSettings.isLiveReloadAvailable) {
|
295 | [items addObject:[RCTDevMenuItem
|
296 | buttonItemWithTitleBlock:^NSString * {
|
297 | return devSettings.isDebuggingRemotely
|
298 | ? @"Systrace Unavailable"
|
299 | : devSettings.isProfilingEnabled ? @"Stop Systrace" : @"Start Systrace";
|
300 | }
|
301 | handler:^{
|
302 | if (devSettings.isDebuggingRemotely) {
|
303 | UIAlertController *alertController =
|
304 | [UIAlertController alertControllerWithTitle:@"Systrace Unavailable"
|
305 | message:@"Stop debugging to enable Systrace."
|
306 | preferredStyle:UIAlertControllerStyleAlert];
|
307 | __weak __typeof__(alertController) weakAlertController = alertController;
|
308 | [alertController
|
309 | addAction:[UIAlertAction actionWithTitle:@"OK"
|
310 | style:UIAlertActionStyleDefault
|
311 | handler:^(__unused UIAlertAction *action) {
|
312 | [weakAlertController
|
313 | dismissViewControllerAnimated:YES
|
314 | completion:nil];
|
315 | }]];
|
316 | [RCTPresentedViewController() presentViewController:alertController
|
317 | animated:YES
|
318 | completion:NULL];
|
319 | } else {
|
320 | devSettings.isProfilingEnabled = !devSettings.isProfilingEnabled;
|
321 | }
|
322 | }]];
|
323 |
|
324 |
|
325 |
|
326 | }
|
327 |
|
328 | [items
|
329 | addObject:[RCTDevMenuItem
|
330 | buttonItemWithTitleBlock:^NSString * {
|
331 | return @"Configure Bundler";
|
332 | }
|
333 | handler:^{
|
334 | UIAlertController *alertController = [UIAlertController
|
335 | alertControllerWithTitle:@"Configure Bundler"
|
336 | message:@"Provide a custom bundler address, port, and entrypoint."
|
337 | preferredStyle:UIAlertControllerStyleAlert];
|
338 | [alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) {
|
339 | textField.placeholder = @"0.0.0.0";
|
340 | }];
|
341 | [alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) {
|
342 | textField.placeholder = @"8081";
|
343 | }];
|
344 | [alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) {
|
345 | textField.placeholder = @"index";
|
346 | }];
|
347 | [alertController
|
348 | addAction:[UIAlertAction
|
349 | actionWithTitle:@"Apply Changes"
|
350 | style:UIAlertActionStyleDefault
|
351 | handler:^(__unused UIAlertAction *action) {
|
352 | NSArray *textfields = alertController.textFields;
|
353 | UITextField *ipTextField = textfields[0];
|
354 | UITextField *portTextField = textfields[1];
|
355 | UITextField *bundleRootTextField = textfields[2];
|
356 | NSString *bundleRoot = bundleRootTextField.text;
|
357 | if (ipTextField.text.length == 0 && portTextField.text.length == 0) {
|
358 | [weakSelf setDefaultJSBundle];
|
359 | return;
|
360 | }
|
361 | NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init];
|
362 | formatter.numberStyle = NSNumberFormatterDecimalStyle;
|
363 | NSNumber *portNumber =
|
364 | [formatter numberFromString:portTextField.text];
|
365 | if (portNumber == nil) {
|
366 | portNumber = [NSNumber numberWithInt:RCT_METRO_PORT];
|
367 | }
|
368 | [RCTBundleURLProvider sharedSettings].jsLocation = [NSString
|
369 | stringWithFormat:@"%@:%d", ipTextField.text, portNumber.intValue];
|
370 | __strong RCTBridge *strongBridge = bridge;
|
371 | if (strongBridge) {
|
372 | NSURL *bundleURL = bundleRoot.length
|
373 | ? [[RCTBundleURLProvider sharedSettings]
|
374 | jsBundleURLForBundleRoot:bundleRoot
|
375 | fallbackResource:nil]
|
376 | : [strongBridge.delegate sourceURLForBridge:strongBridge];
|
377 | strongBridge.bundleURL = bundleURL;
|
378 | RCTTriggerReloadCommandListeners(@"Dev menu - apply changes");
|
379 | }
|
380 | }]];
|
381 | [alertController addAction:[UIAlertAction actionWithTitle:@"Reset to Default"
|
382 | style:UIAlertActionStyleDefault
|
383 | handler:^(__unused UIAlertAction *action) {
|
384 | [weakSelf setDefaultJSBundle];
|
385 | }]];
|
386 | [alertController addAction:[UIAlertAction actionWithTitle:@"Cancel"
|
387 | style:UIAlertActionStyleCancel
|
388 | handler:^(__unused UIAlertAction *action) {
|
389 | return;
|
390 | }]];
|
391 | [RCTPresentedViewController() presentViewController:alertController animated:YES completion:NULL];
|
392 | }]];
|
393 |
|
394 | [items addObjectsFromArray:_extraMenuItems];
|
395 | return items;
|
396 | }
|
397 |
|
398 | RCT_EXPORT_METHOD(show)
|
399 | {
|
400 | if (_actionSheet || !_bridge || RCTRunningInAppExtension()) {
|
401 | return;
|
402 | }
|
403 |
|
404 | NSString *bridgeDescription = _bridge.bridgeDescription;
|
405 | NSString *description =
|
406 | bridgeDescription.length > 0 ? [NSString stringWithFormat:@"Running %@", bridgeDescription] : nil;
|
407 |
|
408 |
|
409 | UIAlertControllerStyle style = [[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone
|
410 | ? UIAlertControllerStyleActionSheet
|
411 | : UIAlertControllerStyleAlert;
|
412 | _actionSheet = [UIAlertController alertControllerWithTitle:@"React Native Debug Menu"
|
413 | message:description
|
414 | preferredStyle:style];
|
415 |
|
416 | NSArray<RCTDevMenuItem *> *items = [self _menuItemsToPresent];
|
417 | for (RCTDevMenuItem *item in items) {
|
418 | [_actionSheet addAction:[UIAlertAction actionWithTitle:item.title
|
419 | style:UIAlertActionStyleDefault
|
420 | handler:[self alertActionHandlerForDevItem:item]]];
|
421 | }
|
422 |
|
423 | [_actionSheet addAction:[UIAlertAction actionWithTitle:@"Cancel"
|
424 | style:UIAlertActionStyleCancel
|
425 | handler:[self alertActionHandlerForDevItem:nil]]];
|
426 |
|
427 | _presentedItems = items;
|
428 | [RCTPresentedViewController() presentViewController:_actionSheet animated:YES completion:nil];
|
429 |
|
430 | [_bridge enqueueJSCall:@"RCTNativeAppEventEmitter"
|
431 | method:@"emit"
|
432 | args:@[@"RCTDevMenuShown"]
|
433 | completion:NULL];
|
434 | }
|
435 |
|
436 | - (RCTDevMenuAlertActionHandler)alertActionHandlerForDevItem:(RCTDevMenuItem *__nullable)item
|
437 | {
|
438 | return ^(__unused UIAlertAction *action) {
|
439 | if (item) {
|
440 | [item callHandler];
|
441 | }
|
442 |
|
443 | self->_actionSheet = nil;
|
444 | };
|
445 | }
|
446 |
|
447 | #pragma mark - deprecated methods and properties
|
448 |
|
449 | #define WARN_DEPRECATED_DEV_MENU_EXPORT() \
|
450 | RCTLogWarn(@"Using deprecated method %s, use RCTDevSettings instead", __func__)
|
451 |
|
452 | - (void)setShakeToShow:(BOOL)shakeToShow
|
453 | {
|
454 | _bridge.devSettings.isShakeToShowDevMenuEnabled = shakeToShow;
|
455 | }
|
456 |
|
457 | - (BOOL)shakeToShow
|
458 | {
|
459 | return _bridge.devSettings.isShakeToShowDevMenuEnabled;
|
460 | }
|
461 |
|
462 | RCT_EXPORT_METHOD(reload)
|
463 | {
|
464 | WARN_DEPRECATED_DEV_MENU_EXPORT();
|
465 | RCTTriggerReloadCommandListeners(@"Unknown from JS");
|
466 | }
|
467 |
|
468 | RCT_EXPORT_METHOD(debugRemotely : (BOOL)enableDebug)
|
469 | {
|
470 | WARN_DEPRECATED_DEV_MENU_EXPORT();
|
471 | _bridge.devSettings.isDebuggingRemotely = enableDebug;
|
472 | }
|
473 |
|
474 | RCT_EXPORT_METHOD(setProfilingEnabled : (BOOL)enabled)
|
475 | {
|
476 | WARN_DEPRECATED_DEV_MENU_EXPORT();
|
477 | _bridge.devSettings.isProfilingEnabled = enabled;
|
478 | }
|
479 |
|
480 | - (BOOL)profilingEnabled
|
481 | {
|
482 | return _bridge.devSettings.isProfilingEnabled;
|
483 | }
|
484 |
|
485 | RCT_EXPORT_METHOD(setHotLoadingEnabled : (BOOL)enabled)
|
486 | {
|
487 | WARN_DEPRECATED_DEV_MENU_EXPORT();
|
488 | _bridge.devSettings.isHotLoadingEnabled = enabled;
|
489 | }
|
490 |
|
491 | - (BOOL)hotLoadingEnabled
|
492 | {
|
493 | return _bridge.devSettings.isHotLoadingEnabled;
|
494 | }
|
495 |
|
496 | - (std::shared_ptr<facebook::react::TurboModule>)getTurboModuleWithJsInvoker:(std::shared_ptr<facebook::react::CallInvoker>)jsInvoker
|
497 | {
|
498 | return std::make_shared<facebook::react::NativeDevMenuSpecJSI>(self, jsInvoker);
|
499 | }
|
500 |
|
501 | @end
|
502 |
|
503 | #else // Unavailable when not in dev mode
|
504 |
|
505 | @interface RCTDevMenu() <NativeDevMenuSpec>
|
506 | @end
|
507 |
|
508 | @implementation RCTDevMenu
|
509 |
|
510 | - (void)show
|
511 | {
|
512 | }
|
513 | - (void)reload
|
514 | {
|
515 | }
|
516 | - (void)addItem:(NSString *)title handler:(dispatch_block_t)handler
|
517 | {
|
518 | }
|
519 | - (void)addItem:(RCTDevMenu *)item
|
520 | {
|
521 | }
|
522 |
|
523 | - (void)debugRemotely : (BOOL)enableDebug
|
524 | {
|
525 | }
|
526 |
|
527 | - (BOOL)isActionSheetShown
|
528 | {
|
529 | return NO;
|
530 | }
|
531 | + (NSString *)moduleName
|
532 | {
|
533 | return @"DevMenu";
|
534 | }
|
535 |
|
536 | - (std::shared_ptr<facebook::react::TurboModule>)getTurboModuleWithJsInvoker:(std::shared_ptr<facebook::react::CallInvoker>)jsInvoker
|
537 | {
|
538 | return std::make_shared<facebook::react::NativeDevMenuSpecJSI>(self, jsInvoker);
|
539 | }
|
540 |
|
541 | @end
|
542 |
|
543 | @implementation RCTDevMenuItem
|
544 |
|
545 | + (instancetype)buttonItemWithTitle:(NSString *)title handler:(void (^)(void))handler
|
546 | {
|
547 | return nil;
|
548 | }
|
549 | + (instancetype)buttonItemWithTitleBlock:(NSString * (^)(void))titleBlock handler:(void (^)(void))handler
|
550 | {
|
551 | return nil;
|
552 | }
|
553 |
|
554 | @end
|
555 |
|
556 | #endif
|
557 |
|
558 | @implementation RCTBridge (RCTDevMenu)
|
559 |
|
560 | - (RCTDevMenu *)devMenu
|
561 | {
|
562 | #if RCT_DEV_MENU
|
563 | return [self moduleForClass:[RCTDevMenu class]];
|
564 | #else
|
565 | return nil;
|
566 | #endif
|
567 | }
|
568 |
|
569 | @end
|
570 |
|
571 | Class RCTDevMenuCls(void) {
|
572 | return RCTDevMenu.class;
|
573 | }
|