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