1// Copyright 2015-present 650 Industries. All rights reserved.
2
3#import "EXAppViewController.h"
4#import "EXErrorRecoveryManager.h"
5#import "EXKernel.h"
6#import "EXKernelAppRecord.h"
7#import "EXReactAppManager.h"
8#import "EXReactAppExceptionHandler.h"
9#import "EXUtil.h"
10
11#import <Crashlytics/Crashlytics.h>
12#import <React/RCTBridge.h>
13#import <React/RCTRedBox.h>
14
15RCTFatalHandler handleFatalReactError = ^(NSError *error) {
16  [EXUtil performSynchronouslyOnMainThread:^{
17    EXKernelAppRecord *record = [[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager appRecordForError:error];
18    if (!record) {
19      // show the error on Home or on the main standalone app if we can't figure out who this error belongs to
20      if ([EXKernel sharedInstance].appRegistry.homeAppRecord) {
21        record = [EXKernel sharedInstance].appRegistry.homeAppRecord;
22      } else if ([EXKernel sharedInstance].appRegistry.standaloneAppRecord) {
23        record = [EXKernel sharedInstance].appRegistry.standaloneAppRecord;
24      }
25    }
26    if (record) {
27      [record.viewController maybeShowError:error];
28    }
29  }];
30};
31
32NS_ASSUME_NONNULL_BEGIN
33
34@interface EXReactAppExceptionHandler ()
35
36@property (nonatomic, weak) EXKernelAppRecord *appRecord;
37
38@end
39
40@implementation EXReactAppExceptionHandler
41
42- (instancetype)initWithAppRecord:(EXKernelAppRecord *)appRecord
43{
44  if (self = [super init]) {
45    _appRecord = appRecord;
46  }
47  return self;
48}
49
50RCT_NOT_IMPLEMENTED(- (instancetype)init)
51
52- (void)handleSoftJSExceptionWithMessage:(nullable NSString *)message
53                                   stack:(nullable NSArray<NSDictionary<NSString *, id> *> *)stack
54                             exceptionId:(NSNumber *)exceptionId
55{
56  [[self _bridgeForRecord].redBox showErrorMessage:message withStack:stack];
57}
58
59- (void)handleFatalJSExceptionWithMessage:(nullable NSString *)message
60                                    stack:(nullable NSArray<NSDictionary<NSString *, id> *> *)stack
61                              exceptionId:(NSNumber *)exceptionId
62{
63  [[self _bridgeForRecord].redBox showErrorMessage:message withStack:stack];
64
65  NSString *description = [@"Unhandled JS Exception: " stringByAppendingString:message];
66  NSDictionary *errorInfo = @{ NSLocalizedDescriptionKey: description, RCTJSStackTraceKey: stack };
67  NSError *error = [NSError errorWithDomain:RCTErrorDomain code:0 userInfo:errorInfo];
68
69  [[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager setError:error forExperienceId:_appRecord.experienceId];
70
71  if ([self _isProdHome]) {
72    [self _recordCrashlyticsExceptionFromMessage:message stack:stack];
73    RCTFatal(error);
74  }
75}
76
77- (void)updateJSExceptionWithMessage:(nullable NSString *)message
78                               stack:(nullable NSArray *)stack
79                         exceptionId:(NSNumber *)exceptionId
80{
81  [[self _bridgeForRecord].redBox updateErrorMessage:message withStack:stack];
82}
83
84#pragma mark - internal
85
86- (RCTBridge *)_bridgeForRecord
87{
88  return _appRecord.appManager.reactBridge;
89}
90
91- (BOOL)_isProdHome
92{
93  if (RCT_DEBUG) {
94    return NO;
95  }
96  return (_appRecord && _appRecord == [EXKernel sharedInstance].appRegistry.homeAppRecord);
97}
98
99- (void)_recordCrashlyticsExceptionFromMessage:(NSString *)message stack:(NSArray *)stack
100{
101  NSError *error;
102  NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"^([a-z$_][a-z\\d$_]*):\\s*"
103                                                                         options:NSRegularExpressionCaseInsensitive
104                                                                           error:&error];
105  if (!regex) {
106    DDLogError(@"Error creating regular expression: %@", error.localizedDescription);
107    return;
108  }
109
110  NSString *errorName;
111  NSString *errorReason;
112  NSTextCheckingResult *match = [regex firstMatchInString:message options:0 range:NSMakeRange(0, message.length)];
113  if (match) {
114    errorName = [message substringWithRange:[match rangeAtIndex:1]];
115    errorName = [message substringFromIndex:match.range.location + match.range.length];
116  } else {
117    errorName = @"UnknownError";
118    errorReason = message;
119  }
120
121  NSMutableArray *frameArray = [NSMutableArray arrayWithCapacity:stack.count];
122  for (NSDictionary *frameInfo in stack) {
123    CLSStackFrame *frame = [CLSStackFrame stackFrame];
124    if ([frameInfo[@"file"] isKindOfClass:[NSString class]]) {
125      frame.fileName = frameInfo[@"file"];
126    }
127    if ([frameInfo[@"methodName"] isKindOfClass:[NSString class]]) {
128      frame.symbol = frameInfo[@"methodName"];
129    }
130    if ([frameInfo[@"lineNumber"] isKindOfClass:[NSNumber class]]) {
131      frame.lineNumber = (uint32_t)MAX([frameInfo[@"lineNumber"] integerValue], 0);
132    }
133    [frameArray addObject:frame];
134  }
135
136  [[Crashlytics sharedInstance] recordCustomExceptionName:errorName reason:errorReason frameArray:frameArray];
137}
138
139@end
140
141NS_ASSUME_NONNULL_END
142