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     // Ensures the universal UpdatesConfig can cast to versioned UpdatesConfig without exception in Swift
24     object_setClass(updatesService?.config, UpdatesConfig.self)
25     super.init(appContext: appContext)
26   }
27 
28   // swiftlint:disable cyclomatic_complexity
29   public func definition() -> ModuleDefinition {
30     Name("ExpoUpdates")
31 
32     Constants {
33       let releaseChannel = updatesService?.config?.releaseChannel
34       let channel = updatesService?.config?.requestHeaders["expo-channel-name"] ?? ""
35       let runtimeVersion = updatesService?.config?.runtimeVersion ?? ""
36       let checkAutomatically = updatesService?.config?.checkOnLaunch.asString ?? CheckAutomaticallyConfig.Always.asString
37       let isMissingRuntimeVersion = updatesService?.config?.isMissingRuntimeVersion()
38 
39       guard let updatesService = updatesService,
40         updatesService.isStarted,
41         let launchedUpdate = updatesService.launchedUpdate else {
42         return [
43           "isEnabled": false,
44           "isEmbeddedLaunch": false,
45           "isMissingRuntimeVersion": isMissingRuntimeVersion,
46           "releaseChannel": releaseChannel,
47           "runtimeVersion": runtimeVersion,
48           "checkAutomatically": checkAutomatically,
49           "channel": channel
50         ]
51       }
52 
53       let commitTime = UInt64(floor(launchedUpdate.commitTime.timeIntervalSince1970 * 1000))
54       return [
55         "isEnabled": true,
56         "isEmbeddedLaunch": updatesService.isEmbeddedLaunch,
57         "isUsingEmbeddedAssets": updatesService.isUsingEmbeddedAssets,
58         "updateId": launchedUpdate.updateId.uuidString,
59         "manifest": launchedUpdate.manifest.rawManifestJSON(),
60         "localAssets": updatesService.assetFilesMap ?? [:],
61         "isEmergencyLaunch": updatesService.isEmergencyLaunch,
62         "isMissingRuntimeVersion": isMissingRuntimeVersion,
63         "releaseChannel": releaseChannel,
64         "runtimeVersion": runtimeVersion,
65         "checkAutomatically": checkAutomatically,
66         "channel": channel,
67         "commitTime": commitTime,
68         "nativeDebug": UpdatesUtils.isNativeDebuggingEnabled()
69       ]
70     }
71 
72     AsyncFunction("reload") { (promise: Promise) in
73       guard let updatesService = updatesService, let config = updatesService.config, config.isEnabled else {
74         throw UpdatesDisabledException()
75       }
76       guard updatesService.canRelaunch else {
77         throw UpdatesNotInitializedException()
78       }
79       updatesService.requestRelaunch { success in
80         if success {
81           promise.resolve(nil)
82         } else {
83           promise.reject(UpdatesReloadException())
84         }
85       }
86     }
87 
88     AsyncFunction("checkForUpdateAsync") { (promise: Promise) in
89       let maybeIsCheckForUpdateEnabled: Bool? = updatesService?.canCheckForUpdateAndFetchUpdate ?? true
90       guard maybeIsCheckForUpdateEnabled ?? false else {
91         promise.reject("ERR_UPDATES_CHECK", "checkForUpdateAsync() is not enabled")
92         return
93       }
94       UpdatesUtils.checkForUpdate { result in
95         if result["message"] != nil {
96           guard let message = result["message"] as? String else {
97             promise.reject("ERR_UPDATES_CHECK", "")
98             return
99           }
100           promise.reject("ERR_UPDATES_CHECK", message)
101           return
102         }
103         if result["manifest"] != nil {
104           promise.resolve([
105             "isAvailable": true,
106             "manifest": result["manifest"]
107           ])
108           return
109         }
110         if result["isRollBackToEmbedded"] != nil {
111           promise.resolve([
112             "isAvailable": false,
113             "isRollBackToEmbedded": result["isRollBackToEmbedded"]
114           ])
115           return
116         }
117         promise.resolve(["isAvailable": false])
118       }
119     }
120 
121     AsyncFunction("getExtraParamsAsync") { (promise: Promise) in
122       guard let updatesService = updatesService,
123         let config = updatesService.config,
124         config.isEnabled else {
125         throw UpdatesDisabledException()
126       }
127 
128       guard let scopeKey = config.scopeKey else {
129         throw Exception(name: "ERR_UPDATES_SCOPE_KEY", description: "Muse have scopeKey in config")
130       }
131 
132       updatesService.database.databaseQueue.async {
133         do {
134           promise.resolve(try updatesService.database.extraParams(withScopeKey: scopeKey))
135         } catch {
136           promise.reject(error)
137         }
138       }
139     }
140 
141     AsyncFunction("setExtraParamAsync") { (key: String, value: String?, promise: Promise) in
142       guard let updatesService = updatesService,
143         let config = updatesService.config,
144         config.isEnabled else {
145         throw UpdatesDisabledException()
146       }
147 
148       guard let scopeKey = config.scopeKey else {
149         throw Exception(name: "ERR_UPDATES_SCOPE_KEY", description: "Muse have scopeKey in config")
150       }
151 
152       updatesService.database.databaseQueue.async {
153         do {
154           try updatesService.database.setExtraParam(key: key, value: value, withScopeKey: scopeKey)
155           promise.resolve(nil)
156         } catch {
157           promise.reject(error)
158         }
159       }
160     }
161 
162     AsyncFunction("readLogEntriesAsync") { (maxAge: Int) -> [[String: Any]] in
163       // maxAge is in milliseconds, convert to seconds
164       do {
165         return try UpdatesLogReader().getLogEntries(newerThan: Date(timeIntervalSinceNow: TimeInterval(-1 * (maxAge / 1000))))
166       } catch {
167         throw Exception(name: "ERR_UPDATES_READ_LOGS", description: error.localizedDescription)
168       }
169     }
170 
171     AsyncFunction("clearLogEntriesAsync") { (promise: Promise) in
172       UpdatesLogReader().purgeLogEntries(olderThan: Date()) { error in
173         guard let error = error else {
174           promise.resolve(nil)
175           return
176         }
177         promise.reject("ERR_UPDATES_READ_LOGS", error.localizedDescription)
178       }
179     }
180 
181     AsyncFunction("fetchUpdateAsync") { (promise: Promise) in
182       let maybeIsCheckForUpdateEnabled: Bool? = updatesService?.canCheckForUpdateAndFetchUpdate ?? true
183       guard maybeIsCheckForUpdateEnabled ?? false else {
184         promise.reject("ERR_UPDATES_FETCH", "fetchUpdateAsync() is not enabled")
185         return
186       }
187       UpdatesUtils.fetchUpdate { result in
188         if result["message"] != nil {
189           guard let message = result["message"] as? String else {
190             promise.reject("ERR_UPDATES_FETCH", "")
191             return
192           }
193           promise.reject("ERR_UPDATES_FETCH", message)
194           return
195         } else {
196           promise.resolve(result)
197         }
198       }
199     }
200   }
201   // swiftlint:enable cyclomatic_complexity
202 }
203 
204 // swiftlint:enable closure_body_length
205 // swiftlint:enable superfluous_else
206