@capawesome/capacitor-live-update 6.7.2 → 7.1.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 (43) hide show
  1. package/CapawesomeCapacitorLiveUpdate.podspec +2 -2
  2. package/Package.swift +32 -0
  3. package/README.md +102 -88
  4. package/android/build.gradle +9 -9
  5. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdate.java +192 -133
  6. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdateConfig.java +6 -27
  7. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdateHttpClient.java +7 -2
  8. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdatePlugin.java +51 -77
  9. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdatePreferences.java +12 -5
  10. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/api/GetLatestBundleResponse.java +27 -0
  11. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/events/DownloadBundleProgressEvent.java +32 -0
  12. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/options/DownloadBundleOptions.java +16 -1
  13. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/FetchLatestBundleResult.java +29 -1
  14. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/ReadyResult.java +32 -0
  15. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/interfaces/DownloadProgressCallback.java +5 -0
  16. package/dist/docs.json +228 -147
  17. package/dist/esm/definitions.d.ts +140 -49
  18. package/dist/esm/definitions.js.map +1 -1
  19. package/dist/esm/index.js +1 -1
  20. package/dist/esm/index.js.map +1 -1
  21. package/dist/esm/web.d.ts +2 -2
  22. package/dist/esm/web.js.map +1 -1
  23. package/dist/plugin.cjs.js +1 -3
  24. package/dist/plugin.cjs.js.map +1 -1
  25. package/dist/plugin.js +1 -3
  26. package/dist/plugin.js.map +1 -1
  27. package/ios/Plugin/Classes/Events/DownloadBundleProgressEvent.swift +25 -0
  28. package/ios/Plugin/Classes/Options/DownloadBundleOptions.swift +7 -1
  29. package/ios/Plugin/Classes/Results/{FetchLatestBundle.swift → FetchLatestBundleResult.swift} +16 -1
  30. package/ios/Plugin/Classes/Results/ReadyResult.swift +22 -0
  31. package/ios/Plugin/LiveUpdate.swift +171 -154
  32. package/ios/Plugin/LiveUpdateConfig.swift +2 -4
  33. package/ios/Plugin/LiveUpdateHttpClient.swift +6 -2
  34. package/ios/Plugin/LiveUpdatePlugin.swift +45 -37
  35. package/ios/Plugin/LiveUpdatePreferences.swift +6 -20
  36. package/ios/Plugin/Protocols/Api/GetLatestBundleResponse.swift +3 -0
  37. package/package.json +17 -18
  38. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/options/SetBundleOptions.java +0 -18
  39. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/GetBundleResult.java +0 -24
  40. package/ios/Plugin/Classes/Options/SetBundleOptions.swift +0 -14
  41. package/ios/Plugin/Classes/Results/GetBundleResult.swift +0 -16
  42. package/ios/Plugin/LiveUpdatePlugin.h +0 -10
  43. package/ios/Plugin/LiveUpdatePlugin.m +0 -27
@@ -1,6 +1,10 @@
1
1
  import Foundation
2
2
  import CryptoKit
3
+ #if canImport(ZipArchive)
4
+ import ZipArchive
5
+ #else
3
6
  import SSZipArchive
7
+ #endif
4
8
  import Capacitor
5
9
  import Alamofire
6
10
  import CommonCrypto
@@ -12,7 +16,6 @@ import CommonCrypto
12
16
  private let config: LiveUpdateConfig
13
17
  private let defaultWebAssetDir = "public" // DO NOT CHANGE! (See https://dub.sh/Buvz4yj)
14
18
  private let defaultServerPathKey = "serverBasePath" // DO NOT CHANGE! (See https://dub.sh/ceDl0zT)
15
- private let host: String
16
19
  private let httpClient: LiveUpdateHttpClient
17
20
  private let libraryDirectoryUrl = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first!
18
21
  private let manifestFileName = "capawesome-live-update-manifest.json" // DO NOT CHANGE!
@@ -20,41 +23,31 @@ import CommonCrypto
20
23
  private let preferences: LiveUpdatePreferences
21
24
 
22
25
  private var rollbackDispatchWorkItem: DispatchWorkItem?
26
+ private var rollbackPerformed = false
23
27
 
