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