1// Copyright 2015-present 650 Industries. All rights reserved.
2
3#import "EXAppLoader+Updates.h"
4#import "EXEnvironment.h"
5#import "EXKernel.h"
6#import "EXKernelAppRecord.h"
7#import "EXReactAppManager.h"
8#import "EXScopedModuleRegistry.h"
9#import "EXUpdatesDatabaseManager.h"
10#import "EXUpdatesManager.h"
11
12#import <EXUpdates/EXUpdatesFileDownloader.h>
13#import <EXUpdates/EXUpdatesRemoteAppLoader.h>
14
15#import <React/RCTBridge.h>
16#import <React/RCTUtils.h>
17
18NSString * const EXUpdatesEventName = @"Expo.nativeUpdatesEvent";
19NSString * const EXUpdatesErrorEventType = @"error";
20NSString * const EXUpdatesUpdateAvailableEventType = @"updateAvailable";
21NSString * const EXUpdatesNotAvailableEventType = @"noUpdateAvailable";
22
23// legacy events
24// TODO: remove once SDK 38 is phased out
25NSString * const EXUpdatesEventNameLegacy = @"Exponent.nativeUpdatesEvent";
26NSString * const EXUpdatesDownloadStartEventType = @"downloadStart";
27NSString * const EXUpdatesDownloadProgressEventType = @"downloadProgress";
28NSString * const EXUpdatesDownloadFinishedEventType = @"downloadFinished";
29
30@interface EXUpdatesManager ()
31
32@property (nonatomic, strong) EXAppLoader *manifestAppLoader;
33
34@end
35
36@implementation EXUpdatesManager
37
38- (void)notifyApp:(EXKernelAppRecord *)appRecord
39ofDownloadWithManifest:(EXUpdatesRawManifest * _Nullable)manifest
40            isNew:(BOOL)isBundleNew
41            error:(NSError * _Nullable)error;
42{
43  NSDictionary *body;
44  NSDictionary *bodyLegacy;
45  if (error) {
46    body = @{
47             @"type": EXUpdatesErrorEventType,
48             @"message": error.localizedDescription
49             };
50  } else if (isBundleNew) {
51    // prevent a crash, but this shouldn't ever happen
52    NSDictionary *rawManifestJSON = manifest ? manifest.rawManifestJSON : @{};
53    bodyLegacy = @{
54                   @"type": EXUpdatesDownloadFinishedEventType,
55                   @"manifest": rawManifestJSON
56                   };
57    body = @{
58             @"type": EXUpdatesUpdateAvailableEventType,
59             @"manifest": rawManifestJSON
60             };
61  } else {
62    body = @{
63             @"type": EXUpdatesNotAvailableEventType
64             };
65  }
66  RCTBridge *bridge = appRecord.appManager.reactBridge;
67  if (appRecord.status == kEXKernelAppRecordStatusRunning) {
68    // for SDK 38 and below
69    [bridge enqueueJSCall:@"RCTDeviceEventEmitter.emit" args:@[EXUpdatesEventNameLegacy, bodyLegacy ?: body]];
70    // for SDK 39+
71    [bridge enqueueJSCall:@"RCTDeviceEventEmitter.emit" args:@[EXUpdatesEventName, body]];
72  }
73}
74
75# pragma mark - internal
76
77- (EXAppLoader *)_appLoaderWithScopedModule:(id)scopedModule
78{
79  NSString *scopeKey = ((EXScopedBridgeModule *)scopedModule).scopeKey;
80  return [self _appLoaderWithScopeKey:scopeKey];
81}
82
83- (EXAppLoader *)_appLoaderWithScopeKey:(NSString *)scopeKey
84{
85  EXKernelAppRecord *appRecord = [[EXKernel sharedInstance].appRegistry newestRecordWithScopeKey:scopeKey];
86  return appRecord.appLoader;
87}
88
89# pragma mark - EXUpdatesBindingDelegate
90
91- (EXUpdatesConfig *)configForScopeKey:(NSString *)scopeKey
92{
93  return [self _appLoaderWithScopeKey:scopeKey].config;
94}
95
96- (EXUpdatesSelectionPolicy *)selectionPolicyForScopeKey:(NSString *)scopeKey
97{
98  return [self _appLoaderWithScopeKey:scopeKey].selectionPolicy;
99}
100
101- (nullable EXUpdatesUpdate *)launchedUpdateForScopeKey:(NSString *)scopeKey
102{
103  return [self _appLoaderWithScopeKey:scopeKey].appLauncher.launchedUpdate;
104}
105
106- (nullable NSDictionary *)assetFilesMapForScopeKey:(NSString *)scopeKey
107{
108  return [self _appLoaderWithScopeKey:scopeKey].appLauncher.assetFilesMap;
109}
110
111- (BOOL)isUsingEmbeddedAssetsForScopeKey:(NSString *)scopeKey
112{
113  return NO;
114}
115
116- (BOOL)isStartedForScopeKey:(NSString *)scopeKey
117{
118  return [self _appLoaderWithScopeKey:scopeKey].appLauncher != nil;
119}
120
121- (BOOL)isEmergencyLaunchForScopeKey:(NSString *)scopeKey
122{
123  return [self _appLoaderWithScopeKey:scopeKey].isEmergencyLaunch;
124}
125
126- (void)requestRelaunchForScopeKey:(NSString *)scopeKey withCompletion:(EXUpdatesAppRelaunchCompletionBlock)completion
127{
128  [[EXKernel sharedInstance] reloadAppFromCacheWithScopeKey:scopeKey];
129  completion(YES);
130}
131
132# pragma mark - EXUpdatesScopedModuleDelegate
133
134- (void)updatesModuleDidSelectReload:(id)scopedModule
135{
136  NSString *scopeKey = ((EXScopedBridgeModule *)scopedModule).scopeKey;
137  [[EXKernel sharedInstance] reloadAppWithScopeKey:scopeKey];
138}
139
140- (void)updatesModuleDidSelectReloadFromCache:(id)scopedModule
141{
142  NSString *scopeKey = ((EXScopedBridgeModule *)scopedModule).scopeKey;
143  [[EXKernel sharedInstance] reloadAppFromCacheWithScopeKey:scopeKey];
144}
145
146- (void)updatesModule:(id)scopedModule
147didRequestManifestWithCacheBehavior:(EXManifestCacheBehavior)cacheBehavior
148              success:(void (^)(EXUpdatesRawManifest * _Nonnull))success
149              failure:(void (^)(NSError * _Nonnull))failure
150{
151  if ([EXEnvironment sharedEnvironment].isDetached && ![EXEnvironment sharedEnvironment].areRemoteUpdatesEnabled) {
152    failure(RCTErrorWithMessage(@"Remote updates are disabled in app.json"));
153    return;
154  }
155
156  EXUpdatesDatabaseManager *databaseKernelService = [EXKernel sharedInstance].serviceRegistry.updatesDatabaseManager;
157  EXAppLoader *appLoader = [self _appLoaderWithScopedModule:scopedModule];
158
159  EXUpdatesFileDownloader *fileDownloader = [[EXUpdatesFileDownloader alloc] initWithUpdatesConfig:appLoader.config];
160  [fileDownloader downloadManifestFromURL:appLoader.config.updateUrl
161                             withDatabase:databaseKernelService.database
162                             extraHeaders:nil
163                             successBlock:^(EXUpdatesUpdate *update) {
164    success(update.rawManifest);
165  } errorBlock:^(NSError *error, NSURLResponse *response) {
166    failure(error);
167  }];
168}
169
170
171- (void)updatesModule:(id)scopedModule
172didRequestBundleWithCompletionQueue:(dispatch_queue_t)completionQueue
173                start:(void (^)(void))startBlock
174              success:(void (^)(EXUpdatesRawManifest * _Nullable))success
175              failure:(void (^)(NSError * _Nonnull))failure
176{
177  if ([EXEnvironment sharedEnvironment].isDetached && ![EXEnvironment sharedEnvironment].areRemoteUpdatesEnabled) {
178    failure(RCTErrorWithMessage(@"Remote updates are disabled in app.json"));
179    return;
180  }
181
182  EXUpdatesDatabaseManager *databaseKernelService = [EXKernel sharedInstance].serviceRegistry.updatesDatabaseManager;
183  EXAppLoader *appLoader = [self _appLoaderWithScopedModule:scopedModule];
184
185  EXUpdatesRemoteAppLoader *remoteAppLoader = [[EXUpdatesRemoteAppLoader alloc] initWithConfig:appLoader.config database:databaseKernelService.database directory:databaseKernelService.updatesDirectory completionQueue:completionQueue];
186  [remoteAppLoader loadUpdateFromUrl:appLoader.config.updateUrl onManifest:^BOOL(EXUpdatesUpdate * _Nonnull update) {
187    BOOL shouldLoad = [appLoader.selectionPolicy shouldLoadNewUpdate:update withLaunchedUpdate:appLoader.appLauncher.launchedUpdate filters:update.manifestFilters];
188    if (shouldLoad) {
189      startBlock();
190    }
191    return shouldLoad;
192  } asset:^(EXUpdatesAsset *asset, NSUInteger successfulAssetCount, NSUInteger failedAssetCount, NSUInteger totalAssetCount) {
193    // do nothing for now
194  } success:^(EXUpdatesUpdate * _Nullable update) {
195    if (update) {
196      success(update.rawManifest);
197    } else {
198      success(nil);
199    }
200  } error:^(NSError * _Nonnull error) {
201    failure(error);
202  }];
203}
204
205@end
206