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