@capawesome/capacitor-live-update 8.5.0 → 8.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CapawesomeCapacitorLiveUpdate.podspec +15 -0
- package/Package.swift +21 -5
- package/README.md +38 -5
- package/android/build.gradle +19 -0
- package/android/consumer-rules.pro +5 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdate.java +129 -55
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdatePlugin.java +31 -1
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdatePreferences.java +12 -14
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/options/DownloadBundleOptions.java +11 -5
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/options/FetchLatestBundleOptions.java +23 -1
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/FetchLatestBundleResult.java +30 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/providers/ionic/LiveUpdateIonicManager.kt +229 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/providers/ionic/LiveUpdateIonicPreferences.kt +85 -0
- package/dist/docs.json +1 -1
- package/dist/esm/definitions.d.ts +3 -0
- package/dist/esm/definitions.js.map +1 -1
- package/ios/Plugin/Classes/Options/DownloadBundleOptions.swift +7 -6
- package/ios/Plugin/Classes/Options/FetchLatestBundleOptions.swift +19 -3
- package/ios/Plugin/Classes/Results/FetchLatestBundleResult.swift +24 -0
- package/ios/Plugin/Enums/CustomError.swift +13 -0
- package/ios/Plugin/LiveUpdate.swift +66 -39
- package/ios/Plugin/LiveUpdatePlugin.swift +2 -2
- package/ios/Plugin/Providers/Ionic/LiveUpdateIonicManager.swift +187 -0
- package/ios/Plugin/Providers/Ionic/LiveUpdateIonicPreferences.swift +81 -0
- package/ios/Plugin/Providers/Ionic/LiveUpdatePlugin+LiveUpdateProvider.swift +15 -0
- package/package.json +2 -1
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
#if CAPAWESOME_INCLUDE_IONIC_PROVIDER
|
|
2
|
+
import Foundation
|
|
3
|
+
import Capacitor
|
|
4
|
+
import LiveUpdateProvider
|
|
5
|
+
|
|
6
|
+
/// Ionic Live Update Provider manager backed by the Capawesome live update plugin.
|
|
7
|
+
///
|
|
8
|
+
/// Each manager instance is scoped by its resolved app ID and channel so that multiple shells
|
|
9
|
+
/// (Portals or Federated Capacitor apps) can persist their own active bundle without colliding
|
|
10
|
+
/// with each other or with the standalone plugin's `currentBundleId` / `nextBundleId` state.
|
|
11
|
+
///
|
|
12
|
+
/// Provider configuration keys (V1):
|
|
13
|
+
/// - `appId` (optional) — Capawesome Cloud app UUID; falls back to plugin config, required in either
|
|
14
|
+
/// - `channel` (optional) — channel to sync; falls back to plugin config
|
|
15
|
+
/// - `publicKey`, `serverDomain`, `httpTimeout` (optional) — same as the plugin config;
|
|
16
|
+
/// only applied to the headless instance created by `init(configuration:)`
|
|
17
|
+
public final class LiveUpdateIonicManager: ProviderManager {
|
|
18
|
+
public private(set) var latestAppDirectory: URL?
|
|
19
|
+
|
|
20
|
+
private let appId: String
|
|
21
|
+
private let channel: String?
|
|
22
|
+
private let liveUpdate: LiveUpdate
|
|
23
|
+
private let preferences: LiveUpdateIonicPreferences
|
|
24
|
+
private var syncedBundleId: String?
|
|
25
|
+
|
|
26
|
+
/// Creates a manager without a running Capacitor plugin, e.g. for Ionic Portals hosts
|
|
27
|
+
/// that construct the manager directly and attach it to a Portal via
|
|
28
|
+
/// `liveUpdateSource: .provider(manager:)`. Uses a headless `LiveUpdate` instance with
|
|
29
|
+
/// the default configuration, overridden by the configuration dictionary.
|
|
30
|
+
public convenience init(configuration: [String: Any]) throws {
|
|
31
|
+
let config = try LiveUpdateIonicManager.buildConfig(configuration: configuration)
|
|
32
|
+
try self.init(configuration: configuration, liveUpdate: LiveUpdate(config: config))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
init(configuration: [String: Any], liveUpdate: LiveUpdate) throws {
|
|
36
|
+
guard let appId = configuration["appId"] as? String ?? liveUpdate.getAppId(), !appId.isEmpty else {
|
|
37
|
+
throw ProviderError.invalidConfiguration(message: CustomError.appIdMissing.localizedDescription)
|
|
38
|
+
}
|
|
39
|
+
let channel = configuration["channel"] as? String ?? liveUpdate.getChannel()
|
|
40
|
+
self.appId = appId
|
|
41
|
+
self.channel = channel
|
|
42
|
+
self.liveUpdate = liveUpdate
|
|
43
|
+
self.preferences = LiveUpdateIonicPreferences(stateKey: Self.buildStateKey(appId: appId, channel: channel))
|
|
44
|
+
preferences.resetLastSyncedBundleIdIfVersionChanged(versionCode: liveUpdate.getVersionCode())
|
|
45
|
+
restoreSyncedBundle()
|
|
46
|
+
deleteSupersededBundles()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private static func buildStateKey(appId: String, channel: String?) -> String {
|
|
50
|
+
guard let channel = channel, !channel.isEmpty else {
|
|
51
|
+
return appId
|
|
52
|
+
}
|
|
53
|
+
return appId + "." + channel
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private static func buildConfig(configuration: [String: Any]) throws -> LiveUpdateConfig {
|
|
57
|
+
var config = LiveUpdateConfig()
|
|
58
|
+
if let publicKey = try optionalValue(configuration, key: "publicKey", type: String.self) {
|
|
59
|
+
config.publicKey = publicKey
|
|
60
|
+
}
|
|
61
|
+
if let serverDomain = try optionalValue(configuration, key: "serverDomain", type: String.self) {
|
|
62
|
+
config.serverDomain = serverDomain
|
|
63
|
+
}
|
|
64
|
+
if let httpTimeout = try optionalValue(configuration, key: "httpTimeout", type: NSNumber.self) {
|
|
65
|
+
config.httpTimeout = httpTimeout.intValue
|
|
66
|
+
}
|
|
67
|
+
return config
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private static func optionalValue<T>(_ configuration: [String: Any], key: String, type: T.Type) throws -> T? {
|
|
71
|
+
guard let value = configuration[key] else {
|
|
72
|
+
return nil
|
|
73
|
+
}
|
|
74
|
+
guard let typedValue = value as? T else {
|
|
75
|
+
let message = CustomError.configurationValueInvalid(key: key).localizedDescription
|
|
76
|
+
throw ProviderError.invalidConfiguration(message: message)
|
|
77
|
+
}
|
|
78
|
+
return typedValue
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
public func sync() async throws -> (any ProviderSyncResult)? {
|
|
82
|
+
do {
|
|
83
|
+
let result = try await liveUpdate.fetchLatestBundle(FetchLatestBundleOptions(appId: appId, bundleId: syncedBundleId, channel: channel))
|
|
84
|
+
|
|
85
|
+
// No update available; nothing to report.
|
|
86
|
+
guard let bundleId = result.getBundleId(), bundleId != syncedBundleId else {
|
|
87
|
+
return nil
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let directory: URL
|
|
91
|
+
if let existingDirectory = liveUpdate.getBundleDirectory(bundleId: bundleId) {
|
|
92
|
+
directory = existingDirectory
|
|
93
|
+
} else {
|
|
94
|
+
directory = try await downloadBundle(bundleId: bundleId, result: result)
|
|
95
|
+
}
|
|
96
|
+
applySyncedBundle(bundleId: bundleId, directory: directory)
|
|
97
|
+
return MetadataSyncResult(metadata: buildMetadata(bundleId: bundleId, customProperties: result.getCustomProperties()))
|
|
98
|
+
} catch {
|
|
99
|
+
CAPLog.print("[", LiveUpdatePlugin.tag, "] Ionic provider sync failed: ", error.localizedDescription)
|
|
100
|
+
throw error
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/// Does not delete the previous bundle, because a visible portal may still serve it.
|
|
105
|
+
/// It is recorded as superseded and deleted on the next launch instead.
|
|
106
|
+
private func applySyncedBundle(bundleId: String, directory: URL) {
|
|
107
|
+
syncedBundleId = bundleId
|
|
108
|
+
latestAppDirectory = directory
|
|
109
|
+
preferences.setLastSyncedBundleId(bundleId, versionCode: liveUpdate.getVersionCode())
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private func buildMetadata(bundleId: String, customProperties: [String: Any]?) -> [String: Any] {
|
|
113
|
+
var metadata: [String: Any] = ["bundleId": bundleId]
|
|
114
|
+
if let channel = channel {
|
|
115
|
+
metadata["channel"] = channel
|
|
116
|
+
}
|
|
117
|
+
if let customProperties = customProperties, !customProperties.isEmpty {
|
|
118
|
+
metadata["customProperties"] = customProperties
|
|
119
|
+
}
|
|
120
|
+
return metadata
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/// Deletes the superseded bundles that no manager points to anymore. Bundles that were not
|
|
124
|
+
/// superseded by a manager are never deleted, because they may belong to the plugin's JS API.
|
|
125
|
+
private func deleteSupersededBundles() {
|
|
126
|
+
let supersededBundleIds = preferences.getSupersededBundleIds()
|
|
127
|
+
if supersededBundleIds.isEmpty {
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
let lastSyncedBundleIds = preferences.getLastSyncedBundleIdsOfAllManagers()
|
|
131
|
+
let deletedBundleIds = supersededBundleIds.filter { bundleId in
|
|
132
|
+
!lastSyncedBundleIds.contains(bundleId) && liveUpdate.tryDeleteUnusedBundle(bundleId: bundleId)
|
|
133
|
+
}
|
|
134
|
+
preferences.removeSupersededBundleIds(deletedBundleIds)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/// Downloads the bundle, or awaits a running download of the same bundle, and returns its directory.
|
|
138
|
+
private func downloadBundle(bundleId: String, result: FetchLatestBundleResult) async throws -> URL {
|
|
139
|
+
guard let downloadUrl = result.getDownloadUrl(), !downloadUrl.isEmpty else {
|
|
140
|
+
throw CustomError.downloadUrlMissing
|
|
141
|
+
}
|
|
142
|
+
let downloadOptions = DownloadBundleOptions(
|
|
143
|
+
artifactType: result.getArtifactType() ?? .zip,
|
|
144
|
+
bundleId: bundleId,
|
|
145
|
+
checksum: result.getChecksum(),
|
|
146
|
+
signature: result.getSignature(),
|
|
147
|
+
url: downloadUrl
|
|
148
|
+
)
|
|
149
|
+
try await BundleDownloads.shared.run(bundleId: bundleId) { [liveUpdate] in
|
|
150
|
+
try await liveUpdate.downloadBundle(downloadOptions)
|
|
151
|
+
}
|
|
152
|
+
guard let directory = liveUpdate.getBundleDirectory(bundleId: bundleId) else {
|
|
153
|
+
throw CustomError.bundleDirectoryNotFound
|
|
154
|
+
}
|
|
155
|
+
return directory
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/// Restores the last synced bundle, if it is still on disk, so the host can load it offline
|
|
159
|
+
/// and the next sync reports it as the bundle in use.
|
|
160
|
+
private func restoreSyncedBundle() {
|
|
161
|
+
guard let bundleId = preferences.getLastSyncedBundleId(),
|
|
162
|
+
let directory = liveUpdate.getBundleDirectory(bundleId: bundleId) else {
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
syncedBundleId = bundleId
|
|
166
|
+
latestAppDirectory = directory
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/// De-duplicates concurrent downloads of the same bundle across all managers in the process.
|
|
171
|
+
/// A second caller for a bundle that is already downloading awaits the running download.
|
|
172
|
+
private actor BundleDownloads {
|
|
173
|
+
static let shared = BundleDownloads()
|
|
174
|
+
|
|
175
|
+
private var tasks: [String: Task<Void, Error>] = [:]
|
|
176
|
+
|
|
177
|
+
func run(bundleId: String, operation: @escaping @Sendable () async throws -> Void) async throws {
|
|
178
|
+
if let task = tasks[bundleId] {
|
|
179
|
+
return try await task.value
|
|
180
|
+
}
|
|
181
|
+
let task = Task { try await operation() }
|
|
182
|
+
tasks[bundleId] = task
|
|
183
|
+
defer { tasks[bundleId] = nil }
|
|
184
|
+
try await task.value
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
#endif
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#if CAPAWESOME_INCLUDE_IONIC_PROVIDER
|
|
2
|
+
import Foundation
|
|
3
|
+
|
|
4
|
+
/// Persisted state of one provider manager, scoped by its state key (`<appId>` or `<appId>.<channel>`).
|
|
5
|
+
///
|
|
6
|
+
/// All managers share the same `UserDefaults`, so a manager can see which bundles
|
|
7
|
+
/// the other managers still point at.
|
|
8
|
+
final class LiveUpdateIonicPreferences {
|
|
9
|
+
private static let keyPrefix = "CapawesomeLiveUpdateIonicProvider."
|
|
10
|
+
private static let lastSyncedBundleIdPrefix = keyPrefix + "lastSyncedBundleId."
|
|
11
|
+
private static let lastVersionCodePrefix = keyPrefix + "lastVersionCode."
|
|
12
|
+
private static let supersededBundleIdsPrefix = keyPrefix + "supersededBundleIds."
|
|
13
|
+
|
|
14
|
+
private let userDefaults: UserDefaults
|
|
15
|
+
private let lastSyncedBundleIdKey: String
|
|
16
|
+
private let lastVersionCodeKey: String
|
|
17
|
+
private let supersededBundleIdsKey: String
|
|
18
|
+
|
|
19
|
+
init(userDefaults: UserDefaults = .standard, stateKey: String) {
|
|
20
|
+
self.userDefaults = userDefaults
|
|
21
|
+
self.lastSyncedBundleIdKey = Self.lastSyncedBundleIdPrefix + stateKey
|
|
22
|
+
self.lastVersionCodeKey = Self.lastVersionCodePrefix + stateKey
|
|
23
|
+
self.supersededBundleIdsKey = Self.supersededBundleIdsPrefix + stateKey
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
func getLastSyncedBundleId() -> String? {
|
|
27
|
+
return userDefaults.string(forKey: lastSyncedBundleIdKey)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/// - Returns: The last synced bundle IDs of all managers.
|
|
31
|
+
func getLastSyncedBundleIdsOfAllManagers() -> Set<String> {
|
|
32
|
+
let bundleIds = userDefaults.dictionaryRepresentation()
|
|
33
|
+
.filter { $0.key.hasPrefix(Self.lastSyncedBundleIdPrefix) }
|
|
34
|
+
.compactMap { $0.value as? String }
|
|
35
|
+
return Set(bundleIds)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// - Returns: The bundles this manager no longer points at and that may be deleted once unused.
|
|
39
|
+
func getSupersededBundleIds() -> Set<String> {
|
|
40
|
+
return Set(userDefaults.stringArray(forKey: supersededBundleIdsKey) ?? [])
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
func removeSupersededBundleIds(_ bundleIds: Set<String>) {
|
|
44
|
+
if bundleIds.isEmpty {
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
setSupersededBundleIds(getSupersededBundleIds().subtracting(bundleIds))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// Forgets the last synced bundle if it was synced on another app version code,
|
|
51
|
+
/// because the bundle was built for that native version.
|
|
52
|
+
func resetLastSyncedBundleIdIfVersionChanged(versionCode: String) {
|
|
53
|
+
guard let lastSyncedBundleId = getLastSyncedBundleId(),
|
|
54
|
+
let lastVersionCode = userDefaults.string(forKey: lastVersionCodeKey),
|
|
55
|
+
lastVersionCode != versionCode else {
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
setSupersededBundleIds(getSupersededBundleIds().union([lastSyncedBundleId]))
|
|
59
|
+
userDefaults.removeObject(forKey: lastSyncedBundleIdKey)
|
|
60
|
+
userDefaults.removeObject(forKey: lastVersionCodeKey)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/// Points this manager at the given bundle, synced on the given app version code,
|
|
64
|
+
/// and records the previous bundle as superseded.
|
|
65
|
+
func setLastSyncedBundleId(_ bundleId: String, versionCode: String) {
|
|
66
|
+
if let previousBundleId = getLastSyncedBundleId(), previousBundleId != bundleId {
|
|
67
|
+
setSupersededBundleIds(getSupersededBundleIds().union([previousBundleId]))
|
|
68
|
+
}
|
|
69
|
+
userDefaults.set(bundleId, forKey: lastSyncedBundleIdKey)
|
|
70
|
+
userDefaults.set(versionCode, forKey: lastVersionCodeKey)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private func setSupersededBundleIds(_ bundleIds: Set<String>) {
|
|
74
|
+
if bundleIds.isEmpty {
|
|
75
|
+
userDefaults.removeObject(forKey: supersededBundleIdsKey)
|
|
76
|
+
} else {
|
|
77
|
+
userDefaults.set(Array(bundleIds), forKey: supersededBundleIdsKey)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
#endif
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#if CAPAWESOME_INCLUDE_IONIC_PROVIDER
|
|
2
|
+
import Foundation
|
|
3
|
+
import LiveUpdateProvider
|
|
4
|
+
|
|
5
|
+
extension LiveUpdatePlugin: LiveUpdateProvider {
|
|
6
|
+
/// Creates a manager for the Ionic Live Update Provider SDK. Invoked natively by
|
|
7
|
+
/// Federated Capacitor after resolving this plugin by its Capacitor plugin name.
|
|
8
|
+
public func createManager(configuration: [String: Any]) throws -> any ProviderManager {
|
|
9
|
+
guard let implementation = implementation else {
|
|
10
|
+
throw ProviderError.invalidConfiguration(message: CustomError.pluginNotInitialized.localizedDescription)
|
|
11
|
+
}
|
|
12
|
+
return try LiveUpdateIonicManager(configuration: configuration, liveUpdate: implementation)
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
#endif
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capawesome/capacitor-live-update",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.7.0",
|
|
4
4
|
"description": "Capacitor plugin to update your app remotely in real-time on Android, iOS and Electron.",
|
|
5
5
|
"main": "dist/plugin.cjs.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
"android/src/main/",
|
|
11
11
|
"android/build.gradle",
|
|
12
|
+
"android/consumer-rules.pro",
|
|
12
13
|
"dist/",
|
|
13
14
|
"electron/dist/",
|
|
14
15
|
"ios/Plugin/",
|