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