24
28
  init(config: LiveUpdateConfig, plugin: LiveUpdatePlugin) {
25
29
  self.config = config
26
- if (config.location != nil) && config.location == "eu" {
27
- self.host = "api.cloud.capawesome.eu"
28
- } else {
29
- self.host = "api.cloud.capawesome.io"
30
- }
31
30
  self.httpClient = LiveUpdateHttpClient(config: config)
32
31
  self.plugin = plugin
33
32
  self.preferences = LiveUpdatePreferences()
34
33
  super.init()
35
34
 
36
- if config.enabled {
37
- if wasUpdated() && config.resetOnUpdate {
38
- CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "App was updated. Resetting to default bundle.")
39
- reset()
40
- } else {
41
- startRollbackTimer()
42
- }
43
- saveCurrentBundleShortVersionStringAndBundleVersion()
44
- }
35
+ // Start the rollback timer to rollback to the default bundle
36
+ // if the app is not ready after a certain time
37
+ startRollbackTimer()
45
38
  }
46
39
 
47
40
  @objc public func deleteBundle(_ options: DeleteBundleOptions, completion: @escaping (Error?) -> Void) {
48
41
  let bundleId = options.getBundleId()
49
42
 
50
- if !hasBundle(bundleId: bundleId) {
43
+ if !hasBundleById(bundleId) {
51
44
  let error = CustomError.bundleNotFound
52
45
  completion(error)
53
46
  return
54
47
  }
55
48
 
56
49
  do {
57
- try deleteBundle(bundleId: bundleId)
50
+ try deleteBundleById(bundleId)
58
51
  completion(nil)
59
52
  } catch {
60
53
  completion(error)
@@ -65,10 +58,11 @@ import CommonCrypto
65
58
  let artifactType = options.getArtifactType()
66
59
  let bundleId = options.getBundleId()
67
60
  let checksum = options.getChecksum()
61
+ let signature = options.getSignature()
68
62
  let url = options.getUrl()
69
63
 
70
64
  // Check if the bundle already exists
71
- if hasBundle(bundleId: bundleId) {
65
+ if hasBundleById(bundleId) {
72
66
  throw CustomError.bundleAlreadyExists
73
67
  }
74
68
 
@@ -76,22 +70,13 @@ import CommonCrypto
76
70
  if artifactType == .manifest {
77
71
  try await downloadBundleOfTypeManifest(bundleId: bundleId, url: url)
78
72
  } else {
79
- try await downloadBundleOfTypeZip(bundleId: bundleId, url: url, checksum: checksum)
73
+ try await downloadBundleOfTypeZip(bundleId: bundleId, checksum: checksum, signature: signature, url: url)
80
74
  }
81
75
  }
82
76
 
83
77
  @objc public func fetchLatestBundle(_ options: FetchLatestBundleOptions) async throws -> FetchLatestBundleResult {
84
78
  let response: GetLatestBundleResponse? = try await self.fetchLatestBundle(options)
85
- return FetchLatestBundleResult(artifactType: response?.artifactType, bundleId: response?.bundleId, downloadUrl: response?.url)
86
- }
87
-
88
- @objc public func getBundle(completion: @escaping (Result?, Error?) -> Void) {
89
- var bundleId = getCurrentBundleId()
90
- if bundleId == defaultWebAssetDir {
91
- bundleId = nil
92
- }
93
- let result = GetBundleResult(bundleId: bundleId)
94
- completion(result, nil)
79
+ return FetchLatestBundleResult(artifactType: response?.artifactType, bundleId: response?.bundleId, checksum: response?.checksum, customProperties: response?.customProperties, downloadUrl: response?.url, signature: response?.signature)
95
80
  }
96
81
 
97
82
  @objc public func getBundles(completion: @escaping (Result?, Error?) -> Void) {
@@ -107,10 +92,7 @@ import CommonCrypto
107
92
  }
108
93
 
109
94
  @objc public func getCurrentBundle(completion: @escaping (Result?, Error?) -> Void) {
110
- var bundleId = getCurrentBundleId()
111
- if bundleId == defaultWebAssetDir {
112
- bundleId = nil
113
- }
95
+ let bundleId = getCurrentBundleId()
114
96
  let result = GetCurrentBundleResult(bundleId: bundleId)
115
97
  completion(result, nil)
116
98
  }
@@ -129,10 +111,7 @@ import CommonCrypto
129
111
  }
130
112
 
131
113
  @objc public func getNextBundle(completion: @escaping (Result?, Error?) -> Void) {
132
- var bundleId: String? = getNextBundleId()
133
- if bundleId == defaultWebAssetDir {
134
- bundleId = nil
135
- }
114
+ let bundleId: String? = getNextBundleId()
136
115
  let result = GetNextBundleResult(bundleId: bundleId)
137
116
  completion(result, nil)
138
117
  }
@@ -149,12 +128,27 @@ import CommonCrypto
149
128
  completion(result, nil)
150
129
  }
151
130
 
152
- @objc public func ready() {
131
+ @objc public func ready(completion: @escaping (Result?, Error?) -> Void) {
153
132
  CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "App is ready.")
133
+ if config.readyTimeout <= 0 {
134
+ CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "Ready timeout is set to 0. Automatic rollback is disabled.")
135
+ }
136
+ // Stop the rollback timer
154
137
  stopRollbackTimer()
138
+ // Delete unused bundles
155
139
  if config.autoDeleteBundles {
156
140
  deleteUnusedBundles()
157
141
  }
142
+ // Get the current and previous bundle IDs
143
+ let currentBundleId = getCurrentBundleId()
144
+ let previousBundleId = getPreviousBundleId()
145
+ // Return the result
146
+ let result = ReadyResult(currentBundleId: currentBundleId, previousBundleId: previousBundleId, rollback: rollbackPerformed)
147
+ completion(result, nil)
148
+ // Set the new previous bundle ID
149
+ setPreviousBundleId(bundleId: currentBundleId)
150
+ // Reset the rollback flag
151
+ rollbackPerformed = false
158
152
  }
