716 строки
27 KiB
Plaintext
716 строки
27 KiB
Plaintext
/*
|
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*/
|
|
|
|
#import "RCTRedBox.h"
|
|
|
|
#import <FBReactNativeSpec/FBReactNativeSpec.h>
|
|
#import <React/RCTBridge.h>
|
|
#import <React/RCTConvert.h>
|
|
#import <React/RCTDefines.h>
|
|
#import <React/RCTErrorInfo.h>
|
|
#import <React/RCTEventDispatcher.h>
|
|
#import <React/RCTJSStackFrame.h>
|
|
#import <React/RCTRedBoxExtraDataViewController.h>
|
|
#import <React/RCTRedBoxSetEnabled.h>
|
|
#import <React/RCTReloadCommand.h>
|
|
#import <React/RCTUtils.h>
|
|
|
|
#import <objc/runtime.h>
|
|
|
|
#import "CoreModulesPlugins.h"
|
|
|
|
#if RCT_DEV_MENU
|
|
|
|
@class RCTRedBoxWindow;
|
|
|
|
@interface UIButton (RCTRedBox)
|
|
|
|
@property (nonatomic) RCTRedBoxButtonPressHandler rct_handler;
|
|
|
|
- (void)rct_addBlock:(RCTRedBoxButtonPressHandler)handler forControlEvents:(UIControlEvents)controlEvents;
|
|
|
|
@end
|
|
|
|
@implementation UIButton (RCTRedBox)
|
|
|
|
- (RCTRedBoxButtonPressHandler)rct_handler
|
|
{
|
|
return objc_getAssociatedObject(self, @selector(rct_handler));
|
|
}
|
|
|
|
- (void)setRct_handler:(RCTRedBoxButtonPressHandler)rct_handler
|
|
{
|
|
objc_setAssociatedObject(self, @selector(rct_handler), rct_handler, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
|
}
|
|
|
|
- (void)rct_callBlock
|
|
{
|
|
if (self.rct_handler) {
|
|
self.rct_handler();
|
|
}
|
|
}
|
|
|
|
- (void)rct_addBlock:(RCTRedBoxButtonPressHandler)handler forControlEvents:(UIControlEvents)controlEvents
|
|
{
|
|
self.rct_handler = handler;
|
|
[self addTarget:self action:@selector(rct_callBlock) forControlEvents:controlEvents];
|
|
}
|
|
|
|
@end
|
|
|
|
@protocol RCTRedBoxWindowActionDelegate <NSObject>
|
|
|
|
- (void)redBoxWindow:(RCTRedBoxWindow *)redBoxWindow openStackFrameInEditor:(RCTJSStackFrame *)stackFrame;
|
|
- (void)reloadFromRedBoxWindow:(RCTRedBoxWindow *)redBoxWindow;
|
|
- (void)loadExtraDataViewController;
|
|
|
|
@end
|
|
|
|
@interface RCTRedBoxWindow : UIWindow <UITableViewDelegate, UITableViewDataSource>
|
|
@property (nonatomic, weak) id<RCTRedBoxWindowActionDelegate> actionDelegate;
|
|
@end
|
|
|
|
@implementation RCTRedBoxWindow
|
|
{
|
|
UITableView *_stackTraceTableView;
|
|
NSString *_lastErrorMessage;
|
|
NSArray<RCTJSStackFrame *> *_lastStackTrace;
|
|
int _lastErrorCookie;
|
|
}
|
|
|
|
- (instancetype)initWithFrame:(CGRect)frame customButtonTitles:(NSArray<NSString *>*)customButtonTitles customButtonHandlers:(NSArray<RCTRedBoxButtonPressHandler> *)customButtonHandlers
|
|
{
|
|
_lastErrorCookie = -1;
|
|
if ((self = [super initWithFrame:frame])) {
|
|
#if TARGET_OS_TV
|
|
self.windowLevel = UIWindowLevelAlert + 1000;
|
|
#else
|
|
self.windowLevel = UIWindowLevelStatusBar - 1;
|
|
#endif
|
|
self.backgroundColor = [UIColor colorWithRed:0.1 green:0.1 blue:0.1 alpha:1];
|
|
self.hidden = YES;
|
|
|
|
UIViewController *rootController = [UIViewController new];
|
|
self.rootViewController = rootController;
|
|
UIView *rootView = rootController.view;
|
|
rootView.backgroundColor = [UIColor clearColor];
|
|
|
|
const CGFloat buttonHeight = 60;
|
|
|
|
CGRect detailsFrame = rootView.bounds;
|
|
detailsFrame.size.height -= buttonHeight + [self bottomSafeViewHeight];
|
|
|
|
_stackTraceTableView = [[UITableView alloc] initWithFrame:detailsFrame style:UITableViewStylePlain];
|
|
_stackTraceTableView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
|
_stackTraceTableView.delegate = self;
|
|
_stackTraceTableView.dataSource = self;
|
|
_stackTraceTableView.backgroundColor = [UIColor clearColor];
|
|
#if !TARGET_OS_TV
|
|
_stackTraceTableView.separatorColor = [UIColor colorWithWhite:1 alpha:0.3];
|
|
_stackTraceTableView.separatorStyle = UITableViewCellSeparatorStyleNone;
|
|
#endif
|
|
_stackTraceTableView.indicatorStyle = UIScrollViewIndicatorStyleWhite;
|
|
[rootView addSubview:_stackTraceTableView];
|
|
|
|
#if TARGET_OS_SIMULATOR
|
|
NSString *reloadText = @"Reload\n(\u2318R)";
|
|
NSString *dismissText = @"Dismiss\n(ESC)";
|
|
NSString *copyText = @"Copy\n(\u2325\u2318C)";
|
|
NSString *extraText = @"Extra Info\n(\u2318E)";
|
|
#else
|
|
NSString *reloadText = @"Reload JS";
|
|
NSString *dismissText = @"Dismiss";
|
|
NSString *copyText = @"Copy";
|
|
NSString *extraText = @"Extra Info";
|
|
#endif
|
|
|
|
UIButton *dismissButton = [self redBoxButton:dismissText accessibilityIdentifier:@"redbox-dismiss" selector:@selector(dismiss) block:nil];
|
|
UIButton *reloadButton = [self redBoxButton:reloadText accessibilityIdentifier:@"redbox-reload" selector:@selector(reload) block:nil];
|
|
UIButton *copyButton = [self redBoxButton:copyText accessibilityIdentifier:@"redbox-copy" selector:@selector(copyStack) block:nil];
|
|
UIButton *extraButton = [self redBoxButton:extraText accessibilityIdentifier:@"redbox-extra" selector:@selector(showExtraDataViewController) block:nil];
|
|
|
|
CGFloat buttonWidth = self.bounds.size.width / (4 + [customButtonTitles count]);
|
|
CGFloat bottomButtonHeight = self.bounds.size.height - buttonHeight - [self bottomSafeViewHeight];
|
|
dismissButton.frame = CGRectMake(0, bottomButtonHeight, buttonWidth, buttonHeight);
|
|
reloadButton.frame = CGRectMake(buttonWidth, bottomButtonHeight, buttonWidth, buttonHeight);
|
|
copyButton.frame = CGRectMake(buttonWidth * 2, bottomButtonHeight, buttonWidth, buttonHeight);
|
|
extraButton.frame = CGRectMake(buttonWidth * 3, bottomButtonHeight, buttonWidth, buttonHeight);
|
|
|
|
[rootView addSubview:dismissButton];
|
|
[rootView addSubview:reloadButton];
|
|
[rootView addSubview:copyButton];
|
|
[rootView addSubview:extraButton];
|
|
|
|
for (NSUInteger i = 0; i < [customButtonTitles count]; i++) {
|
|
UIButton *button = [self redBoxButton:customButtonTitles[i] accessibilityIdentifier:@"" selector:nil block:customButtonHandlers[i]];
|
|
button.frame = CGRectMake(buttonWidth * (4 + i), bottomButtonHeight, buttonWidth, buttonHeight);
|
|
[rootView addSubview:button];
|
|
}
|
|
|
|
UIView *topBorder = [[UIView alloc] initWithFrame:CGRectMake(0, bottomButtonHeight + 1, rootView.frame.size.width, 1)];
|
|
topBorder.backgroundColor = [UIColor colorWithRed:0.70 green:0.70 blue:0.70 alpha:1.0];
|
|
|
|
[rootView addSubview:topBorder];
|
|
|
|
UIView *bottomSafeView = [UIView new];
|
|
bottomSafeView.backgroundColor = [UIColor colorWithRed:0.1 green:0.1 blue:0.1 alpha:1];
|
|
bottomSafeView.frame = CGRectMake(0, self.bounds.size.height - [self bottomSafeViewHeight], self.bounds.size.width, [self bottomSafeViewHeight]);
|
|
|
|
[rootView addSubview:bottomSafeView];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (UIButton *)redBoxButton:(NSString *)title accessibilityIdentifier:(NSString *)accessibilityIdentifier selector:(SEL)selector block:(RCTRedBoxButtonPressHandler)block
|
|
{
|
|
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
|
|
button.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleRightMargin;
|
|
button.accessibilityIdentifier = accessibilityIdentifier;
|
|
button.titleLabel.font = [UIFont systemFontOfSize:13];
|
|
button.titleLabel.lineBreakMode = NSLineBreakByWordWrapping;
|
|
button.titleLabel.textAlignment = NSTextAlignmentCenter;
|
|
button.backgroundColor = [UIColor colorWithRed:0.1 green:0.1 blue:0.1 alpha:1];
|
|
[button setTitle:title forState:UIControlStateNormal];
|
|
[button setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
|
|
[button setTitleColor:[UIColor colorWithWhite:1 alpha:0.5] forState:UIControlStateHighlighted];
|
|
if (selector) {
|
|
[button addTarget:self action:selector forControlEvents:UIControlEventTouchUpInside];
|
|
} else if (block) {
|
|
[button rct_addBlock:block forControlEvents:UIControlEventTouchUpInside];
|
|
}
|
|
return button;
|
|
}
|
|
|
|
- (NSInteger)bottomSafeViewHeight
|
|
{
|
|
if (@available(iOS 11.0, *)) {
|
|
return RCTSharedApplication().delegate.window.safeAreaInsets.bottom;
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder)
|
|
|
|
- (void)dealloc
|
|
{
|
|
_stackTraceTableView.dataSource = nil;
|
|
_stackTraceTableView.delegate = nil;
|
|
}
|
|
|
|
- (NSString *)stripAnsi:(NSString *)text
|
|
{
|
|
NSError *error = nil;
|
|
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"\\x1b\\[[0-9;]*m" options:NSRegularExpressionCaseInsensitive error:&error];
|
|
return [regex stringByReplacingMatchesInString:text options:0 range:NSMakeRange(0, [text length]) withTemplate:@""];
|
|
}
|
|
|
|
- (void)showErrorMessage:(NSString *)message withStack:(NSArray<RCTJSStackFrame *> *)stack isUpdate:(BOOL)isUpdate errorCookie:(int)errorCookie
|
|
{
|
|
// Remove ANSI color codes from the message
|
|
NSString *messageWithoutAnsi = [self stripAnsi:message];
|
|
|
|
// Show if this is a new message, or if we're updating the previous message
|
|
BOOL isNew = self.hidden && !isUpdate;
|
|
BOOL isUpdateForSameMessage = !isNew && (
|
|
!self.hidden && isUpdate && (
|
|
(errorCookie == -1 && [_lastErrorMessage isEqualToString:messageWithoutAnsi]) ||
|
|
(errorCookie == _lastErrorCookie)
|
|
)
|
|
);
|
|
if (isNew || isUpdateForSameMessage) {
|
|
_lastStackTrace = stack;
|
|
// message is displayed using UILabel, which is unable to render text of
|
|
// unlimited length, so we truncate it
|
|
_lastErrorMessage = [messageWithoutAnsi substringToIndex:MIN((NSUInteger)10000, messageWithoutAnsi.length)];
|
|
_lastErrorCookie = errorCookie;
|
|
|
|
[_stackTraceTableView reloadData];
|
|
|
|
if (self.hidden) {
|
|
[_stackTraceTableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]
|
|
atScrollPosition:UITableViewScrollPositionTop
|
|
animated:NO];
|
|
}
|
|
|
|
[self makeKeyAndVisible];
|
|
[self becomeFirstResponder];
|
|
}
|
|
}
|
|
|
|
- (void)dismiss
|
|
{
|
|
self.hidden = YES;
|
|
[self resignFirstResponder];
|
|
[RCTSharedApplication().delegate.window makeKeyWindow];
|
|
}
|
|
|
|
- (void)reload
|
|
{
|
|
[_actionDelegate reloadFromRedBoxWindow:self];
|
|
}
|
|
|
|
- (void)showExtraDataViewController
|
|
{
|
|
[_actionDelegate loadExtraDataViewController];
|
|
}
|
|
|
|
- (void)copyStack
|
|
{
|
|
NSMutableString *fullStackTrace;
|
|
|
|
if (_lastErrorMessage != nil) {
|
|
fullStackTrace = [_lastErrorMessage mutableCopy];
|
|
[fullStackTrace appendString:@"\n\n"];
|
|
}
|
|
else {
|
|
fullStackTrace = [NSMutableString string];
|
|
}
|
|
|
|
for (RCTJSStackFrame *stackFrame in _lastStackTrace) {
|
|
[fullStackTrace appendString:[NSString stringWithFormat:@"%@\n", stackFrame.methodName]];
|
|
if (stackFrame.file) {
|
|
[fullStackTrace appendFormat:@" %@\n", [self formatFrameSource:stackFrame]];
|
|
}
|
|
}
|
|
#if !TARGET_OS_TV
|
|
UIPasteboard *pb = [UIPasteboard generalPasteboard];
|
|
[pb setString:fullStackTrace];
|
|
#endif
|
|
}
|
|
|
|
- (NSString *)formatFrameSource:(RCTJSStackFrame *)stackFrame
|
|
{
|
|
NSString *fileName = RCTNilIfNull(stackFrame.file) ? [stackFrame.file lastPathComponent] : @"<unknown file>";
|
|
NSString *lineInfo = [NSString stringWithFormat:@"%@:%lld",
|
|
fileName,
|
|
(long long)stackFrame.lineNumber];
|
|
|
|
if (stackFrame.column != 0) {
|
|
lineInfo = [lineInfo stringByAppendingFormat:@":%lld", (long long)stackFrame.column];
|
|
}
|
|
return lineInfo;
|
|
}
|
|
|
|
#pragma mark - TableView
|
|
|
|
- (NSInteger)numberOfSectionsInTableView:(__unused UITableView *)tableView
|
|
{
|
|
return 2;
|
|
}
|
|
|
|
- (NSInteger)tableView:(__unused UITableView *)tableView numberOfRowsInSection:(NSInteger)section
|
|
{
|
|
return section == 0 ? 1 : _lastStackTrace.count;
|
|
}
|
|
|
|
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
|
|
{
|
|
if (indexPath.section == 0) {
|
|
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"msg-cell"];
|
|
return [self reuseCell:cell forErrorMessage:_lastErrorMessage];
|
|
}
|
|
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
|
|
NSUInteger index = indexPath.row;
|
|
RCTJSStackFrame *stackFrame = _lastStackTrace[index];
|
|
return [self reuseCell:cell forStackFrame:stackFrame];
|
|
}
|
|
|
|
- (UITableViewCell *)reuseCell:(UITableViewCell *)cell forErrorMessage:(NSString *)message
|
|
{
|
|
if (!cell) {
|
|
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"msg-cell"];
|
|
cell.textLabel.accessibilityIdentifier = @"redbox-error";
|
|
cell.textLabel.textColor = [UIColor whiteColor];
|
|
cell.textLabel.font = [UIFont boldSystemFontOfSize:16];
|
|
cell.textLabel.lineBreakMode = NSLineBreakByWordWrapping;
|
|
cell.textLabel.numberOfLines = 0;
|
|
cell.detailTextLabel.textColor = [UIColor whiteColor];
|
|
cell.backgroundColor = [UIColor colorWithRed:0.82 green:0.10 blue:0.15 alpha:1.0];
|
|
cell.selectionStyle = UITableViewCellSelectionStyleNone;
|
|
}
|
|
|
|
cell.textLabel.text = message;
|
|
|
|
return cell;
|
|
}
|
|
|
|
- (UITableViewCell *)reuseCell:(UITableViewCell *)cell forStackFrame:(RCTJSStackFrame *)stackFrame
|
|
{
|
|
if (!cell) {
|
|
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:@"cell"];
|
|
cell.textLabel.font = [UIFont fontWithName:@"Menlo-Regular" size:14];
|
|
cell.textLabel.lineBreakMode = NSLineBreakByCharWrapping;
|
|
cell.textLabel.numberOfLines = 2;
|
|
cell.detailTextLabel.textColor = [UIColor colorWithRed:0.70 green:0.70 blue:0.70 alpha:1.0];
|
|
cell.detailTextLabel.font = [UIFont fontWithName:@"Menlo-Regular" size:11];
|
|
cell.detailTextLabel.lineBreakMode = NSLineBreakByTruncatingMiddle;
|
|
cell.backgroundColor = [UIColor clearColor];
|
|
cell.selectedBackgroundView = [UIView new];
|
|
cell.selectedBackgroundView.backgroundColor = [UIColor colorWithWhite:0 alpha:0.2];
|
|
}
|
|
|
|
cell.textLabel.text = stackFrame.methodName ?: @"(unnamed method)";
|
|
if (stackFrame.file) {
|
|
cell.detailTextLabel.text = [self formatFrameSource:stackFrame];
|
|
} else {
|
|
cell.detailTextLabel.text = @"";
|
|
}
|
|
cell.textLabel.textColor = stackFrame.collapse ? [UIColor lightGrayColor] : [UIColor whiteColor];
|
|
cell.detailTextLabel.textColor = stackFrame.collapse ?
|
|
[UIColor colorWithRed:0.50 green:0.50 blue:0.50 alpha:1.0] :
|
|
[UIColor colorWithRed:0.70 green:0.70 blue:0.70 alpha:1.0];
|
|
return cell;
|
|
}
|
|
|
|
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
|
|
{
|
|
if (indexPath.section == 0) {
|
|
NSMutableParagraphStyle *paragraphStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
|
|
paragraphStyle.lineBreakMode = NSLineBreakByWordWrapping;
|
|
|
|
NSDictionary *attributes = @{NSFontAttributeName: [UIFont boldSystemFontOfSize:16],
|
|
NSParagraphStyleAttributeName: paragraphStyle};
|
|
CGRect boundingRect = [_lastErrorMessage boundingRectWithSize:CGSizeMake(tableView.frame.size.width - 30, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin attributes:attributes context:nil];
|
|
return ceil(boundingRect.size.height) + 40;
|
|
} else {
|
|
return 50;
|
|
}
|
|
}
|
|
|
|
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
|
|
{
|
|
if (indexPath.section == 1) {
|
|
NSUInteger row = indexPath.row;
|
|
RCTJSStackFrame *stackFrame = _lastStackTrace[row];
|
|
[_actionDelegate redBoxWindow:self openStackFrameInEditor:stackFrame];
|
|
}
|
|
[tableView deselectRowAtIndexPath:indexPath animated:YES];
|
|
}
|
|
|
|
#pragma mark - Key commands
|
|
|
|
- (NSArray<UIKeyCommand *> *)keyCommands
|
|
{
|
|
// NOTE: We could use RCTKeyCommands for this, but since
|
|
// we control this window, we can use the standard, non-hacky
|
|
// mechanism instead
|
|
|
|
return @[
|
|
// Dismiss red box
|
|
[UIKeyCommand keyCommandWithInput:UIKeyInputEscape
|
|
modifierFlags:0
|
|
action:@selector(dismiss)],
|
|
|
|
// Reload
|
|
[UIKeyCommand keyCommandWithInput:@"r"
|
|
modifierFlags:UIKeyModifierCommand
|
|
action:@selector(reload)],
|
|
|
|
// Copy = Cmd-Option C since Cmd-C in the simulator copies the pasteboard from
|
|
// the simulator to the desktop pasteboard.
|
|
[UIKeyCommand keyCommandWithInput:@"c"
|
|
modifierFlags:UIKeyModifierCommand | UIKeyModifierAlternate
|
|
action:@selector(copyStack)],
|
|
|
|
// Extra data
|
|
[UIKeyCommand keyCommandWithInput:@"e"
|
|
modifierFlags:UIKeyModifierCommand
|
|
action:@selector(showExtraDataViewController)]
|
|
];
|
|
}
|
|
|
|
- (BOOL)canBecomeFirstResponder
|
|
{
|
|
return YES;
|
|
}
|
|
|
|
@end
|
|
|
|
@interface RCTRedBox () <RCTInvalidating, RCTRedBoxWindowActionDelegate, RCTRedBoxExtraDataActionDelegate, NativeRedBoxSpec>
|
|
@end
|
|
|
|
@implementation RCTRedBox
|
|
{
|
|
RCTRedBoxWindow *_window;
|
|
NSMutableArray<id<RCTErrorCustomizer>> *_errorCustomizers;
|
|
RCTRedBoxExtraDataViewController *_extraDataViewController;
|
|
NSMutableArray<NSString *> *_customButtonTitles;
|
|
NSMutableArray<RCTRedBoxButtonPressHandler> *_customButtonHandlers;
|
|
}
|
|
|
|
@synthesize bridge = _bridge;
|
|
|
|
RCT_EXPORT_MODULE()
|
|
|
|
- (void)registerErrorCustomizer:(id<RCTErrorCustomizer>)errorCustomizer
|
|
{
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
if (!self->_errorCustomizers) {
|
|
self->_errorCustomizers = [NSMutableArray array];
|
|
}
|
|
if (![self->_errorCustomizers containsObject:errorCustomizer]) {
|
|
[self->_errorCustomizers addObject:errorCustomizer];
|
|
}
|
|
});
|
|
}
|
|
|
|
// WARNING: Should only be called from the main thread/dispatch queue.
|
|
- (RCTErrorInfo *)_customizeError:(RCTErrorInfo *)error
|
|
{
|
|
RCTAssertMainQueue();
|
|
if (!self->_errorCustomizers) {
|
|
return error;
|
|
}
|
|
for (id<RCTErrorCustomizer> customizer in self->_errorCustomizers) {
|
|
RCTErrorInfo *newInfo = [customizer customizeErrorInfo:error];
|
|
if (newInfo) {
|
|
error = newInfo;
|
|
}
|
|
}
|
|
return error;
|
|
}
|
|
|
|
- (void)showError:(NSError *)error
|
|
{
|
|
[self showErrorMessage:error.localizedDescription
|
|
withDetails:error.localizedFailureReason
|
|
stack:error.userInfo[RCTJSStackTraceKey]
|
|
errorCookie:-1];
|
|
}
|
|
|
|
- (void)showErrorMessage:(NSString *)message
|
|
{
|
|
[self showErrorMessage:message withParsedStack:nil isUpdate:NO errorCookie:-1];
|
|
}
|
|
|
|
- (void)showErrorMessage:(NSString *)message withDetails:(NSString *)details
|
|
{
|
|
[self showErrorMessage:message withDetails:details stack:nil errorCookie:-1];
|
|
}
|
|
|
|
- (void)showErrorMessage:(NSString *)message withDetails:(NSString *)details stack:(NSArray<RCTJSStackFrame *> *)stack errorCookie:(int)errorCookie {
|
|
NSString *combinedMessage = message;
|
|
if (details) {
|
|
combinedMessage = [NSString stringWithFormat:@"%@\n\n%@", message, details];
|
|
}
|
|
[self showErrorMessage:combinedMessage withParsedStack:stack isUpdate:NO errorCookie:errorCookie];
|
|
}
|
|
|
|
- (void)showErrorMessage:(NSString *)message withRawStack:(NSString *)rawStack
|
|
{
|
|
[self showErrorMessage:message withRawStack:rawStack errorCookie:-1];
|
|
}
|
|
|
|
- (void)showErrorMessage:(NSString *)message withRawStack:(NSString *)rawStack errorCookie:(int)errorCookie
|
|
{
|
|
NSArray<RCTJSStackFrame *> *stack = [RCTJSStackFrame stackFramesWithLines:rawStack];
|
|
[self showErrorMessage:message withParsedStack:stack isUpdate:NO errorCookie:errorCookie];
|
|
}
|
|
|
|
- (void)showErrorMessage:(NSString *)message withStack:(NSArray<NSDictionary *> *)stack
|
|
{
|
|
[self showErrorMessage:message withStack:stack errorCookie:-1];
|
|
}
|
|
|
|
- (void)updateErrorMessage:(NSString *)message withStack:(NSArray<NSDictionary *> *)stack
|
|
{
|
|
[self updateErrorMessage:message withStack:stack errorCookie:-1];
|
|
}
|
|
|
|
- (void)showErrorMessage:(NSString *)message withStack:(NSArray<NSDictionary *> *)stack errorCookie:(int)errorCookie
|
|
{
|
|
[self showErrorMessage:message withParsedStack:[RCTJSStackFrame stackFramesWithDictionaries:stack] isUpdate:NO errorCookie:errorCookie];
|
|
}
|
|
|
|
- (void)updateErrorMessage:(NSString *)message withStack:(NSArray<NSDictionary *> *)stack errorCookie:(int)errorCookie
|
|
{
|
|
[self showErrorMessage:message withParsedStack:[RCTJSStackFrame stackFramesWithDictionaries:stack] isUpdate:YES errorCookie:errorCookie];
|
|
}
|
|
|
|
- (void)showErrorMessage:(NSString *)message withParsedStack:(NSArray<RCTJSStackFrame *> *)stack
|
|
{
|
|
[self showErrorMessage:message withParsedStack:stack errorCookie:-1];
|
|
}
|
|
|
|
- (void)updateErrorMessage:(NSString *)message withParsedStack:(NSArray<RCTJSStackFrame *> *)stack
|
|
{
|
|
[self updateErrorMessage:message withParsedStack:stack errorCookie:-1];
|
|
}
|
|
|
|
- (void)showErrorMessage:(NSString *)message withParsedStack:(NSArray<RCTJSStackFrame *> *)stack errorCookie:(int)errorCookie
|
|
{
|
|
[self showErrorMessage:message withParsedStack:stack isUpdate:NO errorCookie:errorCookie];
|
|
}
|
|
|
|
- (void)updateErrorMessage:(NSString *)message withParsedStack:(NSArray<RCTJSStackFrame *> *)stack errorCookie:(int)errorCookie
|
|
{
|
|
[self showErrorMessage:message withParsedStack:stack isUpdate:YES errorCookie:errorCookie];
|
|
}
|
|
|
|
- (void)showErrorMessage:(NSString *)message withParsedStack:(NSArray<RCTJSStackFrame *> *)stack isUpdate:(BOOL)isUpdate errorCookie:(int)errorCookie
|
|
{
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
if (self->_extraDataViewController == nil) {
|
|
self->_extraDataViewController = [RCTRedBoxExtraDataViewController new];
|
|
self->_extraDataViewController.actionDelegate = self;
|
|
}
|
|
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
|
[self->_bridge.eventDispatcher sendDeviceEventWithName:@"collectRedBoxExtraData" body:nil];
|
|
#pragma clang diagnostic pop
|
|
|
|
if (!self->_window) {
|
|
self->_window = [[RCTRedBoxWindow alloc] initWithFrame:[UIScreen mainScreen].bounds customButtonTitles:self->_customButtonTitles customButtonHandlers:self->_customButtonHandlers];
|
|
self->_window.actionDelegate = self;
|
|
}
|
|
|
|
RCTErrorInfo *errorInfo = [[RCTErrorInfo alloc] initWithErrorMessage:message
|
|
stack:stack];
|
|
errorInfo = [self _customizeError:errorInfo];
|
|
[self->_window showErrorMessage:errorInfo.errorMessage
|
|
withStack:errorInfo.stack
|
|
isUpdate:isUpdate
|
|
errorCookie:errorCookie];
|
|
});
|
|
}
|
|
|
|
- (void)loadExtraDataViewController {
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
// Make sure the CMD+E shortcut doesn't call this twice
|
|
if (self->_extraDataViewController != nil && ![self->_window.rootViewController presentedViewController]) {
|
|
[self->_window.rootViewController presentViewController:self->_extraDataViewController animated:YES completion:nil];
|
|
}
|
|
});
|
|
}
|
|
|
|
RCT_EXPORT_METHOD(setExtraData:(NSDictionary *)extraData forIdentifier:(NSString *)identifier) {
|
|
[_extraDataViewController addExtraData:extraData forIdentifier:identifier];
|
|
}
|
|
|
|
RCT_EXPORT_METHOD(dismiss)
|
|
{
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[self->_window dismiss];
|
|
});
|
|
}
|
|
|
|
- (void)invalidate
|
|
{
|
|
[self dismiss];
|
|
}
|
|
|
|
- (void)redBoxWindow:(__unused RCTRedBoxWindow *)redBoxWindow openStackFrameInEditor:(RCTJSStackFrame *)stackFrame
|
|
{
|
|
NSURL *const bundleURL = _overrideBundleURL ?: _bridge.bundleURL;
|
|
if (![bundleURL.scheme hasPrefix:@"http"]) {
|
|
RCTLogWarn(@"Cannot open stack frame in editor because you're not connected to the packager.");
|
|
return;
|
|
}
|
|
|
|
NSData *stackFrameJSON = [RCTJSONStringify([stackFrame toDictionary], NULL) dataUsingEncoding:NSUTF8StringEncoding];
|
|
NSString *postLength = [NSString stringWithFormat:@"%tu", stackFrameJSON.length];
|
|
NSMutableURLRequest *request = [NSMutableURLRequest new];
|
|
request.URL = [NSURL URLWithString:@"/open-stack-frame" relativeToURL:bundleURL];
|
|
request.HTTPMethod = @"POST";
|
|
request.HTTPBody = stackFrameJSON;
|
|
[request setValue:postLength forHTTPHeaderField:@"Content-Length"];
|
|
[request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
|
|
|
|
[[[NSURLSession sharedSession] dataTaskWithRequest:request] resume];
|
|
}
|
|
|
|
- (void)reload
|
|
{
|
|
// Window is not used and can be nil
|
|
[self reloadFromRedBoxWindow:nil];
|
|
}
|
|
|
|
- (void)reloadFromRedBoxWindow:(__unused RCTRedBoxWindow *)redBoxWindow
|
|
{
|
|
if (_overrideReloadAction) {
|
|
_overrideReloadAction();
|
|
} else {
|
|
RCTTriggerReloadCommandListeners(@"Redbox");
|
|
}
|
|
[self dismiss];
|
|
}
|
|
|
|
- (void)addCustomButton:(NSString *)title onPressHandler:(RCTRedBoxButtonPressHandler)handler
|
|
{
|
|
if (!_customButtonTitles) {
|
|
_customButtonTitles = [NSMutableArray new];
|
|
_customButtonHandlers = [NSMutableArray new];
|
|
}
|
|
|
|
[_customButtonTitles addObject:title];
|
|
[_customButtonHandlers addObject:handler];
|
|
}
|
|
|
|
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModuleWithJsInvoker:(std::shared_ptr<facebook::react::CallInvoker>)jsInvoker
|
|
{
|
|
return std::make_shared<facebook::react::NativeRedBoxSpecJSI>(self, jsInvoker);
|
|
}
|
|
|
|
@end
|
|
|
|
@implementation RCTBridge (RCTRedBox)
|
|
|
|
- (RCTRedBox *)redBox
|
|
{
|
|
return RCTRedBoxGetEnabled() ? [self moduleForClass:[RCTRedBox class]] : nil;
|
|
}
|
|
|
|
@end
|
|
|
|
#else // Disabled
|
|
|
|
@interface RCTRedBox() <NativeRedBoxSpec>
|
|
@end
|
|
|
|
@implementation RCTRedBox
|
|
|
|
+ (NSString *)moduleName { return nil; }
|
|
- (void)registerErrorCustomizer:(id<RCTErrorCustomizer>)errorCustomizer {}
|
|
- (void)showError:(NSError *)error {}
|
|
- (void)showErrorMessage:(NSString *)message {}
|
|
- (void)showErrorMessage:(NSString *)message withDetails:(NSString *)details {}
|
|
- (void)showErrorMessage:(NSString *)message withRawStack:(NSString *)rawStack {}
|
|
- (void)showErrorMessage:(NSString *)message withRawStack:(NSString *)rawStack errorCookie:(int)errorCookie {}
|
|
- (void)showErrorMessage:(NSString *)message withStack:(NSArray<NSDictionary *> *)stack {}
|
|
- (void)updateErrorMessage:(NSString *)message withStack:(NSArray<NSDictionary *> *)stack {}
|
|
- (void)showErrorMessage:(NSString *)message withStack:(NSArray<NSDictionary *> *)stack errorCookie:(int)errorCookie {}
|
|
- (void)updateErrorMessage:(NSString *)message withStack:(NSArray<NSDictionary *> *)stack errorCookie:(int)errorCookie {}
|
|
- (void)showErrorMessage:(NSString *)message withParsedStack:(NSArray<RCTJSStackFrame *> *)stack {}
|
|
- (void)updateErrorMessage:(NSString *)message withParsedStack:(NSArray<RCTJSStackFrame *> *)stack {}
|
|
- (void)showErrorMessage:(NSString *)message withParsedStack:(NSArray<RCTJSStackFrame *> *)stack errorCookie:(int)errorCookie {}
|
|
- (void)updateErrorMessage:(NSString *)message withParsedStack:(NSArray<RCTJSStackFrame *> *)stack errorCookie:(int)errorCookie {}
|
|
- (void)setExtraData:(NSDictionary *)extraData forIdentifier:(NSString *)identifier {}
|
|
|
|
- (void)dismiss {}
|
|
|
|
- (void)addCustomButton:(NSString *)title onPressHandler:(RCTRedBoxButtonPressHandler)handler {}
|
|
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModuleWithJsInvoker:(std::shared_ptr<facebook::react::CallInvoker>)jsInvoker
|
|
{
|
|
return std::make_shared<facebook::react::NativeRedBoxSpecJSI>(self, jsInvoker);
|
|
}
|
|
|
|
@end
|
|
|
|
@implementation RCTBridge (RCTRedBox)
|
|
|
|
- (RCTRedBox *)redBox { return nil; }
|
|
|
|
@end
|
|
|
|
#endif
|
|
|
|
Class RCTRedBoxCls(void) {
|
|
return RCTRedBox.class;
|
|
}
|