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