1 // Copyright 2015-present 650 Industries. All rights reserved. 2 import SafariServices 3 4 @objc(DevMenuInternalModule) 5 public class DevMenuInternalModule: NSObject, RCTBridgeModule { 6 @objc 7 var redirectResolve: RCTPromiseResolveBlock? 8 @objc 9 var redirectReject: RCTPromiseRejectBlock? 10 @objc 11 var authSession: SFAuthenticationSession? 12 13 public static func moduleName() -> String! { 14 return "ExpoDevMenuInternal" 15 } 16 17 // Module DevMenuInternalModule requires main queue setup since it overrides `constantsToExport`. 18 public static func requiresMainQueueSetup() -> Bool { 19 return true; 20 } 21 22 private static var fontsWereLoaded = false; 23 private static let sessionKey = "expo-dev-menu.session" 24 private static let userLoginEvent = "expo.dev-menu.user-login" 25 private static let userLogoutEvent = "expo.dev-menu.user-logout" 26 private static let defaultScheme = "expo-dev-menu" 27 28 let manager: DevMenuManager 29 30 init(manager: DevMenuManager) { 31 self.manager = manager 32 } 33 34 // MARK: JavaScript API 35 36 @objc 37 public func constantsToExport() -> [AnyHashable : Any] { 38 #if targetEnvironment(simulator) 39 let doesDeviceSupportKeyCommands = true 40 #else 41 let doesDeviceSupportKeyCommands = false 42 #endif 43 return ["doesDeviceSupportKeyCommands": doesDeviceSupportKeyCommands] 44 } 45 46 @objc 47 func loadFontsAsync(_ resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { 48 if (DevMenuInternalModule.fontsWereLoaded) { 49 resolve(nil); 50 return; 51 } 52 53 let fonts = ["MaterialCommunityIcons", "Ionicons"] 54 for font in fonts { 55 guard let path = DevMenuUtils.resourcesBundle()?.path(forResource: font, ofType: "ttf") else { 56 reject("ERR_DEVMENU_CANNOT_FIND_FONT", "Font file for '\(font)' doesn't exist.", nil); 57 return; 58 } 59 guard let data = FileManager.default.contents(atPath: path) else { 60 reject("ERR_DEVMENU_CANNOT_OPEN_FONT_FILE", "Could not open '\(path)'.", nil); 61 return; 62 } 63 64 guard let provider = CGDataProvider(data: data as CFData) else { 65 reject("ERR_DEVMENU_CANNOT_CREATE_FONT_PROVIDER", "Could not create font provider for '\(font)'.", nil); 66 return; 67 } 68 guard let cgFont = CGFont(provider) else { 69 reject("ERR_DEVMENU_CANNOT_CREATE_FONT", "Could not create font for '\(font)'.", nil); 70 return; 71 } 72 73 var error: Unmanaged<CFError>? 74 if !CTFontManagerRegisterGraphicsFont(cgFont, &error) { 75 reject("ERR_DEVMENU_CANNOT_ADD_FONT", "Could not create font from loaded data for '\(font)'. '\(error.debugDescription)'.", nil) 76 return 77 } 78 } 79 80 DevMenuInternalModule.fontsWereLoaded = true 81 resolve(nil) 82 } 83 84 @objc 85 func fetchDataSourceAsync(_ dataSourceId: String?, resolve: @escaping RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { 86 guard let dataSourceId = dataSourceId else { 87 return reject("ERR_DEVMENU_DATA_SOURCE_FAILED", "DataSource ID not provided.", nil) 88 } 89 90 for dataSource in manager.devMenuDataSources { 91 if (dataSource.id == dataSourceId) { 92 dataSource.fetchData { data in 93 resolve(data.map { $0.serialize() }) 94 } 95 return; 96 } 97 } 98 99 return reject("ERR_DEVMENU_DATA_SOURCE_FAILED", "DataSource \(dataSourceId) not founded.", nil) 100 } 101 102 @objc 103 func dispatchCallableAsync(_ callableId: String?, args: [String : Any]?, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { 104 guard let callableId = callableId else { 105 return reject("ERR_DEVMENU_ACTION_FAILED", "Callable ID not provided.", nil) 106 } 107 manager.dispatchCallable(withId: callableId, args: args) 108 resolve(nil) 109 } 110 111 @objc 112 func hideMenu() { 113 manager.hideMenu() 114 } 115 116 @objc 117 func setOnboardingFinished(_ finished: Bool) { 118 DevMenuSettings.isOnboardingFinished = finished 119 } 120 121 @objc 122 func getSettingsAsync(_ resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { 123 resolve(DevMenuSettings.serialize()) 124 } 125 126 @objc 127 func setSettingsAsync(_ dict: [String: Any], resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { 128 if let motionGestureEnabled = dict["motionGestureEnabled"] as? Bool { 129 DevMenuSettings.motionGestureEnabled = motionGestureEnabled 130 } 131 if let touchGestureEnabled = dict["touchGestureEnabled"] as? Bool { 132 DevMenuSettings.touchGestureEnabled = touchGestureEnabled 133 } 134 if let keyCommandsEnabled = dict["keyCommandsEnabled"] as? Bool { 135 DevMenuSettings.keyCommandsEnabled = keyCommandsEnabled 136 } 137 if let showsAtLaunch = dict["showsAtLaunch"] as? Bool { 138 DevMenuSettings.showsAtLaunch = showsAtLaunch 139 } 140 } 141 142 @objc 143 func openDevMenuFromReactNative() { 144 guard let rctDevMenu = manager.session?.bridge.devMenu else { 145 return 146 } 147 148 DispatchQueue.main.async { 149 rctDevMenu.show() 150 } 151 } 152 153 @objc 154 func onScreenChangeAsync(_ currentScreen: String?, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { 155 manager.setCurrentScreen(currentScreen) 156 resolve(nil) 157 } 158 159 @objc 160 func setSessionAsync(_ session: Dictionary<String, Any>?, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { 161 do { 162 try manager.expoSessionDelegate.setSessionAsync(session) 163 resolve(nil) 164 } catch let error { 165 reject("ERR_DEVMENU_CANNOT_SAVE_SESSION", error.localizedDescription, error); 166 } 167 } 168 169 @objc 170 func restoreSessionAsync(_ resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { 171 resolve(manager.expoSessionDelegate.restoreSession()) 172 } 173 174 @objc 175 func getAuthSchemeAsync(_ resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { 176 guard let urlTypesArray = Bundle.main.infoDictionary?["CFBundleURLTypes"] as? [NSDictionary] else { 177 resolve(DevMenuInternalModule.defaultScheme) 178 return 179 } 180 181 if (urlTypesArray 182 .contains(where: { ($0["CFBundleURLSchemes"] as? [String] ?? []) 183 .contains(DevMenuInternalModule.defaultScheme) })) { 184 resolve(DevMenuInternalModule.defaultScheme) 185 return 186 } 187 188 for urlType in urlTypesArray { 189 guard let schemes = urlType["CFBundleURLSchemes"] as? [String] else { 190 continue 191 } 192 193 if schemes.first != nil { 194 resolve(schemes.first) 195 return 196 } 197 } 198 199 resolve(DevMenuInternalModule.defaultScheme) 200 } 201 } 202