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