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