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 public override init() { 31 self.manager = DevMenuManager.shared 32 } 33 34 init(manager: DevMenuManager) { 35 self.manager = manager 36 } 37 38 // MARK: JavaScript API 39 40 @objc 41 public func constantsToExport() -> [AnyHashable: Any] { 42 #if targetEnvironment(simulator) 43 let doesDeviceSupportKeyCommands = true 44 #else 45 let doesDeviceSupportKeyCommands = false 46 #endif 47 return ["doesDeviceSupportKeyCommands": doesDeviceSupportKeyCommands] 48 } 49 50 @objc 51 func loadFontsAsync(_ resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { 52 if DevMenuInternalModule.fontsWereLoaded { 53 resolve(nil) 54 return 55 } 56 57 let fonts = ["MaterialCommunityIcons", "Ionicons"] 58 for font in fonts { 59 guard let path = DevMenuUtils.resourcesBundle()?.path(forResource: font, ofType: "ttf") else { 60 reject("ERR_DEVMENU_CANNOT_FIND_FONT", "Font file for '\(font)' doesn't exist.", nil) 61 return 62 } 63 guard let data = FileManager.default.contents(atPath: path) else { 64 reject("ERR_DEVMENU_CANNOT_OPEN_FONT_FILE", "Could not open '\(path)'.", nil) 65 return 66 } 67 68 guard let provider = CGDataProvider(data: data as CFData) else { 69 reject("ERR_DEVMENU_CANNOT_CREATE_FONT_PROVIDER", "Could not create font provider for '\(font)'.", nil) 70 return 71 } 72 guard let cgFont = CGFont(provider) else { 73 reject("ERR_DEVMENU_CANNOT_CREATE_FONT", "Could not create font for '\(font)'.", nil) 74 return 75 } 76 77 var error: Unmanaged<CFError>? 78 if !CTFontManagerRegisterGraphicsFont(cgFont, &error) { 79 reject("ERR_DEVMENU_CANNOT_ADD_FONT", "Could not create font from loaded data for '\(font)'. '\(error.debugDescription)'.", nil) 80 return 81 } 82 } 83 84 DevMenuInternalModule.fontsWereLoaded = true 85 resolve(nil) 86 } 87 88 @objc 89 func fetchDataSourceAsync(_ dataSourceId: String?, resolve: @escaping RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { 90 guard let dataSourceId = dataSourceId else { 91 return reject("ERR_DEVMENU_DATA_SOURCE_FAILED", "DataSource ID not provided.", nil) 92 } 93 94 for dataSource in manager.devMenuDataSources { 95 if dataSource.id == dataSourceId { 96 dataSource.fetchData { data in 97 resolve(data.map { $0.serialize() }) 98 } 99 return 100 } 101 } 102 103 return reject("ERR_DEVMENU_DATA_SOURCE_FAILED", "DataSource \(dataSourceId) not founded.", nil) 104 } 105 106 @objc 107 func dispatchCallableAsync(_ callableId: String?, args: [String: Any]?, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { 108 guard let callableId = callableId else { 109 return reject("ERR_DEVMENU_ACTION_FAILED", "Callable ID not provided.", nil) 110 } 111 manager.dispatchCallable(withId: callableId, args: args) 112 resolve(nil) 113 } 114 115 @objc 116 func hideMenu() { 117 manager.hideMenu() 118 } 119 120 @objc 121 func setOnboardingFinished(_ finished: Bool) { 122 DevMenuSettings.isOnboardingFinished = finished 123 } 124 125 @objc 126 func getSettingsAsync(_ resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { 127 resolve(DevMenuSettings.serialize()) 128 } 129 130 @objc 131 func setSettingsAsync(_ dict: [String: Any], resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { 132 if let motionGestureEnabled = dict["motionGestureEnabled"] as? Bool { 133 DevMenuSettings.motionGestureEnabled = motionGestureEnabled 134 } 135 if let touchGestureEnabled = dict["touchGestureEnabled"] as? Bool { 136 DevMenuSettings.touchGestureEnabled = touchGestureEnabled 137 } 138 if let keyCommandsEnabled = dict["keyCommandsEnabled"] as? Bool { 139 DevMenuSettings.keyCommandsEnabled = keyCommandsEnabled 140 } 141 if let showsAtLaunch = dict["showsAtLaunch"] as? Bool { 142 DevMenuSettings.showsAtLaunch = showsAtLaunch 143 } 144 } 145 146 @objc 147 func openDevMenuFromReactNative() { 148 guard let rctDevMenu = manager.session?.bridge.devMenu else { 149 return 150 } 151 152 DispatchQueue.main.async { 153 rctDevMenu.show() 154 } 155 } 156 157 @objc 158 func onScreenChangeAsync(_ currentScreen: String?, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { 159 manager.setCurrentScreen(currentScreen) 160 resolve(nil) 161 } 162 163 @objc 164 func setSessionAsync(_ session: [String: Any]?, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { 165 do { 166 try manager.expoSessionDelegate.setSessionAsync(session) 167 resolve(nil) 168 } catch let error { 169 reject("ERR_DEVMENU_CANNOT_SAVE_SESSION", error.localizedDescription, error) 170 } 171 } 172 173 @objc 174 func restoreSessionAsync(_ resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { 175 resolve(manager.expoSessionDelegate.restoreSession()) 176 } 177 178 @objc 179 func getAuthSchemeAsync(_ resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { 180 guard let urlTypesArray = Bundle.main.infoDictionary?["CFBundleURLTypes"] as? [NSDictionary] else { 181 resolve(DevMenuInternalModule.defaultScheme) 182 return 183 } 184 185 if (urlTypesArray 186 .contains(where: { ($0["CFBundleURLSchemes"] as? [String] ?? []) 187 .contains(DevMenuInternalModule.defaultScheme) })) { 188 resolve(DevMenuInternalModule.defaultScheme) 189 return 190 } 191 192 for urlType in urlTypesArray { 193 guard let schemes = urlType["CFBundleURLSchemes"] as? [String] else { 194 continue 195 } 196 197 if schemes.first != nil { 198 resolve(schemes.first) 199 return 200 } 201 } 202 203 resolve(DevMenuInternalModule.defaultScheme) 204 } 205 206 @objc 207 func getBuildInfoAsync(_ resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { 208 let bridge = manager.delegate?.appBridge?(forDevMenuManager: self.manager) 209 let manifest = manager.session?.appInfo ?? [:] 210 211 let buildInfo = EXDevMenuBuildInfo.getFor(bridge as! RCTBridge, andManifest: manifest as Any as! [AnyHashable : Any]) 212 213 resolve([ 214 "appName": buildInfo["appName"], 215 "appIcon": buildInfo["appIcon"], 216 "appVersion": buildInfo["appVersion"], 217 "runtimeVersion": buildInfo["runtimeVersion"], 218 "sdkVersion": buildInfo["sdkVersion"], 219 "hostUrl": buildInfo["hostUrl"], 220 ]) 221 } 222 223 @objc 224 func getDevSettingsAsync(_ resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { 225 let bridge = manager.delegate?.appBridge?(forDevMenuManager: self.manager) 226 227 if let devSettings = bridge?.module(forName: "DevSettings") as? RCTDevSettings { 228 resolve([ 229 "isDebuggingRemotely": devSettings.isDebuggingRemotely, 230 "isElementInspectorShown": devSettings.isElementInspectorShown, 231 "isHotLoadingEnabled": devSettings.isHotLoadingEnabled, 232 "isPerfMonitorShown": devSettings.isPerfMonitorShown, 233 ]) 234 } 235 } 236 } 237