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