1 // Copyright 2019 650 Industries. All rights reserved.
2 
3 // swiftlint:disable closure_body_length
4 // swiftlint:disable superfluous_else
5 
6 import ABI49_0_0ExpoModulesCore
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, ABI49_0_0EXAppLoaderExpoUpdates in
14  * Expo Go and legacy standalone apps) via ABI49_0_0EXUpdatesService, an internal module which is overridden
15  * by ABI49_0_0EXUpdatesBinding, a scoped module, in Expo Go.
16  */
17 public final class UpdatesModule: Module {
18   private let updatesService: ABI49_0_0EXUpdatesModuleInterface?
19   private let methodQueue = UpdatesUtils.methodQueue
20 
21   public required init(appContext: AppContext) {
22     updatesService = appContext.legacyModule(implementing: ABI49_0_0EXUpdatesModuleInterface.self)
23     super.init(appContext: appContext)
24   }
25 
26   // swiftlint:disable cyclomatic_complexity
27   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           ])
106           return
107         }
108         if result["isRollBackToEmbedded"] != nil {
109           promise.resolve([
110             "isAvailable": false,
111             "isRollBackToEmbedded": result["isRollBackToEmbedded"]
112           ])
113           return
114         }
115         promise.resolve(["isAvailable": false])
116       }
117     }
118 
119     AsyncFunction("getExtraParamsAsync") { (promise: Promise) in
120       guard let updatesService = updatesService,
121         let config = updatesService.config,
122         config.isEnabled else {
123         throw UpdatesDisabledException()
124       }
125 
126       guard let scopeKey = config.scopeKey else {
127         throw Exception(name: "ERR_UPDATES_SCOPE_KEY", description: "Muse have scopeKey in config")
128       }
129 
130       updatesService.database.databaseQueue.async {
131         do {
132           promise.resolve(try updatesService.database.extraParams(withScopeKey: scopeKey))
133         } catch {
134           promise.reject(error)
135         }
136       }
137     }
138 
139     AsyncFunction("setExtraParamAsync") { (key: String, value: String?, promise: Promise) in
140       guard let updatesService = updatesService,
141         let config = updatesService.config,
142         config.isEnabled else {
143         throw UpdatesDisabledException()
144       }
145 
146       guard let scopeKey = config.scopeKey else {
147         throw Exception(name: "ERR_UPDATES_SCOPE_KEY", description: "Muse have scopeKey in config")
148       }
149 
150       updatesService.database.databaseQueue.async {
151         do {
152           try updatesService.database.setExtraParam(key: key, value: value, withScopeKey: scopeKey)
153           promise.resolve(nil)
154         } catch {
155           promise.reject(error)
156         }
157       }
158     }
159 
160     AsyncFunction("readLogEntriesAsync") { (maxAge: Int) -> [[String: Any]] in
161       // maxAge is in milliseconds, convert to seconds
162       do {
163         return try UpdatesLogReader().getLogEntries(newerThan: Date(timeIntervalSinceNow: TimeInterval(-1 * (maxAge / 1000))))
164       } catch {
165         throw Exception(name: "ERR_UPDATES_READ_LOGS", description: error.localizedDescription)
166       }
167     }
168 
169     AsyncFunction("clearLogEntriesAsync") { (promise: Promise) in
170       UpdatesLogReader().purgeLogEntries(olderThan: Date()) { error in
171         guard let error = error else {
172           promise.resolve(nil)
173           return
174         }
175         promise.reject("ERR_UPDATES_READ_LOGS", error.localizedDescription)
176       }
177     }
178 
179     AsyncFunction("fetchUpdateAsync") { (promise: Promise) in
180       let maybeIsCheckForUpdateEnabled: Bool? = updatesService?.canCheckForUpdateAndFetchUpdate ?? true
181       guard maybeIsCheckForUpdateEnabled ?? false else {
182         promise.reject("ERR_UPDATES_FETCH", "fetchUpdateAsync() is not enabled")
183         return
184       }
185       UpdatesUtils.fetchUpdate { result in
186         if result["message"] != nil {
187           guard let message = result["message"] as? String else {
188             promise.reject("ERR_UPDATES_FETCH", "")
189             return
190           }
191           promise.reject("ERR_UPDATES_FETCH", message)
192           return
193         } else {
194           promise.resolve(result)
195         }
196       }
197     }
198   }
199   // swiftlint:enable cyclomatic_complexity
200 }
201 
202 // swiftlint:enable closure_body_length
203 // swiftlint:enable superfluous_else
204