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