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