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