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