159
153
 
160
154
  @objc public func reload() {
@@ -164,21 +158,7 @@ import CommonCrypto
164
158
  }
165
159
 
166
160
  @objc public func reset() {
167
- self.setNextCapacitorServerPathToDefaultWebAssetDir()
168
- }
169
-
170
- @objc public func setBundle(_ options: SetBundleOptions, completion: @escaping (Error?) -> Void) {
171
- let bundleId = options.getBundleId()
172
-
173
- // Check if the bundle already exists
174
- if !hasBundle(bundleId: bundleId) {
175
- let error = CustomError.bundleNotFound
176
- completion(error)
177
- return
178
- }
179
-
180
- setNextBundle(bundleId: bundleId)
181
- completion(nil)
161
+ self.setNextBundleById(nil)
182
162
  }
183
163
 
184
164
  @objc public func setChannel(_ options: SetChannelOptions, completion: @escaping (Error?) -> Void) {
@@ -199,8 +179,8 @@ import CommonCrypto
199
179
  let bundleId = options.getBundleId()
200
180
 
201
181
  if let bundleId = bundleId {
202
- if hasBundle(bundleId: bundleId) {
203
- setNextBundle(bundleId: bundleId)
182
+ if hasBundleById(bundleId) {
183
+ setNextBundleById(bundleId)
204
184
  } else {
205
185
  let error = CustomError.bundleNotFound
206
186
  completion(error)
@@ -223,14 +203,16 @@ import CommonCrypto
223
203
  }
224
204
  let artifactType = response.artifactType
225
205
  let latestBundleId = response.bundleId
206
+ let checksum = response.checksum
207
+ let signature = response.signature
226
208
  let downloadUrl = response.url
227
209
  // Check if bundle already exists
228
- if hasBundle(bundleId: latestBundleId) {
210
+ if hasBundleById(latestBundleId) {
229
211
  var nextBundleId: String?
230
212
  let currentBundleId = self.getCurrentBundleId()
231
213
  if latestBundleId != currentBundleId {
232
214
  // Set the next bundle
233
- setNextBundle(bundleId: latestBundleId)
215
+ setNextBundleById(latestBundleId)
234
216
  nextBundleId = latestBundleId
235
217
  }
236
218
  return SyncResult(nextBundleId: nextBundleId)
@@ -239,10 +221,10 @@ import CommonCrypto
239
221
  if artifactType == .manifest {
240
222
  try await downloadBundleOfTypeManifest(bundleId: latestBundleId, url: downloadUrl)
241
223
  } else {
242
- try await downloadBundleOfTypeZip(bundleId: latestBundleId, url: downloadUrl, checksum: nil)
224
+ try await downloadBundleOfTypeZip(bundleId: latestBundleId, checksum: checksum, signature: signature, url: downloadUrl)
243
225
  }
244
226
  // Set the next bundle
245
- setNextBundle(bundleId: latestBundleId)
227
+ setNextBundleById(latestBundleId)
246
228
  return SyncResult(nextBundleId: latestBundleId)
247
229
  }
248
230
 
@@ -271,12 +253,12 @@ import CommonCrypto
271
253
  try self.addBundle(bundleId: bundleId, directory: unzippedDirectory)
272
254
  }
273
255
 
274
- private func buildCapacitorServerPathFor(bundleId: String) -> String {
256
+ private func buildCapacitorServerPathFor(bundleId: String?) -> String {
275
257
  let path: String
276
- if bundleId == "public" {
277
- path = Bundle.main.bundleURL.appendingPathComponent(bundleId).path
278
- } else {
258
+ if let bundleId = bundleId {
279
259
  path = buildBundlePathFor(bundleId: bundleId)
260
+ } else {
261
+ path = Bundle.main.bundleURL.appendingPathComponent(defaultWebAssetDir).path
280
262
  }
281
263
  return path
282
264
  }
@@ -290,31 +272,28 @@ import CommonCrypto
290
272
  return url
291
273
  }
292
274
 
293
- private func copyCurrentBundleFile(href: String, toDirectory: URL) throws {
294
- guard let currentBundleId = getCurrentBundleId() else {
295
- return
296
- }
297
-
298
- let destination = toDirectory.appendingPathComponent(href)
275
+ private func copyCurrentBundleFile(fileToCopy: ManifestItem, toDirectory: URL) throws {
276
+ let currentBundleId = getCurrentBundleId()
277
+ let destination = toDirectory.appendingPathComponent(fileToCopy.href)
299
278
  let parentDirectory = destination.deletingLastPathComponent()
300
279
  try FileManager.default.createDirectory(at: parentDirectory, withIntermediateDirectories: true, attributes: nil)
301
280
 
302
281
  let sourceURL: URL
303
- if currentBundleId == defaultWebAssetDir {
304
- guard let file = Bundle.main.url(forResource: href, withExtension: nil, subdirectory: defaultWebAssetDir) else {
282
+ if let currentBundleId = currentBundleId {
283
+ sourceURL = buildBundleURLFor(bundleId: currentBundleId).appendingPathComponent(fileToCopy.href)
284
+ } else {
285
+ guard let file = Bundle.main.url(forResource: fileToCopy.href, withExtension: nil, subdirectory: defaultWebAssetDir) else {
305
286
  return
306
287
  }
307
288
  sourceURL = file
308
- } else {
309
- sourceURL = buildBundleURLFor(bundleId: currentBundleId).appendingPathComponent(href)
310
289
  }
311
290
 
312
291
  try FileManager.default.copyItem(at: sourceURL, to: destination)
313
292
  }
314
293
 
315
- private func copyCurrentBundleFiles(hrefs: [String], toDirectory: URL) throws {
316
- for href in hrefs {
317
- try copyCurrentBundleFile(href: href, toDirectory: toDirectory)
294
+ private func copyCurrentBundleFiles(filesToCopy: [ManifestItem], toDirectory: URL) throws {
295
+ for fileToCopy in filesToCopy {
296
+ try copyCurrentBundleFile(fileToCopy: fileToCopy, toDirectory: toDirectory)
318
297
  }
319
298
  }
320
299
 
@@ -336,14 +315,14 @@ import CommonCrypto
336
315
  return temporaryDirectory
337
316
  }
338
317
 
339
- private func deleteBundle(bundleId: String) throws {
318
+ private func deleteBundleById(_ bundleId: String) throws {
340
319
  // Delete the bundle directory
341
320
  let path = buildBundlePathFor(bundleId: bundleId)
342
321
  try FileManager.default.removeItem(atPath: path)
343
322
  // Reset the next bundle if it is the deleted bundle
344
323
  let nextBundleId = getNextBundleId()
345
324
  if bundleId == nextBundleId {
346
- setNextCapacitorServerPathToDefaultWebAssetDir()
325
+ setNextBundleById(nil)
347
326
  }
348
327
  }
349
328
 
@@ -355,7 +334,7 @@ import CommonCrypto
355
334
  for bundleId in bundleIds {
356
335
  if bundleId != currentBundleId && bundleId != nextBundleId {
357
336
  do {
358
- try deleteBundle(bundleId: bundleId)
337
+ try deleteBundleById(bundleId)
359
338
  } catch {
360
339
  CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "Failed to delete bundle with id: \(bundleId)")
361
340
  }
@@ -363,12 +342,12 @@ import CommonCrypto
363
342
  }
364
343
  }
365
344
 
366
- private func downloadAndVerifyFile(url: String, file: URL, checksum: String?) async throws {
345
+ private func downloadAndVerifyFile(url: String, file: URL, checksum: String?, signature: String?, callback: ((Progress) -> Void)?) async throws {
367
346
  let destination: DownloadRequest.Destination = { _, _ in
368
347
  return (file, [.createIntermediateDirectories])
369
348
  }
370
349
  let urlComponents = URLComponents(string: url)!
371
- let result = try await httpClient.download(url: urlComponents.asURL(), destination: destination)
350
+ let result = try await httpClient.download(url: urlComponents.asURL(), destination: destination, callback: callback)
372
351
  if let error = result.error {
373
352
  CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "Failed to download file: \(error)")
374
353
  if let urlError = error.underlyingError as? URLError {
@@ -381,31 +360,59 @@ import CommonCrypto
381
360
  guard let response = result.response else {
382
361
  throw CustomError.unknown
383
362
  }
384
- let checksum = checksum == nil ? LiveUpdateHttpClient.getChecksumFromResponse(response: response) : checksum
385
- let signature = LiveUpdateHttpClient.getSignatureFromResponse(response: response)
363
+ let checksum = checksum ?? LiveUpdateHttpClient.getChecksumFromResponse(response: response)
364
+ let signature = signature ?? LiveUpdateHttpClient.getSignatureFromResponse(response: response)
386
365
  try verifyFile(url: file, checksum: checksum, signature: signature)
387
366
  }
388
367
 
389
- private func downloadBundleFile(baseUrl: String, href: String, directory: URL) async throws -> URL {
368
+ private func downloadBundleFile(baseUrl: String, href: String, directory: URL, callback: ((Progress) -> Void)?) async throws -> URL {
390
369
  let fileURL = directory.appendingPathComponent(href)
391
370
  var parameters = [String: String]()
392
371
  parameters["href"] = href
393
372
  var urlComponents = URLComponents(string: baseUrl)!
394
373
  urlComponents.queryItems = parameters.map { URLQueryItem(name: $0.key, value: $0.value) }
395
374
  let url = urlComponents.string!
396
- try await self.downloadAndVerifyFile(url: url, file: fileURL, checksum: nil)
375
+ try await self.downloadAndVerifyFile(url: url, file: fileURL, checksum: nil, signature: nil, callback: callback)
397
376
  return fileURL
398
377
  }
399
378
 
400
- private func downloadBundleFiles(url: String, hrefs: [String], directory: URL) async throws {
379
+ private func downloadBundleFiles(url: String, filesToDownload: [ManifestItem], directory: URL, callback: ((Progress) -> Void)?) async throws {
380
+ let totalBytesToDownload = Int64(filesToDownload.map { $0.sizeInBytes }.reduce(0, +))
381
+ actor TotalBytesDownloaded {
382
+ var value: Int64 = 0
383
+ func add(_ amount: Int64) {
384
+ value += amount
385
+ }
386
+ }
387
+ let totalBytesDownloaded = TotalBytesDownloaded()
401
388
  try await withThrowingTaskGroup(of: Void.self) { group in
402
- for href in hrefs {
389
+ for fileToDownload in filesToDownload {
403
390
  group.addTask {
404
- _ = try await self.downloadBundleFile(baseUrl: url, href: href, directory: directory)
391
+ _ = try await self.downloadBundleFile(baseUrl: url, href: fileToDownload.href, directory: directory, callback: { progress in
392
+ Task {
393
+ // Progress is only reported if the file is downloaded in chunks
394
+ if let callback = callback {
395
+ let totalBytesDownloaded = await totalBytesDownloaded.value
396
+ let totalProgress = Progress(totalUnitCount: totalBytesToDownload, completedUnitCount: progress.completedUnitCount + totalBytesDownloaded)
397
+ callback(totalProgress)
398
+ }
399
+ }
400
+ })
401
+ // Emit the current progress in case the file was not downloaded in chunks
402
+ if let callback = callback {
403
+ await totalBytesDownloaded.add(Int64(fileToDownload.sizeInBytes))
404
+ let totalBytesDownloaded = await totalBytesDownloaded.value
405
+ let totalProgress = Progress(totalUnitCount: totalBytesToDownload, completedUnitCount: totalBytesDownloaded)
406
+ callback(totalProgress)
407
+ }
405
408
  }
406
409
  }
407
-
408
410
  try await group.waitForAll()
411
+ // Call the callback one last time to make sure the progress is at 100%
412
+ if let callback = callback {
413
+ let totalProgress = Progress(totalUnitCount: totalBytesToDownload, completedUnitCount: totalBytesToDownload)
414
+ callback(totalProgress)
415
+ }
409
416
  }
410
417
  }
411
418
 
@@ -413,7 +420,7 @@ import CommonCrypto
413
420
  // Create a temporary directory
414
421
  let temporaryDirectory = try createTemporaryDirectory()
415
422
  // Download the latest manifest
416
- let latestManifestFile = try await downloadBundleFile(baseUrl: url, href: manifestFileName, directory: temporaryDirectory)
423
+ let latestManifestFile = try await downloadBundleFile(baseUrl: url, href: manifestFileName, directory: temporaryDirectory, callback: nil)
417
424
  let latestManifest = try loadManifest(file: latestManifestFile)
418
425
  // Load the current manifest
419
426
  let currentManifest = try loadCurrentManifest()
@@ -427,18 +434,24 @@ import CommonCrypto
427
434
  itemsToDownload.append(contentsOf: latestManifest.items)
428
435
  }
429
436
  // Copy the files
430
- try self.copyCurrentBundleFiles(hrefs: itemsToCopy.map { $0.href }, toDirectory: temporaryDirectory)
437
+ try self.copyCurrentBundleFiles(filesToCopy: itemsToCopy, toDirectory: temporaryDirectory)
431
438
  // Download the files
432
- try await self.downloadBundleFiles(url: url, hrefs: itemsToDownload.map { $0.href }, directory: temporaryDirectory)
439
+ try await self.downloadBundleFiles(url: url, filesToDownload: itemsToDownload, directory: temporaryDirectory, callback: { progress in
440
+ let event = DownloadBundleProgressEvent(bundleId: bundleId, downloadedBytes: progress.completedUnitCount, totalBytes: progress.totalUnitCount)
441
+ self.notifyDownloadBundleProgressListeners(event)
442
+ })
433
443
  // Add the bundle
434
444
  try await addBundleOfTypeManifest(bundleId: bundleId, directory: temporaryDirectory)
435
445
  }
436
446
 
437
- private func downloadBundleOfTypeZip(bundleId: String, url: String, checksum: String?) async throws {
447
+ private func downloadBundleOfTypeZip(bundleId: String, checksum: String?, signature: String?, url: String) async throws {
438
448
  let timestamp = String(Int(Date().timeIntervalSince1970))
439
449
  let temporaryZipFileUrl = self.cachesDirectoryUrl.appendingPathComponent(timestamp + ".zip")
440
450
  // Download the bundle
441
- try await downloadAndVerifyFile(url: url, file: temporaryZipFileUrl, checksum: checksum)
451
+ try await downloadAndVerifyFile(url: url, file: temporaryZipFileUrl, checksum: checksum, signature: signature, callback: { progress in
452
+ let event = DownloadBundleProgressEvent(bundleId: bundleId, downloadedBytes: progress.completedUnitCount, totalBytes: progress.totalUnitCount)
453
+ self.notifyDownloadBundleProgressListeners(event)
454
+ })
442
455
  // Add the bundle
443
456
  try await addBundleOfTypeZip(bundleId: bundleId, zipFile: temporaryZipFileUrl)
444
457
  }
@@ -455,7 +468,7 @@ import CommonCrypto
455
468
  parameters["osVersion"] = await UIDevice.current.systemVersion
456
469
  parameters["platform"] = "1"
457
470
  parameters["pluginVersion"] = LiveUpdatePlugin.version
458
- var urlComponents = URLComponents(string: "https://\(host)/v1/apps/\(config.appId ?? "")/bundles/latest")!
471
+ var urlComponents = URLComponents(string: "https://\(config.serverDomain)/v1/apps/\(config.appId ?? "")/bundles/latest")!
459
472
  urlComponents.queryItems = parameters.map { URLQueryItem(name: $0.key, value: $0.value) }
460
473
  let url = try urlComponents.asURL()
461
474
  CAPLog.print("[", LiveUpdatePlugin.tag, "] Fetching latest bundle: ", url)
@@ -517,12 +530,16 @@ import CommonCrypto
517
530
  return digest.map { String(format: "%02hhx", $0) }.joined()
518
531
  }
519
532
 
520
- /// - Returns: The current bundle ID (`public` for the built-in bundle) or `nil` if no view controller was found.
533
+ /// - Returns: The current bundle ID or `nil` if no view controller was found (should never happen) or the default bundle is in use.
521
534
  private func getCurrentBundleId() -> String? {
522
535
  guard let path = getCurrentCapacitorServerPath() else {
523
536
  return nil
524
537
  }
525
- return URL(fileURLWithPath: path).lastPathComponent
538
+ let bundleId = URL(fileURLWithPath: path).lastPathComponent
539
+ if bundleId == defaultWebAssetDir {
540
+ return nil
541
+ }
542
+ return bundleId
526
543
  }
527
544
 
528
545
  /// - Returns: The path to the current bundle directory or `nil` if no view controller was found.
@@ -538,21 +555,30 @@ import CommonCrypto
538
555
  return deviceId.lowercased()
539
556
  }
540
557
 
541
- /// - Returns: The next bundle ID (`public` for the built-in bundle).
542
- private func getNextBundleId() -> String {
558
+ /// - Returns: The next bundle ID or `nil` if the default bundle will be used.
559
+ private func getNextBundleId() -> String? {
543
560
  let path = getNextCapacitorServerPath()
544
- return URL(fileURLWithPath: path).lastPathComponent
561
+ let bundleId = URL(fileURLWithPath: path).lastPathComponent
562
+ if bundleId == defaultWebAssetDir {
563
+ return nil
564
+ }
565
+ return bundleId
545
566
  }
546
567
 
547
568
  /// - Returns: The absolute path to the next bundle directory.
548
569
  private func getNextCapacitorServerPath() -> String {
549
- let defaultCapacitorServerPath = buildCapacitorServerPathFor(bundleId: defaultWebAssetDir)
570
+ let defaultCapacitorServerPath = buildCapacitorServerPathFor(bundleId: nil)
550
571
  if let path = KeyValueStore.standard[self.defaultServerPathKey, as: String.self] {
551
572
  return path.isEmpty ? defaultCapacitorServerPath : path
552
573
  }
553
574
  return defaultCapacitorServerPath
554
575
  }
555
576
 
577
+ /// - Returns: The previous bundle ID or `nil` if the default bundle was used.
578
+ private func getPreviousBundleId() -> String? {
579
+ return preferences.getPreviousBundleId()
580
+ }
581
+
556
582
  private func getVersionCode() -> String {
557
583
  guard let appVersionCode = Bundle.main.infoDictionary?["CFBundleVersion"] as? String else {
558
584
  return ""
@@ -567,27 +593,24 @@ import CommonCrypto
567
593
  return appVersionName
568
594
  }
569
595
 
570
- private func hasBundle(bundleId: String) -> Bool {
596
+ private func hasBundleById(_ bundleId: String) -> Bool {
571
597
  let path = buildBundlePathFor(bundleId: bundleId)
572
598
  return FileManager.default.fileExists(atPath: path)
573
599
  }
574
600
 
575
601
  private func loadCurrentManifest() throws -> Manifest? {
576
- guard let currentBundleId = getCurrentBundleId() else {
577
- return nil
578
- }
579
- if currentBundleId == defaultWebAssetDir {
580
- let files = Bundle.main.urls(forResourcesWithExtension: nil, subdirectory: defaultWebAssetDir) ?? []
581
- let manifestFileUrl = files.first { $0.lastPathComponent == manifestFileName }
582
- if let manifestFileUrl = manifestFileUrl {
602
+ if let currentBundleId = getCurrentBundleId() {
603
+ let manifestFileUrl = buildBundleURLFor(bundleId: currentBundleId).appendingPathComponent(manifestFileName)
604
+ let manifestFileExists = FileManager.default.fileExists(atPath: manifestFileUrl.path)
605
+ if manifestFileExists {
583
606
  return try loadManifest(file: manifestFileUrl)
584
607
  } else {
585
608
  return nil
586
609
  }
587
610
  } else {
588
- let manifestFileUrl = buildBundleURLFor(bundleId: currentBundleId).appendingPathComponent(manifestFileName)
589
- let manifestFileExists = FileManager.default.fileExists(atPath: manifestFileUrl.path)
590
- if manifestFileExists {
611
+ let files = Bundle.main.urls(forResourcesWithExtension: nil, subdirectory: defaultWebAssetDir) ?? []
612
+ let manifestFileUrl = files.first { $0.lastPathComponent == manifestFileName }
613
+ if let manifestFileUrl = manifestFileUrl {
591
614
  return try loadManifest(file: manifestFileUrl)
592
615
  } else {
593
616
  return nil
@@ -602,21 +625,24 @@ import CommonCrypto
602
625
  return Manifest(items: items)
603
626
  }
604
627
 
628
+ private func notifyDownloadBundleProgressListeners(_ event: DownloadBundleProgressEvent) {
629
+ plugin.notifyDownloadBundleProgressListeners(event)
630
+ }
631
+
605
632
  private func rollback() {
606
- if getCurrentBundleId() == defaultWebAssetDir {
633
+ // Set the rollback flag
634
+ rollbackPerformed = true
635
+ // Set the new previous bundle ID
636
+ let currentBundleId = getCurrentBundleId()
637
+ setPreviousBundleId(bundleId: currentBundleId)
638
+ // Perform the rollback
639
+ if let _ = currentBundleId {
640
+ CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "App is not ready. Rolling back to default bundle.")
641
+ setNextBundleById(nil)
642
+ setCurrentBundleById(nil)
643
+ } else {
607
644
  CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "App is not ready. Default bundle is already in use.")
608
- return
609
645
  }
610
- CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "App is not ready. Rolling back to default bundle.")
611
- setNextCapacitorServerPathToDefaultWebAssetDir()
612
- setCurrentCapacitorServerPathToDefaultWebAssetDir()
613
- }
614
-
615
- private func saveCurrentBundleShortVersionStringAndBundleVersion() {
616
- let currentBundleShortVersionString = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
617
- preferences.setLastBundleShortVersionString(currentBundleShortVersionString)
618
- let currentBundleVersion = Bundle.main.infoDictionary?["CFBundleVersion"] as? String
619
- preferences.setLastBundleVersion(currentBundleVersion)
620
646
  }
621
647
 
622
648
  private func searchIndexHtmlFile(url: URL) -> URL? {
@@ -646,6 +672,12 @@ import CommonCrypto
646
672
  return nil
647
673
  }
648
674
 
675
+ /// - Parameter bundleId: The bundle ID to set as the current bundle. If `nil`, the default bundle will be used.
676
+ private func setCurrentBundleById(_ bundleId: String?) {
677
+ let path = buildCapacitorServerPathFor(bundleId: bundleId)
678
+ setCurrentCapacitorServerPath(path: path)
679
+ }
680
+
649
681
  private func setCurrentCapacitorServerPath(path: String) {
650
682
  guard let viewController = self.plugin.bridge?.viewController as? CAPBridgeViewController else {
651
683
  return
@@ -653,12 +685,8 @@ import CommonCrypto
653
685
  viewController.setServerBasePath(path: path)
654
686
  }
655
687
 
656
- private func setCurrentCapacitorServerPathToDefaultWebAssetDir() {
657
- let path = buildCapacitorServerPathFor(bundleId: defaultWebAssetDir)
658
- setCurrentCapacitorServerPath(path: path)
659
- }
660
-
661
- private func setNextBundle(bundleId: String) {
688
+ /// - Parameter bundleId: The bundle ID to set as the next bundle. If `nil`, the default bundle will be used.
689
+ private func setNextBundleById(_ bundleId: String?) {
662
690
  let path = buildCapacitorServerPathFor(bundleId: bundleId)
663
691
  setNextCapacitorServerPath(path: path)
664
692
  }
@@ -673,12 +701,15 @@ import CommonCrypto
673
701
  }
674
702
  }
675
703
 
676
- private func setNextCapacitorServerPathToDefaultWebAssetDir() {
677
- let path = buildCapacitorServerPathFor(bundleId: defaultWebAssetDir)
678
- setNextCapacitorServerPath(path: path)
704
+ /// - Parameter bundleId: The bundle ID to save as the previous bundle ID. If `nil`, the value will be removed.
705
+ private func setPreviousBundleId(bundleId: String?) {
706
+ preferences.setPreviousBundleId(bundleId)
679
707
  }
680
708
 
681
709
  private func startRollbackTimer() {
710
+ guard config.readyTimeout > 0 else {
711
+ return
712
+ }
682
713
  stopRollbackTimer()
683
714
  rollbackDispatchWorkItem = DispatchWorkItem { [weak self] in
684
715
  self?.rollback()
@@ -785,27 +816,6 @@ import CommonCrypto
785
816
  }
786
817
  return verificationResult
787
818
  }
788
-
789
- private func wasUpdated() -> Bool {
790
- guard let lastBundleShortVersionString = preferences.getLastBundleShortVersionString() else {
791
- // If the last bundle short version is not set, the app may have been updated
792
- return true
793
- }
794
- guard let lastBundleVersionString = preferences.getLastBundleVersion() else {
795
- // If the last bundle version is not set, the app may have been updated
796
- return true
797
- }
798
- guard let currentBundleShortVersionString = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String else {
799
- // If the current bundle short version is not set, return true for safety reasons
800
- return true
801
- }
802
- guard let currentBundleVersionString = Bundle.main.infoDictionary?["CFBundleVersion"] as? String else {
803
- // If the current bundle version is not set, return true for safety reasons
804
- return true
805
- }
806
- // `CFBundleVersion` is not unique across different versions of the app
807
- return lastBundleShortVersionString + "_" + lastBundleVersionString != currentBundleShortVersionString + "_" + currentBundleVersionString
808
- }
809
819
  }
810
820
 
811
821
  extension DispatchQueue {
@@ -820,3 +830,10 @@ extension DispatchQueue {
820
830
  }
821
831
  }
822
832
  }
833
+
834
+ extension Progress {
835
+ convenience init(totalUnitCount: Int64, completedUnitCount: Int64) {
836
+ self.init(totalUnitCount: totalUnitCount)
837
+ self.completedUnitCount = completedUnitCount
838
+ }
839
+ }
@@ -2,10 +2,8 @@ public struct LiveUpdateConfig {
2
2
  var appId: String?
3
3
  var autoDeleteBundles = false
4
4
  var defaultChannel: String?
5
- var enabled = true
6
5
  var httpTimeout = 60000
7
- var location: String?
8
6
  var publicKey: String?
9
- var readyTimeout = 10000
10
- var resetOnUpdate = true
7
+ var readyTimeout = 0
8
+ var serverDomain = "api.cloud.capawesome.io"
11
9
  }