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