xref: /expo/ios/Exponent/Kernel/Views/EXErrorView.m (revision bfcd021e)
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) UILabel *lblError;
13@property (nonatomic, strong) UIButton *btnRetry;
14@property (nonatomic, strong) UIButton *btnBack;
15@property (nonatomic, strong) UILabel *lblUrl;
16@property (nonatomic, strong) UILabel *lblErrorDetail;
17@property (nonatomic, strong) UIScrollView *vContainer;
18
19- (void)_onTapRetry;
20
21@end
22
23@implementation EXErrorView
24
25- (instancetype)initWithFrame:(CGRect)frame
26{
27  if (self = [super initWithFrame:frame]) {
28    self.backgroundColor = [UIColor whiteColor];
29
30    self.vContainer = [[UIScrollView alloc] init];
31    [self addSubview:_vContainer];
32
33    // error description label
34    self.lblError = [[UILabel alloc] init];
35    _lblError.numberOfLines = 0;
36    _lblError.textAlignment = NSTextAlignmentCenter;
37    _lblError.font = [UIFont systemFontOfSize:14.0f];
38    _lblError.textColor = [UIColor blackColor];
39    [_vContainer addSubview:_lblError];
40
41    // retry button
42    self.btnRetry = [UIButton buttonWithType:UIButtonTypeRoundedRect];
43    [_btnRetry setTitle:@"Try again" forState:UIControlStateNormal];
44    [_btnRetry addTarget:self action:@selector(_onTapRetry) forControlEvents:UIControlEventTouchUpInside];
45    [_vContainer addSubview:_btnRetry];
46
47    // back button
48    self.btnBack = [UIButton buttonWithType:UIButtonTypeRoundedRect];
49    [_btnBack setTitle:@"Go back to Expo Home" forState:UIControlStateNormal];
50    [_btnBack addTarget:self action:@selector(_onTapBack) forControlEvents:UIControlEventTouchUpInside];
51    [_vContainer addSubview:_btnBack];
52
53    for (UIButton *btnToStyle in @[ _btnRetry, _btnBack ]) {
54      [btnToStyle setTintColor:[EXUtil colorWithRGB:0x49a7e8]];
55      [btnToStyle.titleLabel setFont:[UIFont boldSystemFontOfSize:14.0f]];
56    }
57
58    // url label
59    self.lblUrl = [[UILabel alloc] init];
60    _lblUrl.numberOfLines = 0;
61    _lblUrl.textAlignment = NSTextAlignmentCenter;
62    [_vContainer addSubview:_lblUrl];
63
64    // error detail label
65    self.lblErrorDetail = [[UILabel alloc] init];
66    _lblErrorDetail.numberOfLines = 0;
67    _lblErrorDetail.textAlignment = NSTextAlignmentCenter;
68    _lblErrorDetail.textColor = [UIColor darkGrayColor];
69    [_vContainer addSubview:_lblErrorDetail];
70
71    for (UILabel *lblToStyle in @[ _lblUrl, _lblErrorDetail ]) {
72      lblToStyle.font = [UIFont systemFontOfSize:14.0f];
73    }
74  }
75  return self;
76}
77
78- (void)setType:(EXFatalErrorType)type
79{
80  _type = type;
81  NSString *appOwnerName = @"the requested app";
82  if (_appRecord) {
83    if (_appRecord == [EXKernel sharedInstance].appRegistry.homeAppRecord) {
84      appOwnerName = @"Expo";
85    } else if (_appRecord.appLoader.manifest && _appRecord.appLoader.manifest.name) {
86      appOwnerName = [NSString stringWithFormat:@"\"%@\"", _appRecord.appLoader.manifest.name];
87    }
88  }
89
90  switch (type) {
91    case kEXFatalErrorTypeLoading: {
92      _lblError.text = [NSString stringWithFormat:@"There was a problem loading %@.", appOwnerName];
93      if (_error.code == kCFURLErrorNotConnectedToInternet) {
94        _lblError.text = [NSString stringWithFormat:@"%@ Make sure you're connected to the internet.", _lblError.text];
95      } else if (_appRecord.appLoader.manifestUrl) {
96        NSString *url = _appRecord.appLoader.manifestUrl.absoluteString;
97        if ([self _urlLooksLikeLAN:url]) {
98          NSString *extraLANPermissionText = @"";
99          if (@available(iOS 14, *)) {
100            extraLANPermissionText = @", and that you have granted Expo Go the Local Network permission in the Settings app,";
101          }
102          _lblError.text = [NSString stringWithFormat:
103                            @"%@\n\nIt looks like you may be using a LAN URL. "
104                            "Make sure your device is on the same network as the server%@ or try using the tunnel connection type.", _lblError.text, extraLANPermissionText];
105        }
106      }
107      break;
108    }
109    case kEXFatalErrorTypeException: {
110      _lblError.text = [NSString stringWithFormat:@"There was a problem running %@.", appOwnerName];
111      break;
112    }
113  }
114  [self _resetUIState];
115}
116
117- (void)setError:(NSError *)error
118{
119  _error = error;
120  _lblErrorDetail.text = [error localizedDescription];
121  [self _resetUIState];
122}
123
124- (void)setAppRecord:(EXKernelAppRecord *)appRecord
125{
126  _appRecord = appRecord;
127  [self _resetUIState];
128}
129
130- (void)layoutSubviews
131{
132  [super layoutSubviews];
133
134  _vContainer.frame = self.bounds;
135  CGFloat maxLabelWidth = self.bounds.size.width - 32.0f;
136
137  _lblError.frame = CGRectMake(0, 0, maxLabelWidth, CGFLOAT_MAX);
138  [_lblError sizeToFit];
139  _lblError.center = CGPointMake(self.bounds.size.width * 0.5f, self.bounds.size.height * 0.25f);
140
141  _btnRetry.frame = CGRectMake(0, 0, self.bounds.size.width, 24.0f);
142  _btnRetry.center = CGPointMake(_lblError.center.x, CGRectGetMaxY(_lblError.frame) + 32.0f);
143
144  _btnBack.frame = CGRectMake(0, 0, self.bounds.size.width, 24.0f);
145  _btnBack.center = CGPointMake(_lblError.center.x, CGRectGetMaxY(_btnRetry.frame) + 24);
146
147  _lblUrl.frame = CGRectMake(0, 0, self.bounds.size.width - 48.0f, CGFLOAT_MAX);
148  [_lblUrl sizeToFit];
149  _lblUrl.center = CGPointMake(_lblError.center.x, CGRectGetMaxY(_btnBack.frame) + 12.0f + CGRectGetMidY(_lblUrl.bounds));
150
151  _lblErrorDetail.frame = CGRectMake(0, 0, maxLabelWidth, CGFLOAT_MAX);
152  [_lblErrorDetail sizeToFit];
153
154  _lblErrorDetail.center = CGPointMake(_lblError.center.x, CGRectGetMaxY(_lblUrl.frame) + 24.0f + CGRectGetMidY(_lblErrorDetail.bounds));
155
156  _vContainer.contentSize = CGSizeMake(_vContainer.bounds.size.width, CGRectGetMaxY(_lblErrorDetail.frame) + 12.0f);
157}
158
159#pragma mark - Internal
160
161- (void)_resetUIState
162{
163  EXKernelAppRecord *homeRecord = [EXKernel sharedInstance].appRegistry.homeAppRecord;
164  _btnBack.hidden = (!homeRecord || _appRecord == homeRecord);
165  _lblUrl.hidden = (!homeRecord && ![self _isDevDetached]);
166  _lblUrl.text = _appRecord.appLoader.manifestUrl.absoluteString;
167  // TODO: maybe hide retry (see BrowserErrorView)
168  [self setNeedsLayout];
169}
170
171- (void)_onTapRetry
172{
173  if (_delegate) {
174    [_delegate errorViewDidSelectRetry:self];
175  }
176}
177
178- (void)_onTapBack
179{
180  if ([EXKernel sharedInstance].browserController) {
181    [[EXKernel sharedInstance].browserController moveHomeToVisible];
182  }
183}
184
185- (BOOL)_urlLooksLikeLAN:(NSString *)url
186{
187  return (
188    url && (
189      [url rangeOfString:@".local"].length > 0 ||
190      [url rangeOfString:@"192."].length > 0 ||
191      [url rangeOfString:@"10."].length > 0 ||
192      [url rangeOfString:@"172."].length > 0
193    )
194  );
195}
196
197- (BOOL)_isDevDetached
198{
199  return [EXEnvironment sharedEnvironment].isDetached && [EXEnvironment sharedEnvironment].isDebugXCodeScheme;
200}
201
202@end
203