@capawesome/capacitor-live-update 7.2.2 → 7.3.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.
@@ -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
@@ -128,6 +131,18 @@ import CommonCrypto
128
131
  completion(result, nil)
129
132
  }
130
133
 
134
+ @objc public func handleLoad() {
135
+ if config.autoUpdateStrategy == "background" {
136
+ performAutoUpdate()
137
+ }
138
+ }
139
+
140
+ @objc public func handleAppWillEnterForeground() {
141
+ if config.autoUpdateStrategy == "background" {
142
+ performAutoUpdate()
143
+ }
144
+ }
145
+
131
146
  @objc public func ready(completion: @escaping (Result?, Error?) -> Void) {
132
147
  CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "App is ready.")
133
148
  if config.readyTimeout <= 0 {
@@ -142,6 +157,10 @@ import CommonCrypto
142
157
  // Get the current and previous bundle IDs
143
158
  let currentBundleId = getCurrentBundleId()
144
159
  let previousBundleId = getPreviousBundleId()
160
+ // Block the rolled back bundle if enabled
161
+ if config.autoBlockRolledBackBundles && rollbackPerformed, let previousBundleId = previousBundleId {
162
+ addBlockedBundleId(previousBundleId)
163
+ }
145
164
  // Return the result
146
165
  let result = ReadyResult(currentBundleId: currentBundleId, previousBundleId: previousBundleId, rollback: rollbackPerformed)
147
166
  completion(result, nil)
@@ -194,6 +213,14 @@ import CommonCrypto
194
213
  }
195
214
 
196
215
  @objc public func sync(_ options: SyncOptions) async throws -> SyncResult {
216
+ if syncInProgress {
217
+ throw CustomError.syncInProgress
218
+ }
219
+ syncInProgress = true
220
+ defer {
221
+ syncInProgress = false
222
+ }
223
+
197
224
  let channel = options.getChannel()
198
225
  // Fetch the latest bundle
199
226
  let fetchLatestBundleOptions = FetchLatestBundleOptions(channel: channel)
@@ -206,6 +233,11 @@ import CommonCrypto
206
233
  let checksum = response.checksum
207
234
  let signature = response.signature
208
235
  let downloadUrl = response.url
236
+ // Check if the bundle is blocked
237
+ if isBlockedBundleId(latestBundleId) {
238
+ CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "Bundle is blocked and will not be downloaded.")
239
+ return SyncResult(nextBundleId: nil)
240
+ }
209
241
  // Check if bundle already exists
210
242
  if hasBundleById(latestBundleId) {
211
243
  var nextBundleId: String?
@@ -645,6 +677,30 @@ import CommonCrypto
645
677
  plugin.notifyDownloadBundleProgressListeners(event)
646
678
  }
647
679
 
