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