1// Copyright 2020-present 650 Industries. All rights reserved. 2 3#import "EXClientReleaseType.h" 4#import "EXEnvironment.h" 5#import "EXErrorRecoveryManager.h" 6#import "EXFileDownloader.h" 7#import "EXKernel.h" 8#import "EXKernelLinkingManager.h" 9#import "EXSession.h" 10#import "EXAppLoaderExpoUpdates.h" 11#import "EXVersions.h" 12 13#import <EXUpdates/EXUpdatesAppLoaderTask.h> 14#import <EXUpdates/EXUpdatesConfig.h> 15#import <EXUpdates/EXUpdatesDatabase.h> 16#import <EXUpdates/EXUpdatesSelectionPolicyNewest.h> 17#import <EXUpdates/EXUpdatesUtils.h> 18#import <React/RCTUtils.h> 19#import <sys/utsname.h> 20 21NS_ASSUME_NONNULL_BEGIN 22 23@interface EXAppLoaderExpoUpdates () 24 25@property (nonatomic, strong, nullable) NSURL *manifestUrl; 26@property (nonatomic, strong, nullable) NSURL *httpManifestUrl; 27 28@property (nonatomic, strong, nullable) NSDictionary *confirmedManifest; 29@property (nonatomic, strong, nullable) NSDictionary *optimisticManifest; 30@property (nonatomic, strong, nullable) NSData *bundle; 31 32@property (nonatomic, strong, nullable) NSError *error; 33 34@property (nonatomic, assign) BOOL shouldUseCacheOnly; 35 36@end 37 38@implementation EXAppLoaderExpoUpdates 39 40@synthesize manifestUrl = _manifestUrl; 41@synthesize bundle = _bundle; 42 43- (instancetype)initWithManifestUrl:(NSURL *)url 44{ 45 if (self = [super init]) { 46 _manifestUrl = url; 47 _httpManifestUrl = [EXAppLoaderExpoUpdates _httpUrlFromManifestUrl:_manifestUrl]; 48 } 49 return self; 50} 51 52#pragma mark - getters and lifecycle 53 54- (void)_reset 55{ 56 _confirmedManifest = nil; 57 _optimisticManifest = nil; 58 _error = nil; 59 _shouldUseCacheOnly = NO; 60} 61 62- (EXAppLoaderStatus)status 63{ 64 if (_error) { 65 return kEXAppLoaderStatusError; 66 } else if (_bundle) { 67 return kEXAppLoaderStatusHasManifestAndBundle; 68 } else if (_optimisticManifest) { 69 return kEXAppLoaderStatusHasManifest; 70 } 71 return kEXAppLoaderStatusNew; 72} 73 74- (nullable NSDictionary *)manifest 75{ 76 if (_confirmedManifest) { 77 return _confirmedManifest; 78 } 79 if (_optimisticManifest) { 80 return _optimisticManifest; 81 } 82 return nil; 83} 84 85- (nullable NSData *)bundle 86{ 87 if (_bundle) { 88 return _bundle; 89 } 90 return nil; 91} 92 93- (void)forceBundleReload 94{ 95 // TODO: reload if dev mode 96} 97 98- (BOOL)supportsBundleReload 99{ 100 // TOOD: if dev mode return true 101 return NO; 102} 103 104#pragma mark - public 105 106- (void)request 107{ 108 [self _reset]; 109 if (_manifestUrl) { 110 [self _beginRequest]; 111 } 112} 113 114- (void)requestFromCache 115{ 116 [self _reset]; 117 _shouldUseCacheOnly = YES; 118 if (_manifestUrl) { 119 [self _beginRequest]; 120 } 121} 122 123#pragma mark - EXUpdatesAppLoaderTaskDelegate 124 125- (BOOL)appLoaderTask:(EXUpdatesAppLoaderTask *)appLoaderTask didLoadCachedUpdate:(EXUpdatesUpdate *)update 126{ 127 // if previous run of this app failed due to a loading error, we want to make sure to check for remote updates 128 if ([[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager experienceIdIsRecoveringFromError:[EXAppFetcher experienceIdWithManifest:update.rawManifest]]) { 129 if (_shouldUseCacheOnly) { 130 _shouldUseCacheOnly = NO; 131 [self _startLoaderTask]; 132 return NO; 133 } 134 } 135 return YES; 136} 137 138- (void)appLoaderTask:(EXUpdatesAppLoaderTask *)appLoaderTask didStartLoadingUpdate:(EXUpdatesUpdate *)update 139{ 140 _optimisticManifest = update.rawManifest; 141 if (self.delegate) { 142 [self.delegate appLoader:self didLoadOptimisticManifest:update.rawManifest]; 143 } 144} 145 146- (void)appLoaderTask:(EXUpdatesAppLoaderTask *)appLoaderTask didFinishWithLauncher:(id<EXUpdatesAppLauncher>)launcher 147{ 148 _confirmedManifest = launcher.launchedUpdate.rawManifest; 149 _bundle = [NSData dataWithContentsOfURL:launcher.launchAssetUrl]; 150 if (self.delegate) { 151 [self.delegate appLoader:self didFinishLoadingManifest:_confirmedManifest bundle:_bundle]; 152 } 153} 154 155- (void)appLoaderTask:(EXUpdatesAppLoaderTask *)appLoaderTask didFinishWithError:(NSError *)error 156{ 157 _error = error; 158 if (self.delegate) { 159 [self.delegate appLoader:self didFailWithError:error]; 160 } 161} 162 163- (void)appLoaderTask:(EXUpdatesAppLoaderTask *)appLoaderTask didFireEventWithType:(NSString *)type body:(NSDictionary *)body 164{ 165 // TODO: add delegate method for this 166} 167 168#pragma mark - internal 169 170+ (NSURL *)_httpUrlFromManifestUrl:(NSURL *)url 171{ 172 NSURLComponents *components = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:YES]; 173 // if scheme is exps or https, use https. Else default to http 174 if (components.scheme && ([components.scheme isEqualToString:@"exps"] || [components.scheme isEqualToString:@"https"])){ 175 components.scheme = @"https"; 176 } else { 177 components.scheme = @"http"; 178 } 179 NSMutableString *path = [((components.path) ? components.path : @"") mutableCopy]; 180 path = [[EXKernelLinkingManager stringByRemovingDeepLink:path] mutableCopy]; 181 components.path = path; 182 return [components URL]; 183} 184 185- (void)_beginRequest 186{ 187 // if we're in dev mode, don't try loading cached manifest 188 if ([_httpManifestUrl.host isEqualToString:@"localhost"] 189 || ([EXEnvironment sharedEnvironment].isDetached && [EXEnvironment sharedEnvironment].isDebugXCodeScheme)) { 190 // we can't pre-detect if this person is using a developer tool, but using localhost is a pretty solid indicator. 191 // TODO: dev mode 192 } else { 193 [self _startLoaderTask]; 194 } 195} 196 197- (void)_startLoaderTask 198{ 199 NSString *sdkVersions; 200 NSArray *versionsAvailable = [EXVersions sharedInstance].versions[@"sdkVersions"]; 201 if (versionsAvailable) { 202 sdkVersions = [versionsAvailable componentsJoinedByString:@","]; 203 } else { 204 sdkVersions = [EXVersions sharedInstance].temporarySdkVersion; 205 } 206 207 NSDictionary *requestHeaders = @{ 208 @"Exponent-SDK-Version": sdkVersions, 209 @"Exponent-Accept-Signature": @"true", 210 @"Exponent-Platform": @"ios", 211 @"Exponent-Version": [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"], 212 @"Expo-Client-Environment": [self _clientEnvironment], 213 @"Expo-Updates-Environment": [self _clientEnvironment], 214 @"User-Agent": [self _userAgentString], 215 @"Expo-Client-Release-Type": [EXClientReleaseType clientReleaseType] 216 }; 217 218 NSString *sessionSecret = [[EXSession sharedInstance] sessionSecret]; 219 if (sessionSecret) { 220 NSMutableDictionary *requestHeadersMutable = [requestHeaders mutableCopy]; 221 requestHeadersMutable[@"Expo-Session"] = sessionSecret; 222 requestHeaders = requestHeadersMutable; 223 } 224 225 EXUpdatesConfig *config = [EXUpdatesConfig configWithDictionary:@{ 226 @"EXUpdatesURL": [[self class] _httpUrlFromManifestUrl:_manifestUrl].absoluteString, 227 @"EXUpdatesSDKVersion": sdkVersions, 228 @"EXUpdatesScopeKey": _manifestUrl.absoluteString, 229 @"EXUpdatesHasEmbeddedUpdate": @(NO), 230 @"EXUpdatesEnabled": @(YES), 231 @"EXUpdatesLaunchWaitMs": _shouldUseCacheOnly ? @(0) : @(10000), 232 @"EXUpdatesCheckOnLaunch": _shouldUseCacheOnly ? @"NEVER" : @"ALWAYS", 233 @"EXUpdatesRequestHeaders": requestHeaders 234 }]; 235 236 NSError *fsError; 237 NSURL *updatesDirectory = [EXUpdatesUtils initializeUpdatesDirectoryWithError:&fsError]; 238 __block NSError *dbError; 239 EXUpdatesDatabase *database = [[EXUpdatesDatabase alloc] init]; 240 dispatch_sync(database.databaseQueue, ^{ 241 [database openDatabaseInDirectory:updatesDirectory withError:&dbError]; 242 }); 243 244 EXUpdatesSelectionPolicyNewest *selectionPolicy = [[EXUpdatesSelectionPolicyNewest alloc] initWithRuntimeVersions:versionsAvailable ?: @[[EXVersions sharedInstance].temporarySdkVersion]]; 245 246 EXUpdatesAppLoaderTask *loaderTask = [[EXUpdatesAppLoaderTask alloc] initWithConfig:config 247 database:database 248 directory:updatesDirectory 249 selectionPolicy:selectionPolicy 250 delegateQueue:dispatch_get_main_queue()]; 251 loaderTask.delegate = self; 252 [loaderTask start]; 253} 254 255- (NSString *)_userAgentString 256{ 257 struct utsname systemInfo; 258 uname(&systemInfo); 259 NSString *deviceModel = [NSString stringWithCString:systemInfo.machine encoding:NSUTF8StringEncoding]; 260 return [NSString stringWithFormat:@"Exponent/%@ (%@; %@ %@; Scale/%.2f; %@)", 261 [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"], 262 deviceModel, 263 [UIDevice currentDevice].systemName, 264 [UIDevice currentDevice].systemVersion, 265 [UIScreen mainScreen].scale, 266 [NSLocale autoupdatingCurrentLocale].localeIdentifier]; 267} 268 269- (NSString *)_clientEnvironment 270{ 271 if ([EXEnvironment sharedEnvironment].isDetached) { 272 return @"STANDALONE"; 273 } else { 274 return @"EXPO_DEVICE"; 275#if TARGET_IPHONE_SIMULATOR 276 return @"EXPO_SIMULATOR"; 277#endif 278 } 279} 280 281@end 282 283NS_ASSUME_NONNULL_END 284