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