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