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 "EXUpdates.h"
10#import "EXUpdatesManager.h"
11
12#import <React/RCTBridge.h>
13#import <React/RCTUtils.h>
14
15NSString * const EXUpdatesEventNameNew = @"Expo.nativeUpdatesEvent";
16NSString * const EXUpdatesUpdateAvailableEventType = @"updateAvailable";
17
18@interface EXUpdatesManager ()
19
20@property (nonatomic, strong) EXAppLoader *manifestAppLoader;
21
22@end
23
24@implementation EXUpdatesManager
25
26- (void)notifyApp:(EXKernelAppRecord *)appRecord
27ofDownloadWithManifest:(NSDictionary * _Nullable)manifest
28            isNew:(BOOL)isBundleNew
29            error:(NSError * _Nullable)error;
30{
31  NSDictionary *body;
32  NSDictionary *bodyLegacy;
33  if (error) {
34    body = @{
35             @"type": EXUpdatesErrorEventType,
36             @"message": error.localizedDescription
37             };
38  } else if (isBundleNew) {
39    if (!manifest) {
40      // prevent a crash, but this shouldn't ever happen
41      manifest = @{};
42    }
43    bodyLegacy = @{
44                   @"type": EXUpdatesDownloadFinishedEventType,
45                   @"manifest": manifest
46                   };
47    body = @{
48             @"type": EXUpdatesUpdateAvailableEventType,
49             @"manifest": manifest
50             };
51  } else {
52    body = @{
53             @"type": EXUpdatesNotAvailableEventType
54             };
55  }
56  RCTBridge *bridge = appRecord.appManager.reactBridge;
57  if (appRecord.status == kEXKernelAppRecordStatusRunning) {
58    // for SDK 38 and below
59    if ([self _doesBridgeSupportUpdatesModule:bridge]) {
60      [bridge.scopedModules.updates sendEventWithBody:bodyLegacy ?: body];
61    }
62    // for SDK 39+
63    [bridge enqueueJSCall:@"RCTDeviceEventEmitter.emit" args:@[EXUpdatesEventNameNew, body]];
64  }
65}
66
67# pragma mark - internal
68
69- (EXAppLoader *)_appLoaderWithScopedModule:(id)scopedModule
70{
71  NSString *experienceId = ((EXScopedBridgeModule *)scopedModule).experienceId;
72  return [self _appLoaderWithExperienceId:experienceId];
73}
74
75- (EXAppLoader *)_appLoaderWithExperienceId:(NSString *)experienceId
76{
77  EXKernelAppRecord *appRecord = [[EXKernel sharedInstance].appRegistry newestRecordWithExperienceId:experienceId];
78  return appRecord.appLoader;
79}
80
81# pragma mark - EXUpdatesBindingDelegate
82
83- (EXUpdatesConfig *)configForExperienceId:(NSString *)experienceId
84{
85  return [self _appLoaderWithExperienceId:experienceId].config;
86}
87
88- (id<EXUpdatesSelectionPolicy>)selectionPolicyForExperienceId:(NSString *)experienceId
89{
90  return [self _appLoaderWithExperienceId:experienceId].selectionPolicy;
91}
92
93- (nullable EXUpdatesUpdate *)launchedUpdateForExperienceId:(NSString *)experienceId
94{
95  return [self _appLoaderWithExperienceId:experienceId].appLauncher.launchedUpdate;
96}
97
98- (nullable NSDictionary *)assetFilesMapForExperienceId:(NSString *)experienceId
99{
100  return [self _appLoaderWithExperienceId:experienceId].appLauncher.assetFilesMap;
101}
102
103- (BOOL)isUsingEmbeddedAssetsForExperienceId:(NSString *)experienceId
104{
105  return NO;
106}
107
108- (BOOL)isStartedForExperienceId:(NSString *)experienceId
109{
110  return [self _appLoaderWithExperienceId:experienceId].appLauncher != nil;
111}
112
113- (BOOL)isEmergencyLaunchForExperienceId:(NSString *)experienceId
114{
115  return NO;
116}
117
118- (void)requestRelaunchForExperienceId:(NSString *)experienceId withCompletion:(EXUpdatesAppRelaunchCompletionBlock)completion
119{
120  [[EXKernel sharedInstance] reloadAppFromCacheWithExperienceId:experienceId];
121  completion(YES);
122}
123
124# pragma mark - EXUpdatesScopedModuleDelegate
125
126- (void)updatesModuleDidSelectReload:(id)scopedModule
127{
128  NSString *experienceId = ((EXScopedBridgeModule *)scopedModule).experienceId;
129  [[EXKernel sharedInstance] reloadAppWithExperienceId:experienceId];
130}
131
132- (void)updatesModuleDidSelectReloadFromCache:(id)scopedModule
133{
134  NSString *experienceId = ((EXScopedBridgeModule *)scopedModule).experienceId;
135  [[EXKernel sharedInstance] reloadAppFromCacheWithExperienceId:experienceId];
136}
137
138- (void)updatesModule:(id)scopedModule
139didRequestManifestWithCacheBehavior:(EXManifestCacheBehavior)cacheBehavior
140              success:(void (^)(NSDictionary * _Nonnull))success
141              failure:(void (^)(NSError * _Nonnull))failure
142{
143  if ([EXEnvironment sharedEnvironment].isDetached && ![EXEnvironment sharedEnvironment].areRemoteUpdatesEnabled) {
144    failure(RCTErrorWithMessage(@"Remote updates are disabled in app.json"));
145    return;
146  }
147  EXAppLoader *appLoader = [self _appLoaderWithScopedModule:scopedModule];
148  [appLoader fetchManifestWithCacheBehavior:cacheBehavior success:success failure:failure];
149  if (cacheBehavior == EXManifestPrepareToCache) {
150    _manifestAppLoader = appLoader;
151  }
152}
153
154- (void)updatesModule:(id)scopedModule
155didRequestBundleWithManifest:(NSDictionary *)manifest
156             progress:(void (^)(NSDictionary * _Nonnull))progressBlock
157              success:(void (^)(NSData * _Nonnull))success
158              failure:(void (^)(NSError * _Nonnull))failure
159{
160  if ([EXEnvironment sharedEnvironment].isDetached && ![EXEnvironment sharedEnvironment].areRemoteUpdatesEnabled) {
161    failure(RCTErrorWithMessage(@"Remote updates are disabled in app.json"));
162    return;
163  }
164  void (^progressDictBlock)(EXLoadingProgress * _Nonnull) = ^void(EXLoadingProgress * _Nonnull progress) {
165    progressBlock(@{
166                    @"status": progress.status,
167                    @"done": progress.done,
168                    @"total": progress.total
169                    });
170  };
171  EXAppLoader *appLoader = _manifestAppLoader ?: [self _appLoaderWithScopedModule:scopedModule];
172  [appLoader fetchJSBundleWithManifest:manifest
173                         cacheBehavior:EXCachedResourceWriteToCache
174                       timeoutInterval:kEXJSBundleTimeout
175                              progress:progressDictBlock
176                               success:^(NSData * _Nonnull data) {
177                                         if (self->_manifestAppLoader) {
178                                           [self->_manifestAppLoader writeManifestToCache];
179                                         }
180                                         self->_manifestAppLoader = nil;
181                                         success(data);
182                                       }
183                                 error:^(NSError * _Nonnull error) {
184                                         self->_manifestAppLoader = nil;
185                                         failure(error);
186                                       }];
187}
188
189- (BOOL)_doesBridgeSupportUpdatesModule:(RCTBridge *)bridge
190{
191  // sdk versions prior to 26 didn't include this module.
192  return ([bridge.scopedModules respondsToSelector:@selector(updates)]);
193}
194
195@end
196