1// Copyright 2015-present 650 Industries. All rights reserved.
2
3#import "EXApiUtil.h"
4#import "EXEnvironment.h"
5#import "EXJavaScriptResource.h"
6#import "EXKernelUtil.h"
7
8#import <React/RCTJavaScriptLoader.h>
9
10@interface EXJavaScriptResource ()
11
12@property (nonatomic, assign) BOOL devToolsEnabled;
13
14@end
15
16@implementation EXJavaScriptResource
17
18- (instancetype)initWithBundleName:(NSString *)bundleName remoteUrl:(NSURL *)url devToolsEnabled:(BOOL)devToolsEnabled
19{
20  if (self = [super initWithResourceName:bundleName resourceType:@"bundle" remoteUrl:url cachePath:[[self class] javaScriptCachePath]]) {
21    self.urlCache = [[self class] javaScriptCache];
22    self.devToolsEnabled = devToolsEnabled;
23  }
24  return self;
25}
26
27+ (NSString *)javaScriptCachePath
28{
29  return [[self class] cachePathWithName:@"Sources"];
30}
31
32+ (NSURLCache *)javaScriptCache
33{
34  static NSURLCache *cache;
35
36  static dispatch_once_t onceToken;
37  dispatch_once(&onceToken, ^{
38    NSString *sourceDirectory = [self javaScriptCachePath];
39    if (sourceDirectory) {
40      cache = [[NSURLCache alloc] initWithMemoryCapacity:4 * 1024 * 1024 diskCapacity:20 * 1024 * 1024 diskPath:sourceDirectory];
41    }
42  });
43
44  return cache;
45}
46
47- (void)loadResourceWithBehavior:(EXCachedResourceBehavior)behavior
48                   progressBlock:(__nullable EXCachedResourceProgressBlock)progressBlock
49                    successBlock:(EXCachedResourceSuccessBlock)successBlock
50                      errorBlock:(EXCachedResourceErrorBlock)errorBlock
51{
52  // For dev builds that use the packager use RCTJavaScriptLoader which handles the packager's multipart
53  // responses to show loading progress.
54  if (self.devToolsEnabled) {
55    __block EXLoadingProgress *progress = [EXLoadingProgress new];
56    [RCTJavaScriptLoader loadBundleAtURL:self.remoteUrl onProgress:^(RCTLoadingProgress *progressData) {
57      progress.total = progressData.total;
58      progress.done = progressData.done;
59      progress.status = progressData.status ?: @"Building JavaScript bundle...";
60      if (progressBlock) {
61        progressBlock(progress);
62      }
63    } onComplete:^(NSError *error, RCTSource *source) {
64      if (error != nil) {
65        // In case we received something else than JS add more info to the error specific to expo for
66        // things like tunnel errors.
67        if ([error.domain isEqualToString:@"JSServer"] && error.code == NSURLErrorCannotParseResponse) {
68          NSString *errDescription = [self _getContentErrorDescriptionForResponse:error.userInfo[@"headers"] data:error.userInfo[@"data"]];
69          error = [NSError errorWithDomain:NSURLErrorDomain
70                                      code:NSURLErrorCannotParseResponse
71                                  userInfo:@{
72                                             NSLocalizedDescriptionKey: errDescription,
73                                             @"headers": error.userInfo[@"headers"],
74                                             @"data": error.userInfo[@"data"]
75                                             }];
76        }
77        errorBlock(error);
78      } else {
79        successBlock(source.data);
80      }
81    }];
82  } else {
83    [super loadResourceWithBehavior:behavior
84                      progressBlock:progressBlock
85                       successBlock:successBlock
86                         errorBlock:errorBlock];
87  }
88}
89
90- (BOOL)isUsingEmbeddedResource
91{
92  if ([EXEnvironment sharedEnvironment].isDetached) {
93    // if the URL of our request matches the remote URL of the embedded JS bundle,
94    // skip checking any caches and just immediately open the NSBundle copy
95    if ([EXEnvironment sharedEnvironment].embeddedBundleUrl &&
96        [self.remoteUrl isEqual:[EXApiUtil encodedUrlFromString:[EXEnvironment sharedEnvironment].embeddedBundleUrl]]) {
97      return YES;
98    } else {
99      return NO;
100    }
101  } else {
102    // we only need this because the bundle URL of prod home never changes, so we need
103    // to use the legacy logic and load embedded home if and only if a cached copy doesn't exist.
104    // TODO: get rid of this branch once prod home is loaded like any other bundle!!!!!!!
105    return [super isUsingEmbeddedResource];
106  }
107}
108
109- (NSError *)_validateResponseData:(NSData *)data response:(NSURLResponse *)response
110{
111  if (![response.MIMEType isEqualToString:@"application/javascript"]) {
112    NSString *errDescription = [self _getContentErrorDescriptionForResponse:((NSHTTPURLResponse *)response).allHeaderFields data:data];
113    return [NSError errorWithDomain:NSURLErrorDomain
114                               code:NSURLErrorCannotParseResponse
115                           userInfo:@{
116                                      NSLocalizedDescriptionKey: errDescription,
117                                      @"response": response,
118                                      @"data": data
119                                      }];
120  }
121  return nil;
122}
123
124- (NSString *)_getContentErrorDescriptionForResponse:(NSDictionary *)headers data:(NSData *)data
125{
126  NSString *result;
127  NSString *responseContentType = headers[@"Content-Type"];
128  if ([responseContentType isEqualToString:@"application/json"]) {
129    NSString *dataString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
130    result = [NSString stringWithFormat:@"Expected JavaScript, but got JSON: %@", dataString];
131  } else {
132    NSString *recoverySuggestion = @"Check that your internet connection is working.";
133    if ([responseContentType rangeOfString:@"text"].length > 0) {
134      NSString *dataString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
135      if ([dataString rangeOfString:@"tunnel"].length > 0) {
136        recoverySuggestion = @"Check that your internet connection is working and try restarting your tunnel.";
137      }
138    }
139    result = [NSString stringWithFormat:@"Expected JavaScript, but got content type '%@'. %@", responseContentType, recoverySuggestion];
140  }
141  return result;
142}
143
144@end
145