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