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{ 29*1506aaceSEric 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 { 102a7c0409cSEric Samelson // we only need this because the bundle URL of prod home never changes, so we need 103a7c0409cSEric Samelson // to use the legacy logic and load embedded home if and only if a cached copy doesn't exist. 104a7c0409cSEric Samelson // TODO: get rid of this branch once prod home is loaded like any other bundle!!!!!!! 105a7c0409cSEric Samelson return [super isUsingEmbeddedResource]; 106a7c0409cSEric Samelson } 1079211cde4SEric Samelson} 1089211cde4SEric Samelson 1099088dd0eSBen Roth- (NSError *)_validateResponseData:(NSData *)data response:(NSURLResponse *)response 1109088dd0eSBen Roth{ 1119088dd0eSBen Roth if (![response.MIMEType isEqualToString:@"application/javascript"]) { 1129088dd0eSBen Roth NSString *errDescription = [self _getContentErrorDescriptionForResponse:((NSHTTPURLResponse *)response).allHeaderFields data:data]; 1139088dd0eSBen Roth return [NSError errorWithDomain:NSURLErrorDomain 1149088dd0eSBen Roth code:NSURLErrorCannotParseResponse 1159088dd0eSBen Roth userInfo:@{ 1169088dd0eSBen Roth NSLocalizedDescriptionKey: errDescription, 1179088dd0eSBen Roth @"response": response, 1189088dd0eSBen Roth @"data": data 1199088dd0eSBen Roth }]; 1209088dd0eSBen Roth } 1219088dd0eSBen Roth return nil; 1229088dd0eSBen Roth} 1239088dd0eSBen Roth 1249088dd0eSBen Roth- (NSString *)_getContentErrorDescriptionForResponse:(NSDictionary *)headers data:(NSData *)data 1259088dd0eSBen Roth{ 1269088dd0eSBen Roth NSString *result; 1279088dd0eSBen Roth NSString *responseContentType = headers[@"Content-Type"]; 1289088dd0eSBen Roth if ([responseContentType isEqualToString:@"application/json"]) { 1299088dd0eSBen Roth NSString *dataString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; 1309088dd0eSBen Roth result = [NSString stringWithFormat:@"Expected JavaScript, but got JSON: %@", dataString]; 1319088dd0eSBen Roth } else { 1329088dd0eSBen Roth NSString *recoverySuggestion = @"Check that your internet connection is working."; 1339088dd0eSBen Roth if ([responseContentType rangeOfString:@"text"].length > 0) { 1349088dd0eSBen Roth NSString *dataString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; 1359088dd0eSBen Roth if ([dataString rangeOfString:@"tunnel"].length > 0) { 1369088dd0eSBen Roth recoverySuggestion = @"Check that your internet connection is working and try restarting your tunnel."; 1379088dd0eSBen Roth } 1389088dd0eSBen Roth } 1399088dd0eSBen Roth result = [NSString stringWithFormat:@"Expected JavaScript, but got content type '%@'. %@", responseContentType, recoverySuggestion]; 1409088dd0eSBen Roth } 1419088dd0eSBen Roth return result; 1429088dd0eSBen Roth} 1439088dd0eSBen Roth 1449088dd0eSBen Roth@end 145