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  NSString *cachesDirectory = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject;
30  NSString *sourceDirectory = [cachesDirectory stringByAppendingPathComponent:@"Sources"];
31
32  BOOL cacheDirectoryExists = [[NSFileManager defaultManager] fileExistsAtPath:sourceDirectory isDirectory:nil];
33  if (!cacheDirectoryExists) {
34    NSError *error;
35    BOOL created = [[NSFileManager defaultManager] createDirectoryAtPath:sourceDirectory
36                                             withIntermediateDirectories:YES
37                                                              attributes:nil
38                                                                   error:&error];
39    if (created) {
40      cacheDirectoryExists = YES;
41    } else {
42      DDLogError(@"Could not create source cache directory: %@", error.localizedDescription);
43    }
44  }
45
46  return (cacheDirectoryExists) ? sourceDirectory : nil;
47}
48
49+ (NSURLCache *)javaScriptCache
50{
51  static NSURLCache *cache;
52
53  static dispatch_once_t onceToken;
54  dispatch_once(&onceToken, ^{
55    NSString *sourceDirectory = [self javaScriptCachePath];
56    if (sourceDirectory) {
57      cache = [[NSURLCache alloc] initWithMemoryCapacity:4 * 1024 * 1024 diskCapacity:20 * 1024 * 1024 diskPath:sourceDirectory];
58    }
59  });
60
61  return cache;
62}
63
64- (void)loadResourceWithBehavior:(EXCachedResourceBehavior)behavior
65                   progressBlock:(__nullable EXCachedResourceProgressBlock)progressBlock
66                    successBlock:(EXCachedResourceSuccessBlock)successBlock
67                      errorBlock:(EXCachedResourceErrorBlock)errorBlock
68{
69  // For dev builds that use the packager use RCTJavaScriptLoader which handles the packager's multipart
70  // responses to show loading progress.
71  if (self.devToolsEnabled) {
72    __block EXLoadingProgress *progress = [EXLoadingProgress new];
73    [RCTJavaScriptLoader loadBundleAtURL:self.remoteUrl onProgress:^(RCTLoadingProgress *progressData) {
74      progress.total = progressData.total;
75      progress.done = progressData.done;
76      progress.status = progressData.status ?: @"Building JavaScript bundle...";
77      if (progressBlock) {
78        progressBlock(progress);
79      }
80    } onComplete:^(NSError *error, RCTSource *source) {
81      if (error != nil) {
82        // In case we received something else than JS add more info to the error specific to expo for
83        // things like tunnel errors.
84        if ([error.domain isEqualToString:@"JSServer"] && error.code == NSURLErrorCannotParseResponse) {
85          NSString *errDescription = [self _getContentErrorDescriptionForResponse:error.userInfo[@"headers"] data:error.userInfo[@"data"]];
86          error = [NSError errorWithDomain:NSURLErrorDomain
87                                      code:NSURLErrorCannotParseResponse
88                                  userInfo:@{
89                                             NSLocalizedDescriptionKey: errDescription,
90                                             @"headers": error.userInfo[@"headers"],
91                                             @"data": error.userInfo[@"data"]
92                                             }];
93        }
94        errorBlock(error);
95      } else {
96        successBlock(source.data);
97      }
98    }];
99  } else {
100    [super loadResourceWithBehavior:behavior
101                      progressBlock:progressBlock
102                       successBlock:successBlock
103                         errorBlock:errorBlock];
104  }
105}
106
107- (BOOL)isUsingEmbeddedResource
108{
109  // if the URL of our request matches the remote URL of the embedded JS bundle,
110  // skip checking any caches and just immediately open the NSBundle copy
111  if ([EXEnvironment sharedEnvironment].isDetached &&
112      [EXEnvironment sharedEnvironment].embeddedBundleUrl &&
113      [self.remoteUrl isEqual:[EXApiUtil encodedUrlFromString:[EXEnvironment sharedEnvironment].embeddedBundleUrl]]) {
114    return YES;
115  } else {
116    return [super isUsingEmbeddedResource];
117  }
118}
119
120- (NSError *)_validateResponseData:(NSData *)data response:(NSURLResponse *)response
121{
122  if (![response.MIMEType isEqualToString:@"application/javascript"]) {
123    NSString *errDescription = [self _getContentErrorDescriptionForResponse:((NSHTTPURLResponse *)response).allHeaderFields data:data];
124    return [NSError errorWithDomain:NSURLErrorDomain
125                               code:NSURLErrorCannotParseResponse
126                           userInfo:@{
127                                      NSLocalizedDescriptionKey: errDescription,
128                                      @"response": response,
129                                      @"data": data
130                                      }];
131  }
132  return nil;
133}
134
135- (NSString *)_getContentErrorDescriptionForResponse:(NSDictionary *)headers data:(NSData *)data
136{
137  NSString *result;
138  NSString *responseContentType = headers[@"Content-Type"];
139  if ([responseContentType isEqualToString:@"application/json"]) {
140    NSString *dataString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
141    result = [NSString stringWithFormat:@"Expected JavaScript, but got JSON: %@", dataString];
142  } else {
143    NSString *recoverySuggestion = @"Check that your internet connection is working.";
144    if ([responseContentType rangeOfString:@"text"].length > 0) {
145      NSString *dataString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
146      if ([dataString rangeOfString:@"tunnel"].length > 0) {
147        recoverySuggestion = @"Check that your internet connection is working and try restarting your tunnel.";
148      }
149    }
150    result = [NSString stringWithFormat:@"Expected JavaScript, but got content type '%@'. %@", responseContentType, recoverySuggestion];
151  }
152  return result;
153}
154
155@end
156