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 
24   let manager: DevMenuManager
25 
26   init(manager: DevMenuManager) {
27     self.manager = manager
28   }
29 
30   // MARK: JavaScript API
31 
32   @objc
33   func constantsToExport() -> [String : Any] {
34 #if targetEnvironment(simulator)
35     let doesDeviceSupportKeyCommands = true
36 #else
37     let doesDeviceSupportKeyCommands = false
38 #endif
39     return ["doesDeviceSupportKeyCommands": doesDeviceSupportKeyCommands]
40   }
41 
42   @objc
43   func loadFontsAsync(_ resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
44     if (DevMenuInternalModule.fontsWereLoaded) {
45       resolve(nil);
46       return;
47     }
48 
49     let fonts = ["MaterialCommunityIcons", "Ionicons"]
50     for font in fonts {
51       guard let path = DevMenuUtils.resourcesBundle()?.path(forResource: font, ofType: "ttf") else {
52         reject("ERR_DEVMENU_CANNOT_FIND_FONT", "Font file for '\(font)' doesn't exist.", nil);
53         return;
54       }
55       guard let data = FileManager.default.contents(atPath: path) else {
56         reject("ERR_DEVMENU_CANNOT_OPEN_FONT_FILE", "Could not open '\(path)'.", nil);
57         return;
58       }
59 
60       guard let provider = CGDataProvider(data: data as CFData) else {
61         reject("ERR_DEVMENU_CANNOT_CREATE_FONT_PROVIDER", "Could not create font provider for '\(font)'.", nil);
62         return;
63       }
64       guard let cgFont = CGFont(provider) else {
65         reject("ERR_DEVMENU_CANNOT_CREATE_FONT", "Could not create font for '\(font)'.", nil);
66         return;
67       }
68 
69       var error: Unmanaged<CFError>?
70       if !CTFontManagerRegisterGraphicsFont(cgFont, &error) {
71         reject("ERR_DEVMENU_CANNOT_ADD_FONT", "Could not create font from loaded data for '\(font)'. '\(error.debugDescription)'.", nil)
72         return
73       }
74     }
75 
76     DevMenuInternalModule.fontsWereLoaded = true
77     resolve(nil)
78   }
79 
80   @objc
81   func dispatchActionAsync(_ actionId: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
82     if actionId == nil {
83       return reject("ERR_DEVMENU_ACTION_FAILED", "Action ID not provided.", nil)
84     }
85     manager.dispatchAction(withId: actionId)
86     resolve(nil)
87   }
88 
89   @objc
90   func hideMenu() {
91     manager.hideMenu()
92   }
93 
94   @objc
95   func setOnboardingFinished(_ finished: Bool) {
96     DevMenuSettings.isOnboardingFinished = finished
97   }
98 
99   @objc
100   func getSettingsAsync(_ resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
101     resolve(DevMenuSettings.serialize())
102   }
103 
104   @objc
105   func setSettingsAsync(_ dict: [String: Any], resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
106     if let motionGestureEnabled = dict["motionGestureEnabled"] as? Bool {
107       DevMenuSettings.motionGestureEnabled = motionGestureEnabled
108     }
109     if let touchGestureEnabled = dict["touchGestureEnabled"] as? Bool {
110       DevMenuSettings.touchGestureEnabled = touchGestureEnabled
111     }
112     if let keyCommandsEnabled = dict["keyCommandsEnabled"] as? Bool {
113       DevMenuSettings.keyCommandsEnabled = keyCommandsEnabled
114     }
115     if let showsAtLaunch = dict["showsAtLaunch"] as? Bool {
116       DevMenuSettings.showsAtLaunch = showsAtLaunch
117     }
118   }
119 
120   @objc
121   func openDevMenuFromReactNative() {
122     guard let rctDevMenu = manager.session?.bridge.devMenu else {
123       return
124     }
125 
126     DispatchQueue.main.async {
127       rctDevMenu.show()
128     }
129   }
130 
131   @objc
132   func onScreenChangeAsync(_ currentScreen: String?, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
133     manager.setCurrentScreen(currentScreen)
134     resolve(nil)
135   }
136 
137   @objc
138   func saveAsync(_ key: String, data: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
139     UserDefaults.standard.set(data, forKey: key)
140     resolve(nil)
141   }
142 
143   @objc
144   func getAsync(_ key: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
145     resolve(UserDefaults.standard.string(forKey: key))
146   }
147 }
148