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:(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
399  _config = [EXUpdatesConfig configFromDictionary:updatesConfig];
400
401  if (![EXEnvironment sharedEnvironment].areRemoteUpdatesEnabled) {
402    [self _launchWithNoDatabaseAndError:nil];
403    return;
404  }
405
406  EXUpdatesDatabaseManager *updatesDatabaseManager = [EXKernel sharedInstance].serviceRegistry.updatesDatabaseManager;
407
408  NSMutableArray *sdkVersions = [[EXVersions sharedInstance].versions[@"sdkVersions"] ?: @[[EXVersions sharedInstance].temporarySdkVersion] mutableCopy];
409  [sdkVersions addObject:@"UNVERSIONED"];
410
411  NSMutableArray *sdkVersionRuntimeVersions = [[NSMutableArray alloc] initWithCapacity:sdkVersions.count];
412  for (NSString *sdkVersion in sdkVersions) {
413    [sdkVersionRuntimeVersions addObject:[NSString stringWithFormat:@"exposdk:%@", sdkVersion]];
414  }
415  [sdkVersionRuntimeVersions addObject:@"exposdk:UNVERSIONED"];
416  [sdkVersions addObjectsFromArray:sdkVersionRuntimeVersions];
417
418
419  _selectionPolicy = [[EXUpdatesSelectionPolicy alloc]
420                      initWithLauncherSelectionPolicy:[[EXUpdatesLauncherSelectionPolicyFilterAware alloc] initWithRuntimeVersions:sdkVersions]
421                      loaderSelectionPolicy:[EXUpdatesLoaderSelectionPolicyFilterAware new]
422                      reaperSelectionPolicy:[EXUpdatesReaperSelectionPolicyDevelopmentClient new]];
423
424  EXUpdatesAppLoaderTask *loaderTask = [[EXUpdatesAppLoaderTask alloc] initWithConfig:_config
425                                                                             database:updatesDatabaseManager.database
426                                                                            directory:updatesDatabaseManager.updatesDirectory
427                                                                      selectionPolicy:_selectionPolicy
428                                                                        delegateQueue:_appLoaderQueue];
429  loaderTask.delegate = self;
430  [loaderTask start];
431}
432
433- (void)_launchWithNoDatabaseAndError:(nullable NSError *)error
434{
435  EXUpdatesAppLauncherNoDatabase *appLauncher = [[EXUpdatesAppLauncherNoDatabase alloc] init];
436  [appLauncher launchUpdateWithConfig:_config];
437
438  _confirmedManifest = [self _processManifest:appLauncher.launchedUpdate.manifest];
439  if (_confirmedManifest == nil) {
440    return;
441  }
442  _optimisticManifest = _confirmedManifest;
443  _bundle = [NSData dataWithContentsOfURL:appLauncher.launchAssetUrl];
444  _appLauncher = appLauncher;
445  if (self.delegate) {
446    [self.delegate appLoader:self didLoadOptimisticManifest:_confirmedManifest];
447    [self.delegate appLoader:self didFinishLoadingManifest:_confirmedManifest bundle:_bundle];
448  }
449
450  [[EXUpdatesErrorRecovery new] writeErrorOrExceptionToLog:error];
451}
452
453- (void)_runReaper
454{
455  if (_appLauncher.launchedUpdate) {
456    EXUpdatesDatabaseManager *updatesDatabaseManager = [EXKernel sharedInstance].serviceRegistry.updatesDatabaseManager;
457    [EXUpdatesReaper reapUnusedUpdatesWithConfig:_config
458                                        database:updatesDatabaseManager.database
459                                       directory:updatesDatabaseManager.updatesDirectory
460                                 selectionPolicy:_selectionPolicy
461                                  launchedUpdate:_appLauncher.launchedUpdate];
462  }
463}
464
465- (void)_setOptimisticManifest:(EXManifestsManifest *)manifest
466{
467  _optimisticManifest = manifest;
468  if (self.delegate) {
469    [self.delegate appLoader:self didLoadOptimisticManifest:_optimisticManifest];
470  }
471}
472
473- (void)_setShouldShowRemoteUpdateStatus:(EXManifestsManifest *)manifest
474{
475  // we don't want to show the cached experience alert when Updates.reloadAsync() is called
476  if (_shouldUseCacheOnly) {
477    _shouldShowRemoteUpdateStatus = NO;
478    return;
479  }
480
481  if (manifest) {
482    if (manifest.isDevelopmentSilentLaunch) {
483      _shouldShowRemoteUpdateStatus = NO;
484      return;
485    }
486  }
487  _shouldShowRemoteUpdateStatus = YES;
488}
489
490- (void)_loadDevelopmentJavaScriptResource
491{
492  _isLoadingDevelopmentJavaScriptResource = YES;
493  EXAppFetcher *appFetcher = [[EXAppFetcher alloc] initWithAppLoader:self];
494  [appFetcher fetchJSBundleWithManifest:self.optimisticManifest cacheBehavior:EXCachedResourceNoCache timeoutInterval:kEXJSBundleTimeout progress:^(EXLoadingProgress *progress) {
495    if (self.delegate) {
496      [self.delegate appLoader:self didLoadBundleWithProgress:progress];
497    }
498  } success:^(NSData *bundle) {
499    self.isUpToDate = YES;
500    self.bundle = bundle;
501    self.isLoadingDevelopmentJavaScriptResource = NO;
502    if (self.delegate) {
503      [self.delegate appLoader:self didFinishLoadingManifest:self.optimisticManifest bundle:self.bundle];
504    }
505  } error:^(NSError *error) {
506    self.error = error;
507    self.isLoadingDevelopmentJavaScriptResource = NO;
508    if (self.delegate) {
509      [self.delegate appLoader:self didFailWithError:error];
510    }
511  }];
512}
513
514# pragma mark - manifest processing
515
516- (nullable EXManifestsManifest *)_processManifest:(EXManifestsManifest *)manifest
517{
518  @try {
519    NSMutableDictionary *mutableManifest = [manifest.rawManifestJSON mutableCopy];
520
521    // If legacy manifest is not yet verified, served by a third party, not standalone, and not an anonymous experience
522    // then scope it locally by using the manifest URL as a scopeKey (id) and consider it verified.
523    if (!mutableManifest[@"isVerified"] &&
524        !EXEnvironment.sharedEnvironment.isDetached &&
525        ![EXKernelLinkingManager isExpoHostedUrl:_httpManifestUrl] &&
526        ![EXAppLoaderExpoUpdates _isAnonymousExperience:manifest] &&
527        [manifest isKindOfClass:[EXManifestsLegacyManifest class]]) {
528      // the manifest id in a legacy manifest determines the namespace/experience id an app is sandboxed with
529      // if manifest is hosted by third parties, we sandbox it with the hostname to avoid clobbering exp.host namespaces
530      // for https urls, sandboxed id is of form quinlanj.github.io/myProj-myApp
531      // for http urls, sandboxed id is of form UNVERIFIED-quinlanj.github.io/myProj-myApp
532      NSString *securityPrefix = [_httpManifestUrl.scheme isEqualToString:@"https"] ? @"" : @"UNVERIFIED-";
533      NSString *slugSuffix = manifest.slug ? [@"-" stringByAppendingString:manifest.slug]: @"";
534      mutableManifest[@"id"] = [NSString stringWithFormat:@"%@%@%@%@", securityPrefix, _httpManifestUrl.host, _httpManifestUrl.path ?: @"", slugSuffix];
535      mutableManifest[@"isVerified"] = @(YES);
536    }
537
538    // set verified to false by default
539    if (!mutableManifest[@"isVerified"]) {
540      mutableManifest[@"isVerified"] = @(NO);
541    }
542
543    // if the app bypassed verification or the manifest is scoped to a random anonymous
544    // scope key, automatically verify it
545    if (![mutableManifest[@"isVerified"] boolValue] && (EXEnvironment.sharedEnvironment.isManifestVerificationBypassed || [EXAppLoaderExpoUpdates _isAnonymousExperience:manifest])) {
546      mutableManifest[@"isVerified"] = @(YES);
547    }
548
549    // when the manifest is not verified at this point, make the scope key a salted and hashed version of the claimed scope key
550    if (![mutableManifest[@"isVerified"] boolValue]) {
551      NSString *currentScopeKeyAndSaltToHash = [NSString stringWithFormat:@"unverified-%@", manifest.scopeKey];
552      NSString *currentScopeKeyHash = [currentScopeKeyAndSaltToHash hexEncodedSHA256];
553      NSString *newScopeKey = [NSString stringWithFormat:@"%@-%@", currentScopeKeyAndSaltToHash, currentScopeKeyHash];
554      if ([manifest isKindOfClass:EXManifestsNewManifest.class]) {
555        NSDictionary *extra = mutableManifest[@"extra"] ?: @{};
556        NSMutableDictionary *mutableExtra = [extra mutableCopy];
557        mutableExtra[@"scopeKey"] = newScopeKey;
558        mutableManifest[@"extra"] = mutableExtra;
559      } else {
560        mutableManifest[@"scopeKey"] = newScopeKey;
561        mutableManifest[@"id"] = newScopeKey;
562      }
563    }
564
565    return [EXManifestsManifestFactory manifestForManifestJSON:[mutableManifest copy]];
566  }
567  @catch (NSException *exception) {
568    // Catch parsing errors related to invalid or unexpected manifest properties. For example, if a manifest
569    // is missing the `id` property, it'll raise an exception which we want to forward to the user so they
570    // can adjust their manifest JSON accordingly.
571    _error = [NSError errorWithDomain:@"ExpoParsingManifest"
572                                             code:1025
573                                         userInfo:@{NSLocalizedDescriptionKey: [@"Failed to parse manifest JSON: " stringByAppendingString:exception.reason] }];
574    if (self.delegate) {
575      [self.delegate appLoader:self didFailWithError:_error];
576    }
577  }
578  return nil;
579}
580
581+ (BOOL)_isAnonymousExperience:(EXManifestsManifest *)manifest
582{
583  return [manifest.scopeKey hasPrefix:@"@anonymous/"];
584}
585
586#pragma mark - headers
587
588- (NSDictionary *)_requestHeaders
589{
590  NSDictionary *requestHeaders = @{
591      @"Exponent-SDK-Version": [self _sdkVersions],
592      @"Exponent-Accept-Signature": @"true",
593      @"Exponent-Platform": @"ios",
594      @"Exponent-Version": [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"],
595      @"Expo-Client-Environment": [self _clientEnvironment],
596      @"Expo-Updates-Environment": [self _clientEnvironment],
597      @"User-Agent": [self _userAgentString],
598      @"Expo-Client-Release-Type": [EXClientReleaseType clientReleaseType]
599  };
600
601  NSString *sessionSecret = [[EXSession sharedInstance] sessionSecret];
602  if (sessionSecret) {
603    NSMutableDictionary *requestHeadersMutable = [requestHeaders mutableCopy];
604    requestHeadersMutable[@"Expo-Session"] = sessionSecret;
605    requestHeaders = requestHeadersMutable;
606  }
607
608  return requestHeaders;
609}
610
611- (NSString *)_userAgentString
612{
613  struct utsname systemInfo;
614  uname(&systemInfo);
615  NSString *deviceModel = [NSString stringWithCString:systemInfo.machine encoding:NSUTF8StringEncoding];
616  return [NSString stringWithFormat:@"Exponent/%@ (%@; %@ %@; Scale/%.2f; %@)",
617          [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"],
618          deviceModel,
619          [UIDevice currentDevice].systemName,
620          [UIDevice currentDevice].systemVersion,
621          [UIScreen mainScreen].scale,
622          [NSLocale autoupdatingCurrentLocale].localeIdentifier];
623}
624
625- (NSString *)_clientEnvironment
626{
627  if ([EXEnvironment sharedEnvironment].isDetached) {
628    return @"STANDALONE";
629  } else {
630    return @"EXPO_DEVICE";
631#if TARGET_IPHONE_SIMULATOR
632    return @"EXPO_SIMULATOR";
633#endif
634  }
635}
636
637- (NSString *)_sdkVersions
638{
639  NSArray *versionsAvailable = [EXVersions sharedInstance].versions[@"sdkVersions"];
640  if (versionsAvailable) {
641    return [versionsAvailable componentsJoinedByString:@","];
642  } else {
643    return [EXVersions sharedInstance].temporarySdkVersion;
644  }
645}
646
647@end
648
649NS_ASSUME_NONNULL_END
650