1 // Copyright 2019 650 Industries. All rights reserved.
2 
3 // swiftlint:disable closure_body_length
4 // swiftlint:disable superfluous_else
5 
6 import ExpoModulesCore
7 
8 /**
9  * Exported module which provides to the JS runtime information about the currently running update
10  * and updates state, along with methods to check for and download new updates, reload with the
11  * newest downloaded update applied, and read/clear native log entries.
12  *
13  * Communicates with the updates hub (AppController in most apps, EXAppLoaderExpoUpdates in
14  * Expo Go and legacy standalone apps) via EXUpdatesService, an internal module which is overridden
15  * by EXUpdatesBinding, a scoped module, in Expo Go.
16  */
17 public final class UpdatesModule: Module {
18   private let updatesService: EXUpdatesModuleInterface?
19   private let methodQueue = UpdatesUtils.methodQueue
20 
21   public required init(appContext: AppContext) {
22     updatesService = appContext.legacyModule(implementing: EXUpdatesModuleInterface.self)
23     super.init(appContext: appContext)
24   }
25 
26   // swiftlint:disable cyclomatic_complexity
definitionnull27   public func definition() -> ModuleDefinition {
28     Name("ExpoUpdates")
29 
30     Constants {
31       let releaseChannel = updatesService?.config?.releaseChannel
32       let channel = updatesService?.config?.requestHeaders["expo-channel-name"] ?? ""
33       let runtimeVersion = updatesService?.config?.runtimeVersion ?? ""
34       let checkAutomatically = updatesService?.config?.checkOnLaunch.asString ?? CheckAutomaticallyConfig.Always.asString
35       let isMissingRuntimeVersion = updatesService?.config?.isMissingRuntimeVersion()
36 
37       guard let updatesService = updatesService,
38         updatesService.isStarted,
39         let launchedUpdate = updatesService.launchedUpdate else {
40         return [
41           "isEnabled": false,
42           "isEmbeddedLaunch": false,
43           "isMissingRuntimeVersion": isMissingRuntimeVersion,
44           "releaseChannel": releaseChannel,
45           "runtimeVersion": runtimeVersion,
46           "checkAutomatically": checkAutomatically,
47           "channel": channel
48         ]
49       }
50 
51       let commitTime = UInt64(floor(launchedUpdate.commitTime.timeIntervalSince1970 * 1000))
52       return [
53         "isEnabled": true,
54         "isEmbeddedLaunch": updatesService.isEmbeddedLaunch,
55         "isUsingEmbeddedAssets": updatesService.isUsingEmbeddedAssets,
56         "updateId": launchedUpdate.updateId.uuidString,
57         "manifest": launchedUpdate.manifest.rawManifestJSON(),
58         "localAssets": updatesService.assetFilesMap ?? [:],
59         "isEmergencyLaunch": updatesService.isEmergencyLaunch,
60         "isMissingRuntimeVersion": isMissingRuntimeVersion,
61         "releaseChannel": releaseChannel,
62         "runtimeVersion": runtimeVersion,
63         "checkAutomatically": checkAutomatically,
64         "channel": channel,
65         "commitTime": commitTime,
66         "nativeDebug": UpdatesUtils.isNativeDebuggingEnabled()
67       ]
68     }
69 
70     AsyncFunction("reload") { (promise: Promise) in
71       guard let updatesService = updatesService, let config = updatesService.config, config.isEnabled else {
72         throw UpdatesDisabledException()
73       }
74       guard updatesService.canRelaunch else {
75         throw UpdatesNotInitializedException()
76       }
77       updatesService.requestRelaunch { success in
78         if success {
79           promise.resolve(nil)
80         } else {
81           promise.reject(UpdatesReloadException())
82         }
83       }
84     }
85 
86     AsyncFunction("checkForUpdateAsync") { (promise: Promise) in
87       let maybeIsCheckForUpdateEnabled: Bool? = updatesService?.canCheckForUpdateAndFetchUpdate ?? true
88       guard maybeIsCheckForUpdateEnabled ?? false else {
89         promise.reject("ERR_UPDATES_CHECK", "checkForUpdateAsync() is not enabled")
90         return
91       }
92       UpdatesUtils.checkForUpdate { result in
93         if result["message"] != nil {
94           guard let message = result["message"] as? String else {
95             promise.reject("ERR_UPDATES_CHECK", "")
96             return
97           }
98           promise.reject("ERR_UPDATES_CHECK", message)
99           return
100         }
101         if result["manifest"] != nil {
102           promise.resolve([
103             "isAvailable": true,
104             "manifest": result["manifest"],
105             "isRollBackToEmbedded": false
106           ])
107           return
108         }
109         if result["isRollBackToEmbedded"] != nil {
110           promise.resolve([
111             "isAvailable": false,
112             "isRollBackToEmbedded": result["isRollBackToEmbedded"]
113           ])
114           return
115         }
116         if result["reason"] != nil {
117           promise.resolve([
118             "isAvailable": false,
119             "isRollBackToEmbedded": false,
120             "reason": result["reason"]
121           ])
122           return
123         }
124         promise.resolve([
125           "isAvailable": false,
126           "isRollBackToEmbedded": false
127         ])
128       }
129     }
130 
131     AsyncFunction("getExtraParamsAsync") { (promise: Promise) in
132       guard let updatesService = updatesService,
133         let config = updatesService.config,
134         config.isEnabled else {
135         throw UpdatesDisabledException()
136       }
137 
138       guard let scopeKey = config.scopeKey else {
139         throw Exception(name: "ERR_UPDATES_SCOPE_KEY", description: "Muse have scopeKey in config")
140       }
141 
142       updatesService.database.databaseQueue.async {
143         do {
144           promise.resolve(try updatesService.database.extraParams(withScopeKey: scopeKey))
145         } catch {
146           promise.reject(error)
147         }
148       }
149     }
150 
151     AsyncFunction("setExtraParamAsync") { (key: String, value: String?, promise: Promise) in
152       guard let updatesService = updatesService,
153         let config = updatesService.config,
154         config.isEnabled else {
155         throw UpdatesDisabledException()
156       }
157 
158       guard let scopeKey = config.scopeKey else {
159         throw Exception(name: "ERR_UPDATES_SCOPE_KEY", description: "Muse have scopeKey in config")
160       }
161 
162       updatesService.database.databaseQueue.async {
163         do {
164           try updatesService.database.setExtraParam(key: key, value: value, withScopeKey: scopeKey)
165           promise.resolve(nil)
166         } catch {
167           promise.reject(error)
168         }
169       }
170     }
171 
172     AsyncFunction("readLogEntriesAsync") { (maxAge: Int) -> [[String: Any]] in
173       // maxAge is in milliseconds, convert to seconds
174       do {
175         return try UpdatesLogReader().getLogEntries(newerThan: Date(timeIntervalSinceNow: TimeInterval(-1 * (maxAge / 1000))))
176       } catch {
177         throw Exception(name: "ERR_UPDATES_READ_LOGS", description: error.localizedDescription)
178       }
179     }
180 
181     AsyncFunction("clearLogEntriesAsync") { (promise: Promise) in
182       UpdatesLogReader().purgeLogEntries(olderThan: Date()) { error in
183         guard let error = error else {
184           promise.resolve(nil)
185           return
186         }
187         promise.reject("ERR_UPDATES_READ_LOGS", error.localizedDescription)
188       }
189     }
190 
191     AsyncFunction("fetchUpdateAsync") { (promise: Promise) in
192       let maybeIsCheckForUpdateEnabled: Bool? = updatesService?.canCheckForUpdateAndFetchUpdate ?? true
193       guard maybeIsCheckForUpdateEnabled ?? false else {
194         promise.reject("ERR_UPDATES_FETCH", "fetchUpdateAsync() is not enabled")
195         return
196       }
197       UpdatesUtils.fetchUpdate { result in
198         if result["message"] != nil {
199           guard let message = result["message"] as? String else {
200             promise.reject("ERR_UPDATES_FETCH", "")
201             return
202           }
203           promise.reject("ERR_UPDATES_FETCH", message)
204           return
205         } else {
206           promise.resolve(result)
207         }
208       }
209     }
210 
211     // Getter used internally by useUpdates()
212     // to initialize its state
213     AsyncFunction("getNativeStateMachineContextAsync") { (promise: Promise) in
214       let maybeIsCheckForUpdateEnabled: Bool? = updatesService?.canCheckForUpdateAndFetchUpdate ?? true
215       guard maybeIsCheckForUpdateEnabled ?? false else {
216         promise.resolve(UpdatesUtils.defaultNativeStateMachineContextJson())
217         return
218       }
219       UpdatesUtils.getNativeStateMachineContextJson { result in
220         if result["message"] != nil {
221           guard let message = result["message"] as? String else {
222             promise.reject("ERR_UPDATES_CHECK", "")
223             return
224           }
225           promise.reject("ERR_UPDATES_CHECK", message)
226           return
227         } else {
228           promise.resolve(result)
229         }
230       }
231     }
232   }
233   // swiftlint:enable cyclomatic_complexity
234 }
235 
236 // swiftlint:enable closure_body_length
237 // swiftlint:enable superfluous_else
238