680
+ private func performAutoUpdate() {
681
+ // Check if enough time has passed since the last check
682
+ let now = Int64(Date().timeIntervalSince1970 * 1000)
683
+ if lastAutoUpdateCheckTimestamp > 0 && (now - lastAutoUpdateCheckTimestamp) < autoUpdateIntervalMs {
684
+ CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "Auto-update skipped. Last check was less than 15 minutes ago.")
685
+ return
686
+ }
687
+
688
+ // Update the timestamp
689
+ lastAutoUpdateCheckTimestamp = now
690
+
691
+ // Run sync in background task
692
+ Task {
693
+ do {
694
+ CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "Auto-update started.")
695
+ let options = SyncOptions(channel: nil)
696
+ _ = try await sync(options)
697
+ CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "Auto-update completed successfully.")
698
+ } catch {
699
+ CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "Auto-update failed: ", error.localizedDescription)
700
+ }
701
+ }
702
+ }
703
+
648
704
  private func rollback() {
649
705
  // Set the rollback flag
650
706
  rollbackPerformed = true
@@ -705,6 +761,51 @@ import CommonCrypto
705
761
  private func setNextBundleById(_ bundleId: String?) {
706
762
  let path = buildCapacitorServerPathFor(bundleId: bundleId)
707
763
  setNextCapacitorServerPath(path: path)
764
+
765
+ // Notify listeners
766
+ notifyNextBundleSetListeners(bundleId)
767
+ }
768
+
769
+ private func notifyNextBundleSetListeners(_ bundleId: String?) {
770
+ let event = NextBundleSetEvent(bundleId: bundleId)
771
+ plugin.notifyNextBundleSetListeners(event)
772
+ }
773
+
774
+ private func addBlockedBundleId(_ bundleId: String) {
775
+ var blockedList: [String] = []
776
+
777
+ // Parse existing blocked IDs
778
+ if let blockedIds = preferences.getBlockedBundleIds(), !blockedIds.isEmpty {
779
+ blockedList = blockedIds.split(separator: ",").map(String.init)
780
+ }
781
+
782
+ // Skip if already blocked
783
+ if blockedList.contains(bundleId) {
784
+ return
785
+ }
786
+
787
+ // Remove oldest if limit reached
788
+ if blockedList.count >= 100 {
789
+ blockedList.removeFirst()
790
+ }
791
+
792
+ // Add new bundle
793
+ blockedList.append(bundleId)
794
+
795
+ // Save back to preferences
796
+ let newBlockedIds = blockedList.joined(separator: ",")
797
+ preferences.setBlockedBundleIds(newBlockedIds)
798
+
799
+ CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "Bundle blocked: ", bundleId)
800
+ }
801
+
802
+ private func isBlockedBundleId(_ bundleId: String) -> Bool {
803
+ guard let blockedIds = preferences.getBlockedBundleIds(), !blockedIds.isEmpty else {
804
+ return false
805
+ }
806
+
807
+ let blockedList = blockedIds.split(separator: ",").map(String.init)
808
+ return blockedList.contains(bundleId)
708
809
  }
709
810
 
