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