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