@capawesome/capacitor-live-update 7.2.2 → 7.4.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.
Files changed (37) hide show
  1. package/README.md +284 -26
  2. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdate.java +623 -220
  3. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdateConfig.java +23 -0
  4. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdateHttpClient.java +33 -5
  5. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdatePlugin.java +185 -15
  6. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdatePreferences.java +42 -0
  7. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/api/GetLatestBundleResponse.java +2 -2
  8. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/events/NextBundleSetEvent.java +26 -0
  9. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/options/SetConfigOptions.java +19 -0
  10. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/options/SyncOptions.java +4 -0
  11. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/GetBlockedBundlesResult.java +27 -0
  12. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/GetConfigResult.java +40 -0
  13. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/GetDownloadedBundlesResult.java +27 -0
  14. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/IsSyncingResult.java +26 -0
  15. package/dist/docs.json +428 -1
  16. package/dist/esm/definitions.d.ts +230 -2
  17. package/dist/esm/definitions.js.map +1 -1
  18. package/dist/esm/web.d.ts +8 -1
  19. package/dist/esm/web.js +21 -0
  20. package/dist/esm/web.js.map +1 -1
  21. package/dist/plugin.cjs.js +21 -0
  22. package/dist/plugin.cjs.js.map +1 -1
  23. package/dist/plugin.js +21 -0
  24. package/dist/plugin.js.map +1 -1
  25. package/ios/Plugin/Classes/Events/NextBundleSetEvent.swift +16 -0
  26. package/ios/Plugin/Classes/Options/SetConfigOptions.swift +14 -0
  27. package/ios/Plugin/Classes/Options/SyncOptions.swift +4 -0
  28. package/ios/Plugin/Classes/Results/GetBlockedBundlesResult.swift +16 -0
  29. package/ios/Plugin/Classes/Results/GetConfigResult.swift +19 -0
  30. package/ios/Plugin/Classes/Results/GetDownloadedBundlesResult.swift +16 -0
  31. package/ios/Plugin/Classes/Results/IsSyncingResult.swift +16 -0
  32. package/ios/Plugin/LiveUpdate.swift +178 -4
  33. package/ios/Plugin/LiveUpdateConfig.swift +2 -0
  34. package/ios/Plugin/LiveUpdateHttpClient.swift +4 -2
  35. package/ios/Plugin/LiveUpdatePlugin.swift +106 -11
  36. package/ios/Plugin/LiveUpdatePreferences.swift +56 -0
  37. package/package.json +1 -1
package/dist/plugin.js CHANGED
@@ -6,6 +6,9 @@ var capacitorLiveUpdate = (function (exports, core) {
6
6
  });
7
7
 
