1// Copyright 2020-present 650 Industries. All rights reserved.
2
3#import "EXClientReleaseType.h"
4#import "EXEnvironment.h"
5#import "EXErrorRecoveryManager.h"
6#import "EXFileDownloader.h"
7#import "EXKernel.h"
8#import "EXKernelLinkingManager.h"
9#import "EXSession.h"
10#import "EXAppLoaderExpoUpdates.h"
11#import "EXVersions.h"
12
13#import <EXUpdates/EXUpdatesAppLoaderTask.h>
14#import <EXUpdates/EXUpdatesConfig.h>
15#import <EXUpdates/EXUpdatesDatabase.h>
16#import <EXUpdates/EXUpdatesSelectionPolicyNewest.h>
17#import <EXUpdates/EXUpdatesUtils.h>
18#import <React/RCTUtils.h>
19#import <sys/utsname.h>
20
21NS_ASSUME_NONNULL_BEGIN
22
23@interface EXAppLoaderExpoUpdates ()
24
25@property (nonatomic, strong, nullable) NSURL *manifestUrl;
26@property (nonatomic, strong, nullable) NSURL *httpManifestUrl;
27
28@property (nonatomic, strong, nullable) NSDictionary *confirmedManifest;
29@property (nonatomic, strong, nullable) NSDictionary *optimisticManifest;
30@property (nonatomic, strong, nullable) NSData *bundle;
31
32@property (nonatomic, strong, nullable) NSError *error;
33
34@property (nonatomic, assign) BOOL shouldUseCacheOnly;
35
36@end
37
38@implementation EXAppLoaderExpoUpdates
39
40@synthesize manifestUrl = _manifestUrl;
41@synthesize bundle = _bundle;
42
43- (instancetype)initWithManifestUrl:(NSURL *)url
44{
45  if (self = [super init]) {
46    _manifestUrl = url;
47    _httpManifestUrl = [EXAppLoaderExpoUpdates _httpUrlFromManifestUrl:_manifestUrl];
48  }
49  return self;
50}
51
52#pragma mark - getters and lifecycle
53
54- (void)_reset
55{
56  _confirmedManifest = nil;
57  _optimisticManifest = nil;
58  _error = nil;
59  _shouldUseCacheOnly = NO;
60}
61
62- (EXAppLoaderStatus)status
63{
64  if (_error) {
65    return kEXAppLoaderStatusError;
66  } else if (_bundle) {
67    return kEXAppLoaderStatusHasManifestAndBundle;
68  } else if (_optimisticManifest) {
69    return kEXAppLoaderStatusHasManifest;
70  }
71  return kEXAppLoaderStatusNew;
72}
73
74- (nullable NSDictionary *)manifest
75{
76  if (_confirmedManifest) {
77    return _confirmedManifest;
78  }
79  if (_optimisticManifest) {
80    return _optimisticManifest;
81  }
82  return nil;
83}
84
85- (nullable NSData *)bundle
86{
87  if (_bundle) {
88    return _bundle;
89  }
90  return nil;
91}
92
93- (void)forceBundleReload
94{
95  // TODO: reload if dev mode
96}
97
98- (BOOL)supportsBundleReload
99{
100  // TOOD: if dev mode return true
101  return NO;
102}
103
104#pragma mark - public
105
106- (void)request
107{
108  [self _reset];
109  if (_manifestUrl) {
110    [self _beginRequest];
111  }
112}
113
114- (void)requestFromCache
115{
116  [self _reset];
117  _shouldUseCacheOnly = YES;
118  if (_manifestUrl) {
119    [self _beginRequest];
120  }
121}
122
123#pragma mark - EXUpdatesAppLoaderTaskDelegate
124
125- (BOOL)appLoaderTask:(EXUpdatesAppLoaderTask *)appLoaderTask didLoadCachedUpdate:(EXUpdatesUpdate *)update
126{
127  // if previous run of this app failed due to a loading error, we want to make sure to check for remote updates
128  if ([[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager experienceIdIsRecoveringFromError:[EXAppFetcher experienceIdWithManifest:update.rawManifest]]) {
129    if (_shouldUseCacheOnly) {
130      _shouldUseCacheOnly = NO;
131      [self _startLoaderTask];
132      return NO;
133    }
134  }
135  return YES;
136}
137
138- (void)appLoaderTask:(EXUpdatesAppLoaderTask *)appLoaderTask didStartLoadingUpdate:(EXUpdatesUpdate *)update
139{
140  _optimisticManifest = update.rawManifest;
141  if (self.delegate) {
142    [self.delegate appLoader:self didLoadOptimisticManifest:update.rawManifest];
143  }
144}
145
146- (void)appLoaderTask:(EXUpdatesAppLoaderTask *)appLoaderTask didFinishWithLauncher:(id<EXUpdatesAppLauncher>)launcher
147{
148  _confirmedManifest = launcher.launchedUpdate.rawManifest;
149  _bundle = [NSData dataWithContentsOfURL:launcher.launchAssetUrl];
150  if (self.delegate) {
151    [self.delegate appLoader:self didFinishLoadingManifest:_confirmedManifest bundle:_bundle];
152  }
153}
154
155- (void)appLoaderTask:(EXUpdatesAppLoaderTask *)appLoaderTask didFinishWithError:(NSError *)error
156{
157  _error = error;
158  if (self.delegate) {
159    [self.delegate appLoader:self didFailWithError:error];
160  }
161}
162
163- (void)appLoaderTask:(EXUpdatesAppLoaderTask *)appLoaderTask didFireEventWithType:(NSString *)type body:(NSDictionary *)body
164{
165  // TODO: add delegate method for this
166}
167
168#pragma mark - internal
169
170+ (NSURL *)_httpUrlFromManifestUrl:(NSURL *)url
171{
172  NSURLComponents *components = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:YES];
173  // if scheme is exps or https, use https. Else default to http
174  if (components.scheme && ([components.scheme isEqualToString:@"exps"] || [components.scheme isEqualToString:@"https"])){
175    components.scheme = @"https";
176  } else {
177    components.scheme = @"http";
178  }
179  NSMutableString *path = [((components.path) ? components.path : @"") mutableCopy];
180  path = [[EXKernelLinkingManager stringByRemovingDeepLink:path] mutableCopy];
181  components.path = path;
182  return [components URL];
183}
184
185- (void)_beginRequest
186{
187  // if we're in dev mode, don't try loading cached manifest
188  if ([_httpManifestUrl.host isEqualToString:@"localhost"]
189      || ([EXEnvironment sharedEnvironment].isDetached && [EXEnvironment sharedEnvironment].isDebugXCodeScheme)) {
190    // we can't pre-detect if this person is using a developer tool, but using localhost is a pretty solid indicator.
191    // TODO: dev mode
192  } else {
193    [self _startLoaderTask];
194  }
195}
196
197- (void)_startLoaderTask
198{
199  NSString *sdkVersions;
200  NSArray *versionsAvailable = [EXVersions sharedInstance].versions[@"sdkVersions"];
201  if (versionsAvailable) {
202    sdkVersions = [versionsAvailable componentsJoinedByString:@","];
203  } else {
204    sdkVersions = [EXVersions sharedInstance].temporarySdkVersion;
205  }
206
207  NSDictionary *requestHeaders = @{
208      @"Exponent-SDK-Version": sdkVersions,
209      @"Exponent-Accept-Signature": @"true",
210      @"Exponent-Platform": @"ios",
211      @"Exponent-Version": [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"],
212      @"Expo-Client-Environment": [self _clientEnvironment],
213      @"Expo-Updates-Environment": [self _clientEnvironment],
214      @"User-Agent": [self _userAgentString],
215      @"Expo-Client-Release-Type": [EXClientReleaseType clientReleaseType]
216  };
217
218  NSString *sessionSecret = [[EXSession sharedInstance] sessionSecret];
219  if (sessionSecret) {
220    NSMutableDictionary *requestHeadersMutable = [requestHeaders mutableCopy];
221    requestHeadersMutable[@"Expo-Session"] = sessionSecret;
222    requestHeaders = requestHeadersMutable;
223  }
224
225  EXUpdatesConfig *config = [EXUpdatesConfig configWithDictionary:@{
226    @"EXUpdatesURL": [[self class] _httpUrlFromManifestUrl:_manifestUrl].absoluteString,
227    @"EXUpdatesSDKVersion": sdkVersions,
228    @"EXUpdatesScopeKey": _manifestUrl.absoluteString,
229    @"EXUpdatesHasEmbeddedUpdate": @(NO),
230    @"EXUpdatesEnabled": @(YES),
231    @"EXUpdatesLaunchWaitMs": _shouldUseCacheOnly ? @(0) : @(10000),
232    @"EXUpdatesCheckOnLaunch": _shouldUseCacheOnly ? @"NEVER" : @"ALWAYS",
233    @"EXUpdatesRequestHeaders": requestHeaders
234  }];
235
236  NSError *fsError;
237  NSURL *updatesDirectory = [EXUpdatesUtils initializeUpdatesDirectoryWithError:&fsError];
238  __block NSError *dbError;
239  EXUpdatesDatabase *database = [[EXUpdatesDatabase alloc] init];
240  dispatch_sync(database.databaseQueue, ^{
241    [database openDatabaseInDirectory:updatesDirectory withError:&dbError];
242  });
243
244  EXUpdatesSelectionPolicyNewest *selectionPolicy = [[EXUpdatesSelectionPolicyNewest alloc] initWithRuntimeVersions:versionsAvailable ?: @[[EXVersions sharedInstance].temporarySdkVersion]];
245
246  EXUpdatesAppLoaderTask *loaderTask = [[EXUpdatesAppLoaderTask alloc] initWithConfig:config
247                                                                             database:database
248                                                                            directory:updatesDirectory
249                                                                      selectionPolicy:selectionPolicy
250                                                                        delegateQueue:dispatch_get_main_queue()];
251  loaderTask.delegate = self;
252  [loaderTask start];
253}
254
255- (NSString *)_userAgentString
256{
257  struct utsname systemInfo;
258  uname(&systemInfo);
259  NSString *deviceModel = [NSString stringWithCString:systemInfo.machine encoding:NSUTF8StringEncoding];
260  return [NSString stringWithFormat:@"Exponent/%@ (%@; %@ %@; Scale/%.2f; %@)",
261          [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"],
262          deviceModel,
263          [UIDevice currentDevice].systemName,
264          [UIDevice currentDevice].systemVersion,
265          [UIScreen mainScreen].scale,
266          [NSLocale autoupdatingCurrentLocale].localeIdentifier];
267}
268
269- (NSString *)_clientEnvironment
270{
271  if ([EXEnvironment sharedEnvironment].isDetached) {
272    return @"STANDALONE";
273  } else {
274    return @"EXPO_DEVICE";
275#if TARGET_IPHONE_SIMULATOR
276    return @"EXPO_SIMULATOR";
277#endif
278  }
279}
280
281@end
282
283NS_ASSUME_NONNULL_END
284