UNPKG

27.5 kBPlain TextView Raw
1/*
2 * Copyright (c) Facebook, Inc. and its affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8#import "RCTRedBox.h"
9
10#import <FBReactNativeSpec/FBReactNativeSpec.h>
11#import <React/RCTBridge.h>
12#import <React/RCTConvert.h>
13#import <React/RCTDefines.h>
14#import <React/RCTErrorInfo.h>
15#import <React/RCTEventDispatcher.h>
16#import <React/RCTJSStackFrame.h>
17#import <React/RCTRedBoxExtraDataViewController.h>
18#import <React/RCTRedBoxSetEnabled.h>
19#import <React/RCTReloadCommand.h>
20#import <React/RCTUtils.h>
21
22#import <objc/runtime.h>
23
24#import "CoreModulesPlugins.h"
25
26#if RCT_DEV_MENU
27
28@class RCTRedBoxWindow;
29
30@interface UIButton (RCTRedBox)
31
32@property (nonatomic) RCTRedBoxButtonPressHandler rct_handler;
33
34- (void)rct_addBlock:(RCTRedBoxButtonPressHandler)handler forControlEvents:(UIControlEvents)controlEvents;
35
36@end
37
38@implementation UIButton (RCTRedBox)
39
40- (RCTRedBoxButtonPressHandler)rct_handler
41{
42 return objc_getAssociatedObject(self, @selector(rct_handler));
43}
44
45- (void)setRct_handler:(RCTRedBoxButtonPressHandler)rct_handler
46{
47 objc_setAssociatedObject(self, @selector(rct_handler), rct_handler, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
48}
49
50- (void)rct_callBlock
51{
52 if (self.rct_handler) {
53 self.rct_handler();
54 }
55}
56
57- (void)rct_addBlock:(RCTRedBoxButtonPressHandler)handler forControlEvents:(UIControlEvents)controlEvents
58{
59 self.rct_handler = handler;
60 [self addTarget:self action:@selector(rct_callBlock) forControlEvents:controlEvents];
61}
62
63@end
64
65@protocol RCTRedBoxWindowActionDelegate <NSObject>
66
67- (void)redBoxWindow:(RCTRedBoxWindow *)redBoxWindow openStackFrameInEditor:(RCTJSStackFrame *)stackFrame;
68- (void)reloadFromRedBoxWindow:(RCTRedBoxWindow *)redBoxWindow;
69- (void)loadExtraDataViewController;
70
71@end
72
73@interface RCTRedBoxWindow : UIWindow <UITableViewDelegate, UITableViewDataSource>
74@property (nonatomic, weak) id<RCTRedBoxWindowActionDelegate> actionDelegate;
75@end
76
77@implementation RCTRedBoxWindow
78{
79 UITableView *_stackTraceTableView;
80 NSString *_lastErrorMessage;
81 NSArray<RCTJSStackFrame *> *_lastStackTrace;
82 int _lastErrorCookie;
83}
84
85- (instancetype)initWithFrame:(CGRect)frame customButtonTitles:(NSArray<NSString *>*)customButtonTitles customButtonHandlers:(NSArray<RCTRedBoxButtonPressHandler> *)customButtonHandlers
86{
87 _lastErrorCookie = -1;
88 if ((self = [super initWithFrame:frame])) {
89#if TARGET_OS_TV
90 self.windowLevel = UIWindowLevelAlert + 1000;
91#else
92 self.windowLevel = UIWindowLevelStatusBar - 1;
93#endif
94 self.backgroundColor = [UIColor colorWithRed:0.1 green:0.1 blue:0.1 alpha:1];
95 self.hidden = YES;
96
97 UIViewController *rootController = [UIViewController new];
98 self.rootViewController = rootController;
99 UIView *rootView = rootController.view;
100 rootView.backgroundColor = [UIColor clearColor];
101
102 const CGFloat buttonHeight = 60;
103
104 CGRect detailsFrame = rootView.bounds;
105 detailsFrame.size.height -= buttonHeight + [self bottomSafeViewHeight];
106
107 _stackTraceTableView = [[UITableView alloc] initWithFrame:detailsFrame style:UITableViewStylePlain];
108 _stackTraceTableView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
109 _stackTraceTableView.delegate = self;
110 _stackTraceTableView.dataSource = self;
111 _stackTraceTableView.backgroundColor = [UIColor clearColor];
112#if !TARGET_OS_TV
113 _stackTraceTableView.separatorColor = [UIColor colorWithWhite:1 alpha:0.3];
114 _stackTraceTableView.separatorStyle = UITableViewCellSeparatorStyleNone;
115#endif
116 _stackTraceTableView.indicatorStyle = UIScrollViewIndicatorStyleWhite;
117 [rootView addSubview:_stackTraceTableView];
118
119#if TARGET_OS_SIMULATOR
120 NSString *reloadText = @"Reload\n(\u2318R)";
121 NSString *dismissText = @"Dismiss\n(ESC)";
122 NSString *copyText = @"Copy\n(\u2325\u2318C)";
123 NSString *extraText = @"Extra Info\n(\u2318E)";
124#else
125 NSString *reloadText = @"Reload JS";
126 NSString *dismissText = @"Dismiss";
127 NSString *copyText = @"Copy";
128 NSString *extraText = @"Extra Info";
129#endif
130
131 UIButton *dismissButton = [self redBoxButton:dismissText accessibilityIdentifier:@"redbox-dismiss" selector:@selector(dismiss) block:nil];
132 UIButton *reloadButton = [self redBoxButton:reloadText accessibilityIdentifier:@"redbox-reload" selector:@selector(reload) block:nil];
133 UIButton *copyButton = [self redBoxButton:copyText accessibilityIdentifier:@"redbox-copy" selector:@selector(copyStack) block:nil];
134 UIButton *extraButton = [self redBoxButton:extraText accessibilityIdentifier:@"redbox-extra" selector:@selector(showExtraDataViewController) block:nil];
135
136 CGFloat buttonWidth = self.bounds.size.width / (4 + [customButtonTitles count]);
137 CGFloat bottomButtonHeight = self.bounds.size.height - buttonHeight - [self bottomSafeViewHeight];
138 dismissButton.frame = CGRectMake(0, bottomButtonHeight, buttonWidth, buttonHeight);
139 reloadButton.frame = CGRectMake(buttonWidth, bottomButtonHeight, buttonWidth, buttonHeight);
140 copyButton.frame = CGRectMake(buttonWidth * 2, bottomButtonHeight, buttonWidth, buttonHeight);
141 extraButton.frame = CGRectMake(buttonWidth * 3, bottomButtonHeight, buttonWidth, buttonHeight);
142
143 [rootView addSubview:dismissButton];
144 [rootView addSubview:reloadButton];
145 [rootView addSubview:copyButton];
146 [rootView addSubview:extraButton];
147
148 for (NSUInteger i = 0; i < [customButtonTitles count]; i++) {
149 UIButton *button = [self redBoxButton:customButtonTitles[i] accessibilityIdentifier:@"" selector:nil block:customButtonHandlers[i]];
150 button.frame = CGRectMake(buttonWidth * (4 + i), bottomButtonHeight, buttonWidth, buttonHeight);
151 [rootView addSubview:button];
152 }
153
154 UIView *topBorder = [[UIView alloc] initWithFrame:CGRectMake(0, bottomButtonHeight + 1, rootView.frame.size.width, 1)];
155 topBorder.backgroundColor = [UIColor colorWithRed:0.70 green:0.70 blue:0.70 alpha:1.0];
156
157 [rootView addSubview:topBorder];
158
159 UIView *bottomSafeView = [UIView new];
160 bottomSafeView.backgroundColor = [UIColor colorWithRed:0.1 green:0.1 blue:0.1 alpha:1];
161 bottomSafeView.frame = CGRectMake(0, self.bounds.size.height - [self bottomSafeViewHeight], self.bounds.size.width, [self bottomSafeViewHeight]);
162
163 [rootView addSubview:bottomSafeView];
164 }
165 return self;
166}
167
168- (UIButton *)redBoxButton:(NSString *)title accessibilityIdentifier:(NSString *)accessibilityIdentifier selector:(SEL)selector block:(RCTRedBoxButtonPressHandler)block
169{
170 UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
171 button.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleRightMargin;
172 button.accessibilityIdentifier = accessibilityIdentifier;
173 button.titleLabel.font = [UIFont systemFontOfSize:13];
174 button.titleLabel.lineBreakMode = NSLineBreakByWordWrapping;
175 button.titleLabel.textAlignment = NSTextAlignmentCenter;
176 button.backgroundColor = [UIColor colorWithRed:0.1 green:0.1 blue:0.1 alpha:1];
177 [button setTitle:title forState:UIControlStateNormal];
178 [button setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
179 [button setTitleColor:[UIColor colorWithWhite:1 alpha:0.5] forState:UIControlStateHighlighted];
180 if (selector) {
181 [button addTarget:self action:selector forControlEvents:UIControlEventTouchUpInside];
182 } else if (block) {
183 [button rct_addBlock:block forControlEvents:UIControlEventTouchUpInside];
184 }
185 return button;
186}
187
188- (NSInteger)bottomSafeViewHeight
189{
190 if (@available(iOS 11.0, *)) {
191 return RCTSharedApplication().delegate.window.safeAreaInsets.bottom;
192 } else {
193 return 0;
194 }
195}
196
197RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder)
198
199- (void)dealloc
200{
201 _stackTraceTableView.dataSource = nil;
202 _stackTraceTableView.delegate = nil;
203}
204
205- (NSString *)stripAnsi:(NSString *)text
206{
207 NSError *error = nil;
208 NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"\\x1b\\[[0-9;]*m" options:NSRegularExpressionCaseInsensitive error:&error];
209 return [regex stringByReplacingMatchesInString:text options:0 range:NSMakeRange(0, [text length]) withTemplate:@""];
210}
211
212- (void)showErrorMessage:(NSString *)message withStack:(NSArray<RCTJSStackFrame *> *)stack isUpdate:(BOOL)isUpdate errorCookie:(int)errorCookie
213{
214 // Remove ANSI color codes from the message
215 NSString *messageWithoutAnsi = [self stripAnsi:message];
216
217 // Show if this is a new message, or if we're updating the previous message
218 BOOL isNew = self.hidden && !isUpdate;
219 BOOL isUpdateForSameMessage = !isNew && (
220 !self.hidden && isUpdate && (
221 (errorCookie == -1 && [_lastErrorMessage isEqualToString:messageWithoutAnsi]) ||
222 (errorCookie == _lastErrorCookie)
223 )
224 );
225 if (isNew || isUpdateForSameMessage) {
226 _lastStackTrace = stack;
227 // message is displayed using UILabel, which is unable to render text of
228 // unlimited length, so we truncate it
229 _lastErrorMessage = [messageWithoutAnsi substringToIndex:MIN((NSUInteger)10000, messageWithoutAnsi.length)];
230 _lastErrorCookie = errorCookie;
231
232 [_stackTraceTableView reloadData];
233
234 if (self.hidden) {
235 [_stackTraceTableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]
236 atScrollPosition:UITableViewScrollPositionTop
237 animated:NO];
238 }
239
240 [self makeKeyAndVisible];
241 [self becomeFirstResponder];
242 }
243}
244
245- (void)dismiss
246{
247 self.hidden = YES;
248 [self resignFirstResponder];
249 [RCTSharedApplication().delegate.window makeKeyWindow];
250}
251
252- (void)reload
253{
254 [_actionDelegate reloadFromRedBoxWindow:self];
255}
256
257- (void)showExtraDataViewController
258{
259 [_actionDelegate loadExtraDataViewController];
260}
261
262- (void)copyStack
263{
264 NSMutableString *fullStackTrace;
265
266 if (_lastErrorMessage != nil) {
267 fullStackTrace = [_lastErrorMessage mutableCopy];
268 [fullStackTrace appendString:@"\n\n"];
269 }
270 else {
271 fullStackTrace = [NSMutableString string];
272 }
273
274 for (RCTJSStackFrame *stackFrame in _lastStackTrace) {
275 [fullStackTrace appendString:[NSString stringWithFormat:@"%@\n", stackFrame.methodName]];
276 if (stackFrame.file) {
277 [fullStackTrace appendFormat:@" %@\n", [self formatFrameSource:stackFrame]];
278 }
279 }
280#if !TARGET_OS_TV
281 UIPasteboard *pb = [UIPasteboard generalPasteboard];
282 [pb setString:fullStackTrace];
283#endif
284}
285
286- (NSString *)formatFrameSource:(RCTJSStackFrame *)stackFrame
287{
288 NSString *fileName = RCTNilIfNull(stackFrame.file) ? [stackFrame.file lastPathComponent] : @"<unknown file>";
289 NSString *lineInfo = [NSString stringWithFormat:@"%@:%lld",
290 fileName,
291 (long long)stackFrame.lineNumber];
292
293 if (stackFrame.column != 0) {
294 lineInfo = [lineInfo stringByAppendingFormat:@":%lld", (long long)stackFrame.column];
295 }
296 return lineInfo;
297}
298
299#pragma mark - TableView
300
301- (NSInteger)numberOfSectionsInTableView:(__unused UITableView *)tableView
302{
303 return 2;
304}
305
306- (NSInteger)tableView:(__unused UITableView *)tableView numberOfRowsInSection:(NSInteger)section
307{
308 return section == 0 ? 1 : _lastStackTrace.count;
309}
310
311- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
312{
313 if (indexPath.section == 0) {
314 UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"msg-cell"];
315 return [self reuseCell:cell forErrorMessage:_lastErrorMessage];
316 }
317 UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
318 NSUInteger index = indexPath.row;
319 RCTJSStackFrame *stackFrame = _lastStackTrace[index];
320 return [self reuseCell:cell forStackFrame:stackFrame];
321}
322
323- (UITableViewCell *)reuseCell:(UITableViewCell *)cell forErrorMessage:(NSString *)message
324{
325 if (!cell) {
326 cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"msg-cell"];
327 cell.textLabel.accessibilityIdentifier = @"redbox-error";
328 cell.textLabel.textColor = [UIColor whiteColor];
329 cell.textLabel.font = [UIFont boldSystemFontOfSize:16];
330 cell.textLabel.lineBreakMode = NSLineBreakByWordWrapping;
331 cell.textLabel.numberOfLines = 0;
332 cell.detailTextLabel.textColor = [UIColor whiteColor];
333 cell.backgroundColor = [UIColor colorWithRed:0.82 green:0.10 blue:0.15 alpha:1.0];
334 cell.selectionStyle = UITableViewCellSelectionStyleNone;
335 }
336
337 cell.textLabel.text = message;
338
339 return cell;
340}
341
342- (UITableViewCell *)reuseCell:(UITableViewCell *)cell forStackFrame:(RCTJSStackFrame *)stackFrame
343{
344 if (!cell) {
345 cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:@"cell"];
346 cell.textLabel.font = [UIFont fontWithName:@"Menlo-Regular" size:14];
347 cell.textLabel.lineBreakMode = NSLineBreakByCharWrapping;
348 cell.textLabel.numberOfLines = 2;
349 cell.detailTextLabel.textColor = [UIColor colorWithRed:0.70 green:0.70 blue:0.70 alpha:1.0];
350 cell.detailTextLabel.font = [UIFont fontWithName:@"Menlo-Regular" size:11];
351 cell.detailTextLabel.lineBreakMode = NSLineBreakByTruncatingMiddle;
352 cell.backgroundColor = [UIColor clearColor];
353 cell.selectedBackgroundView = [UIView new];
354 cell.selectedBackgroundView.backgroundColor = [UIColor colorWithWhite:0 alpha:0.2];
355 }
356
357 cell.textLabel.text = stackFrame.methodName ?: @"(unnamed method)";
358 if (stackFrame.file) {
359 cell.detailTextLabel.text = [self formatFrameSource:stackFrame];
360 } else {
361 cell.detailTextLabel.text = @"";
362 }
363 cell.textLabel.textColor = stackFrame.collapse ? [UIColor lightGrayColor] : [UIColor whiteColor];
364 cell.detailTextLabel.textColor = stackFrame.collapse ?
365 [UIColor colorWithRed:0.50 green:0.50 blue:0.50 alpha:1.0] :
366 [UIColor colorWithRed:0.70 green:0.70 blue:0.70 alpha:1.0];
367 return cell;
368}
369
370- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
371{
372 if (indexPath.section == 0) {
373 NSMutableParagraphStyle *paragraphStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
374 paragraphStyle.lineBreakMode = NSLineBreakByWordWrapping;
375
376 NSDictionary *attributes = @{NSFontAttributeName: [UIFont boldSystemFontOfSize:16],
377 NSParagraphStyleAttributeName: paragraphStyle};
378 CGRect boundingRect = [_lastErrorMessage boundingRectWithSize:CGSizeMake(tableView.frame.size.width - 30, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin attributes:attributes context:nil];
379 return ceil(boundingRect.size.height) + 40;
380 } else {
381 return 50;
382 }
383}
384
385- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
386{
387 if (indexPath.section == 1) {
388 NSUInteger row = indexPath.row;
389 RCTJSStackFrame *stackFrame = _lastStackTrace[row];
390 [_actionDelegate redBoxWindow:self openStackFrameInEditor:stackFrame];
391 }
392 [tableView deselectRowAtIndexPath:indexPath animated:YES];
393}
394
395#pragma mark - Key commands
396
397- (NSArray<UIKeyCommand *> *)keyCommands
398{
399 // NOTE: We could use RCTKeyCommands for this, but since
400 // we control this window, we can use the standard, non-hacky
401 // mechanism instead
402
403 return @[
404 // Dismiss red box
405 [UIKeyCommand keyCommandWithInput:UIKeyInputEscape
406 modifierFlags:0
407 action:@selector(dismiss)],
408
409 // Reload
410 [UIKeyCommand keyCommandWithInput:@"r"
411 modifierFlags:UIKeyModifierCommand
412 action:@selector(reload)],
413
414 // Copy = Cmd-Option C since Cmd-C in the simulator copies the pasteboard from
415 // the simulator to the desktop pasteboard.
416 [UIKeyCommand keyCommandWithInput:@"c"
417 modifierFlags:UIKeyModifierCommand | UIKeyModifierAlternate
418 action:@selector(copyStack)],
419
420 // Extra data
421 [UIKeyCommand keyCommandWithInput:@"e"
422 modifierFlags:UIKeyModifierCommand
423 action:@selector(showExtraDataViewController)]
424 ];
425}
426
427- (BOOL)canBecomeFirstResponder
428{
429 return YES;
430}
431
432@end
433
434@interface RCTRedBox () <RCTInvalidating, RCTRedBoxWindowActionDelegate, RCTRedBoxExtraDataActionDelegate, NativeRedBoxSpec>
435@end
436
437@implementation RCTRedBox
438{
439 RCTRedBoxWindow *_window;
440 NSMutableArray<id<RCTErrorCustomizer>> *_errorCustomizers;
441 RCTRedBoxExtraDataViewController *_extraDataViewController;
442 NSMutableArray<NSString *> *_customButtonTitles;
443 NSMutableArray<RCTRedBoxButtonPressHandler> *_customButtonHandlers;
444}
445
446@synthesize bridge = _bridge;
447
448RCT_EXPORT_MODULE()
449
450- (void)registerErrorCustomizer:(id<RCTErrorCustomizer>)errorCustomizer
451{
452 dispatch_async(dispatch_get_main_queue(), ^{
453 if (!self->_errorCustomizers) {
454 self->_errorCustomizers = [NSMutableArray array];
455 }
456 if (![self->_errorCustomizers containsObject:errorCustomizer]) {
457 [self->_errorCustomizers addObject:errorCustomizer];
458 }
459 });
460}
461
462// WARNING: Should only be called from the main thread/dispatch queue.
463- (RCTErrorInfo *)_customizeError:(RCTErrorInfo *)error
464{
465 RCTAssertMainQueue();
466 if (!self->_errorCustomizers) {
467 return error;
468 }
469 for (id<RCTErrorCustomizer> customizer in self->_errorCustomizers) {
470 RCTErrorInfo *newInfo = [customizer customizeErrorInfo:error];
471 if (newInfo) {
472 error = newInfo;
473 }
474 }
475 return error;
476}
477
478- (void)showError:(NSError *)error
479{
480 [self showErrorMessage:error.localizedDescription
481 withDetails:error.localizedFailureReason
482 stack:error.userInfo[RCTJSStackTraceKey]
483 errorCookie:-1];
484}
485
486- (void)showErrorMessage:(NSString *)message
487{
488 [self showErrorMessage:message withParsedStack:nil isUpdate:NO errorCookie:-1];
489}
490
491- (void)showErrorMessage:(NSString *)message withDetails:(NSString *)details
492{
493 [self showErrorMessage:message withDetails:details stack:nil errorCookie:-1];
494}
495
496- (void)showErrorMessage:(NSString *)message withDetails:(NSString *)details stack:(NSArray<RCTJSStackFrame *> *)stack errorCookie:(int)errorCookie {
497 NSString *combinedMessage = message;
498 if (details) {
499 combinedMessage = [NSString stringWithFormat:@"%@\n\n%@", message, details];
500 }
501 [self showErrorMessage:combinedMessage withParsedStack:stack isUpdate:NO errorCookie:errorCookie];
502}
503
504- (void)showErrorMessage:(NSString *)message withRawStack:(NSString *)rawStack
505{
506 [self showErrorMessage:message withRawStack:rawStack errorCookie:-1];
507}
508
509- (void)showErrorMessage:(NSString *)message withRawStack:(NSString *)rawStack errorCookie:(int)errorCookie
510{
511 NSArray<RCTJSStackFrame *> *stack = [RCTJSStackFrame stackFramesWithLines:rawStack];
512 [self showErrorMessage:message withParsedStack:stack isUpdate:NO errorCookie:errorCookie];
513}
514
515- (void)showErrorMessage:(NSString *)message withStack:(NSArray<NSDictionary *> *)stack
516{
517 [self showErrorMessage:message withStack:stack errorCookie:-1];
518}
519
520- (void)updateErrorMessage:(NSString *)message withStack:(NSArray<NSDictionary *> *)stack
521{
522 [self updateErrorMessage:message withStack:stack errorCookie:-1];
523}
524
525- (void)showErrorMessage:(NSString *)message withStack:(NSArray<NSDictionary *> *)stack errorCookie:(int)errorCookie
526{
527 [self showErrorMessage:message withParsedStack:[RCTJSStackFrame stackFramesWithDictionaries:stack] isUpdate:NO errorCookie:errorCookie];
528}
529
530- (void)updateErrorMessage:(NSString *)message withStack:(NSArray<NSDictionary *> *)stack errorCookie:(int)errorCookie
531{
532 [self showErrorMessage:message withParsedStack:[RCTJSStackFrame stackFramesWithDictionaries:stack] isUpdate:YES errorCookie:errorCookie];
533}
534
535- (void)showErrorMessage:(NSString *)message withParsedStack:(NSArray<RCTJSStackFrame *> *)stack
536{
537 [self showErrorMessage:message withParsedStack:stack errorCookie:-1];
538}
539
540- (void)updateErrorMessage:(NSString *)message withParsedStack:(NSArray<RCTJSStackFrame *> *)stack
541{
542 [self updateErrorMessage:message withParsedStack:stack errorCookie:-1];
543}
544
545- (void)showErrorMessage:(NSString *)message withParsedStack:(NSArray<RCTJSStackFrame *> *)stack errorCookie:(int)errorCookie
546{
547 [self showErrorMessage:message withParsedStack:stack isUpdate:NO errorCookie:errorCookie];
548}
549
550- (void)updateErrorMessage:(NSString *)message withParsedStack:(NSArray<RCTJSStackFrame *> *)stack errorCookie:(int)errorCookie
551{
552 [self showErrorMessage:message withParsedStack:stack isUpdate:YES errorCookie:errorCookie];
553}
554
555- (void)showErrorMessage:(NSString *)message withParsedStack:(NSArray<RCTJSStackFrame *> *)stack isUpdate:(BOOL)isUpdate errorCookie:(int)errorCookie
556{
557 dispatch_async(dispatch_get_main_queue(), ^{
558 if (self->_extraDataViewController == nil) {
559 self->_extraDataViewController = [RCTRedBoxExtraDataViewController new];
560 self->_extraDataViewController.actionDelegate = self;
561 }
562
563#pragma clang diagnostic push
564#pragma clang diagnostic ignored "-Wdeprecated-declarations"
565 [self->_bridge.eventDispatcher sendDeviceEventWithName:@"collectRedBoxExtraData" body:nil];
566#pragma clang diagnostic pop
567
568 if (!self->_window) {
569 self->_window = [[RCTRedBoxWindow alloc] initWithFrame:[UIScreen mainScreen].bounds customButtonTitles:self->_customButtonTitles customButtonHandlers:self->_customButtonHandlers];
570 self->_window.actionDelegate = self;
571 }
572
573 RCTErrorInfo *errorInfo = [[RCTErrorInfo alloc] initWithErrorMessage:message
574 stack:stack];
575 errorInfo = [self _customizeError:errorInfo];
576 [self->_window showErrorMessage:errorInfo.errorMessage
577 withStack:errorInfo.stack
578 isUpdate:isUpdate
579 errorCookie:errorCookie];
580 });
581}
582
583- (void)loadExtraDataViewController {
584 dispatch_async(dispatch_get_main_queue(), ^{
585 // Make sure the CMD+E shortcut doesn't call this twice
586 if (self->_extraDataViewController != nil && ![self->_window.rootViewController presentedViewController]) {
587 [self->_window.rootViewController presentViewController:self->_extraDataViewController animated:YES completion:nil];
588 }
589 });
590}
591
592RCT_EXPORT_METHOD(setExtraData:(NSDictionary *)extraData forIdentifier:(NSString *)identifier) {
593 [_extraDataViewController addExtraData:extraData forIdentifier:identifier];
594}
595
596RCT_EXPORT_METHOD(dismiss)
597{
598 dispatch_async(dispatch_get_main_queue(), ^{
599 [self->_window dismiss];
600 });
601}
602
603- (void)invalidate
604{
605 [self dismiss];
606}
607
608- (void)redBoxWindow:(__unused RCTRedBoxWindow *)redBoxWindow openStackFrameInEditor:(RCTJSStackFrame *)stackFrame
609{
610 NSURL *const bundleURL = _overrideBundleURL ?: _bridge.bundleURL;
611 if (![bundleURL.scheme hasPrefix:@"http"]) {
612 RCTLogWarn(@"Cannot open stack frame in editor because you're not connected to the packager.");
613 return;
614 }
615
616 NSData *stackFrameJSON = [RCTJSONStringify([stackFrame toDictionary], NULL) dataUsingEncoding:NSUTF8StringEncoding];
617 NSString *postLength = [NSString stringWithFormat:@"%tu", stackFrameJSON.length];
618 NSMutableURLRequest *request = [NSMutableURLRequest new];
619 request.URL = [NSURL URLWithString:@"/open-stack-frame" relativeToURL:bundleURL];
620 request.HTTPMethod = @"POST";
621 request.HTTPBody = stackFrameJSON;
622 [request setValue:postLength forHTTPHeaderField:@"Content-Length"];
623 [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
624
625 [[[NSURLSession sharedSession] dataTaskWithRequest:request] resume];
626}
627
628- (void)reload
629{
630 // Window is not used and can be nil
631 [self reloadFromRedBoxWindow:nil];
632}
633
634- (void)reloadFromRedBoxWindow:(__unused RCTRedBoxWindow *)redBoxWindow
635{
636 if (_overrideReloadAction) {
637 _overrideReloadAction();
638 } else {
639 RCTTriggerReloadCommandListeners(@"Redbox");
640 }
641 [self dismiss];
642}
643
644- (void)addCustomButton:(NSString *)title onPressHandler:(RCTRedBoxButtonPressHandler)handler
645{
646 if (!_customButtonTitles) {
647 _customButtonTitles = [NSMutableArray new];
648 _customButtonHandlers = [NSMutableArray new];
649 }
650
651 [_customButtonTitles addObject:title];
652 [_customButtonHandlers addObject:handler];
653}
654
655- (std::shared_ptr<facebook::react::TurboModule>)getTurboModuleWithJsInvoker:(std::shared_ptr<facebook::react::CallInvoker>)jsInvoker
656{
657 return std::make_shared<facebook::react::NativeRedBoxSpecJSI>(self, jsInvoker);
658}
659
660@end
661
662@implementation RCTBridge (RCTRedBox)
663
664- (RCTRedBox *)redBox
665{
666 return RCTRedBoxGetEnabled() ? [self moduleForClass:[RCTRedBox class]] : nil;
667}
668
669@end
670
671#else // Disabled
672
673@interface RCTRedBox() <NativeRedBoxSpec>
674@end
675
676@implementation RCTRedBox
677
678+ (NSString *)moduleName { return nil; }
679- (void)registerErrorCustomizer:(id<RCTErrorCustomizer>)errorCustomizer {}
680- (void)showError:(NSError *)error {}
681- (void)showErrorMessage:(NSString *)message {}
682- (void)showErrorMessage:(NSString *)message withDetails:(NSString *)details {}
683- (void)showErrorMessage:(NSString *)message withRawStack:(NSString *)rawStack {}
684- (void)showErrorMessage:(NSString *)message withRawStack:(NSString *)rawStack errorCookie:(int)errorCookie {}
685- (void)showErrorMessage:(NSString *)message withStack:(NSArray<NSDictionary *> *)stack {}
686- (void)updateErrorMessage:(NSString *)message withStack:(NSArray<NSDictionary *> *)stack {}
687- (void)showErrorMessage:(NSString *)message withStack:(NSArray<NSDictionary *> *)stack errorCookie:(int)errorCookie {}
688- (void)updateErrorMessage:(NSString *)message withStack:(NSArray<NSDictionary *> *)stack errorCookie:(int)errorCookie {}
689- (void)showErrorMessage:(NSString *)message withParsedStack:(NSArray<RCTJSStackFrame *> *)stack {}
690- (void)updateErrorMessage:(NSString *)message withParsedStack:(NSArray<RCTJSStackFrame *> *)stack {}
691- (void)showErrorMessage:(NSString *)message withParsedStack:(NSArray<RCTJSStackFrame *> *)stack errorCookie:(int)errorCookie {}
692- (void)updateErrorMessage:(NSString *)message withParsedStack:(NSArray<RCTJSStackFrame *> *)stack errorCookie:(int)errorCookie {}
693- (void)setExtraData:(NSDictionary *)extraData forIdentifier:(NSString *)identifier {}
694
695- (void)dismiss {}
696
697- (void)addCustomButton:(NSString *)title onPressHandler:(RCTRedBoxButtonPressHandler)handler {}
698- (std::shared_ptr<facebook::react::TurboModule>)getTurboModuleWithJsInvoker:(std::shared_ptr<facebook::react::CallInvoker>)jsInvoker
699{
700 return std::make_shared<facebook::react::NativeRedBoxSpecJSI>(self, jsInvoker);
701}
702
703@end
704
705@implementation RCTBridge (RCTRedBox)
706
707- (RCTRedBox *)redBox { return nil; }
708
709@end
710
711#endif
712
713Class RCTRedBoxCls(void) {
714 return RCTRedBox.class;
715}