8
8
  class LiveUpdateWeb extends core.WebPlugin {
9
+ async clearBlockedBundles() {
10
+ this.throwUnimplementedError();
11
+ }
9
12
  async deleteBundle(_options) {
10
13
  this.throwUnimplementedError();
11
14
  }
@@ -15,15 +18,24 @@ var capacitorLiveUpdate = (function (exports, core) {
15
18
  async fetchLatestBundle() {
16
19
  this.throwUnimplementedError();
17
20
  }
21
+ async getBlockedBundles() {
22
+ this.throwUnimplementedError();
23
+ }
18
24
  async getBundle() {
19
25
  this.throwUnimplementedError();
20
26
  }
21
27
  async getBundles() {
22
28
  this.throwUnimplementedError();
23
29
  }
30
+ async getDownloadedBundles() {
31
+ this.throwUnimplementedError();
32
+ }
24
33
  async getChannel() {
25
34
  this.throwUnimplementedError();
26
35
  }
36
+ async getConfig() {
37
+ this.throwUnimplementedError();
38
+ }
27
39
  async getCurrentBundle() {
28
40
  this.throwUnimplementedError();
29
41
  }
@@ -42,6 +54,9 @@ var capacitorLiveUpdate = (function (exports, core) {
42
54
  async getVersionName() {
43
55
  this.throwUnimplementedError();
44
56
  }
57
+ async isSyncing() {
58
+ this.throwUnimplementedError();
59
+ }
45
60
  async ready() {
46
61
  this.throwUnimplementedError();
47
62
  }
@@ -51,12 +66,18 @@ var capacitorLiveUpdate = (function (exports, core) {
51
66
  async reset() {
52
67
  this.throwUnimplementedError();
53
68
  }
69
+ async resetConfig() {
70
+ this.throwUnimplementedError();
71
+ }
54
72
  async setBundle(_options) {
55
73
  this.throwUnimplementedError();
56
74
  }
57
75
  async setChannel(_options) {
58
76
  this.throwUnimplementedError();
59
77
  }
78
+ async setConfig(_options) {
79
+ this.throwUnimplementedError();
80
+ }
60
81
  async setCustomId(_options) {
61
82
  this.throwUnimplementedError();
62
83
  }
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from '@capacitor/core';\nconst LiveUpdate = registerPlugin('LiveUpdate', {\n web: () => import('./web').then(m => new m.LiveUpdateWeb()),\n});\nexport * from './definitions';\nexport { LiveUpdate };\n//# sourceMappingURL=index.js.map","import { WebPlugin } from '@capacitor/core';\nexport class LiveUpdateWeb extends WebPlugin {\n async deleteBundle(_options) {\n this.throwUnimplementedError();\n }\n async downloadBundle(_options) {\n this.throwUnimplementedError();\n }\n async fetchLatestBundle() {\n this.throwUnimplementedError();\n }\n async getBundle() {\n this.throwUnimplementedError();\n }\n async getBundles() {\n this.throwUnimplementedError();\n }\n async getChannel() {\n this.throwUnimplementedError();\n }\n async getCurrentBundle() {\n this.throwUnimplementedError();\n }\n async getCustomId() {\n this.throwUnimplementedError();\n }\n async getDeviceId() {\n this.throwUnimplementedError();\n }\n async getNextBundle() {\n this.throwUnimplementedError();\n }\n async getVersionCode() {\n this.throwUnimplementedError();\n }\n async getVersionName() {\n this.throwUnimplementedError();\n }\n async ready() {\n this.throwUnimplementedError();\n }\n async reload() {\n this.throwUnimplementedError();\n }\n async reset() {\n this.throwUnimplementedError();\n }\n async setBundle(_options) {\n this.throwUnimplementedError();\n }\n async setChannel(_options) {\n this.throwUnimplementedError();\n }\n async setCustomId(_options) {\n this.throwUnimplementedError();\n }\n async setNextBundle(_options) {\n this.throwUnimplementedError();\n }\n async sync() {\n this.throwUnimplementedError();\n }\n throwUnimplementedError() {\n throw this.unimplemented('Not implemented on web.');\n }\n}\n//# sourceMappingURL=web.js.map"],"names":["registerPlugin","WebPlugin"],"mappings":";;;AACK,UAAC,UAAU,GAAGA,mBAAc,CAAC,YAAY,EAAE;IAChD,IAAI,GAAG,EAAE,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,aAAa,EAAE,CAAC;IAC/D,CAAC;;ICFM,MAAM,aAAa,SAASC,cAAS,CAAC;IAC7C,IAAI,MAAM,YAAY,CAAC,QAAQ,EAAE;IACjC,QAAQ,IAAI,CAAC,uBAAuB,EAAE;IACtC;IACA,IAAI,MAAM,cAAc,CAAC,QAAQ,EAAE;IACnC,QAAQ,IAAI,CAAC,uBAAuB,EAAE;IACtC;IACA,IAAI,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,IAAI,CAAC,uBAAuB,EAAE;IACtC;IACA,IAAI,MAAM,SAAS,GAAG;IACtB,QAAQ,IAAI,CAAC,uBAAuB,EAAE;IACtC;IACA,IAAI,MAAM,UAAU,GAAG;IACvB,QAAQ,IAAI,CAAC,uBAAuB,EAAE;IACtC;IACA,IAAI,MAAM,UAAU,GAAG;IACvB,QAAQ,IAAI,CAAC,uBAAuB,EAAE;IACtC;IACA,IAAI,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,IAAI,CAAC,uBAAuB,EAAE;IACtC;IACA,IAAI,MAAM,WAAW,GAAG;IACxB,QAAQ,IAAI,CAAC,uBAAuB,EAAE;IACtC;IACA,IAAI,MAAM,WAAW,GAAG;IACxB,QAAQ,IAAI,CAAC,uBAAuB,EAAE;IACtC;IACA,IAAI,MAAM,aAAa,GAAG;IAC1B,QAAQ,IAAI,CAAC,uBAAuB,EAAE;IACtC;IACA,IAAI,MAAM,cAAc,GAAG;IAC3B,QAAQ,IAAI,CAAC,uBAAuB,EAAE;IACtC;IACA,IAAI,MAAM,cAAc,GAAG;IAC3B,QAAQ,IAAI,CAAC,uBAAuB,EAAE;IACtC;IACA,IAAI,MAAM,KAAK,GAAG;IAClB,QAAQ,IAAI,CAAC,uBAAuB,EAAE;IACtC;IACA,IAAI,MAAM,MAAM,GAAG;IACnB,QAAQ,IAAI,CAAC,uBAAuB,EAAE;IACtC;IACA,IAAI,MAAM,KAAK,GAAG;IAClB,QAAQ,IAAI,CAAC,uBAAuB,EAAE;IACtC;IACA,IAAI,MAAM,SAAS,CAAC,QAAQ,EAAE;IAC9B,QAAQ,IAAI,CAAC,uBAAuB,EAAE;IACtC;IACA,IAAI,MAAM,UAAU,CAAC,QAAQ,EAAE;IAC/B,QAAQ,IAAI,CAAC,uBAAuB,EAAE;IACtC;IACA,IAAI,MAAM,WAAW,CAAC,QAAQ,EAAE;IAChC,QAAQ,IAAI,CAAC,uBAAuB,EAAE;IACtC;IACA,IAAI,MAAM,aAAa,CAAC,QAAQ,EAAE;IAClC,QAAQ,IAAI,CAAC,uBAAuB,EAAE;IACtC;IACA,IAAI,MAAM,IAAI,GAAG;IACjB,QAAQ,IAAI,CAAC,uBAAuB,EAAE;IACtC;IACA,IAAI,uBAAuB,GAAG;IAC9B,QAAQ,MAAM,IAAI,CAAC,aAAa,CAAC,yBAAyB,CAAC;IAC3D;IACA;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"plugin.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from '@capacitor/core';\nconst LiveUpdate = registerPlugin('LiveUpdate', {\n web: () => import('./web').then(m => new m.LiveUpdateWeb()),\n});\nexport * from './definitions';\nexport { LiveUpdate };\n//# sourceMappingURL=index.js.map","import { WebPlugin } from '@capacitor/core';\nexport class LiveUpdateWeb extends WebPlugin {\n async clearBlockedBundles() {\n this.throwUnimplementedError();\n }\n async deleteBundle(_options) {\n this.throwUnimplementedError();\n }\n async downloadBundle(_options) {\n this.throwUnimplementedError();\n }\n async fetchLatestBundle() {\n this.throwUnimplementedError();\n }\n async getBlockedBundles() {\n this.throwUnimplementedError();\n }\n async getBundle() {\n this.throwUnimplementedError();\n }\n async getBundles() {\n this.throwUnimplementedError();\n }\n async getDownloadedBundles() {\n this.throwUnimplementedError();\n }\n async getChannel() {\n this.throwUnimplementedError();\n }\n async getConfig() {\n this.throwUnimplementedError();\n }\n async getCurrentBundle() {\n this.throwUnimplementedError();\n }\n async getCustomId() {\n this.throwUnimplementedError();\n }\n async getDeviceId() {\n this.throwUnimplementedError();\n }\n async getNextBundle() {\n this.throwUnimplementedError();\n }\n async getVersionCode() {\n this.throwUnimplementedError();\n }\n async getVersionName() {\n this.throwUnimplementedError();\n }\n async isSyncing() {\n this.throwUnimplementedError();\n }\n async ready() {\n this.throwUnimplementedError();\n }\n async reload() {\n this.throwUnimplementedError();\n }\n async reset() {\n this.throwUnimplementedError();\n }\n async resetConfig() {\n this.throwUnimplementedError();\n }\n async setBundle(_options) {\n this.throwUnimplementedError();\n }\n async setChannel(_options) {\n this.throwUnimplementedError();\n }\n async setConfig(_options) {\n this.throwUnimplementedError();\n }\n async setCustomId(_options) {\n this.throwUnimplementedError();\n }\n async setNextBundle(_options) {\n this.throwUnimplementedError();\n }\n async sync() {\n this.throwUnimplementedError();\n }\n throwUnimplementedError() {\n throw this.unimplemented('Not implemented on web.');\n }\n}\n//# sourceMappingURL=web.js.map"],"names":["registerPlugin","WebPlugin"],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ import Foundation
2
+ import Capacitor
3
+
4
+ @objc public class NextBundleSetEvent: NSObject {
5
+ let bundleId: String?
6
+
7
+ init(bundleId: String?) {
8
+ self.bundleId = bundleId
9
+ }
10
+
11
+ public func toJSObject() -> JSObject {
12
+ var result = JSObject()
13
+ result["bundleId"] = bundleId == nil ? NSNull() : bundleId
14
+ return result
15
+ }
16
+ }
@@ -0,0 +1,14 @@
1
+ import Foundation
2
+ import Capacitor
3
+
4
+ @objc public class SetConfigOptions: NSObject {
5
+ let appId: String?
6
+
7
+ init(_ call: CAPPluginCall) {
8
+ self.appId = call.getString("appId")
9
+ }
10
+
11
+ public func getAppId() -> String? {
12
+ return appId
13
+ }
14
+ }
@@ -8,6 +8,10 @@ import Capacitor
8
8
  self.channel = call.getString("channel")
9
9
  }
10
10
 
11
+ init(channel: String?) {
12
+ self.channel = channel
13
+ }
14
+
11
15
  func getChannel() -> String? {
12
16
  return channel
13
17
  }
@@ -0,0 +1,16 @@
1
+ import Foundation
2
+ import Capacitor
3
+
4
+ @objc public class GetBlockedBundlesResult: NSObject, Result {
5
+ let bundleIds: [String]
6
+
7
+ init(bundleIds: [String]) {
8
+ self.bundleIds = bundleIds
9
+ }
10
+
11
+ public func toJSObject() -> AnyObject {
12
+ var result = JSObject()
13
+ result["bundleIds"] = bundleIds
14
+ return result as AnyObject
15
+ }
16
+ }
@@ -0,0 +1,19 @@
1
+ import Foundation
2
+ import Capacitor
3
+
4
+ @objc public class GetConfigResult: NSObject, Result {
5
+ let appId: String?
6
+ let autoUpdateStrategy: String
7
+
8
+ init(appId: String?, autoUpdateStrategy: String) {
9
+ self.appId = appId
10
+ self.autoUpdateStrategy = autoUpdateStrategy
11
+ }
12
+
13
+ public func toJSObject() -> AnyObject {
14
+ var result = JSObject()
15
+ result["appId"] = appId == nil ? NSNull() : appId
16
+ result["autoUpdateStrategy"] = autoUpdateStrategy
17
+ return result as AnyObject
18
+ }
19
+ }
@@ -0,0 +1,16 @@
1
+ import Foundation
2
+ import Capacitor
3
+
4
+ @objc public class GetDownloadedBundlesResult: NSObject, Result {
5
+ let bundleIds: [String]
6
+
7
+ init(bundleIds: [String]) {
8
+ self.bundleIds = bundleIds
9
+ }
10
+
11
+ public func toJSObject() -> AnyObject {
12
+ var result = JSObject()
13
+ result["bundleIds"] = bundleIds
14
+ return result as AnyObject
15
+ }
16
+ }
@@ -0,0 +1,16 @@
1
+ import Foundation
2
+ import Capacitor
3
+
4
+ @objc public class IsSyncingResult: NSObject, Result {
5
+ let syncing: Bool
6
+
7
+ init(syncing: Bool) {
8
+ self.syncing = syncing
9
+ }
10
+
11
+ @objc public func toJSObject() -> AnyObject {
12
+ var result = JSObject()
13
+ result["syncing"] = syncing
14
+ return result as AnyObject
15
+ }
16
+ }
@@ -11,6 +11,7 @@ import CommonCrypto
11
11
 
12
12
  // swiftlint:disable type_body_length
13
13
  @objc public class LiveUpdate: NSObject {
14
+ private let autoUpdateIntervalMs: Int64 = 15 * 60 * 1000 // 15 minutes
14
15
  private let bundlesDirectory = "NoCloud/ionic_built_snapshots" // DO NOT CHANGE! (See https://dub.sh/BLluidt)
15
16
  private let cachesDirectoryUrl = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
16
17
  private let config: LiveUpdateConfig
@@ -24,6 +25,8 @@ import CommonCrypto
24
25
 
25
26
  private var rollbackDispatchWorkItem: DispatchWorkItem?
26
27
  private var rollbackPerformed = false
28
+ private var lastAutoUpdateCheckTimestamp: Int64 = 0
29
+ private var syncInProgress = false
27
30
 
28
31
  init(config: LiveUpdateConfig, plugin: LiveUpdatePlugin) {
29
32
  self.config = config
@@ -32,11 +35,18 @@ import CommonCrypto
32
35
  self.preferences = LiveUpdatePreferences()
33
36
  super.init()
34
37
 
38
+ // Check version and reset config if version changed
39
+ checkAndResetConfigIfVersionChanged()
40
+
35
41
  // Start the rollback timer to rollback to the default bundle
36
42
  // if the app is not ready after a certain time
37
43
  startRollbackTimer()
38
44
  }
39
45
 
46
+ @objc public func clearBlockedBundles() {
47
+ preferences.setBlockedBundleIds(nil)
48
+ }
49
+
40
50
  @objc public func deleteBundle(_ options: DeleteBundleOptions, completion: @escaping (Error?) -> Void) {
41
51
  let bundleId = options.getBundleId()
42
52
 
@@ -79,18 +89,40 @@ import CommonCrypto
79
89
  return FetchLatestBundleResult(artifactType: response?.artifactType, bundleId: response?.bundleId, checksum: response?.checksum, customProperties: response?.customProperties, downloadUrl: response?.url, signature: response?.signature)
80
90
  }
81
91
 
92
+ @objc public func getBlockedBundles(completion: @escaping (Result?, Error?) -> Void) {
93
+ var bundleIds: [String] = []
94
+ if let blockedIds = preferences.getBlockedBundleIds(), !blockedIds.isEmpty {
95
+ bundleIds = blockedIds.split(separator: ",").map(String.init)
96
+ }
97
+ let result = GetBlockedBundlesResult(bundleIds: bundleIds)
98
+ completion(result, nil)
99
+ }
100
+
82
101
  @objc public func getBundles(completion: @escaping (Result?, Error?) -> Void) {
83
- let bundleIds = getBundleIds()
102
+ let bundleIds = getDownloadedBundleIds()
84
103
  let result = GetBundlesResult(bundleIds: bundleIds)
85
104
  completion(result, nil)
86
105
  }
87
106
 
107
+ @objc public func getDownloadedBundles(completion: @escaping (Result?, Error?) -> Void) {
108
+ let bundleIds = getDownloadedBundleIds()
109
+ let result = GetDownloadedBundlesResult(bundleIds: bundleIds)
110
+ completion(result, nil)
111
+ }
112
+
88
113
  @objc public func getChannel(completion: @escaping (Result?, Error?) -> Void) {
89
114
  let channel = getChannel()
90
115
  let result = GetChannelResult(channel: channel)
91
116
  completion(result, nil)
92
117
  }
93
118
 
119
+ @objc public func getConfig(completion: @escaping (Result?, Error?) -> Void) {
120
+ let appId = getAppId()
121
+ let autoUpdateStrategy = config.autoUpdateStrategy
122
+ let result = GetConfigResult(appId: appId, autoUpdateStrategy: autoUpdateStrategy)
123
+ completion(result, nil)
124
+ }
125
+
94
126
  @objc public func getCurrentBundle(completion: @escaping (Result?, Error?) -> Void) {
95
127
  let bundleId = getCurrentBundleId()
96
128
  let result = GetCurrentBundleResult(bundleId: bundleId)
@@ -128,6 +160,23 @@ import CommonCrypto
128
160
  completion(result, nil)
129
161
  }
130
162
 
163
+ @objc public func isSyncing(completion: @escaping (Result?, Error?) -> Void) {
164
+ let result = IsSyncingResult(syncing: syncInProgress)
165
+ completion(result, nil)
166
+ }
167
+
168
+ @objc public func handleLoad() {
169
+ if config.autoUpdateStrategy == "background" {
170
+ performAutoUpdate()
171
+ }
172
+ }
173
+
174
+ @objc public func handleAppWillEnterForeground() {
175
+ if config.autoUpdateStrategy == "background" {
176
+ performAutoUpdate()
177
+ }
178
+ }
179
+
131
180
  @objc public func ready(completion: @escaping (Result?, Error?) -> Void) {
132
181
  CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "App is ready.")
133
182
  if config.readyTimeout <= 0 {
@@ -142,6 +191,10 @@ import CommonCrypto
142
191
  // Get the current and previous bundle IDs
143
192
  let currentBundleId = getCurrentBundleId()
144
193
  let previousBundleId = getPreviousBundleId()
194
+ // Block the rolled back bundle if enabled
195
+ if config.autoBlockRolledBackBundles && rollbackPerformed, let previousBundleId = previousBundleId {
196
+ addBlockedBundleId(previousBundleId)
197
+ }
145
198
  // Return the result
146
199
  let result = ReadyResult(currentBundleId: currentBundleId, previousBundleId: previousBundleId, rollback: rollbackPerformed)
147
200
  completion(result, nil)
@@ -161,6 +214,10 @@ import CommonCrypto
161
214
  self.setNextBundleById(nil)
162
215
  }
163
216
 
217
+ @objc public func resetConfig() {
218
+ preferences.setAppId(nil)
219
+ }
220
+
164
221
  @objc public func setChannel(_ options: SetChannelOptions, completion: @escaping (Error?) -> Void) {
165
222
  let channel = options.getChannel()
166
223
 
@@ -168,6 +225,11 @@ import CommonCrypto
168
225
  completion(nil)
169
226
  }
170
227
 
228
+ @objc public func setConfig(_ options: SetConfigOptions) {
229
+ let appId = options.getAppId()
230
+ preferences.setAppId(appId)
231
+ }
232
+
171
233
  @objc public func setCustomId(_ options: SetCustomIdOptions, completion: @escaping (Error?) -> Void) {
172
234
  let customId = options.getCustomId()
173
235
 
@@ -194,6 +256,14 @@ import CommonCrypto
194
256
  }
195
257
 
196
258
  @objc public func sync(_ options: SyncOptions) async throws -> SyncResult {
259
+ if syncInProgress {
260
+ throw CustomError.syncInProgress
261
+ }
262
+ syncInProgress = true
263
+ defer {
264
+ syncInProgress = false
265
+ }
266
+
197
267
  let channel = options.getChannel()
198
268
  // Fetch the latest bundle
199
269
  let fetchLatestBundleOptions = FetchLatestBundleOptions(channel: channel)
@@ -206,6 +276,11 @@ import CommonCrypto
206
276
  let checksum = response.checksum
207
277
  let signature = response.signature
208
278
  let downloadUrl = response.url
279
+ // Check if the bundle is blocked
280
+ if isBlockedBundleId(latestBundleId) {
281
+ CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "Bundle is blocked and will not be downloaded.")
282
+ return SyncResult(nextBundleId: nil)
283
+ }
209
284
  // Check if bundle already exists
210
285
  if hasBundleById(latestBundleId) {
211
286
  var nextBundleId: String?
@@ -339,7 +414,7 @@ import CommonCrypto
339
414
  }
340
415
 
341
416
  private func deleteUnusedBundles() {
342
- let bundleIds = getBundleIds()
417
+ let bundleIds = getDownloadedBundleIds()
343
418
  let currentBundleId = getCurrentBundleId()
344
419
  let nextBundleId = getNextBundleId()
345
420
 
@@ -484,7 +559,7 @@ import CommonCrypto
484
559
  parameters["osVersion"] = await UIDevice.current.systemVersion
485
560
  parameters["platform"] = "1"
486
561
  parameters["pluginVersion"] = LiveUpdatePlugin.version
487
- var urlComponents = URLComponents(string: "https://\(config.serverDomain)/v1/apps/\(config.appId ?? "")/bundles/latest")!
562
+ var urlComponents = URLComponents(string: "https://\(config.serverDomain)/v1/apps/\(getAppId() ?? "")/bundles/latest")!
488
563
  urlComponents.queryItems = parameters.map { URLQueryItem(name: $0.key, value: $0.value) }
489
564
  let url = try urlComponents.asURL()
490
565
  CAPLog.print("[", LiveUpdatePlugin.tag, "] Fetching latest bundle: ", url)
@@ -507,7 +582,7 @@ import CommonCrypto
507
582
  }
508
583
  }
509
584
 
510
- private func getBundleIds() -> [String] {
585
+ private func getDownloadedBundleIds() -> [String] {
511
586
  let url = libraryDirectoryUrl.appendingPathComponent(bundlesDirectory)
512
587
  do {
513
588
  let pathExists = FileManager.default.fileExists(atPath: url.path)
@@ -521,6 +596,13 @@ import CommonCrypto
521
596
  }
522
597
  }
523
598
 
599
+ private func getAppId() -> String? {
600
+ if let appId = preferences.getAppId() {
601
+ return appId
602
+ }
603
+ return config.appId
604
+ }
605
+
524
606
  private func getChannel() -> String? {
525
607
  var channel: String?
526
608
  if let _ = config.defaultChannel {
@@ -645,6 +727,30 @@ import CommonCrypto
645
727
  plugin.notifyDownloadBundleProgressListeners(event)
646
728
  }
647
729
 
730
+ private func performAutoUpdate() {
731
+ // Check if enough time has passed since the last check
732
+ let now = Int64(Date().timeIntervalSince1970 * 1000)
733
+ if lastAutoUpdateCheckTimestamp > 0 && (now - lastAutoUpdateCheckTimestamp) < autoUpdateIntervalMs {
734
+ CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "Auto-update skipped. Last check was less than 15 minutes ago.")
735
+ return
736
+ }
737
+
738
+ // Update the timestamp
739
+ lastAutoUpdateCheckTimestamp = now
740
+
741
+ // Run sync in background task
742
+ Task {
743
+ do {
744
+ CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "Auto-update started.")
745
+ let options = SyncOptions(channel: nil)
746
+ _ = try await sync(options)
747
+ CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "Auto-update completed successfully.")
748
+ } catch {
749
+ CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "Auto-update failed: ", error.localizedDescription)
750
+ }
751
+ }
752
+ }
753
+
648
754
  private func rollback() {
649
755
  // Set the rollback flag
650
756
  rollbackPerformed = true
@@ -699,12 +805,63 @@ import CommonCrypto
699
805
  return
700
806
  }
701
807
  viewController.setServerBasePath(path: path)
808
+ // Notify listeners
809
+ notifyReloadedListeners()
702
810
  }
703
811
 
704
812
  /// - Parameter bundleId: The bundle ID to set as the next bundle. If `nil`, the default bundle will be used.
705
813
  private func setNextBundleById(_ bundleId: String?) {
706
814
  let path = buildCapacitorServerPathFor(bundleId: bundleId)
707
815
  setNextCapacitorServerPath(path: path)
816
+
817
+ // Notify listeners
818
+ notifyNextBundleSetListeners(bundleId)
819
+ }
820
+
821
+ private func notifyNextBundleSetListeners(_ bundleId: String?) {
822
+ let event = NextBundleSetEvent(bundleId: bundleId)
823
+ plugin.notifyNextBundleSetListeners(event)
824
+ }
825
+
826
+ private func notifyReloadedListeners() {
827
+ plugin.notifyReloadedListeners()
828
+ }
829
+
830
+ private func addBlockedBundleId(_ bundleId: String) {
831
+ var blockedList: [String] = []
832
+
833
+ // Parse existing blocked IDs
834
+ if let blockedIds = preferences.getBlockedBundleIds(), !blockedIds.isEmpty {
835
+ blockedList = blockedIds.split(separator: ",").map(String.init)
836
+ }
837
+
838
+ // Skip if already blocked
839
+ if blockedList.contains(bundleId) {
840
+ return
841
+ }
842
+
843
+ // Remove oldest if limit reached
844
+ if blockedList.count >= 100 {
845
+ blockedList.removeFirst()
846
+ }
847
+
848
+ // Add new bundle
849
+ blockedList.append(bundleId)
850
+
851
+ // Save back to preferences
852
+ let newBlockedIds = blockedList.joined(separator: ",")
853
+ preferences.setBlockedBundleIds(newBlockedIds)
854
+
855
+ CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "Bundle blocked: ", bundleId)
856
+ }
857
+
858
+ private func isBlockedBundleId(_ bundleId: String) -> Bool {
859
+ guard let blockedIds = preferences.getBlockedBundleIds(), !blockedIds.isEmpty else {
860
+ return false
861
+ }
862
+
863
+ let blockedList = blockedIds.split(separator: ",").map(String.init)
864
+ return blockedList.contains(bundleId)
708
865
  }
709
866
 
710
867
  private func setNextCapacitorServerPath(path: String) {
@@ -717,6 +874,23 @@ import CommonCrypto
717
874
  }
718
875
  }
719
876
 
877
+ private func checkAndResetConfigIfVersionChanged() {
878
+ let currentVersionCode = getVersionCode()
879
+ let currentVersionName = getVersionName()
880
+ let lastVersionCode = preferences.getLastVersionCode()
881
+ let lastVersionName = preferences.getLastVersionName()
882
+
883
+ if lastVersionCode == nil || lastVersionName == nil || lastVersionCode != currentVersionCode || lastVersionName != currentVersionName {
884
+ CAPLog.print(
885
+ "[", LiveUpdatePlugin.tag, "] ",
886
+ "App version changed (last: \(lastVersionName ?? "nil")/\(lastVersionCode ?? "nil"), current: \(currentVersionName)/\(currentVersionCode)), resetting config."
887
+ )
888
+ resetConfig()
889
+ preferences.setLastVersionCode(currentVersionCode)
890
+ preferences.setLastVersionName(currentVersionName)
891
+ }
892
+ }
893
+
720
894
  /// - Parameter bundleId: The bundle ID to save as the previous bundle ID. If `nil`, the value will be removed.
721
895
  private func setPreviousBundleId(bundleId: String?) {
722
896
  preferences.setPreviousBundleId(bundleId)
@@ -1,6 +1,8 @@
1
1
  public struct LiveUpdateConfig {
2
2
  var appId: String?
3
+ var autoBlockRolledBackBundles = false
3
4
  var autoDeleteBundles = false
5
+ var autoUpdateStrategy = "none"
4
6
  var defaultChannel: String?
5
7
  var httpTimeout = 60000
6
8
  var publicKey: String?
@@ -7,11 +7,13 @@ public class LiveUpdateHttpClient: NSObject {
7
7
  private let config: LiveUpdateConfig
8
8
 
9
9
  public static func getChecksumFromResponse(response: HTTPURLResponse) -> String? {
10
- return response.allHeaderFields["x-checksum"] as? String // Must be lowercase!
10
+ guard let headers = response.allHeaderFields as? [String: String] else { return nil }
11
+ return headers.first(where: { $0.key.lowercased() == "x-checksum" })?.value
11
12
  }
12
13
 
13
14
  public static func getSignatureFromResponse(response: HTTPURLResponse) -> String? {
14
- return response.allHeaderFields["x-signature"] as? String // Must be lowercase!
15
+ guard let headers = response.allHeaderFields as? [String: String] else { return nil }
16
+ return headers.first(where: { $0.key.lowercased() == "x-signature" })?.value
15
17
  }
16
18
 
17
19
  init(config: LiveUpdateConfig) {