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// Implement empty stubs
195- (void)didStartCheckingForRemoteUpdate {}
196- (void)didFinishCheckingForRemoteUpdate:(NSDictionary<NSString *,id> *)body {}
197- (void)appLoaderTask:(EXUpdatesAppLoaderTask *)_ didLoadAsset:(EXUpdatesAsset *)asset successfulAssetCount:(NSInteger)successfulAssetCount failedAssetCount:(NSInteger)failedAssetCount totalAssetCount:(NSInteger)totalAssetCount {}
198
199- (BOOL)appLoaderTask:(EXUpdatesAppLoaderTask *)appLoaderTask didLoadCachedUpdate:(EXUpdatesUpdate *)update
200{
201  [self _setShouldShowRemoteUpdateStatus:update.manifest];
202  // 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
203  if (update.manifest.isUsingDeveloperTool || [[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager scopeKeyIsRecoveringFromError:update.manifest.scopeKey]) {
204    return NO;
205  }
206  return YES;
207}
208
209- (void)appLoaderTask:(EXUpdatesAppLoaderTask *)appLoaderTask didStartLoadingUpdate:(nullable EXUpdatesUpdate *)update
210{
211  // expo-cli does not always respect our SDK version headers and respond with a compatible update or an error
212  // so we need to check the compatibility here
213  EXManifestResource *manifestResource = [[EXManifestResource alloc] initWithManifestUrl:_httpManifestUrl originalUrl:_manifestUrl];
214  NSError *manifestCompatibilityError = [manifestResource verifyManifestSdkVersion:update.manifest];
215  if (manifestCompatibilityError) {
216    _error = manifestCompatibilityError;
217    if (self.delegate) {
218      [self.delegate appLoader:self didFailWithError:_error];
219      return;
220    }
221  }
222
223  _remoteUpdateStatus = kEXAppLoaderRemoteUpdateStatusDownloading;
224  [self _setShouldShowRemoteUpdateStatus:update.manifest];
225  EXManifestsManifest *processedManifest = [self _processManifest:update.manifest];
226  if (processedManifest == nil) {
227    return;
228  }
229  [self _setOptimisticManifest:processedManifest];
230}
231
232- (void)appLoaderTask:(EXUpdatesAppLoaderTask *)appLoaderTask didFinishWithLauncher:(id<EXUpdatesAppLauncher>)launcher isUpToDate:(BOOL)isUpToDate
233{
234  if (_error) {
235    return;
236  }
237
238  if (!_optimisticManifest) {
239    EXManifestsManifest *processedManifest = [self _processManifest:launcher.launchedUpdate.manifest];
240    if (processedManifest == nil) {
241      return;
242    }
243    [self _setOptimisticManifest:processedManifest];
244  }
245  _isUpToDate = isUpToDate;
246  if (launcher.launchedUpdate.manifest.isUsingDeveloperTool) {
247    // in dev mode, we need to set an optimistic manifest but nothing else
248    return;
249  }
250  _confirmedManifest = [self _processManifest:launcher.launchedUpdate.manifest];
251  if (_confirmedManifest == nil) {
252    return;
253  }
254  _bundle = [NSData dataWithContentsOfURL:launcher.launchAssetUrl];
255  _appLauncher = launcher;
256  if (self.delegate) {
257    [self.delegate appLoader:self didFinishLoadingManifest:_confirmedManifest bundle:_bundle];
258  }
259}
260
261- (void)appLoaderTask:(EXUpdatesAppLoaderTask *)appLoaderTask didFinishWithError:(NSError *)error
262{
263  if ([EXEnvironment sharedEnvironment].isDetached) {
264    _isEmergencyLaunch = YES;
265    [self _launchWithNoDatabaseAndError:error];
266  } else if (!_error) {
267    _error = error;
268
269    // if the error payload conforms to the error protocol, we can parse it and display
270    // a slightly nicer error message to the user
271    id errorJson = [NSJSONSerialization JSONObjectWithData:[error.localizedDescription dataUsingEncoding:NSUTF8StringEncoding] options:kNilOptions error:nil];
272    if (errorJson && [errorJson isKindOfClass:[NSDictionary class]]) {
273      EXManifestResource *manifestResource = [[EXManifestResource alloc] initWithManifestUrl:_httpManifestUrl originalUrl:_manifestUrl];
274      _error = [manifestResource formatError:[NSError errorWithDomain:EXNetworkErrorDomain code:error.code userInfo:errorJson]];
275    }
276
277    if (self.delegate) {
278      [self.delegate appLoader:self didFailWithError:_error];
279    }
280  }
281}
282
283- (void)appLoaderTask:(EXUpdatesAppLoaderTask *)appLoaderTask didFinishBackgroundUpdateWithStatus:(EXUpdatesBackgroundUpdateStatus)status update:(nullable EXUpdatesUpdate *)update error:(nullable NSError *)error
284{
285  if (self.delegate) {
286    [self.delegate appLoader:self didResolveUpdatedBundleWithManifest:update.manifest isFromCache:(status == EXUpdatesBackgroundUpdateStatusNoUpdateAvailable) error:error];
287  }
288}
289
290#pragma mark - internal
291
292+ (NSURL *)_httpUrlFromManifestUrl:(NSURL *)url
293{
294  NSURLComponents *components = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:YES];
295  // if scheme is exps or https, use https. Else default to http
296  if (components.scheme && ([components.scheme isEqualToString:@"exps"] || [components.scheme isEqualToString:@"https"])){
297    components.scheme = @"https";
298  } else {
299    components.scheme = @"http";
300  }
301  NSMutableString *path = [((components.path) ? components.path : @"") mutableCopy];
302  path = [[EXKernelLinkingManager stringByRemovingDeepLink:path] mutableCopy];
303  components.path = path;
304  return [components URL];
305}
306
307- (BOOL)_initializeDatabase
308{
309  EXUpdatesDatabaseManager *updatesDatabaseManager = [EXKernel sharedInstance].serviceRegistry.updatesDatabaseManager;
310  BOOL success = updatesDatabaseManager.isDatabaseOpen;
311  if (!updatesDatabaseManager.isDatabaseOpen) {
312    success = [updatesDatabaseManager openDatabase];
313  }
314
315  if (!success) {
316    _error = updatesDatabaseManager.error;
317    if (self.delegate) {
318      [self.delegate appLoader:self didFailWithError:_error];
319    }
320    return NO;
321  } else {
322    return YES;
323  }
324}
325
326- (void)_beginRequest
327{
328  if (![self _initializeDatabase]) {
329    return;
330  }
331  [self _startLoaderTask];
332}
333
334- (void)_startLoaderTask
335{
336  BOOL shouldCheckOnLaunch;
337  NSNumber *launchWaitMs;
338  if (_shouldUseCacheOnly) {
339    shouldCheckOnLaunch = NO;
340    launchWaitMs = @(0);
341  } else {
342    if ([EXEnvironment sharedEnvironment].isDetached) {
343      shouldCheckOnLaunch = [EXEnvironment sharedEnvironment].updatesCheckAutomatically;
344      launchWaitMs = [EXEnvironment sharedEnvironment].updatesFallbackToCacheTimeout;
345    } else {
346      shouldCheckOnLaunch = YES;
347      launchWaitMs = @(60000);
348    }
349  }
350
351  NSURL *httpManifestUrl = [[self class] _httpUrlFromManifestUrl:_manifestUrl];
352
353  NSString *releaseChannel = [EXEnvironment sharedEnvironment].releaseChannel;
354  if (![EXEnvironment sharedEnvironment].isDetached) {
355    // in Expo Go, the release channel can change at runtime depending on the URL we load
356    NSURLComponents *manifestUrlComponents = [NSURLComponents componentsWithURL:httpManifestUrl resolvingAgainstBaseURL:YES];
357    releaseChannel = [EXKernelLinkingManager releaseChannelWithUrlComponents:manifestUrlComponents];
358  }
359
360  NSMutableDictionary *updatesConfig = [[NSMutableDictionary alloc] initWithDictionary:@{
361    EXUpdatesConfig.EXUpdatesConfigUpdateUrlKey: httpManifestUrl.absoluteString,
362    EXUpdatesConfig.EXUpdatesConfigSDKVersionKey: [self _sdkVersions],
363    EXUpdatesConfig.EXUpdatesConfigScopeKeyKey: httpManifestUrl.absoluteString,
364    EXUpdatesConfig.EXUpdatesConfigReleaseChannelKey: releaseChannel,
365    EXUpdatesConfig.EXUpdatesConfigHasEmbeddedUpdateKey: @([EXEnvironment sharedEnvironment].isDetached),
366    EXUpdatesConfig.EXUpdatesConfigEnabledKey: @([EXEnvironment sharedEnvironment].areRemoteUpdatesEnabled),
367    EXUpdatesConfig.EXUpdatesConfigLaunchWaitMsKey: launchWaitMs,
368    EXUpdatesConfig.EXUpdatesConfigCheckOnLaunchKey: shouldCheckOnLaunch ? EXUpdatesConfig.EXUpdatesConfigCheckOnLaunchValueAlways : EXUpdatesConfig.EXUpdatesConfigCheckOnLaunchValueNever,
369    EXUpdatesConfig.EXUpdatesConfigExpectsSignedManifestKey: @YES,
370    EXUpdatesConfig.EXUpdatesConfigRequestHeadersKey: [self _requestHeaders]
371  }];
372
373  if (!EXEnvironment.sharedEnvironment.isDetached) {
374    // in Expo Go, embed the Expo Root Certificate and get the Expo Go intermediate certificate and development certificates
375    // from the multipart manifest response part
376
377    NSString *expoRootCertPath = [[NSBundle mainBundle] pathForResource:@"expo-root" ofType:@"pem"];
378    if (!expoRootCertPath) {
379      @throw [NSException exceptionWithName:NSInternalInconsistencyException
380                                     reason:@"No expo-root certificate found in bundle"
381                                   userInfo:@{}];
382    }
383
384    NSError *error;
385    NSString *expoRootCert = [NSString stringWithContentsOfFile:expoRootCertPath encoding:NSUTF8StringEncoding error:&error];
386    if (error) {
387      expoRootCert = nil;
388    }
389    if (!expoRootCert) {
390      @throw [NSException exceptionWithName:NSInternalInconsistencyException
391                                     reason:@"Error reading expo-root certificate from bundle"
392                                   userInfo:@{ @"underlyingError": error.localizedDescription }];
393    }
394
395    updatesConfig[EXUpdatesConfig.EXUpdatesConfigCodeSigningCertificateKey] = expoRootCert;
396    updatesConfig[EXUpdatesConfig.EXUpdatesConfigCodeSigningMetadataKey] = @{
397      @"keyid": @"expo-root",
398      @"alg": @"rsa-v1_5-sha256",
399    };
400    updatesConfig[EXUpdatesConfig.EXUpdatesConfigCodeSigningIncludeManifestResponseCertificateChainKey] = @YES;
401    updatesConfig[EXUpdatesConfig.EXUpdatesConfigCodeSigningAllowUnsignedManifestsKey] = @YES;
402
403    // in Expo Go, ignore directives in manifest responses and require a manifest. the current directives
404    // (no update available, roll back) don't have any practical use outside of standalone apps
405    updatesConfig[EXUpdatesConfig.EXUpdatesConfigEnableExpoUpdatesProtocolV0CompatibilityModeKey] = @YES;
406  }
407
408  _config = [EXUpdatesConfig configFromDictionary:updatesConfig];
409
410  if (![EXEnvironment sharedEnvironment].areRemoteUpdatesEnabled) {
411    [self _launchWithNoDatabaseAndError:nil];
412    return;
413  }
414
415  EXUpdatesDatabaseManager *updatesDatabaseManager = [EXKernel sharedInstance].serviceRegistry.updatesDatabaseManager;
416
417  NSMutableArray *sdkVersions = [[EXVersions sharedInstance].versions[@"sdkVersions"] ?: @[[EXVersions sharedInstance].temporarySdkVersion] mutableCopy];
418  [sdkVersions addObject:@"UNVERSIONED"];
419
420  NSMutableArray *sdkVersionRuntimeVersions = [[NSMutableArray alloc] initWithCapacity:sdkVersions.count];
421  for (NSString *sdkVersion in sdkVersions) {
422    [sdkVersionRuntimeVersions addObject:[NSString stringWithFormat:@"exposdk:%@", sdkVersion]];
423  }
424  [sdkVersionRuntimeVersions addObject:@"exposdk:UNVERSIONED"];
425  [sdkVersions addObjectsFromArray:sdkVersionRuntimeVersions];
426
427  _selectionPolicy = [[EXUpdatesSelectionPolicy alloc]
428                      initWithLauncherSelectionPolicy:[[EXExpoGoLauncherSelectionPolicyFilterAware alloc] initWithSdkVersions:sdkVersions]
429                      loaderSelectionPolicy:[EXUpdatesLoaderSelectionPolicyFilterAware new]
430                      reaperSelectionPolicy:[EXUpdatesReaperSelectionPolicyDevelopmentClient new]];
431
432  EXUpdatesAppLoaderTask *loaderTask = [[EXUpdatesAppLoaderTask alloc] initWithConfig:_config
433                                                                             database:updatesDatabaseManager.database
434                                                                            directory:updatesDatabaseManager.updatesDirectory
435                                                                      selectionPolicy:_selectionPolicy
436                                                                        delegateQueue:_appLoaderQueue];
437  loaderTask.delegate = self;
438  [loaderTask start];
439}
440
441- (void)_launchWithNoDatabaseAndError:(nullable NSError *)error
442{
443  EXUpdatesAppLauncherNoDatabase *appLauncher = [[EXUpdatesAppLauncherNoDatabase alloc] init];
444  [appLauncher launchUpdateWithConfig:_config];
445
446  _confirmedManifest = [self _processManifest:appLauncher.launchedUpdate.manifest];
447  if (_confirmedManifest == nil) {
448    return;
449  }
450  _optimisticManifest = _confirmedManifest;
451  _bundle = [NSData dataWithContentsOfURL:appLauncher.launchAssetUrl];
452  _appLauncher = appLauncher;
453  if (self.delegate) {
454    [self.delegate appLoader:self didLoadOptimisticManifest:_confirmedManifest];
455    [self.delegate appLoader:self didFinishLoadingManifest:_confirmedManifest bundle:_bundle];
456  }
457
458  [[EXUpdatesErrorRecovery new] writeErrorOrExceptionToLog:error];
459}
460
461- (void)_runReaper
462{
463  if (_appLauncher.launchedUpdate) {
464    EXUpdatesDatabaseManager *updatesDatabaseManager = [EXKernel sharedInstance].serviceRegistry.updatesDatabaseManager;
465    [EXUpdatesReaper reapUnusedUpdatesWithConfig:_config
466                                        database:updatesDatabaseManager.database
467                                       directory:updatesDatabaseManager.updatesDirectory
468                                 selectionPolicy:_selectionPolicy
469                                  launchedUpdate:_appLauncher.launchedUpdate];
470  }
471}
472
473- (void)_setOptimisticManifest:(EXManifestsManifest *)manifest
474{
475  _optimisticManifest = manifest;
476  if (self.delegate) {
477    [self.delegate appLoader:self didLoadOptimisticManifest:_optimisticManifest];
478  }
479}
480
481- (void)_setShouldShowRemoteUpdateStatus:(EXManifestsManifest *)manifest
482{
483  // we don't want to show the cached experience alert when Updates.reloadAsync() is called
484  if (_shouldUseCacheOnly) {
485    _shouldShowRemoteUpdateStatus = NO;
486    return;
487  }
488
489  if (manifest) {
490    if (manifest.isDevelopmentSilentLaunch) {
491      _shouldShowRemoteUpdateStatus = NO;
492      return;
493    }
494  }
495  _shouldShowRemoteUpdateStatus = YES;
496}
497
498- (void)_loadDevelopmentJavaScriptResource
499{
500  _isLoadingDevelopmentJavaScriptResource = YES;
501  EXAppFetcher *appFetcher = [[EXAppFetcher alloc] initWithAppLoader:self];
502  [appFetcher fetchJSBundleWithManifest:self.optimisticManifest cacheBehavior:EXCachedResourceNoCache timeoutInterval:kEXJSBundleTimeout progress:^(EXLoadingProgress *progress) {
503    if (self.delegate) {
504      [self.delegate appLoader:self didLoadBundleWithProgress:progress];
505    }
506  } success:^(NSData *bundle) {
507    self.isUpToDate = YES;
508    self.bundle = bundle;
509    self.isLoadingDevelopmentJavaScriptResource = NO;
510    if (self.delegate) {
511      [self.delegate appLoader:self didFinishLoadingManifest:self.optimisticManifest bundle:self.bundle];
512    }
513  } error:^(NSError *error) {
514    self.error = error;
515    self.isLoadingDevelopmentJavaScriptResource = NO;
516    if (self.delegate) {
517      [self.delegate appLoader:self didFailWithError:error];
518    }
519  }];
520}
521
522# pragma mark - manifest processing
523
524- (nullable EXManifestsManifest *)_processManifest:(EXManifestsManifest *)manifest
525{
526  @try {
527    NSMutableDictionary *mutableManifest = [manifest.rawManifestJSON mutableCopy];
528
529    // If legacy manifest is not yet verified, served by a third party, not standalone, and not an anonymous experience
530    // then scope it locally by using the manifest URL as a scopeKey (id) and consider it verified.
531    if (!mutableManifest[@"isVerified"] &&
532        !EXEnvironment.sharedEnvironment.isDetached &&
533        ![EXKernelLinkingManager isExpoHostedUrl:_httpManifestUrl] &&
534        ![EXAppLoaderExpoUpdates _isAnonymousExperience:manifest] &&
535        [manifest isKindOfClass:[EXManifestsLegacyManifest class]]) {
536      // the manifest id in a legacy manifest determines the namespace/experience id an app is sandboxed with
537      // if manifest is hosted by third parties, we sandbox it with the hostname to avoid clobbering exp.host namespaces
538      // for https urls, sandboxed id is of form quinlanj.github.io/myProj-myApp
539      // for http urls, sandboxed id is of form UNVERIFIED-quinlanj.github.io/myProj-myApp
540      NSString *securityPrefix = [_httpManifestUrl.scheme isEqualToString:@"https"] ? @"" : @"UNVERIFIED-";
541      NSString *slugSuffix = manifest.slug ? [@"-" stringByAppendingString:manifest.slug]: @"";
542      mutableManifest[@"id"] = [NSString stringWithFormat:@"%@%@%@%@", securityPrefix, _httpManifestUrl.host, _httpManifestUrl.path ?: @"", slugSuffix];
543      mutableManifest[@"isVerified"] = @(YES);
544    }
545
546    // set verified to false by default
547    if (!mutableManifest[@"isVerified"]) {
548      mutableManifest[@"isVerified"] = @(NO);
549    }
550
551    // if the app bypassed verification or the manifest is scoped to a random anonymous
552    // scope key, automatically verify it
553    if (![mutableManifest[@"isVerified"] boolValue] && (EXEnvironment.sharedEnvironment.isManifestVerificationBypassed || [EXAppLoaderExpoUpdates _isAnonymousExperience:manifest])) {
554      mutableManifest[@"isVerified"] = @(YES);
555    }
556
557    // when the manifest is not verified at this point, make the scope key a salted and hashed version of the claimed scope key
558    if (![mutableManifest[@"isVerified"] boolValue]) {
559      NSString *currentScopeKeyAndSaltToHash = [NSString stringWithFormat:@"unverified-%@", manifest.scopeKey];
560      NSString *currentScopeKeyHash = [currentScopeKeyAndSaltToHash hexEncodedSHA256];
561      NSString *newScopeKey = [NSString stringWithFormat:@"%@-%@", currentScopeKeyAndSaltToHash, currentScopeKeyHash];
562      if ([manifest isKindOfClass:EXManifestsNewManifest.class]) {
563        NSDictionary *extra = mutableManifest[@"extra"] ?: @{};
564        NSMutableDictionary *mutableExtra = [extra mutableCopy];
565        mutableExtra[@"scopeKey"] = newScopeKey;
566        mutableManifest[@"extra"] = mutableExtra;
567      } else {
568        mutableManifest[@"scopeKey"] = newScopeKey;
569        mutableManifest[@"id"] = newScopeKey;
570      }
571    }
572
573    return [EXManifestsManifestFactory manifestForManifestJSON:[mutableManifest copy]];
574  }
575  @catch (NSException *exception) {
576    // Catch parsing errors related to invalid or unexpected manifest properties. For example, if a manifest
577    // is missing the `id` property, it'll raise an exception which we want to forward to the user so they
578    // can adjust their manifest JSON accordingly.
579    _error = [NSError errorWithDomain:@"ExpoParsingManifest"
580                                             code:1025
581                                         userInfo:@{NSLocalizedDescriptionKey: [@"Failed to parse manifest JSON: " stringByAppendingString:exception.reason] }];
582    if (self.delegate) {
583      [self.delegate appLoader:self didFailWithError:_error];
584    }
585  }
586  return nil;
587}
588
589+ (BOOL)_isAnonymousExperience:(EXManifestsManifest *)manifest
590{
591  return [manifest.scopeKey hasPrefix:@"@anonymous/"];
592}
593
594#pragma mark - headers
595
596- (NSDictionary *)_requestHeaders
597{
598  NSDictionary *requestHeaders = @{
599      @"Exponent-SDK-Version": [self _sdkVersions],
600      @"Exponent-Accept-Signature": @"true",
601      @"Exponent-Platform": @"ios",
602      @"Exponent-Version": [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"],
603      @"Expo-Client-Environment": [self _clientEnvironment],
604      @"Expo-Updates-Environment": [self _clientEnvironment],
605      @"User-Agent": [self _userAgentString],
606      @"Expo-Client-Release-Type": [EXClientReleaseType clientReleaseType]
607  };
608
609  NSString *sessionSecret = [[EXSession sharedInstance] sessionSecret];
610  if (sessionSecret) {
611    NSMutableDictionary *requestHeadersMutable = [requestHeaders mutableCopy];
612    requestHeadersMutable[@"Expo-Session"] = sessionSecret;
613    requestHeaders = requestHeadersMutable;
614  }
615
616  return requestHeaders;
617}
618
619- (NSString *)_userAgentString
620{
621  struct utsname systemInfo;
622  uname(&systemInfo);
623  NSString *deviceModel = [NSString stringWithCString:systemInfo.machine encoding:NSUTF8StringEncoding];
624  return [NSString stringWithFormat:@"Exponent/%@ (%@; %@ %@; Scale/%.2f; %@)",
625          [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"],
626          deviceModel,
627          [UIDevice currentDevice].systemName,
628          [UIDevice currentDevice].systemVersion,
629          [UIScreen mainScreen].scale,
630          [NSLocale autoupdatingCurrentLocale].localeIdentifier];
631}
632
633- (NSString *)_clientEnvironment
634{
635  if ([EXEnvironment sharedEnvironment].isDetached) {
636    return @"STANDALONE";
637  } else {
638    return @"EXPO_DEVICE";
639#if TARGET_IPHONE_SIMULATOR
640    return @"EXPO_SIMULATOR";
641#endif
642  }
643}
644
645- (NSString *)_sdkVersions
646{
647  NSArray *versionsAvailable = [EXVersions sharedInstance].versions[@"sdkVersions"];
648  if (versionsAvailable) {
649    return [versionsAvailable componentsJoinedByString:@","];
650  } else {
651    return [EXVersions sharedInstance].temporarySdkVersion;
652  }
653}
654
655@end
656
657NS_ASSUME_NONNULL_END
658