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
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         promise.resolve(["isAvailable": false, "isRollBackToEmbedded": false])
117       }
118     }
119 
120     AsyncFunction("getExtraParamsAsync") { (promise: Promise) in
121       guard let updatesService = updatesService,
122         let config = updatesService.config,
123         config.isEnabled else {
124         throw UpdatesDisabledException()
125       }
126 
127       guard let scopeKey = config.scopeKey else {
128         throw Exception(name: "ERR_UPDATES_SCOPE_KEY", description: "Muse have scopeKey in config")
129       }
130 
131       updatesService.database.databaseQueue.async {
132         do {
133           promise.resolve(try updatesService.database.extraParams(withScopeKey: scopeKey))
134         } catch {
135           promise.reject(error)
136         }
137       }
138     }
139 
140     AsyncFunction("setExtraParamAsync") { (key: String, value: String?, promise: Promise) in
141       guard let updatesService = updatesService,
142         let config = updatesService.config,
143         config.isEnabled else {
144         throw UpdatesDisabledException()
145       }
146 
147       guard let scopeKey = config.scopeKey else {
148         throw Exception(name: "ERR_UPDATES_SCOPE_KEY", description: "Muse have scopeKey in config")
149       }
150 
151       updatesService.database.databaseQueue.async {
152         do {
153           try updatesService.database.setExtraParam(key: key, value: value, withScopeKey: scopeKey)
154           promise.resolve(nil)
155         } catch {
156           promise.reject(error)
157         }
158       }
159     }
160 
161     AsyncFunction("readLogEntriesAsync") { (maxAge: Int) -> [[String: Any]] in
162       // maxAge is in milliseconds, convert to seconds
163       do {
164         return try UpdatesLogReader().getLogEntries(newerThan: Date(timeIntervalSinceNow: TimeInterval(-1 * (maxAge / 1000))))
165       } catch {
166         throw Exception(name: "ERR_UPDATES_READ_LOGS", description: error.localizedDescription)
167       }
168     }
169 
170     AsyncFunction("clearLogEntriesAsync") { (promise: Promise) in
171       UpdatesLogReader().purgeLogEntries(olderThan: Date()) { error in
172         guard let error = error else {
173           promise.resolve(nil)
174           return
175         }
176         promise.reject("ERR_UPDATES_READ_LOGS", error.localizedDescription)
177       }
178     }
179 
180     AsyncFunction("fetchUpdateAsync") { (promise: Promise) in
181       let maybeIsCheckForUpdateEnabled: Bool? = updatesService?.canCheckForUpdateAndFetchUpdate ?? true
182       guard maybeIsCheckForUpdateEnabled ?? false else {
183         promise.reject("ERR_UPDATES_FETCH", "fetchUpdateAsync() is not enabled")
184         return
185       }
186       UpdatesUtils.fetchUpdate { result in
187         if result["message"] != nil {
188           guard let message = result["message"] as? String else {
189             promise.reject("ERR_UPDATES_FETCH", "")
190             return
191           }
192           promise.reject("ERR_UPDATES_FETCH", message)
193           return
194         } else {
195           promise.resolve(result)
196         }
197       }
198     }
199   }
200   // swiftlint:enable cyclomatic_complexity
201 }
202 
203 // swiftlint:enable closure_body_length
204 // swiftlint:enable superfluous_else
205