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 func constantsToExport() -> [String : 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 dispatchActionAsync(_ actionId: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { 86 if actionId == nil { 87 return reject("ERR_DEVMENU_ACTION_FAILED", "Action ID not provided.", nil) 88 } 89 manager.dispatchAction(withId: actionId) 90 resolve(nil) 91 } 92 93 @objc 94 func hideMenu() { 95 manager.hideMenu() 96 } 97 98 @objc 99 func setOnboardingFinished(_ finished: Bool) { 100 DevMenuSettings.isOnboardingFinished = finished 101 } 102 103 @objc 104 func getSettingsAsync(_ resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { 105 resolve(DevMenuSettings.serialize()) 106 } 107 108 @objc 109 func setSettingsAsync(_ dict: [String: Any], resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { 110 if let motionGestureEnabled = dict["motionGestureEnabled"] as? Bool { 111 DevMenuSettings.motionGestureEnabled = motionGestureEnabled 112 } 113 if let touchGestureEnabled = dict["touchGestureEnabled"] as? Bool { 114 DevMenuSettings.touchGestureEnabled = touchGestureEnabled 115 } 116 if let keyCommandsEnabled = dict["keyCommandsEnabled"] as? Bool { 117 DevMenuSettings.keyCommandsEnabled = keyCommandsEnabled 118 } 119 if let showsAtLaunch = dict["showsAtLaunch"] as? Bool { 120 DevMenuSettings.showsAtLaunch = showsAtLaunch 121 } 122 } 123 124 @objc 125 func openDevMenuFromReactNative() { 126 guard let rctDevMenu = manager.session?.bridge.devMenu else { 127 return 128 } 129 130 DispatchQueue.main.async { 131 rctDevMenu.show() 132 } 133 } 134 135 @objc 136 func onScreenChangeAsync(_ currentScreen: String?, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { 137 manager.setCurrentScreen(currentScreen) 138 resolve(nil) 139 } 140 141 @objc 142 func setSessionAsync(_ session: Dictionary<String, Any>?, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { 143 do { 144 try manager.expoSessionDelegate.setSessionAsync(session) 145 resolve(nil) 146 } catch let error { 147 reject("ERR_DEVMENU_CANNOT_SAVE_SESSION", error.localizedDescription, error); 148 } 149 } 150 151 @objc 152 func restoreSessionAsync(_ resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { 153 resolve(manager.expoSessionDelegate.restoreSession()) 154 } 155 156 @objc 157 func getAuthSchemeAsync(_ resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { 158 guard let urlTypesArray = Bundle.main.infoDictionary?["CFBundleURLTypes"] as? [NSDictionary] else { 159 resolve(DevMenuInternalModule.defaultScheme) 160 return 161 } 162 163 if (urlTypesArray 164 .contains(where: { ($0["CFBundleURLSchemes"] as? [String] ?? []) 165 .contains(DevMenuInternalModule.defaultScheme) })) { 166 resolve(DevMenuInternalModule.defaultScheme) 167 return 168 } 169 170 for urlType in urlTypesArray { 171 guard let schemes = urlType["CFBundleURLSchemes"] as? [String] else { 172 continue 173 } 174 175 if schemes.first != nil { 176 resolve(schemes.first) 177 return 178 } 179 } 180 181 resolve(DevMenuInternalModule.defaultScheme) 182 } 183 } 184