710
811
  private func setNextCapacitorServerPath(path: String) {
@@ -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) {
@@ -8,7 +8,7 @@ import Capacitor
8
8
  @objc(LiveUpdatePlugin)
9
9
  public class LiveUpdatePlugin: CAPPlugin, CAPBridgedPlugin {
10
10
  public static let tag = "LiveUpdate"
11
- public static let version = "7.2.2"
11
+ public static let version = "7.3.0"
12
12
  public static let userDefaultsPrefix = "CapawesomeLiveUpdate" // DO NOT CHANGE
13
13
 
14
14
  public let identifier = "LiveUpdatePlugin"
@@ -35,14 +35,25 @@ public class LiveUpdatePlugin: CAPPlugin, CAPBridgedPlugin {
35
35
  ]
36
36
 
37
37
  private let eventDownloadBundleProgess = "downloadBundleProgress"
38
+ private let eventNextBundleSet = "nextBundleSet"
38
39
 
39
40
  private var config: LiveUpdateConfig?
40
41
  private var implementation: LiveUpdate?
41
- private var syncInProgress = false
42
42
 
43
43
  override public func load() {
44
44
  self.config = liveUpdateConfig()
45
45
  self.implementation = LiveUpdate(config: config!, plugin: self)
46
+
47
+ // Notify implementation about load
48
+ implementation?.handleLoad()
49
+
50
+ // Register for app resume notifications
51
+ NotificationCenter.default.addObserver(
52
+ self,
53
+ selector: #selector(handleAppWillEnterForeground),
54
+ name: UIApplication.willEnterForegroundNotification,
55
+ object: nil
56
+ )
46
57
  }
47
58
 
48
59
  @objc func deleteBundle(_ call: CAPPluginCall) {
@@ -90,6 +101,11 @@ public class LiveUpdatePlugin: CAPPlugin, CAPBridgedPlugin {
90
101
  @objc func fetchLatestBundle(_ call: CAPPluginCall) {
91
102
  Task {
92
103
  do {
104
+ guard let appId = config?.appId, !appId.isEmpty else {
105
+ call.reject(CustomError.appIdMissing.localizedDescription)
106
+ return
107
+ }
108
+
93
109
  let options = FetchLatestBundleOptions(call)
94
110
  let result = try await implementation?.fetchLatestBundle(options)
95
111
  if let result = result?.toJSObject() as? JSObject {
@@ -265,25 +281,17 @@ public class LiveUpdatePlugin: CAPPlugin, CAPBridgedPlugin {
265
281
  @objc func sync(_ call: CAPPluginCall) {
266
282
  Task {
267
283
  do {
268
- guard let _ = config?.appId else {
284
+ guard let appId = config?.appId, !appId.isEmpty else {
269
285
  call.reject(CustomError.appIdMissing.localizedDescription)
270
286
  return
271
287
  }
272
288
 
273
- guard !syncInProgress else {
274
- call.reject(CustomError.syncInProgress.localizedDescription)
275
- return
276
- }
277
- syncInProgress = true
278
-
279
289
  let options = SyncOptions(call)
280
290
  let result = try await implementation?.sync(options)
281
- self.syncInProgress = false
282
291
  if let result = result?.toJSObject() as? JSObject {
283
292
  resolveCall(call, result)
284
293
  }
285
294
  } catch {
286
- self.syncInProgress = false
287
295
  rejectCall(call, error)
288
296
  }
289
297
  }
@@ -295,11 +303,21 @@ public class LiveUpdatePlugin: CAPPlugin, CAPBridgedPlugin {
295
303
  }
296
304
  }
297
305
 
306
+ func notifyNextBundleSetListeners(_ event: NextBundleSetEvent) {
307
+ notifyListeners(eventNextBundleSet, data: event.toJSObject(), retainUntilConsumed: false)
308
+ }
309
+
310
+ @objc private func handleAppWillEnterForeground() {
311
+ implementation?.handleAppWillEnterForeground()
312
+ }
313
+
298
314
  private func liveUpdateConfig() -> LiveUpdateConfig {
299
315
  var config = LiveUpdateConfig()
300
316
 
301
317
  config.appId = getConfig().getString("appId", config.appId)
318
+ config.autoBlockRolledBackBundles = getConfig().getBoolean("autoBlockRolledBackBundles", config.autoBlockRolledBackBundles)
302
319
  config.autoDeleteBundles = getConfig().getBoolean("autoDeleteBundles", config.autoDeleteBundles)
320
+ config.autoUpdateStrategy = getConfig().getString("autoUpdateStrategy", config.autoUpdateStrategy) ?? config.autoUpdateStrategy
303
321
  config.defaultChannel = getConfig().getString("defaultChannel", config.defaultChannel)
304
322
  config.httpTimeout = getConfig().getInt("httpTimeout", config.httpTimeout)
305
323
  config.publicKey = getConfig().getString("publicKey", config.publicKey)
@@ -1,6 +1,7 @@
1
1
  import Foundation
2
2
 
3
3
  public class LiveUpdatePreferences: NSObject {
4
+ private let blockedBundleIdsKey = "blockedBundleIds" // DO NOT CHANGE
4
5
  private let channelKey = "channel" // DO NOT CHANGE
5
6
  private let customIdKey = "customId" // DO NOT CHANGE
6
7
  private let previousBundleIdKey = "previousBundleIdKey" // DO NOT CHANGE
@@ -17,6 +18,19 @@ public class LiveUpdatePreferences: NSObject {
17
18
  return UserDefaults.standard.string(forKey: applyPrefix(to: previousBundleIdKey))
18
19
  }
19
20
 
21
+ public func getBlockedBundleIds() -> String? {
22
+ return UserDefaults.standard.string(forKey: applyPrefix(to: blockedBundleIdsKey))
23
+ }
24
+
25
+ public func setBlockedBundleIds(_ value: String?) {
26
+ if let value = value {
27
+ UserDefaults.standard.set(value, forKey: applyPrefix(to: blockedBundleIdsKey))
28
+ } else {
29
+ UserDefaults.standard.removeObject(forKey: applyPrefix(to: blockedBundleIdsKey))
30
+ }
31
+ UserDefaults.standard.synchronize()
32
+ }
33
+
20
34
  public func setChannel(_ value: String?) {
21
35
  if let value = value {
22
36
  UserDefaults.standard.set(value, forKey: applyPrefix(to: channelKey))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capawesome/capacitor-live-update",
3
- "version": "7.2.2",
3
+ "version": "7.3.0",
4
4
  "description": "Capacitor plugin to update your app remotely in real-time.",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",