1 // Copyright 2015-present 650 Industries. All rights reserved.
2 
3 import SafariServices
4 import React
5 
6 @objc(DevMenuInternalModule)
7 public class DevMenuInternalModule: NSObject, RCTBridgeModule {
8   public static func moduleName() -> String! {
9     return "ExpoDevMenuInternal"
10   }
11 
12   // Module DevMenuInternalModule requires main queue setup since it overrides `constantsToExport`.
13   public static func requiresMainQueueSetup() -> Bool {
14     return true
15   }
16 
17   let manager: DevMenuManager
18 
19   public override init() {
20     self.manager = DevMenuManager.shared
21   }
22 
23   init(manager: DevMenuManager) {
24     self.manager = manager
25   }
26 
27   // MARK: JavaScript API
28   @objc
29   public func constantsToExport() -> [AnyHashable: Any] {
30 #if targetEnvironment(simulator)
31     let doesDeviceSupportKeyCommands = true
32 #else
33     let doesDeviceSupportKeyCommands = false
34 #endif
35     return [
36       "doesDeviceSupportKeyCommands": doesDeviceSupportKeyCommands,
37     ]
38   }
39 
40   @objc
41   func fetchDataSourceAsync(_ dataSourceId: String?, resolve: @escaping RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
42     guard let dataSourceId = dataSourceId else {
43       return reject("ERR_DEVMENU_DATA_SOURCE_FAILED", "DataSource ID not provided.", nil)
44     }
45 
46     for dataSource in manager.devMenuDataSources {
47       if dataSource.id == dataSourceId {
48         dataSource.fetchData { data in
49           resolve(data.map { $0.serialize() })
50         }
51         return
52       }
53     }
54 
55     return reject("ERR_DEVMENU_DATA_SOURCE_FAILED", "DataSource \(dataSourceId) not founded.", nil)
56   }
57 
58   @objc
59   func dispatchCallableAsync(_ callableId: String?, args: [String: Any]?, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
60     guard let callableId = callableId else {
61       return reject("ERR_DEVMENU_ACTION_FAILED", "Callable ID not provided.", nil)
62     }
63     manager.dispatchCallable(withId: callableId, args: args)
64     resolve(nil)
65   }
66 
67   @objc
68   func loadFontsAsync(_ resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
69     manager.loadFonts()
70     resolve(nil)
71   }
72 
73   @objc
74   func hideMenu() {
75     manager.hideMenu()
76   }
77 
78   @objc
79   func closeMenu() {
80     manager.closeMenu()
81   }
82 
83   @objc
84   func setOnboardingFinished(_ finished: Bool) {
85     DevMenuPreferences.isOnboardingFinished = finished
86   }
87 
88   @objc
89   func openDevMenuFromReactNative() {
90     guard let rctDevMenu = manager.currentBridge?.devMenu else {
91       return
92     }
93 
94     DispatchQueue.main.async {
95       rctDevMenu.show()
96     }
97   }
98 
99   @objc
100   func onScreenChangeAsync(_ currentScreen: String?, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
101     manager.setCurrentScreen(currentScreen)
102     resolve(nil)
103   }
104 
105   @objc
106   func fireCallback(_ name: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
107     guard let callback = manager.registeredCallbacks.first(where: { $0.name == name }) else {
108       return reject("ERR_DEVMENU_ACTION_FAILED", "\(name) is not a registered callback", nil)
109     }
110 
111     manager.sendEventToDelegateBridge("registeredCallbackFired", data: name)
112     if callback.shouldCollapse {
113       closeMenu()
114     }
115     return resolve(nil)
116   }
117 }
118