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