1// Copyright 2015-present 650 Industries. All rights reserved.
2
3#import "EXEnvironment.h"
4#import "EXHomeModule.h"
5#import "EXSession.h"
6#import "EXUnversioned.h"
7#import "EXClientReleaseType.h"
8#import "EXKernelDevKeyCommands.h"
9
10#import <React/RCTEventDispatcher.h>
11
12@interface EXHomeModule ()
13
14@property (nonatomic, assign) BOOL hasListeners;
15@property (nonatomic, strong) NSMutableDictionary *eventSuccessBlocks;
16@property (nonatomic, strong) NSMutableDictionary *eventFailureBlocks;
17@property (nonatomic, strong) NSArray * _Nonnull sdkVersions;
18@property (nonatomic, weak) id<EXHomeModuleDelegate> delegate;
19
20@end
21
22@implementation EXHomeModule
23
24+ (NSString *)moduleName { return @"ExponentKernel"; }
25
26- (instancetype)initWithExperienceId:(NSString *)experienceId kernelServiceDelegate:(id)kernelServiceInstance params:(NSDictionary *)params
27{
28  if (self = [super initWithExperienceId:experienceId kernelServiceDelegate:kernelServiceInstance params:params]) {
29    _eventSuccessBlocks = [NSMutableDictionary dictionary];
30    _eventFailureBlocks = [NSMutableDictionary dictionary];
31    _sdkVersions = params[@"constants"][@"supportedExpoSdks"];
32    _delegate = kernelServiceInstance;
33
34    // Register keyboard commands like Cmd+D for the simulator.
35    [[EXKernelDevKeyCommands sharedInstance] registerDevCommands];
36  }
37  return self;
38}
39
40+ (BOOL)requiresMainQueueSetup
41{
42  return NO;
43}
44
45- (NSDictionary *)constantsToExport
46{
47  return @{ @"sdkVersions": _sdkVersions,
48            @"IOSClientReleaseType": [EXClientReleaseType clientReleaseType] };
49}
50
51#pragma mark - RCTEventEmitter methods
52
53- (NSArray<NSString *> *)supportedEvents
54{
55  return @[];
56}
57
58/**
59 *  Override this method to avoid the [self supportedEvents] validation
60 */
61- (void)sendEventWithName:(NSString *)eventName body:(id)body
62{
63  // Note that this could be a versioned bridge!
64  [self.bridge enqueueJSCall:@"RCTDeviceEventEmitter.emit"
65                        args:body ? @[eventName, body] : @[eventName]];
66}
67
68#pragma mark -
69
70- (void)dispatchJSEvent:(NSString *)eventName body:(NSDictionary *)eventBody onSuccess:(void (^)(NSDictionary *))success onFailure:(void (^)(NSString *))failure
71{
72  NSString *qualifiedEventName = [NSString stringWithFormat:@"ExponentKernel.%@", eventName];
73  NSMutableDictionary *qualifiedEventBody = (eventBody) ? [eventBody mutableCopy] : [NSMutableDictionary dictionary];
74
75  if (success && failure) {
76    NSString *eventId = [[NSUUID UUID] UUIDString];
77    [_eventSuccessBlocks setObject:success forKey:eventId];
78    [_eventFailureBlocks setObject:failure forKey:eventId];
79    [qualifiedEventBody setObject:eventId forKey:@"eventId"];
80  }
81
82  [self sendEventWithName:qualifiedEventName body:qualifiedEventBody];
83}
84
85/**
86 * Requests JavaScript side to start closing the dev menu (start the animation or so).
87 * Fully closes the dev menu once it receives a response from that event.
88 */
89- (void)requestToCloseDevMenu
90{
91  __weak typeof(self) weakSelf = self;
92  void (^close)(id) = ^(id arg){
93    __strong typeof(weakSelf) strongSelf = weakSelf;
94    if (strongSelf->_delegate) {
95      [strongSelf->_delegate homeModuleDidSelectCloseMenu:strongSelf];
96    }
97  };
98  [self dispatchJSEvent:@"requestToCloseDevMenu" body:nil onSuccess:close onFailure:close];
99}
100
101/**
102 *  Duplicates Linking.openURL but does not validate that this is an exponent URL;
103 *  in other words, we just take your word for it and never hand it off to iOS.
104 *  Used by the home screen URL bar.
105 */
106RCT_EXPORT_METHOD(openURL:(NSURL *)URL
107                  resolve:(RCTPromiseResolveBlock)resolve
108                  reject:(__unused RCTPromiseRejectBlock)reject)
109{
110  if (URL) {
111    [_delegate homeModule:self didOpenUrl:URL.absoluteString];
112    resolve(@YES);
113  } else {
114    NSError *err = [NSError errorWithDomain:EX_UNVERSIONED(@"EXKernelErrorDomain") code:-1 userInfo:@{ NSLocalizedDescriptionKey: @"Cannot open a nil url" }];
115    reject(@"E_INVALID_URL", err.localizedDescription, err);
116  }
117}
118
119/**
120 * Returns boolean value determining whether the current app supports developer tools.
121 */
122RCT_REMAP_METHOD(doesCurrentTaskEnableDevtoolsAsync,
123                 doesCurrentTaskEnableDevtoolsWithResolver:(RCTPromiseResolveBlock)resolve
124                 reject:(RCTPromiseRejectBlock)reject)
125{
126  if (_delegate) {
127    resolve(@([_delegate homeModuleShouldEnableDevtools:self]));
128  } else {
129    // don't reject, just disable devtools
130    resolve(@NO);
131  }
132}
133
134/**
135 * Gets a dictionary of dev menu options available in the currently shown experience,
136 * If the experience doesn't support developer tools just returns an empty response.
137 */
138RCT_REMAP_METHOD(getDevMenuItemsToShowAsync,
139                 getDevMenuItemsToShowWithResolver:(RCTPromiseResolveBlock)resolve
140                 reject:(RCTPromiseRejectBlock)reject)
141{
142  if (_delegate && [_delegate homeModuleShouldEnableDevtools:self]) {
143    resolve([_delegate devMenuItemsForHomeModule:self]);
144  } else {
145    // don't reject, just show no devtools
146    resolve(@{});
147  }
148}
149
150/**
151 * Function called every time the dev menu option is selected.
152 */
153RCT_EXPORT_METHOD(selectDevMenuItemWithKeyAsync:(NSString *)key)
154{
155  if (_delegate) {
156    [_delegate homeModule:self didSelectDevMenuItemWithKey:key];
157  }
158}
159
160/**
161 * Reloads currently shown app with the manifest.
162 */
163RCT_EXPORT_METHOD(reloadAppAsync)
164{
165  if (_delegate) {
166    [_delegate homeModuleDidSelectRefresh:self];
167  }
168}
169
170/**
171 * Immediately closes the dev menu if it's visible.
172 * Note: It skips the animation that would have been applied by the JS side.
173 */
174RCT_EXPORT_METHOD(closeDevMenuAsync)
175{
176  if (_delegate) {
177    [_delegate homeModuleDidSelectCloseMenu:self];
178  }
179}
180
181/**
182 * Goes back to the home app.
183 */
184RCT_EXPORT_METHOD(goToHomeAsync)
185{
186  if (_delegate) {
187    [_delegate homeModuleDidSelectGoToHome:self];
188  }
189}
190
191/**
192 * Opens QR scanner to open another app by scanning its QR code.
193 */
194RCT_EXPORT_METHOD(selectQRReader)
195{
196  if (_delegate) {
197    [_delegate homeModuleDidSelectQRReader:self];
198  }
199}
200
201RCT_REMAP_METHOD(getSessionAsync,
202                 getSessionAsync:(RCTPromiseResolveBlock)resolve
203                 rejecter:(RCTPromiseRejectBlock)reject)
204{
205  NSDictionary *session = [[EXSession sharedInstance] session];
206  resolve(session);
207}
208
209RCT_REMAP_METHOD(setSessionAsync,
210                 setSessionAsync:(NSDictionary *)session
211                 resolver:(RCTPromiseResolveBlock)resolve
212                 rejecter:(RCTPromiseRejectBlock)reject)
213{
214  NSError *error;
215  BOOL success = [[EXSession sharedInstance] saveSessionToKeychain:session error:&error];
216  if (success) {
217    resolve(nil);
218  } else {
219    reject(@"ERR_SESSION_NOT_SAVED", @"Could not save session", error);
220  }
221}
222
223RCT_REMAP_METHOD(removeSessionAsync,
224                 removeSessionAsync:(RCTPromiseResolveBlock)resolve
225                 rejecter:(RCTPromiseRejectBlock)reject)
226{
227  NSError *error;
228  BOOL success = [[EXSession sharedInstance] deleteSessionFromKeychainWithError:&error];
229  if (success) {
230    resolve(nil);
231  } else {
232    reject(@"ERR_SESSION_NOT_REMOVED", @"Could not remove session", error);
233  }
234}
235
236/**
237 * Checks whether the dev menu onboarding is already finished.
238 * Onboarding is a screen that shows the dev menu to the user that opens any experience for the first time.
239*/
240RCT_REMAP_METHOD(getIsOnboardingFinishedAsync,
241                 getIsOnboardingFinishedWithResolver:(RCTPromiseResolveBlock)resolve
242                 rejecter:(RCTPromiseRejectBlock)reject)
243{
244  if (_delegate) {
245    BOOL isFinished = [_delegate homeModuleShouldFinishNux:self];
246    resolve(@(isFinished));
247  } else {
248    resolve(@(NO));
249  }
250}
251
252/**
253 * Sets appropriate setting in user defaults that user's onboarding has finished.
254 */
255RCT_REMAP_METHOD(setIsOnboardingFinishedAsync,
256                 setIsOnboardingFinished:(BOOL)isOnboardingFinished)
257{
258  if (_delegate) {
259    [_delegate homeModule:self didFinishNux:isOnboardingFinished];
260  }
261}
262
263/**
264 * Called when the native event has succeeded on the JS side.
265 */
266RCT_REMAP_METHOD(onEventSuccess,
267                 eventId:(NSString *)eventId
268                 body:(NSDictionary *)body)
269{
270  void (^success)(NSDictionary *) = [_eventSuccessBlocks objectForKey:eventId];
271  if (success) {
272    success(body);
273    [_eventSuccessBlocks removeObjectForKey:eventId];
274    [_eventFailureBlocks removeObjectForKey:eventId];
275  }
276}
277
278/**
279 * Called when the native event has failed on the JS side.
280 */
281RCT_REMAP_METHOD(onEventFailure,
282                 eventId:(NSString *)eventId
283                 message:(NSString *)message)
284{
285  void (^failure)(NSString *) = [_eventFailureBlocks objectForKey:eventId];
286  if (failure) {
287    failure(message);
288    [_eventSuccessBlocks removeObjectForKey:eventId];
289    [_eventFailureBlocks removeObjectForKey:eventId];
290  }
291}
292
293@end
294