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