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