19088dd0eSBen Roth// Copyright 2015-present 650 Industries. All rights reserved.
29088dd0eSBen Roth
39211cde4SEric Samelson#import "EXApiUtil.h"
400fcd3c6SBen 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{
291506aaceSEric Samelson  return [[self class] cachePathWithName:@"Sources"];
309088dd0eSBen Roth}
319088dd0eSBen Roth
329088dd0eSBen Roth+ (NSURLCache *)javaScriptCache
339088dd0eSBen Roth{
349088dd0eSBen Roth  static NSURLCache *cache;
359088dd0eSBen Roth
369088dd0eSBen Roth  static dispatch_once_t onceToken;
379088dd0eSBen Roth  dispatch_once(&onceToken, ^{
389088dd0eSBen Roth    NSString *sourceDirectory = [self javaScriptCachePath];
399088dd0eSBen Roth    if (sourceDirectory) {
409088dd0eSBen Roth      cache = [[NSURLCache alloc] initWithMemoryCapacity:4 * 1024 * 1024 diskCapacity:20 * 1024 * 1024 diskPath:sourceDirectory];
419088dd0eSBen Roth    }
429088dd0eSBen Roth  });
439088dd0eSBen Roth
449088dd0eSBen Roth  return cache;
459088dd0eSBen Roth}
469088dd0eSBen Roth
479088dd0eSBen Roth- (void)loadResourceWithBehavior:(EXCachedResourceBehavior)behavior
489088dd0eSBen Roth                   progressBlock:(__nullable EXCachedResourceProgressBlock)progressBlock
499088dd0eSBen Roth                    successBlock:(EXCachedResourceSuccessBlock)successBlock
509088dd0eSBen Roth                      errorBlock:(EXCachedResourceErrorBlock)errorBlock
519088dd0eSBen Roth{
529088dd0eSBen Roth  // For dev builds that use the packager use RCTJavaScriptLoader which handles the packager's multipart
539088dd0eSBen Roth  // responses to show loading progress.
549088dd0eSBen Roth  if (self.devToolsEnabled) {
559088dd0eSBen Roth    __block EXLoadingProgress *progress = [EXLoadingProgress new];
569088dd0eSBen Roth    [RCTJavaScriptLoader loadBundleAtURL:self.remoteUrl onProgress:^(RCTLoadingProgress *progressData) {
579088dd0eSBen Roth      progress.total = progressData.total;
589088dd0eSBen Roth      progress.done = progressData.done;
599088dd0eSBen Roth      progress.status = progressData.status ?: @"Building JavaScript bundle...";
609088dd0eSBen Roth      if (progressBlock) {
619088dd0eSBen Roth        progressBlock(progress);
629088dd0eSBen Roth      }
639088dd0eSBen Roth    } onComplete:^(NSError *error, RCTSource *source) {
649088dd0eSBen Roth      if (error != nil) {
659088dd0eSBen Roth        // In case we received something else than JS add more info to the error specific to expo for
669088dd0eSBen Roth        // things like tunnel errors.
679088dd0eSBen Roth        if ([error.domain isEqualToString:@"JSServer"] && error.code == NSURLErrorCannotParseResponse) {
689088dd0eSBen Roth          NSString *errDescription = [self _getContentErrorDescriptionForResponse:error.userInfo[@"headers"] data:error.userInfo[@"data"]];
699088dd0eSBen Roth          error = [NSError errorWithDomain:NSURLErrorDomain
709088dd0eSBen Roth                                      code:NSURLErrorCannotParseResponse
719088dd0eSBen Roth                                  userInfo:@{
729088dd0eSBen Roth                                             NSLocalizedDescriptionKey: errDescription,
739088dd0eSBen Roth                                             @"headers": error.userInfo[@"headers"],
749088dd0eSBen Roth                                             @"data": error.userInfo[@"data"]
759088dd0eSBen Roth                                             }];
769088dd0eSBen Roth        }
779088dd0eSBen Roth        errorBlock(error);
789088dd0eSBen Roth      } else {
799088dd0eSBen Roth        successBlock(source.data);
809088dd0eSBen Roth      }
819088dd0eSBen Roth    }];
829088dd0eSBen Roth  } else {
839088dd0eSBen Roth    [super loadResourceWithBehavior:behavior
849088dd0eSBen Roth                      progressBlock:progressBlock
859088dd0eSBen Roth                       successBlock:successBlock
869088dd0eSBen Roth                         errorBlock:errorBlock];
879088dd0eSBen Roth  }
889088dd0eSBen Roth}
899088dd0eSBen Roth
909211cde4SEric Samelson- (BOOL)isUsingEmbeddedResource
919211cde4SEric Samelson{
92a7c0409cSEric Samelson  if ([EXEnvironment sharedEnvironment].isDetached) {
939211cde4SEric Samelson    // if the URL of our request matches the remote URL of the embedded JS bundle,
949211cde4SEric Samelson    // skip checking any caches and just immediately open the NSBundle copy
95a7c0409cSEric Samelson    if ([EXEnvironment sharedEnvironment].embeddedBundleUrl &&
9600fcd3c6SBen Roth        [self.remoteUrl isEqual:[EXApiUtil encodedUrlFromString:[EXEnvironment sharedEnvironment].embeddedBundleUrl]]) {
979211cde4SEric Samelson      return YES;
989211cde4SEric Samelson    } else {
99c5307526SEric Samelson      return NO;
1009211cde4SEric Samelson    }
101a7c0409cSEric Samelson  } else {
102*92764b50STomasz Sapeta#if DEBUG
103*92764b50STomasz Sapeta    return NO;
104*92764b50STomasz Sapeta#else
105a7c0409cSEric Samelson    // we only need this because the bundle URL of prod home never changes, so we need
106a7c0409cSEric Samelson    // to use the legacy logic and load embedded home if and only if a cached copy doesn't exist.
107a7c0409cSEric Samelson    return [super isUsingEmbeddedResource];
108*92764b50STomasz Sapeta#endif
109a7c0409cSEric Samelson  }
1109211cde4SEric Samelson}
1119211cde4SEric Samelson
1129088dd0eSBen Roth- (NSError *)_validateResponseData:(NSData *)data response:(NSURLResponse *)response
1139088dd0eSBen Roth{
1149088dd0eSBen Roth  if (![response.MIMEType isEqualToString:@"application/javascript"]) {
1159088dd0eSBen Roth    NSString *errDescription = [self _getContentErrorDescriptionForResponse:((NSHTTPURLResponse *)response).allHeaderFields data:data];
1169088dd0eSBen Roth    return [NSError errorWithDomain:NSURLErrorDomain
1179088dd0eSBen Roth                               code:NSURLErrorCannotParseResponse
1189088dd0eSBen Roth                           userInfo:@{
1199088dd0eSBen Roth                                      NSLocalizedDescriptionKey: errDescription,
1209088dd0eSBen Roth                                      @"response": response,
1219088dd0eSBen Roth                                      @"data": data
1229088dd0eSBen Roth                                      }];
1239088dd0eSBen Roth  }
1249088dd0eSBen Roth  return nil;
1259088dd0eSBen Roth}
1269088dd0eSBen Roth
1279088dd0eSBen Roth- (NSString *)_getContentErrorDescriptionForResponse:(NSDictionary *)headers data:(NSData *)data
1289088dd0eSBen Roth{
1299088dd0eSBen Roth  NSString *result;
1309088dd0eSBen Roth  NSString *responseContentType = headers[@"Content-Type"];
1319088dd0eSBen Roth  if ([responseContentType isEqualToString:@"application/json"]) {
1329088dd0eSBen Roth    NSString *dataString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
1339088dd0eSBen Roth    result = [NSString stringWithFormat:@"Expected JavaScript, but got JSON: %@", dataString];
1349088dd0eSBen Roth  } else {
1359088dd0eSBen Roth    NSString *recoverySuggestion = @"Check that your internet connection is working.";
1369088dd0eSBen Roth    if ([responseContentType rangeOfString:@"text"].length > 0) {
1379088dd0eSBen Roth      NSString *dataString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
1389088dd0eSBen Roth      if ([dataString rangeOfString:@"tunnel"].length > 0) {
1399088dd0eSBen Roth        recoverySuggestion = @"Check that your internet connection is working and try restarting your tunnel.";
1409088dd0eSBen Roth      }
1419088dd0eSBen Roth    }
1429088dd0eSBen Roth    result = [NSString stringWithFormat:@"Expected JavaScript, but got content type '%@'. %@", responseContentType, recoverySuggestion];
1439088dd0eSBen Roth  }
1449088dd0eSBen Roth  return result;
1459088dd0eSBen Roth}
1469088dd0eSBen Roth
1479088dd0eSBen Roth@end
148