1// Copyright 2015-present 650 Industries. All rights reserved. 2 3#import "EXAbstractLoader.h" 4#import "EXErrorView.h" 5#import "EXEnvironment.h" 6#import "EXKernel.h" 7#import "EXKernelAppRecord.h" 8#import "EXManifestResource.h" 9#import "EXUtil.h" 10 11@import EXManifests; 12 13@interface EXErrorView () 14 15@property (nonatomic, strong) IBOutlet UILabel *lblError; 16@property (nonatomic, strong) IBOutlet UIButton *btnRetry; 17@property (nonatomic, strong) IBOutlet UIButton *btnBack; 18@property (nonatomic, strong) IBOutlet UIStackView *btnStack; 19@property (nonatomic, strong) IBOutlet UIView *btnStackContainer; 20@property (nonatomic, strong) IBOutlet UILabel *lblUrl; 21@property (nonatomic, strong) IBOutlet UITextView *txtErrorDetail; 22@property (nonatomic, strong) IBOutlet UIScrollView *vContainer; 23 24- (void)_onTapRetry; 25 26@end 27 28@implementation EXErrorView 29 30- (instancetype)initWithFrame:(CGRect)frame 31{ 32 if (self = [super initWithFrame:frame]) { 33 [[NSBundle mainBundle] loadNibNamed:@"EXErrorView" owner:self options:nil]; 34 [self addSubview:_vContainer]; 35 36 [_btnRetry addTarget:self action:@selector(_onTapRetry) forControlEvents:UIControlEventTouchUpInside]; 37 [_btnBack addTarget:self action:@selector(_onTapBack) forControlEvents:UIControlEventTouchUpInside]; 38 39 [_txtErrorDetail setTextContainerInset:UIEdgeInsetsZero]; 40 _txtErrorDetail.textContainer.lineFragmentPadding = 0; 41 42 for (UIButton *btnToStyle in @[ _btnRetry, _btnBack ]) { 43 btnToStyle.layer.cornerRadius = 4.0; 44 btnToStyle.layer.masksToBounds = YES; 45 } 46 } 47 return self; 48} 49 50- (void)setType:(EXFatalErrorType)type 51{ 52 _type = type; 53 NSString *appOwnerName = @"the requested app"; 54 if (_appRecord) { 55 if (_appRecord == [EXKernel sharedInstance].appRegistry.homeAppRecord) { 56 appOwnerName = @"Expo"; 57 } else if (_appRecord.appLoader.manifest && _appRecord.appLoader.manifest.name) { 58 appOwnerName = [NSString stringWithFormat:@"\"%@\"", _appRecord.appLoader.manifest.name]; 59 } 60 } 61 62 switch (type) { 63 case kEXFatalErrorTypeLoading: { 64 _lblError.text = [NSString stringWithFormat:@"There was a problem loading %@.", appOwnerName]; 65 break; 66 } 67 case kEXFatalErrorTypeException: { 68 _lblError.text = [NSString stringWithFormat:@"There was a problem running %@.", appOwnerName]; 69 break; 70 } 71 } 72 [self _resetUIState]; 73} 74 75- (void)setError:(NSError *)error 76{ 77 _error = error; 78 NSString *errorHeader = [EXManifestResource formatHeader:error]; 79 NSString *errorDetail = [error localizedDescription]; 80 81 if (errorHeader != nil) { 82 _lblError.text = errorHeader; 83 } 84 85 switch (_type) { 86 case kEXFatalErrorTypeLoading: { 87 if (_error.code == kCFURLErrorNotConnectedToInternet) { 88 errorDetail = [NSString stringWithFormat:@"%@ Make sure you're connected to the internet.", errorDetail]; 89 } else if (_appRecord.appLoader.manifestUrl) { 90 NSString *url = _appRecord.appLoader.manifestUrl.absoluteString; 91 if ([self _urlLooksLikeLAN:url]) { 92 NSString *extraLANPermissionText = @""; 93 if (@available(iOS 14, *)) { 94 extraLANPermissionText = @", and that you have granted Expo Go the Local Network permission in the Settings app,"; 95 } 96 errorDetail = [NSString stringWithFormat: 97 @"%@\n\nIt looks like you may be using a LAN URL. " 98 "Make sure your device is on the same network as the server%@ or try using the tunnel connection type.", errorDetail, extraLANPermissionText]; 99 } 100 } 101 break; 102 } 103 case kEXFatalErrorTypeException: { 104 break; 105 } 106 } 107 NSAttributedString *attributedErrorString = [EXManifestResource addErrorStringHyperlinks:errorDetail]; 108 109 UIFont *font = _txtErrorDetail.font; 110 _txtErrorDetail.attributedText = attributedErrorString; 111 _txtErrorDetail.font = font; 112 113 [self _resetUIState]; 114} 115 116- (void)setAppRecord:(EXKernelAppRecord *)appRecord 117{ 118 _appRecord = appRecord; 119 [self _resetUIState]; 120} 121 122- (void)layoutSubviews 123{ 124 [super layoutSubviews]; 125 126 if (@available(iOS 12.0, *)) { 127 switch (UIScreen.mainScreen.traitCollection.userInterfaceStyle) { 128 case UIUserInterfaceStyleDark: 129 self.backgroundColor = [EXUtil colorWithRGB:0x25292E]; 130 break; 131 case UIUserInterfaceStyleLight: 132 case UIUserInterfaceStyleUnspecified: 133 break; 134 default: 135 break; 136 } 137 } 138 139 _vContainer.translatesAutoresizingMaskIntoConstraints = NO; 140 141 UILayoutGuide *guide = self.safeAreaLayoutGuide; 142 [_vContainer.leadingAnchor constraintEqualToAnchor:guide.leadingAnchor].active = YES; 143 [_vContainer.trailingAnchor constraintEqualToAnchor:guide.trailingAnchor].active = YES; 144 [_vContainer.topAnchor constraintEqualToAnchor:guide.topAnchor].active = YES; 145 [_vContainer.bottomAnchor constraintEqualToAnchor:guide.bottomAnchor].active = YES; 146 147 UIImage *btnRetryBgImage = [self imageWithSize:_btnRetry.frame.size color: [EXUtil colorWithRGB:0x25292E]]; 148 [_btnRetry setBackgroundImage:btnRetryBgImage forState:UIControlStateNormal]; 149 150 UIImage *btnBackBgImage = [self imageWithSize:_btnBack.frame.size color: [EXUtil colorWithRGB:0xF0F1F2]]; 151 [_btnBack setBackgroundImage:btnBackBgImage forState:UIControlStateNormal]; 152} 153 154#pragma mark - Internal 155 156- (void)_resetUIState 157{ 158 EXKernelAppRecord *homeRecord = [EXKernel sharedInstance].appRegistry.homeAppRecord; 159 _btnBack.hidden = (!homeRecord || _appRecord == homeRecord); 160 _lblUrl.hidden = (!homeRecord && ![self _isDevDetached]); 161 _lblUrl.text = _appRecord.appLoader.manifestUrl.absoluteString; 162 // TODO: maybe hide retry (see BrowserErrorView) 163 [self setNeedsLayout]; 164} 165 166- (void)_onTapRetry 167{ 168 if (_delegate) { 169 [_delegate errorViewDidSelectRetry:self]; 170 } 171} 172 173- (void)_onTapBack 174{ 175 if ([EXKernel sharedInstance].browserController) { 176 [[EXKernel sharedInstance].browserController moveHomeToVisible]; 177 } 178} 179 180- (BOOL)_urlLooksLikeLAN:(NSString *)url 181{ 182 return ( 183 url && ( 184 [url rangeOfString:@".local"].length > 0 || 185 [url rangeOfString:@"192."].length > 0 || 186 [url rangeOfString:@"10."].length > 0 || 187 [url rangeOfString:@"172."].length > 0 188 ) 189 ); 190} 191 192- (BOOL)_isDevDetached 193{ 194 return [EXEnvironment sharedEnvironment].isDetached && [EXEnvironment sharedEnvironment].isDebugXCodeScheme; 195} 196 197// for creating a filled button background in iOS < 15 198- (UIImage *)imageWithSize:(CGSize)size color:(UIColor *)color 199{ 200 UIGraphicsBeginImageContextWithOptions(size, true, 0.0); 201 [color setFill]; 202 UIRectFill(CGRectMake(0.0, 0.0, size.width, size.height)); 203 UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); 204 UIGraphicsEndImageContext(); 205 return image; 206} 207 208@end 209