1// Copyright 2020-present 650 Industries. All rights reserved. 2 3#import "EXAppFetcher.h" 4#import "EXAppLoaderExpoUpdates.h" 5#import "EXClientReleaseType.h" 6#import "EXEnvironment.h" 7#import "EXErrorRecoveryManager.h" 8#import "EXFileDownloader.h" 9#import "EXKernel.h" 10#import "EXKernelLinkingManager.h" 11#import "EXManifestResource.h" 12#import "EXSession.h" 13#import "EXUpdatesDatabaseManager.h" 14#import "EXVersions.h" 15 16#import <EXUpdates/EXUpdatesAppLauncherNoDatabase.h> 17#import <EXUpdates/EXUpdatesAppLoaderTask.h> 18#import <EXUpdates/EXUpdatesConfig.h> 19#import <EXUpdates/EXUpdatesDatabase.h> 20#import <EXUpdates/EXUpdatesErrorRecovery.h> 21#import <EXUpdates/EXUpdatesFileDownloader.h> 22#import <EXUpdates/EXUpdatesLauncherSelectionPolicyFilterAware.h> 23#import <EXUpdates/EXUpdatesLoaderSelectionPolicyFilterAware.h> 24#import <EXUpdates/EXUpdatesReaper.h> 25#import <EXUpdates/EXUpdatesReaperSelectionPolicyDevelopmentClient.h> 26#import <EXUpdates/EXUpdatesSelectionPolicy.h> 27#import <EXUpdates/EXUpdatesUtils.h> 28#import <EXManifests/EXManifestsManifestFactory.h> 29#import <EXManifests/EXManifestsLegacyManifest.h> 30#import <React/RCTUtils.h> 31#import <sys/utsname.h> 32 33NS_ASSUME_NONNULL_BEGIN 34 35@interface EXAppLoaderExpoUpdates () 36 37@property (nonatomic, strong, nullable) NSURL *manifestUrl; 38@property (nonatomic, strong, nullable) NSURL *httpManifestUrl; 39 40@property (nonatomic, strong, nullable) EXManifestsManifest *confirmedManifest; 41@property (nonatomic, strong, nullable) EXManifestsManifest *optimisticManifest; 42@property (nonatomic, strong, nullable) NSData *bundle; 43@property (nonatomic, assign) EXAppLoaderRemoteUpdateStatus remoteUpdateStatus; 44@property (nonatomic, assign) BOOL shouldShowRemoteUpdateStatus; 45@property (nonatomic, assign) BOOL isUpToDate; 46 47/** 48 * Stateful variable to let us prevent multiple simultaneous fetches from the development server. 49 * This can happen when reloading a bundle with remote debugging enabled; 50 * RN requests the bundle multiple times for some reason. 51 */ 52@property (nonatomic, assign) BOOL isLoadingDevelopmentJavaScriptResource; 53 54@property (nonatomic, strong, nullable) NSError *error; 55 56@property (nonatomic, assign) BOOL shouldUseCacheOnly; 57 58@property (nonatomic, strong) dispatch_queue_t appLoaderQueue; 59 60@property (nonatomic, nullable) EXUpdatesConfig *config; 61@property (nonatomic, nullable) EXUpdatesSelectionPolicy *selectionPolicy; 62@property (nonatomic, nullable) id<EXUpdatesAppLauncher> appLauncher; 63@property (nonatomic, assign) BOOL isEmergencyLaunch; 64 65@end 66 67@implementation EXAppLoaderExpoUpdates 68 69@synthesize manifestUrl = _manifestUrl; 70@synthesize bundle = _bundle; 71@synthesize remoteUpdateStatus = _remoteUpdateStatus; 72@synthesize shouldShowRemoteUpdateStatus = _shouldShowRemoteUpdateStatus; 73@synthesize config = _config; 74@synthesize selectionPolicy = _selectionPolicy; 75@synthesize appLauncher = _appLauncher; 76@synthesize isEmergencyLaunch = _isEmergencyLaunch; 77@synthesize isUpToDate = _isUpToDate; 78 79- (instancetype)initWithManifestUrl:(NSURL *)url 80{ 81 if (self = [super init]) { 82 _manifestUrl = url; 83 _httpManifestUrl = [EXAppLoaderExpoUpdates _httpUrlFromManifestUrl:_manifestUrl]; 84 _appLoaderQueue = dispatch_queue_create("host.exp.exponent.LoaderQueue", DISPATCH_QUEUE_SERIAL); 85 } 86 return self; 87} 88 89#pragma mark - getters and lifecycle 90 91- (void)_reset 92{ 93 _confirmedManifest = nil; 94 _optimisticManifest = nil; 95 _bundle = nil; 96 _config = nil; 97 _selectionPolicy = nil; 98 _appLauncher = nil; 99 _error = nil; 100 _shouldUseCacheOnly = NO; 101 _isEmergencyLaunch = NO; 102 _remoteUpdateStatus = kEXAppLoaderRemoteUpdateStatusChecking; 103 _shouldShowRemoteUpdateStatus = YES; 104 _isUpToDate = NO; 105 _isLoadingDevelopmentJavaScriptResource = NO; 106} 107 108- (EXAppLoaderStatus)status 109{ 110 if (_error) { 111 return kEXAppLoaderStatusError; 112 } else if (_bundle) { 113 return kEXAppLoaderStatusHasManifestAndBundle; 114 } else if (_optimisticManifest) { 115 return kEXAppLoaderStatusHasManifest; 116 } 117 return kEXAppLoaderStatusNew; 118} 119 120- (nullable EXManifestsManifest *)manifest 121{ 122 if (_confirmedManifest) { 123 return _confirmedManifest; 124 } 125 if (_optimisticManifest) { 126 return _optimisticManifest; 127 } 128 return nil; 129} 130 131- (nullable NSData *)bundle 132{ 133 if (_bundle) { 134 return _bundle; 135 } 136 return nil; 137} 138 139- (void)forceBundleReload 140{ 141 if (self.status == kEXAppLoaderStatusNew) { 142 @throw [NSException exceptionWithName:NSInternalInconsistencyException 143 reason:@"Tried to load a bundle from an AppLoader with no manifest." 144 userInfo:@{}]; 145 } 146 NSAssert([self supportsBundleReload], @"Tried to force a bundle reload on a non-development bundle"); 147 if (self.isLoadingDevelopmentJavaScriptResource) { 148 // prevent multiple simultaneous fetches from the development server. 149 // this can happen when reloading a bundle with remote debugging enabled; 150 // RN requests the bundle multiple times for some reason. 151 // TODO: fix inside of RN 152 return; 153 } 154 [self _loadDevelopmentJavaScriptResource]; 155} 156 157- (BOOL)supportsBundleReload 158{ 159 if (_optimisticManifest) { 160 return _optimisticManifest.isUsingDeveloperTool; 161 } 162 return NO; 163} 164 165#pragma mark - public 166 167- (void)request 168{ 169 [self _reset]; 170 if (_manifestUrl) { 171 [self _beginRequest]; 172 } 173} 174 175- (void)requestFromCache 176{ 177 [self _reset]; 178 _shouldUseCacheOnly = YES; 179 if (_manifestUrl) { 180 [self _beginRequest]; 181 } 182} 183 184#pragma mark - EXUpdatesAppLoaderTaskDelegate 185 186- (BOOL)appLoaderTask:(EXUpdatesAppLoaderTask *)appLoaderTask didLoadCachedUpdate:(EXUpdatesUpdate *)update 187{ 188 [self _setShouldShowRemoteUpdateStatus:update.manifest]; 189 // if cached manifest was dev mode, or a previous run of this app failed due to a loading error, we want to make sure to check for remote updates 190 if (update.manifest.isUsingDeveloperTool || [[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager scopeKeyIsRecoveringFromError:update.manifest.scopeKey]) { 191 return NO; 192 } 193 return YES; 194} 195 196- (void)appLoaderTask:(EXUpdatesAppLoaderTask *)appLoaderTask didStartLoadingUpdate:(EXUpdatesUpdate *)update 197{ 198 // expo-cli does not always respect our SDK version headers and respond with a compatible update or an error 199 // so we need to check the compatibility here 200 EXManifestResource *manifestResource = [[EXManifestResource alloc] initWithManifestUrl:_httpManifestUrl originalUrl:_manifestUrl]; 201 NSError *manifestCompatibilityError = [manifestResource verifyManifestSdkVersion:update.manifest]; 202 if (manifestCompatibilityError) { 203 _error = manifestCompatibilityError; 204 if (self.delegate) { 205 [self.delegate appLoader:self didFailWithError:_error]; 206 return; 207 } 208 } 209 210 _remoteUpdateStatus = kEXAppLoaderRemoteUpdateStatusDownloading; 211 [self _setShouldShowRemoteUpdateStatus:update.manifest]; 212 EXManifestsManifest *processedManifest = [self _processManifest:update.manifest]; 213 if (processedManifest == nil) { 214 return; 215 } 216 [self _setOptimisticManifest:processedManifest]; 217} 218 219- (void)appLoaderTask:(EXUpdatesAppLoaderTask *)appLoaderTask didFinishWithLauncher:(id<EXUpdatesAppLauncher>)launcher isUpToDate:(BOOL)isUpToDate 220{ 221 if (_error) { 222 return; 223 } 224 225 if (!_optimisticManifest) { 226 EXManifestsManifest *processedManifest = [self _processManifest:launcher.launchedUpdate.manifest]; 227 if (processedManifest == nil) { 228 return; 229 } 230 [self _setOptimisticManifest:processedManifest]; 231 } 232 _isUpToDate = isUpToDate; 233 if (launcher.launchedUpdate.manifest.isUsingDeveloperTool) { 234 // in dev mode, we need to set an optimistic manifest but nothing else 235 return; 236 } 237 _confirmedManifest = [self _processManifest:launcher.launchedUpdate.manifest]; 238 if (_confirmedManifest == nil) { 239 return; 240 } 241 _bundle = [NSData dataWithContentsOfURL:launcher.launchAssetUrl]; 242 _appLauncher = launcher; 243 if (self.delegate) { 244 [self.delegate appLoader:self didFinishLoadingManifest:_confirmedManifest bundle:_bundle]; 245 } 246} 247 248- (void)appLoaderTask:(EXUpdatesAppLoaderTask *)appLoaderTask didFinishWithError:(NSError *)error 249{ 250 if ([EXEnvironment sharedEnvironment].isDetached) { 251 _isEmergencyLaunch = YES; 252 [self _launchWithNoDatabaseAndError:error]; 253 } else if (!_error) { 254 _error = error; 255 256 // if the error payload conforms to the error protocol, we can parse it and display 257 // a slightly nicer error message to the user 258 id errorJson = [NSJSONSerialization JSONObjectWithData:[error.localizedDescription dataUsingEncoding:NSUTF8StringEncoding] options:kNilOptions error:nil]; 259 if (errorJson && [errorJson isKindOfClass:[NSDictionary class]]) { 260 EXManifestResource *manifestResource = [[EXManifestResource alloc] initWithManifestUrl:_httpManifestUrl originalUrl:_manifestUrl]; 261 _error = [manifestResource formatError:[NSError errorWithDomain:EXNetworkErrorDomain code:error.code userInfo:errorJson]]; 262 } 263 264 if (self.delegate) { 265 [self.delegate appLoader:self didFailWithError:_error]; 266 } 267 } 268} 269 270- (void)appLoaderTask:(EXUpdatesAppLoaderTask *)appLoaderTask didFinishBackgroundUpdateWithStatus:(EXUpdatesBackgroundUpdateStatus)status update:(nullable EXUpdatesUpdate *)update error:(nullable NSError *)error 271{ 272 if (self.delegate) { 273 [self.delegate appLoader:self didResolveUpdatedBundleWithManifest:update.manifest isFromCache:(status == EXUpdatesBackgroundUpdateStatusNoUpdateAvailable) error:error]; 274 } 275} 276 277#pragma mark - internal 278 279+ (NSURL *)_httpUrlFromManifestUrl:(NSURL *)url 280{ 281 NSURLComponents *components = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:YES]; 282 // if scheme is exps or https, use https. Else default to http 283 if (components.scheme && ([components.scheme isEqualToString:@"exps"] || [components.scheme isEqualToString:@"https"])){ 284 components.scheme = @"https"; 285 } else { 286 components.scheme = @"http"; 287 } 288 NSMutableString *path = [((components.path) ? components.path : @"") mutableCopy]; 289 path = [[EXKernelLinkingManager stringByRemovingDeepLink:path] mutableCopy]; 290 components.path = path; 291 return [components URL]; 292} 293 294- (BOOL)_initializeDatabase 295{ 296 EXUpdatesDatabaseManager *updatesDatabaseManager = [EXKernel sharedInstance].serviceRegistry.updatesDatabaseManager; 297 BOOL success = updatesDatabaseManager.isDatabaseOpen; 298 if (!updatesDatabaseManager.isDatabaseOpen) { 299 success = [updatesDatabaseManager openDatabase]; 300 } 301 302 if (!success) { 303 _error = updatesDatabaseManager.error; 304 if (self.delegate) { 305 [self.delegate appLoader:self didFailWithError:_error]; 306 } 307 return NO; 308 } else { 309 return YES; 310 } 311} 312 313- (void)_beginRequest 314{ 315 if (![self _initializeDatabase]) { 316 return; 317 } 318 [self _startLoaderTask]; 319} 320 321- (void)_startLoaderTask 322{ 323 BOOL shouldCheckOnLaunch; 324 NSNumber *launchWaitMs; 325 if (_shouldUseCacheOnly) { 326 shouldCheckOnLaunch = NO; 327 launchWaitMs = @(0); 328 } else { 329 if ([EXEnvironment sharedEnvironment].isDetached) { 330 shouldCheckOnLaunch = [EXEnvironment sharedEnvironment].updatesCheckAutomatically; 331 launchWaitMs = [EXEnvironment sharedEnvironment].updatesFallbackToCacheTimeout; 332 } else { 333 shouldCheckOnLaunch = YES; 334 launchWaitMs = @(60000); 335 } 336 } 337 338 NSURL *httpManifestUrl = [[self class] _httpUrlFromManifestUrl:_manifestUrl]; 339 340 NSString *releaseChannel = [EXEnvironment sharedEnvironment].releaseChannel; 341 if (![EXEnvironment sharedEnvironment].isDetached) { 342 // in Expo Go, the release channel can change at runtime depending on the URL we load 343 NSURLComponents *manifestUrlComponents = [NSURLComponents componentsWithURL:httpManifestUrl resolvingAgainstBaseURL:YES]; 344 releaseChannel = [EXKernelLinkingManager releaseChannelWithUrlComponents:manifestUrlComponents]; 345 } 346 347 NSMutableDictionary *updatesConfig = [[NSMutableDictionary alloc] initWithDictionary:@{ 348 EXUpdatesConfigUpdateUrlKey: httpManifestUrl.absoluteString, 349 EXUpdatesConfigSDKVersionKey: [self _sdkVersions], 350 EXUpdatesConfigScopeKeyKey: httpManifestUrl.absoluteString, 351 EXUpdatesConfigReleaseChannelKey: releaseChannel, 352 EXUpdatesConfigHasEmbeddedUpdateKey: @([EXEnvironment sharedEnvironment].isDetached), 353 EXUpdatesConfigEnabledKey: @([EXEnvironment sharedEnvironment].areRemoteUpdatesEnabled), 354 EXUpdatesConfigLaunchWaitMsKey: launchWaitMs, 355 EXUpdatesConfigCheckOnLaunchKey: shouldCheckOnLaunch ? EXUpdatesConfigCheckOnLaunchValueAlways : EXUpdatesConfigCheckOnLaunchValueNever, 356 EXUpdatesConfigExpectsSignedManifestKey: @YES, 357 EXUpdatesConfigRequestHeadersKey: [self _requestHeaders] 358 }]; 359 360 if (!EXEnvironment.sharedEnvironment.isDetached) { 361 // in Expo Go, embed the Expo Root Certificate and get the Expo Go intermediate certificate and development certificates 362 // from the multipart manifest response part 363 364 NSString *expoRootCertPath = [[NSBundle mainBundle] pathForResource:@"expo-root" ofType:@"pem"]; 365 if (!expoRootCertPath) { 366 @throw [NSException exceptionWithName:NSInternalInconsistencyException 367 reason:@"No expo-root certificate found in bundle" 368 userInfo:@{}]; 369 } 370 371 NSError *error; 372 NSString *expoRootCert = [NSString stringWithContentsOfFile:expoRootCertPath encoding:NSUTF8StringEncoding error:&error]; 373 if (error) { 374 expoRootCert = nil; 375 } 376 if (!expoRootCert) { 377 @throw [NSException exceptionWithName:NSInternalInconsistencyException 378 reason:@"Error reading expo-root certificate from bundle" 379 userInfo:@{ @"underlyingError": error.localizedDescription }]; 380 } 381 382 updatesConfig[EXUpdatesConfigCodeSigningCertificateKey] = expoRootCert; 383 updatesConfig[EXUpdatesConfigCodeSigningMetadataKey] = @{ 384 @"keyid": @"expo-root", 385 @"alg": @"rsa-v1_5-sha256", 386 }; 387 updatesConfig[EXUpdatesConfigCodeSigningIncludeManifestResponseCertificateChainKey] = @YES; 388 updatesConfig[EXUpdatesConfigCodeSigningAllowUnsignedManifestsKey] = @YES; 389 } 390 391 _config = [EXUpdatesConfig configWithDictionary:updatesConfig]; 392 393 if (![EXEnvironment sharedEnvironment].areRemoteUpdatesEnabled) { 394 [self _launchWithNoDatabaseAndError:nil]; 395 return; 396 } 397 398 EXUpdatesDatabaseManager *updatesDatabaseManager = [EXKernel sharedInstance].serviceRegistry.updatesDatabaseManager; 399 400 NSMutableArray *sdkVersions = [[EXVersions sharedInstance].versions[@"sdkVersions"] ?: @[[EXVersions sharedInstance].temporarySdkVersion] mutableCopy]; 401 [sdkVersions addObject:@"UNVERSIONED"]; 402 403 NSMutableArray *sdkVersionRuntimeVersions = [[NSMutableArray alloc] initWithCapacity:sdkVersions.count]; 404 for (NSString *sdkVersion in sdkVersions) { 405 [sdkVersionRuntimeVersions addObject:[NSString stringWithFormat:@"exposdk:%@", sdkVersion]]; 406 } 407 [sdkVersionRuntimeVersions addObject:@"exposdk:UNVERSIONED"]; 408 [sdkVersions addObjectsFromArray:sdkVersionRuntimeVersions]; 409 410 411 _selectionPolicy = [[EXUpdatesSelectionPolicy alloc] 412 initWithLauncherSelectionPolicy:[[EXUpdatesLauncherSelectionPolicyFilterAware alloc] initWithRuntimeVersions:sdkVersions] 413 loaderSelectionPolicy:[EXUpdatesLoaderSelectionPolicyFilterAware new] 414 reaperSelectionPolicy:[EXUpdatesReaperSelectionPolicyDevelopmentClient new]]; 415 416 EXUpdatesAppLoaderTask *loaderTask = [[EXUpdatesAppLoaderTask alloc] initWithConfig:_config 417 database:updatesDatabaseManager.database 418 directory:updatesDatabaseManager.updatesDirectory 419 selectionPolicy:_selectionPolicy 420 delegateQueue:_appLoaderQueue]; 421 loaderTask.delegate = self; 422 [loaderTask start]; 423} 424 425- (void)_launchWithNoDatabaseAndError:(nullable NSError *)error 426{ 427 EXUpdatesAppLauncherNoDatabase *appLauncher = [[EXUpdatesAppLauncherNoDatabase alloc] init]; 428 [appLauncher launchUpdateWithConfig:_config]; 429 430 _confirmedManifest = [self _processManifest:appLauncher.launchedUpdate.manifest]; 431 if (_confirmedManifest == nil) { 432 return; 433 } 434 _optimisticManifest = _confirmedManifest; 435 _bundle = [NSData dataWithContentsOfURL:appLauncher.launchAssetUrl]; 436 _appLauncher = appLauncher; 437 if (self.delegate) { 438 [self.delegate appLoader:self didLoadOptimisticManifest:_confirmedManifest]; 439 [self.delegate appLoader:self didFinishLoadingManifest:_confirmedManifest bundle:_bundle]; 440 } 441 442 [[EXUpdatesErrorRecovery new] writeErrorOrExceptionToLog:error]; 443} 444 445- (void)_runReaper 446{ 447 if (_appLauncher.launchedUpdate) { 448 EXUpdatesDatabaseManager *updatesDatabaseManager = [EXKernel sharedInstance].serviceRegistry.updatesDatabaseManager; 449 [EXUpdatesReaper reapUnusedUpdatesWithConfig:_config 450 database:updatesDatabaseManager.database 451 directory:updatesDatabaseManager.updatesDirectory 452 selectionPolicy:_selectionPolicy 453 launchedUpdate:_appLauncher.launchedUpdate]; 454 } 455} 456 457- (void)_setOptimisticManifest:(EXManifestsManifest *)manifest 458{ 459 _optimisticManifest = manifest; 460 if (self.delegate) { 461 [self.delegate appLoader:self didLoadOptimisticManifest:_optimisticManifest]; 462 } 463} 464 465- (void)_setShouldShowRemoteUpdateStatus:(EXManifestsManifest *)manifest 466{ 467 // we don't want to show the cached experience alert when Updates.reloadAsync() is called 468 if (_shouldUseCacheOnly) { 469 _shouldShowRemoteUpdateStatus = NO; 470 return; 471 } 472 473 if (manifest) { 474 if (manifest.isDevelopmentSilentLaunch) { 475 _shouldShowRemoteUpdateStatus = NO; 476 return; 477 } 478 } 479 _shouldShowRemoteUpdateStatus = YES; 480} 481 482- (void)_loadDevelopmentJavaScriptResource 483{ 484 _isLoadingDevelopmentJavaScriptResource = YES; 485 EXAppFetcher *appFetcher = [[EXAppFetcher alloc] initWithAppLoader:self]; 486 [appFetcher fetchJSBundleWithManifest:self.optimisticManifest cacheBehavior:EXCachedResourceNoCache timeoutInterval:kEXJSBundleTimeout progress:^(EXLoadingProgress *progress) { 487 if (self.delegate) { 488 [self.delegate appLoader:self didLoadBundleWithProgress:progress]; 489 } 490 } success:^(NSData *bundle) { 491 self.isUpToDate = YES; 492 self.bundle = bundle; 493 self.isLoadingDevelopmentJavaScriptResource = NO; 494 if (self.delegate) { 495 [self.delegate appLoader:self didFinishLoadingManifest:self.optimisticManifest bundle:self.bundle]; 496 } 497 } error:^(NSError *error) { 498 self.error = error; 499 self.isLoadingDevelopmentJavaScriptResource = NO; 500 if (self.delegate) { 501 [self.delegate appLoader:self didFailWithError:error]; 502 } 503 }]; 504} 505 506# pragma mark - manifest processing 507 508- (nullable EXManifestsManifest *)_processManifest:(EXManifestsManifest *)manifest 509{ 510 @try { 511 NSMutableDictionary *mutableManifest = [manifest.rawManifestJSON mutableCopy]; 512 513 // If legacy manifest is not yet verified, served by a third party, not standalone, and not an anonymous experience 514 // then scope it locally by using the manifest URL as a scopeKey (id) and consider it verified. 515 if (!mutableManifest[@"isVerified"] && 516 !EXEnvironment.sharedEnvironment.isDetached && 517 ![EXKernelLinkingManager isExpoHostedUrl:_httpManifestUrl] && 518 ![EXAppLoaderExpoUpdates _isAnonymousExperience:manifest] && 519 [manifest isKindOfClass:[EXManifestsLegacyManifest class]]) { 520 // the manifest id in a legacy manifest determines the namespace/experience id an app is sandboxed with 521 // if manifest is hosted by third parties, we sandbox it with the hostname to avoid clobbering exp.host namespaces 522 // for https urls, sandboxed id is of form quinlanj.github.io/myProj-myApp 523 // for http urls, sandboxed id is of form UNVERIFIED-quinlanj.github.io/myProj-myApp 524 NSString *securityPrefix = [_httpManifestUrl.scheme isEqualToString:@"https"] ? @"" : @"UNVERIFIED-"; 525 NSString *slugSuffix = manifest.slug ? [@"-" stringByAppendingString:manifest.slug]: @""; 526 mutableManifest[@"id"] = [NSString stringWithFormat:@"%@%@%@%@", securityPrefix, _httpManifestUrl.host, _httpManifestUrl.path ?: @"", slugSuffix]; 527 mutableManifest[@"isVerified"] = @(YES); 528 } 529 530 // set verified to false by default 531 if (!mutableManifest[@"isVerified"]) { 532 mutableManifest[@"isVerified"] = @(NO); 533 } 534 535 // if the app bypassed verification or the manifest is scoped to a random anonymous 536 // scope key, automatically verify it 537 if (![mutableManifest[@"isVerified"] boolValue] && (EXEnvironment.sharedEnvironment.isManifestVerificationBypassed || [EXAppLoaderExpoUpdates _isAnonymousExperience:manifest])) { 538 mutableManifest[@"isVerified"] = @(YES); 539 } 540 541 return [EXManifestsManifestFactory manifestForManifestJSON:[mutableManifest copy]]; 542 } 543 @catch (NSException *exception) { 544 // Catch parsing errors related to invalid or unexpected manifest properties. For example, if a manifest 545 // is missing the `id` property, it'll raise an exception which we want to forward to the user so they 546 // can adjust their manifest JSON accordingly. 547 _error = [NSError errorWithDomain:@"ExpoParsingManifest" 548 code:1025 549 userInfo:@{NSLocalizedDescriptionKey: [@"Failed to parse manifest JSON: " stringByAppendingString:exception.reason] }]; 550 if (self.delegate) { 551 [self.delegate appLoader:self didFailWithError:_error]; 552 } 553 } 554 return nil; 555} 556 557+ (BOOL)_isAnonymousExperience:(EXManifestsManifest *)manifest 558{ 559 return [manifest.scopeKey hasPrefix:@"@anonymous/"]; 560} 561 562#pragma mark - headers 563 564- (NSDictionary *)_requestHeaders 565{ 566 NSDictionary *requestHeaders = @{ 567 @"Exponent-SDK-Version": [self _sdkVersions], 568 @"Exponent-Accept-Signature": @"true", 569 @"Exponent-Platform": @"ios", 570 @"Exponent-Version": [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"], 571 @"Expo-Client-Environment": [self _clientEnvironment], 572 @"Expo-Updates-Environment": [self _clientEnvironment], 573 @"User-Agent": [self _userAgentString], 574 @"Expo-Client-Release-Type": [EXClientReleaseType clientReleaseType] 575 }; 576 577 NSString *sessionSecret = [[EXSession sharedInstance] sessionSecret]; 578 if (sessionSecret) { 579 NSMutableDictionary *requestHeadersMutable = [requestHeaders mutableCopy]; 580 requestHeadersMutable[@"Expo-Session"] = sessionSecret; 581 requestHeaders = requestHeadersMutable; 582 } 583 584 return requestHeaders; 585} 586 587- (NSString *)_userAgentString 588{ 589 struct utsname systemInfo; 590 uname(&systemInfo); 591 NSString *deviceModel = [NSString stringWithCString:systemInfo.machine encoding:NSUTF8StringEncoding]; 592 return [NSString stringWithFormat:@"Exponent/%@ (%@; %@ %@; Scale/%.2f; %@)", 593 [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"], 594 deviceModel, 595 [UIDevice currentDevice].systemName, 596 [UIDevice currentDevice].systemVersion, 597 [UIScreen mainScreen].scale, 598 [NSLocale autoupdatingCurrentLocale].localeIdentifier]; 599} 600 601- (NSString *)_clientEnvironment 602{ 603 if ([EXEnvironment sharedEnvironment].isDetached) { 604 return @"STANDALONE"; 605 } else { 606 return @"EXPO_DEVICE"; 607#if TARGET_IPHONE_SIMULATOR 608 return @"EXPO_SIMULATOR"; 609#endif 610 } 611} 612 613- (NSString *)_sdkVersions 614{ 615 NSArray *versionsAvailable = [EXVersions sharedInstance].versions[@"sdkVersions"]; 616 if (versionsAvailable) { 617 return [versionsAvailable componentsJoinedByString:@","]; 618 } else { 619 return [EXVersions sharedInstance].temporarySdkVersion; 620 } 621} 622 623@end 624 625NS_ASSUME_NONNULL_END 626