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