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