1 // Copyright 2015-present 650 Industries. All rights reserved.
2 
3 let motionGestureEnabledKey = "EXDevMenuMotionGestureEnabled"
4 let touchGestureEnabledKey = "EXDevMenuTouchGestureEnabled"
5 let keyCommandsEnabledKey = "EXDevMenuKeyCommandsEnabled"
6 let showsAtLaunchKey = "EXDevMenuShowsAtLaunch"
7 let isOnboardingFinishedKey = "EXDevMenuIsOnboardingFinished"
8 
9 @objc(DevMenuPreferences)
10 public class DevMenuPreferences: NSObject, RCTBridgeModule {
11   /**
12    Initializes dev menu preferences by registering user defaults
13    and applying some preferences to static classes like interceptors.
14    */
15   static func setup() {
16     UserDefaults.standard.register(defaults: [
17       motionGestureEnabledKey: true,
18       touchGestureEnabledKey: true,
19       keyCommandsEnabledKey: true,
20       showsAtLaunchKey: false,
21       isOnboardingFinishedKey: false
22     ])
23 
24     /**
25      We don't want to uninstall `DevMenuMotionInterceptor`, because otherwise, the app on shake gesture will bring up the dev-menu from the RN.
26      So we added `isEnabled` to disable it, but not uninstall.
27      */
28     DevMenuMotionInterceptor.isInstalled = true
29     DevMenuMotionInterceptor.isEnabled = DevMenuPreferences.motionGestureEnabled
30     DevMenuTouchInterceptor.isInstalled = DevMenuPreferences.touchGestureEnabled
31     DevMenuKeyCommandsInterceptor.isInstalled = DevMenuPreferences.keyCommandsEnabled
32   }
33 
34   /**
35    Whether to enable shake gesture.
36    */
37   static var motionGestureEnabled: Bool {
38     get {
39       return boolForKey(motionGestureEnabledKey)
40     }
41     set {
42       setBool(newValue, forKey: motionGestureEnabledKey)
43       DevMenuMotionInterceptor.isEnabled = newValue
44     }
45   }
46 
47   /**
48    Whether to enable three-finger long press gesture.
49    */
50   static var touchGestureEnabled: Bool {
51     get {
52       return boolForKey(touchGestureEnabledKey)
53     }
54     set {
55       setBool(newValue, forKey: touchGestureEnabledKey)
56       DevMenuTouchInterceptor.isInstalled = newValue
57     }
58   }
59 
60   /**
61    Whether to enable key commands.
62    */
63   static var keyCommandsEnabled: Bool {
64     get {
65       return boolForKey(keyCommandsEnabledKey)
66     }
67     set {
68       setBool(newValue, forKey: keyCommandsEnabledKey)
69       DevMenuKeyCommandsInterceptor.isInstalled = newValue
70     }
71   }
72 
73   /**
74    Whether to automatically show the dev menu once its delegate is set and the bridge is loaded.
75    */
76   static var showsAtLaunch: Bool {
77     get {
78       return DevMenuTestInterceptorManager.interceptor?.shouldShowAtLaunch ?? boolForKey(showsAtLaunchKey)
79     }
80     set {
81       setBool(newValue, forKey: showsAtLaunchKey)
82     }
83   }
84 
85   /**
86    Returns `true` only if the user finished onboarding, `false` otherwise.
87    This is now public because expo-dev-launcher needs access
88    in order to disable the onboarding popup for certain bundle URLs
89    */
90   public static var isOnboardingFinished: Bool {
91     get {
92       return DevMenuTestInterceptorManager.interceptor?.isOnboardingFinishedKey ?? boolForKey(isOnboardingFinishedKey)
93     }
94     set {
95       setBool(newValue, forKey: isOnboardingFinishedKey)
96     }
97   }
98 
99   /**
100    Serializes settings into a dictionary so they can be passed through the bridge.
101    */
102   static func serialize() -> [String: Any] {
103     return [
104       "motionGestureEnabled": DevMenuPreferences.motionGestureEnabled,
105       "touchGestureEnabled": DevMenuPreferences.touchGestureEnabled,
106       "keyCommandsEnabled": DevMenuPreferences.keyCommandsEnabled,
107       "showsAtLaunch": DevMenuPreferences.showsAtLaunch,
108       "isOnboardingFinished": DevMenuPreferences.isOnboardingFinished
109     ]
110   }
111 
112   static func setSettings(_ settings: [String: Any]) {
113     if let motionGestureEnabled = settings["motionGestureEnabled"] as? Bool {
114       DevMenuPreferences.motionGestureEnabled = motionGestureEnabled
115     }
116     if let touchGestureEnabled = settings["touchGestureEnabled"] as? Bool {
117       DevMenuPreferences.touchGestureEnabled = touchGestureEnabled
118     }
119     if let keyCommandsEnabled = settings["keyCommandsEnabled"] as? Bool {
120       DevMenuPreferences.keyCommandsEnabled = keyCommandsEnabled
121     }
122     if let showsAtLaunch = settings["showsAtLaunch"] as? Bool {
123       DevMenuPreferences.showsAtLaunch = showsAtLaunch
124     }
125   }
126 
127   // MARK - RCTBridgeModule
128 
129   public static func moduleName() -> String! {
130     return "DevMenuPreferences"
131   }
132 
133   public static func requiresMainQueueSetup() -> Bool {
134     return true
135   }
136 
137   @objc
138   func getPreferencesAsync(_ resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
139     resolve(DevMenuPreferences.serialize())
140   }
141 
142   @objc
143   func setPreferencesAsync(_ settings: [String: Any], resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
144     DevMenuPreferences.setSettings(settings)
145     resolve(nil)
146   }
147 }
148 
149 
150 
151 private func boolForKey(_ key: String) -> Bool {
152   return UserDefaults.standard.bool(forKey: key)
153 }
154 
155 private func setBool(_ value: Bool, forKey key: String) {
156   UserDefaults.standard.set(value, forKey: key)
157 }
158