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