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