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:(NSDictionary * _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    if (!manifest) {
52      // prevent a crash, but this shouldn't ever happen
53      manifest = @{};
54    }
55    bodyLegacy = @{
56                   @"type": EXUpdatesDownloadFinishedEventType,
57                   @"manifest": manifest
58                   };
59    body = @{
60             @"type": EXUpdatesUpdateAvailableEventType,
61             @"manifest": manifest
62             };
63  } else {
64    body = @{
65             @"type": EXUpdatesNotAvailableEventType
66             };
67  }
68  RCTBridge *bridge = appRecord.appManager.reactBridge;
69  if (appRecord.status == kEXKernelAppRecordStatusRunning) {
70    // for SDK 38 and below
71    [bridge enqueueJSCall:@"RCTDeviceEventEmitter.emit" args:@[EXUpdatesEventNameLegacy, bodyLegacy ?: body]];
72    // for SDK 39+
73    [bridge enqueueJSCall:@"RCTDeviceEventEmitter.emit" args:@[EXUpdatesEventName, body]];
74  }
75}
76
77# pragma mark - internal
78
79- (EXAppLoader *)_appLoaderWithScopedModule:(id)scopedModule
80{
81  NSString *experienceId = ((EXScopedBridgeModule *)scopedModule).experienceId;
82  return [self _appLoaderWithExperienceId:experienceId];
83}
84
85- (EXAppLoader *)_appLoaderWithExperienceId:(NSString *)experienceId
86{
87  EXKernelAppRecord *appRecord = [[EXKernel sharedInstance].appRegistry newestRecordWithExperienceId:experienceId];
88  return appRecord.appLoader;
89}
90
91# pragma mark - EXUpdatesBindingDelegate
92
93- (EXUpdatesConfig *)configForExperienceId:(NSString *)experienceId
94{
95  return [self _appLoaderWithExperienceId:experienceId].config;
96}
97
98- (id<EXUpdatesSelectionPolicy>)selectionPolicyForExperienceId:(NSString *)experienceId
99{
100  return [self _appLoaderWithExperienceId:experienceId].selectionPolicy;
101}
102
103- (nullable EXUpdatesUpdate *)launchedUpdateForExperienceId:(NSString *)experienceId
104{
105  return [self _appLoaderWithExperienceId:experienceId].appLauncher.launchedUpdate;
106}
107
108- (nullable NSDictionary *)assetFilesMapForExperienceId:(NSString *)experienceId
109{
110  return [self _appLoaderWithExperienceId:experienceId].appLauncher.assetFilesMap;
111}
112
113- (BOOL)isUsingEmbeddedAssetsForExperienceId:(NSString *)experienceId
114{
115  return NO;
116}
117
118- (BOOL)isStartedForExperienceId:(NSString *)experienceId
119{
120  return [self _appLoaderWithExperienceId:experienceId].appLauncher != nil;
121}
122
123- (BOOL)isEmergencyLaunchForExperienceId:(NSString *)experienceId
124{
125  return [self _appLoaderWithExperienceId:experienceId].isEmergencyLaunch;
126}
127
128- (void)requestRelaunchForExperienceId:(NSString *)experienceId withCompletion:(EXUpdatesAppRelaunchCompletionBlock)completion
129{
130  [[EXKernel sharedInstance] reloadAppFromCacheWithExperienceId:experienceId];
131  completion(YES);
132}
133
134# pragma mark - EXUpdatesScopedModuleDelegate
135
136- (void)updatesModuleDidSelectReload:(id)scopedModule
137{
138  NSString *experienceId = ((EXScopedBridgeModule *)scopedModule).experienceId;
139  [[EXKernel sharedInstance] reloadAppWithExperienceId:experienceId];
140}
141
142- (void)updatesModuleDidSelectReloadFromCache:(id)scopedModule
143{
144  NSString *experienceId = ((EXScopedBridgeModule *)scopedModule).experienceId;
145  [[EXKernel sharedInstance] reloadAppFromCacheWithExperienceId:experienceId];
146}
147
148- (void)updatesModule:(id)scopedModule
149didRequestManifestWithCacheBehavior:(EXManifestCacheBehavior)cacheBehavior
150              success:(void (^)(NSDictionary * _Nonnull))success
151              failure:(void (^)(NSError * _Nonnull))failure
152{
153  if ([EXEnvironment sharedEnvironment].isDetached && ![EXEnvironment sharedEnvironment].areRemoteUpdatesEnabled) {
154    failure(RCTErrorWithMessage(@"Remote updates are disabled in app.json"));
155    return;
156  }
157
158  EXUpdatesDatabaseManager *databaseKernelService = [EXKernel sharedInstance].serviceRegistry.updatesDatabaseManager;
159  EXAppLoader *appLoader = [self _appLoaderWithScopedModule:scopedModule];
160
161  EXUpdatesFileDownloader *fileDownloader = [[EXUpdatesFileDownloader alloc] initWithUpdatesConfig:appLoader.config];
162  [fileDownloader downloadManifestFromURL:appLoader.config.updateUrl
163                             withDatabase:databaseKernelService.database
164                             extraHeaders:nil
165                             successBlock:^(EXUpdatesUpdate *update) {
166    success(update.rawManifest);
167  } errorBlock:^(NSError *error, NSURLResponse *response) {
168    failure(error);
169  }];
170}
171
172
173- (void)updatesModule:(id)scopedModule
174didRequestBundleWithCompletionQueue:(dispatch_queue_t)completionQueue
175                start:(void (^)(void))startBlock
176              success:(void (^)(NSDictionary * _Nullable))success
177              failure:(void (^)(NSError * _Nonnull))failure
178{
179  if ([EXEnvironment sharedEnvironment].isDetached && ![EXEnvironment sharedEnvironment].areRemoteUpdatesEnabled) {
180    failure(RCTErrorWithMessage(@"Remote updates are disabled in app.json"));
181    return;
182  }
183
184  EXUpdatesDatabaseManager *databaseKernelService = [EXKernel sharedInstance].serviceRegistry.updatesDatabaseManager;
185  EXAppLoader *appLoader = [self _appLoaderWithScopedModule:scopedModule];
186
187  EXUpdatesRemoteAppLoader *remoteAppLoader = [[EXUpdatesRemoteAppLoader alloc] initWithConfig:appLoader.config database:databaseKernelService.database directory:databaseKernelService.updatesDirectory completionQueue:completionQueue];
188  [remoteAppLoader loadUpdateFromUrl:appLoader.config.updateUrl onManifest:^BOOL(EXUpdatesUpdate * _Nonnull update) {
189    BOOL shouldLoad = [appLoader.selectionPolicy shouldLoadNewUpdate:update withLaunchedUpdate:appLoader.appLauncher.launchedUpdate filters:update.manifestFilters];
190    if (shouldLoad) {
191      startBlock();
192    }
193    return shouldLoad;
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