xref: /expo/ios/Exponent/Kernel/Views/EXErrorView.m (revision 9d7b0c19)
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