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