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