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