19088dd0eSBen Roth// Copyright 2015-present 650 Industries. All rights reserved.
29088dd0eSBen Roth
39211cde4SEric Samelson#import "EXApiUtil.h"
4*00fcd3c6SBen Roth#import "EXEnvironment.h"
59088dd0eSBen Roth#import "EXJavaScriptResource.h"
69088dd0eSBen Roth#import "EXKernelUtil.h"
79088dd0eSBen Roth
89088dd0eSBen Roth#import <React/RCTJavaScriptLoader.h>
99088dd0eSBen Roth
109088dd0eSBen Roth@interface EXJavaScriptResource ()
119088dd0eSBen Roth
129088dd0eSBen Roth@property (nonatomic, assign) BOOL devToolsEnabled;
139088dd0eSBen Roth
149088dd0eSBen Roth@end
159088dd0eSBen Roth
169088dd0eSBen Roth@implementation EXJavaScriptResource
179088dd0eSBen Roth
189088dd0eSBen Roth- (instancetype)initWithBundleName:(NSString *)bundleName remoteUrl:(NSURL *)url devToolsEnabled:(BOOL)devToolsEnabled
199088dd0eSBen Roth{
209088dd0eSBen Roth  if (self = [super initWithResourceName:bundleName resourceType:@"bundle" remoteUrl:url cachePath:[[self class] javaScriptCachePath]]) {
219088dd0eSBen Roth    self.urlCache = [[self class] javaScriptCache];
229088dd0eSBen Roth    self.devToolsEnabled = devToolsEnabled;
239088dd0eSBen Roth  }
249088dd0eSBen Roth  return self;
259088dd0eSBen Roth}
269088dd0eSBen Roth
279088dd0eSBen Roth+ (NSString *)javaScriptCachePath
289088dd0eSBen Roth{
299088dd0eSBen Roth  NSString *cachesDirectory = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject;
309088dd0eSBen Roth  NSString *sourceDirectory = [cachesDirectory stringByAppendingPathComponent:@"Sources"];
319088dd0eSBen Roth
329088dd0eSBen Roth  BOOL cacheDirectoryExists = [[NSFileManager defaultManager] fileExistsAtPath:sourceDirectory isDirectory:nil];
339088dd0eSBen Roth  if (!cacheDirectoryExists) {
349088dd0eSBen Roth    NSError *error;
359088dd0eSBen Roth    BOOL created = [[NSFileManager defaultManager] createDirectoryAtPath:sourceDirectory
369088dd0eSBen Roth                                             withIntermediateDirectories:YES
379088dd0eSBen Roth                                                              attributes:nil
389088dd0eSBen Roth                                                                   error:&error];
399088dd0eSBen Roth    if (created) {
409088dd0eSBen Roth      cacheDirectoryExists = YES;
419088dd0eSBen Roth    } else {
429088dd0eSBen Roth      DDLogError(@"Could not create source cache directory: %@", error.localizedDescription);
439088dd0eSBen Roth    }
449088dd0eSBen Roth  }
459088dd0eSBen Roth
469088dd0eSBen Roth  return (cacheDirectoryExists) ? sourceDirectory : nil;
479088dd0eSBen Roth}
489088dd0eSBen Roth
499088dd0eSBen Roth+ (NSURLCache *)javaScriptCache
509088dd0eSBen Roth{
519088dd0eSBen Roth  static NSURLCache *cache;
529088dd0eSBen Roth
539088dd0eSBen Roth  static dispatch_once_t onceToken;
549088dd0eSBen Roth  dispatch_once(&onceToken, ^{
559088dd0eSBen Roth    NSString *sourceDirectory = [self javaScriptCachePath];
569088dd0eSBen Roth    if (sourceDirectory) {
579088dd0eSBen Roth      cache = [[NSURLCache alloc] initWithMemoryCapacity:4 * 1024 * 1024 diskCapacity:20 * 1024 * 1024 diskPath:sourceDirectory];
589088dd0eSBen Roth    }
599088dd0eSBen Roth  });
609088dd0eSBen Roth
619088dd0eSBen Roth  return cache;
629088dd0eSBen Roth}
639088dd0eSBen Roth
649088dd0eSBen Roth- (void)loadResourceWithBehavior:(EXCachedResourceBehavior)behavior
659088dd0eSBen Roth                   progressBlock:(__nullable EXCachedResourceProgressBlock)progressBlock
669088dd0eSBen Roth                    successBlock:(EXCachedResourceSuccessBlock)successBlock
679088dd0eSBen Roth                      errorBlock:(EXCachedResourceErrorBlock)errorBlock
689088dd0eSBen Roth{
699088dd0eSBen Roth  // For dev builds that use the packager use RCTJavaScriptLoader which handles the packager's multipart
709088dd0eSBen Roth  // responses to show loading progress.
719088dd0eSBen Roth  if (self.devToolsEnabled) {
729088dd0eSBen Roth    __block EXLoadingProgress *progress = [EXLoadingProgress new];
739088dd0eSBen Roth    [RCTJavaScriptLoader loadBundleAtURL:self.remoteUrl onProgress:^(RCTLoadingProgress *progressData) {
749088dd0eSBen Roth      progress.total = progressData.total;
759088dd0eSBen Roth      progress.done = progressData.done;
769088dd0eSBen Roth      progress.status = progressData.status ?: @"Building JavaScript bundle...";
779088dd0eSBen Roth      if (progressBlock) {
789088dd0eSBen Roth        progressBlock(progress);
799088dd0eSBen Roth      }
809088dd0eSBen Roth    } onComplete:^(NSError *error, RCTSource *source) {
819088dd0eSBen Roth      if (error != nil) {
829088dd0eSBen Roth        // In case we received something else than JS add more info to the error specific to expo for
839088dd0eSBen Roth        // things like tunnel errors.
849088dd0eSBen Roth        if ([error.domain isEqualToString:@"JSServer"] && error.code == NSURLErrorCannotParseResponse) {
859088dd0eSBen Roth          NSString *errDescription = [self _getContentErrorDescriptionForResponse:error.userInfo[@"headers"] data:error.userInfo[@"data"]];
869088dd0eSBen Roth          error = [NSError errorWithDomain:NSURLErrorDomain
879088dd0eSBen Roth                                      code:NSURLErrorCannotParseResponse
889088dd0eSBen Roth                                  userInfo:@{
899088dd0eSBen Roth                                             NSLocalizedDescriptionKey: errDescription,
909088dd0eSBen Roth                                             @"headers": error.userInfo[@"headers"],
919088dd0eSBen Roth                                             @"data": error.userInfo[@"data"]
929088dd0eSBen Roth                                             }];
939088dd0eSBen Roth        }
949088dd0eSBen Roth        errorBlock(error);
959088dd0eSBen Roth      } else {
969088dd0eSBen Roth        successBlock(source.data);
979088dd0eSBen Roth      }
989088dd0eSBen Roth    }];
999088dd0eSBen Roth  } else {
1009088dd0eSBen Roth    [super loadResourceWithBehavior:behavior
1019088dd0eSBen Roth                      progressBlock:progressBlock
1029088dd0eSBen Roth                       successBlock:successBlock
1039088dd0eSBen Roth                         errorBlock:errorBlock];
1049088dd0eSBen Roth  }
1059088dd0eSBen Roth}
1069088dd0eSBen Roth
1079211cde4SEric Samelson- (BOOL)isUsingEmbeddedResource
1089211cde4SEric Samelson{
1099211cde4SEric Samelson  // if the URL of our request matches the remote URL of the embedded JS bundle,
1109211cde4SEric Samelson  // skip checking any caches and just immediately open the NSBundle copy
111*00fcd3c6SBen Roth  if ([EXEnvironment sharedEnvironment].isShell &&
112*00fcd3c6SBen Roth      [EXEnvironment sharedEnvironment].embeddedBundleUrl &&
113*00fcd3c6SBen Roth      [self.remoteUrl isEqual:[EXApiUtil encodedUrlFromString:[EXEnvironment sharedEnvironment].embeddedBundleUrl]]) {
1149211cde4SEric Samelson    return YES;
1159211cde4SEric Samelson  } else {
1169211cde4SEric Samelson    return [super isUsingEmbeddedResource];
1179211cde4SEric Samelson  }
1189211cde4SEric Samelson}
1199211cde4SEric Samelson
1209088dd0eSBen Roth- (NSError *)_validateResponseData:(NSData *)data response:(NSURLResponse *)response
1219088dd0eSBen Roth{
1229088dd0eSBen Roth  if (![response.MIMEType isEqualToString:@"application/javascript"]) {
1239088dd0eSBen Roth    NSString *errDescription = [self _getContentErrorDescriptionForResponse:((NSHTTPURLResponse *)response).allHeaderFields data:data];
1249088dd0eSBen Roth    return [NSError errorWithDomain:NSURLErrorDomain
1259088dd0eSBen Roth                               code:NSURLErrorCannotParseResponse
1269088dd0eSBen Roth                           userInfo:@{
1279088dd0eSBen Roth                                      NSLocalizedDescriptionKey: errDescription,
1289088dd0eSBen Roth                                      @"response": response,
1299088dd0eSBen Roth                                      @"data": data
1309088dd0eSBen Roth                                      }];
1319088dd0eSBen Roth  }
1329088dd0eSBen Roth  return nil;
1339088dd0eSBen Roth}
1349088dd0eSBen Roth
1359088dd0eSBen Roth- (NSString *)_getContentErrorDescriptionForResponse:(NSDictionary *)headers data:(NSData *)data
1369088dd0eSBen Roth{
1379088dd0eSBen Roth  NSString *result;
1389088dd0eSBen Roth  NSString *responseContentType = headers[@"Content-Type"];
1399088dd0eSBen Roth  if ([responseContentType isEqualToString:@"application/json"]) {
1409088dd0eSBen Roth    NSString *dataString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
1419088dd0eSBen Roth    result = [NSString stringWithFormat:@"Expected JavaScript, but got JSON: %@", dataString];
1429088dd0eSBen Roth  } else {
1439088dd0eSBen Roth    NSString *recoverySuggestion = @"Check that your internet connection is working.";
1449088dd0eSBen Roth    if ([responseContentType rangeOfString:@"text"].length > 0) {
1459088dd0eSBen Roth      NSString *dataString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
1469088dd0eSBen Roth      if ([dataString rangeOfString:@"tunnel"].length > 0) {
1479088dd0eSBen Roth        recoverySuggestion = @"Check that your internet connection is working and try restarting your tunnel.";
1489088dd0eSBen Roth      }
1499088dd0eSBen Roth    }
1509088dd0eSBen Roth    result = [NSString stringWithFormat:@"Expected JavaScript, but got content type '%@'. %@", responseContentType, recoverySuggestion];
1519088dd0eSBen Roth  }
1529088dd0eSBen Roth  return result;
1539088dd0eSBen Roth}
1549088dd0eSBen Roth
1559088dd0eSBen Roth@end
156