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