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 setOnboardingFinished(_ finished: Bool) {
80     DevMenuPreferences.isOnboardingFinished = finished
81   }
82 
83   @objc
84   func openDevMenuFromReactNative() {
85     guard let rctDevMenu = manager.currentBridge?.devMenu else {
86       return
87     }
88 
89     DispatchQueue.main.async {
90       rctDevMenu.show()
91     }
92   }
93 
94   @objc
95   func onScreenChangeAsync(_ currentScreen: String?, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
96     manager.setCurrentScreen(currentScreen)
97     resolve(nil)
98   }
99 
100   @objc
101   func fireCallback(_ name: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
102 
103     if (!manager.registeredCallbacks.contains(name)) {
104       return reject("ERR_DEVMENU_ACTION_FAILED", "\(name) is not a registered callback", nil)
105     }
106     manager.sendEventToDelegateBridge("registeredCallbackFired", data: name)
107 
108     return resolve(nil)
109   }
110 }
111