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