// Copyright 2019 650 Industries. All rights reserved. // swiftlint:disable closure_body_length // swiftlint:disable superfluous_else import ExpoModulesCore /** * Exported module which provides to the JS runtime information about the currently running update * and updates state, along with methods to check for and download new updates, reload with the * newest downloaded update applied, and read/clear native log entries. * * Communicates with the updates hub (AppController in most apps, EXAppLoaderExpoUpdates in * Expo Go and legacy standalone apps) via EXUpdatesService, an internal module which is overridden * by EXUpdatesBinding, a scoped module, in Expo Go. */ public final class UpdatesModule: Module { private let updatesService: EXUpdatesModuleInterface? private let methodQueue = UpdatesUtils.methodQueue public required init(appContext: AppContext) { updatesService = appContext.legacyModule(implementing: EXUpdatesModuleInterface.self) super.init(appContext: appContext) } // swiftlint:disable cyclomatic_complexity public func definition() -> ModuleDefinition { Name("ExpoUpdates") Constants { let releaseChannel = updatesService?.config?.releaseChannel let channel = updatesService?.config?.requestHeaders["expo-channel-name"] ?? "" let runtimeVersion = updatesService?.config?.runtimeVersion ?? "" let checkAutomatically = updatesService?.config?.checkOnLaunch.asString ?? CheckAutomaticallyConfig.Always.asString let isMissingRuntimeVersion = updatesService?.config?.isMissingRuntimeVersion() guard let updatesService = updatesService, updatesService.isStarted, let launchedUpdate = updatesService.launchedUpdate else { return [ "isEnabled": false, "isEmbeddedLaunch": false, "isMissingRuntimeVersion": isMissingRuntimeVersion, "releaseChannel": releaseChannel, "runtimeVersion": runtimeVersion, "checkAutomatically": checkAutomatically, "channel": channel ] } let commitTime = UInt64(floor(launchedUpdate.commitTime.timeIntervalSince1970 * 1000)) return [ "isEnabled": true, "isEmbeddedLaunch": updatesService.isEmbeddedLaunch, "isUsingEmbeddedAssets": updatesService.isUsingEmbeddedAssets, "updateId": launchedUpdate.updateId.uuidString, "manifest": launchedUpdate.manifest.rawManifestJSON(), "localAssets": updatesService.assetFilesMap ?? [:], "isEmergencyLaunch": updatesService.isEmergencyLaunch, "isMissingRuntimeVersion": isMissingRuntimeVersion, "releaseChannel": releaseChannel, "runtimeVersion": runtimeVersion, "checkAutomatically": checkAutomatically, "channel": channel, "commitTime": commitTime, "nativeDebug": UpdatesUtils.isNativeDebuggingEnabled() ] } AsyncFunction("reload") { (promise: Promise) in guard let updatesService = updatesService, let config = updatesService.config, config.isEnabled else { throw UpdatesDisabledException() } guard updatesService.canRelaunch else { throw UpdatesNotInitializedException() } updatesService.requestRelaunch { success in if success { promise.resolve(nil) } else { promise.reject(UpdatesReloadException()) } } } AsyncFunction("checkForUpdateAsync") { (promise: Promise) in let maybeIsCheckForUpdateEnabled: Bool? = updatesService?.canCheckForUpdateAndFetchUpdate ?? true guard maybeIsCheckForUpdateEnabled ?? false else { promise.reject("ERR_UPDATES_CHECK", "checkForUpdateAsync() is not enabled") return } UpdatesUtils.checkForUpdate { result in if result["message"] != nil { guard let message = result["message"] as? String else { promise.reject("ERR_UPDATES_CHECK", "") return } promise.reject("ERR_UPDATES_CHECK", message) return } if result["manifest"] != nil { promise.resolve([ "isAvailable": true, "manifest": result["manifest"], "isRollBackToEmbedded": false ]) return } if result["isRollBackToEmbedded"] != nil { promise.resolve([ "isAvailable": false, "isRollBackToEmbedded": result["isRollBackToEmbedded"] ]) return } promise.resolve(["isAvailable": false, "isRollBackToEmbedded": false]) } } AsyncFunction("getExtraParamsAsync") { (promise: Promise) in guard let updatesService = updatesService, let config = updatesService.config, config.isEnabled else { throw UpdatesDisabledException() } guard let scopeKey = config.scopeKey else { throw Exception(name: "ERR_UPDATES_SCOPE_KEY", description: "Muse have scopeKey in config") } updatesService.database.databaseQueue.async { do { promise.resolve(try updatesService.database.extraParams(withScopeKey: scopeKey)) } catch { promise.reject(error) } } } AsyncFunction("setExtraParamAsync") { (key: String, value: String?, promise: Promise) in guard let updatesService = updatesService, let config = updatesService.config, config.isEnabled else { throw UpdatesDisabledException() } guard let scopeKey = config.scopeKey else { throw Exception(name: "ERR_UPDATES_SCOPE_KEY", description: "Muse have scopeKey in config") } updatesService.database.databaseQueue.async { do { try updatesService.database.setExtraParam(key: key, value: value, withScopeKey: scopeKey) promise.resolve(nil) } catch { promise.reject(error) } } } AsyncFunction("readLogEntriesAsync") { (maxAge: Int) -> [[String: Any]] in // maxAge is in milliseconds, convert to seconds do { return try UpdatesLogReader().getLogEntries(newerThan: Date(timeIntervalSinceNow: TimeInterval(-1 * (maxAge / 1000)))) } catch { throw Exception(name: "ERR_UPDATES_READ_LOGS", description: error.localizedDescription) } } AsyncFunction("clearLogEntriesAsync") { (promise: Promise) in UpdatesLogReader().purgeLogEntries(olderThan: Date()) { error in guard let error = error else { promise.resolve(nil) return } promise.reject("ERR_UPDATES_READ_LOGS", error.localizedDescription) } } AsyncFunction("fetchUpdateAsync") { (promise: Promise) in let maybeIsCheckForUpdateEnabled: Bool? = updatesService?.canCheckForUpdateAndFetchUpdate ?? true guard maybeIsCheckForUpdateEnabled ?? false else { promise.reject("ERR_UPDATES_FETCH", "fetchUpdateAsync() is not enabled") return } UpdatesUtils.fetchUpdate { result in if result["message"] != nil { guard let message = result["message"] as? String else { promise.reject("ERR_UPDATES_FETCH", "") return } promise.reject("ERR_UPDATES_FETCH", message) return } else { promise.resolve(result) } } } // Getter used internally by @expo/use-updates useUpdates() // to initialize its state AsyncFunction("getNativeStateMachineContextAsync") { (promise: Promise) in let maybeIsCheckForUpdateEnabled: Bool? = updatesService?.canCheckForUpdateAndFetchUpdate ?? true guard maybeIsCheckForUpdateEnabled ?? false else { promise.resolve(UpdatesUtils.defaultNativeStateMachineContextJson()) return } UpdatesUtils.getNativeStateMachineContextJson { result in if result["message"] != nil { guard let message = result["message"] as? String else { promise.reject("ERR_UPDATES_CHECK", "") return } promise.reject("ERR_UPDATES_CHECK", message) return } else { promise.resolve(result) } } } } // swiftlint:enable cyclomatic_complexity } // swiftlint:enable closure_body_length // swiftlint:enable superfluous_else