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