@capawesome/capacitor-live-update 6.7.2 → 7.0.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 +91 -81
  4. package/android/build.gradle +9 -9
  5. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdate.java +165 -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 +50 -78
  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 +9 -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 +1 -10
  13. package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/classes/results/FetchLatestBundleResult.java +13 -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 +191 -158
  17. package/dist/esm/definitions.d.ts +107 -52
  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 +1 -7
  29. package/ios/Plugin/Classes/Results/{FetchLatestBundle.swift → FetchLatestBundleResult.swift} +6 -1
  30. package/ios/Plugin/Classes/Results/ReadyResult.swift +22 -0
  31. package/ios/Plugin/LiveUpdate.swift +167 -154
  32. package/ios/Plugin/LiveUpdateConfig.swift +2 -4
  33. package/ios/Plugin/LiveUpdateHttpClient.swift +6 -2
  34. package/ios/Plugin/LiveUpdatePlugin.swift +44 -38
  35. package/ios/Plugin/LiveUpdatePreferences.swift +6 -20
  36. package/ios/Plugin/Protocols/Api/GetLatestBundleResponse.swift +1 -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)
@@ -64,11 +57,10 @@ import CommonCrypto
64
57
  @objc public func downloadBundle(_ options: DownloadBundleOptions) async throws {
65
58
  let artifactType = options.getArtifactType()
66
59
  let bundleId = options.getBundleId()
67
- let checksum = options.getChecksum()
68
60
  let url = options.getUrl()
69
61
 
70
62
  // Check if the bundle already exists
71
- if hasBundle(bundleId: bundleId) {
63
+ if hasBundleById(bundleId) {
72
64
  throw CustomError.bundleAlreadyExists
73
65
  }
74
66
 
@@ -76,22 +68,13 @@ import CommonCrypto
76
68
  if artifactType == .manifest {
77
69
  try await downloadBundleOfTypeManifest(bundleId: bundleId, url: url)
78
70
  } else {
79
- try await downloadBundleOfTypeZip(bundleId: bundleId, url: url, checksum: checksum)
71
+ try await downloadBundleOfTypeZip(bundleId: bundleId, url: url)
80
72
  }
81
73
  }
82
74
 
83
75
  @objc public func fetchLatestBundle(_ options: FetchLatestBundleOptions) async throws -> FetchLatestBundleResult {
84
76
  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)
77
+ return FetchLatestBundleResult(artifactType: response?.artifactType, bundleId: response?.bundleId, customProperties: response?.customProperties, downloadUrl: response?.url)
95
78
  }
96
79
 
97
80
  @objc public func getBundles(completion: @escaping (Result?, Error?) -> Void) {
@@ -107,10 +90,7 @@ import CommonCrypto
107
90
  }
108
91
 
109
92
  @objc public func getCurrentBundle(completion: @escaping (Result?, Error?) -> Void) {
110
- var bundleId = getCurrentBundleId()
111
- if bundleId == defaultWebAssetDir {
112
- bundleId = nil
113
- }
93
+ let bundleId = getCurrentBundleId()
114
94
  let result = GetCurrentBundleResult(bundleId: bundleId)
115
95
  completion(result, nil)
116
96
  }
@@ -129,10 +109,7 @@ import CommonCrypto
129
109
  }
130
110
 
131
111
  @objc public func getNextBundle(completion: @escaping (Result?, Error?) -> Void) {
132
- var bundleId: String? = getNextBundleId()
133
- if bundleId == defaultWebAssetDir {
134
- bundleId = nil
135
- }
112
+ let bundleId: String? = getNextBundleId()
136
113
  let result = GetNextBundleResult(bundleId: bundleId)
137
114
  completion(result, nil)
138
115
  }
@@ -149,12 +126,27 @@ import CommonCrypto
149
126
  completion(result, nil)
150
127
  }
151
128
 
152
- @objc public func ready() {
129
+ @objc public func ready(completion: @escaping (Result?, Error?) -> Void) {
153
130
  CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "App is ready.")
131
+ if config.readyTimeout <= 0 {
132
+ CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "Ready timeout is set to 0. Automatic rollback is disabled.")
133
+ }
134
+ // Stop the rollback timer
154
135
  stopRollbackTimer()
136
+ // Delete unused bundles
155
137
  if config.autoDeleteBundles {
156
138
  deleteUnusedBundles()
157
139
  }
140
+ // Get the current and previous bundle IDs
141
+ let currentBundleId = getCurrentBundleId()
142
+ let previousBundleId = getPreviousBundleId()
143
+ // Return the result
144
+ let result = ReadyResult(currentBundleId: currentBundleId, previousBundleId: previousBundleId, rollback: rollbackPerformed)
145
+ completion(result, nil)
146
+ // Set the new previous bundle ID
147
+ setPreviousBundleId(bundleId: currentBundleId)
148
+ // Reset the rollback flag
149
+ rollbackPerformed = false
158
150
  }
159
151
 
160
152
  @objc public func reload() {
@@ -164,21 +156,7 @@ import CommonCrypto
164
156
  }
165
157
 
166
158
  @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)
159
+ self.setNextBundleById(nil)
182
160
  }
183
161
 
184
162
  @objc public func setChannel(_ options: SetChannelOptions, completion: @escaping (Error?) -> Void) {
@@ -199,8 +177,8 @@ import CommonCrypto
199
177
  let bundleId = options.getBundleId()
200
178
 
201
179
  if let bundleId = bundleId {
202
- if hasBundle(bundleId: bundleId) {
203
- setNextBundle(bundleId: bundleId)
180
+ if hasBundleById(bundleId) {
181
+ setNextBundleById(bundleId)
204
182
  } else {
205
183
  let error = CustomError.bundleNotFound
206
184
  completion(error)
@@ -225,12 +203,12 @@ import CommonCrypto
225
203
  let latestBundleId = response.bundleId
226
204
  let downloadUrl = response.url
227
205
  // Check if bundle already exists
228
- if hasBundle(bundleId: latestBundleId) {
206
+ if hasBundleById(latestBundleId) {
229
207
  var nextBundleId: String?
230
208
  let currentBundleId = self.getCurrentBundleId()
231
209
  if latestBundleId != currentBundleId {
232
210
  // Set the next bundle
233
- setNextBundle(bundleId: latestBundleId)
211
+ setNextBundleById(latestBundleId)
234
212
  nextBundleId = latestBundleId
235
213
  }
236
214
  return SyncResult(nextBundleId: nextBundleId)
@@ -239,10 +217,10 @@ import CommonCrypto
239
217
  if artifactType == .manifest {
240
218
  try await downloadBundleOfTypeManifest(bundleId: latestBundleId, url: downloadUrl)
241
219
  } else {
242
- try await downloadBundleOfTypeZip(bundleId: latestBundleId, url: downloadUrl, checksum: nil)
220
+ try await downloadBundleOfTypeZip(bundleId: latestBundleId, url: downloadUrl)
243
221
  }
244
222
  // Set the next bundle
245
- setNextBundle(bundleId: latestBundleId)
223
+ setNextBundleById(latestBundleId)
246
224
  return SyncResult(nextBundleId: latestBundleId)
247
225
  }
248
226
 
@@ -271,12 +249,12 @@ import CommonCrypto
271
249
  try self.addBundle(bundleId: bundleId, directory: unzippedDirectory)
272
250
  }
273
251
 
274
- private func buildCapacitorServerPathFor(bundleId: String) -> String {
252
+ private func buildCapacitorServerPathFor(bundleId: String?) -> String {
275
253
  let path: String
276
- if bundleId == "public" {
277
- path = Bundle.main.bundleURL.appendingPathComponent(bundleId).path
278
- } else {
254
+ if let bundleId = bundleId {
279
255
  path = buildBundlePathFor(bundleId: bundleId)
256
+ } else {
257
+ path = Bundle.main.bundleURL.appendingPathComponent(defaultWebAssetDir).path
280
258
  }
281
259
  return path
282
260
  }
@@ -290,31 +268,28 @@ import CommonCrypto
290
268
  return url
291
269
  }
292
270
 
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)
271
+ private func copyCurrentBundleFile(fileToCopy: ManifestItem, toDirectory: URL) throws {
272
+ let currentBundleId = getCurrentBundleId()
273
+ let destination = toDirectory.appendingPathComponent(fileToCopy.href)
299
274
  let parentDirectory = destination.deletingLastPathComponent()
300
275
  try FileManager.default.createDirectory(at: parentDirectory, withIntermediateDirectories: true, attributes: nil)
301
276
 
302
277
  let sourceURL: URL
303
- if currentBundleId == defaultWebAssetDir {
304
- guard let file = Bundle.main.url(forResource: href, withExtension: nil, subdirectory: defaultWebAssetDir) else {
278
+ if let currentBundleId = currentBundleId {
279
+ sourceURL = buildBundleURLFor(bundleId: currentBundleId).appendingPathComponent(fileToCopy.href)
280
+ } else {
281
+ guard let file = Bundle.main.url(forResource: fileToCopy.href, withExtension: nil, subdirectory: defaultWebAssetDir) else {
305
282
  return
306
283
  }
307
284
  sourceURL = file
308
- } else {
309
- sourceURL = buildBundleURLFor(bundleId: currentBundleId).appendingPathComponent(href)
310
285
  }
311
286
 
312
287
  try FileManager.default.copyItem(at: sourceURL, to: destination)
313
288
  }
314
289
 
315
- private func copyCurrentBundleFiles(hrefs: [String], toDirectory: URL) throws {
316
- for href in hrefs {
317
- try copyCurrentBundleFile(href: href, toDirectory: toDirectory)
290
+ private func copyCurrentBundleFiles(filesToCopy: [ManifestItem], toDirectory: URL) throws {
291
+ for fileToCopy in filesToCopy {
292
+ try copyCurrentBundleFile(fileToCopy: fileToCopy, toDirectory: toDirectory)
318
293
  }
319
294
  }
320
295
 
@@ -336,14 +311,14 @@ import CommonCrypto
336
311
  return temporaryDirectory
337
312
  }
338
313
 
339
- private func deleteBundle(bundleId: String) throws {
314
+ private func deleteBundleById(_ bundleId: String) throws {
340
315
  // Delete the bundle directory
341
316
  let path = buildBundlePathFor(bundleId: bundleId)
342
317
  try FileManager.default.removeItem(atPath: path)
343
318
  // Reset the next bundle if it is the deleted bundle
344
319
  let nextBundleId = getNextBundleId()
345
320
  if bundleId == nextBundleId {
346
- setNextCapacitorServerPathToDefaultWebAssetDir()
321
+ setNextBundleById(nil)
347
322
  }
348
323
  }
349
324
 
@@ -355,7 +330,7 @@ import CommonCrypto
355
330
  for bundleId in bundleIds {
356
331
  if bundleId != currentBundleId && bundleId != nextBundleId {
357
332
  do {
358
- try deleteBundle(bundleId: bundleId)
333
+ try deleteBundleById(bundleId)
359
334
  } catch {
360
335
  CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "Failed to delete bundle with id: \(bundleId)")
361
336
  }
@@ -363,12 +338,12 @@ import CommonCrypto
363
338
  }
364
339
  }
365
340
 
366
- private func downloadAndVerifyFile(url: String, file: URL, checksum: String?) async throws {
341
+ private func downloadAndVerifyFile(url: String, file: URL, callback: ((Progress) -> Void)?) async throws {
367
342
  let destination: DownloadRequest.Destination = { _, _ in
368
343
  return (file, [.createIntermediateDirectories])
369
344
  }
370
345
  let urlComponents = URLComponents(string: url)!
371
- let result = try await httpClient.download(url: urlComponents.asURL(), destination: destination)
346
+ let result = try await httpClient.download(url: urlComponents.asURL(), destination: destination, callback: callback)
372
347
  if let error = result.error {
373
348
  CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "Failed to download file: \(error)")
374
349
  if let urlError = error.underlyingError as? URLError {
@@ -381,31 +356,59 @@ import CommonCrypto
381
356
  guard let response = result.response else {
382
357
  throw CustomError.unknown
383
358
  }
384
- let checksum = checksum == nil ? LiveUpdateHttpClient.getChecksumFromResponse(response: response) : checksum
359
+ let checksum = LiveUpdateHttpClient.getChecksumFromResponse(response: response)
385
360
  let signature = LiveUpdateHttpClient.getSignatureFromResponse(response: response)
386
361
  try verifyFile(url: file, checksum: checksum, signature: signature)
387
362
  }
388
363
 
389
- private func downloadBundleFile(baseUrl: String, href: String, directory: URL) async throws -> URL {
364
+ private func downloadBundleFile(baseUrl: String, href: String, directory: URL, callback: ((Progress) -> Void)?) async throws -> URL {
390
365
  let fileURL = directory.appendingPathComponent(href)
391
366
  var parameters = [String: String]()
392
367
  parameters["href"] = href
393
368
  var urlComponents = URLComponents(string: baseUrl)!
394
369
  urlComponents.queryItems = parameters.map { URLQueryItem(name: $0.key, value: $0.value) }
395
370
  let url = urlComponents.string!
396
- try await self.downloadAndVerifyFile(url: url, file: fileURL, checksum: nil)
371
+ try await self.downloadAndVerifyFile(url: url, file: fileURL, callback: callback)
397
372
  return fileURL
398
373
  }
399
374
 
400
- private func downloadBundleFiles(url: String, hrefs: [String], directory: URL) async throws {
375
+ private func downloadBundleFiles(url: String, filesToDownload: [ManifestItem], directory: URL, callback: ((Progress) -> Void)?) async throws {
376
+ let totalBytesToDownload = Int64(filesToDownload.map { $0.sizeInBytes }.reduce(0, +))
377
+ actor TotalBytesDownloaded {
378
+ var value: Int64 = 0
379
+ func add(_ amount: Int64) {
380
+ value += amount
381
+ }
382
+ }
383
+ let totalBytesDownloaded = TotalBytesDownloaded()
401
384
  try await withThrowingTaskGroup(of: Void.self) { group in
402
- for href in hrefs {
385
+ for fileToDownload in filesToDownload {
403
386
  group.addTask {
404
- _ = try await self.downloadBundleFile(baseUrl: url, href: href, directory: directory)
387
+ _ = try await self.downloadBundleFile(baseUrl: url, href: fileToDownload.href, directory: directory, callback: { progress in
388
+ Task {
389
+ // Progress is only reported if the file is downloaded in chunks
390
+ if let callback = callback {
391
+ let totalBytesDownloaded = await totalBytesDownloaded.value
392
+ let totalProgress = Progress(totalUnitCount: totalBytesToDownload, completedUnitCount: progress.completedUnitCount + totalBytesDownloaded)
393
+ callback(totalProgress)
394
+ }
395
+ }
396
+ })
397
+ // Emit the current progress in case the file was not downloaded in chunks
398
+ if let callback = callback {
399
+ await totalBytesDownloaded.add(Int64(fileToDownload.sizeInBytes))
400
+ let totalBytesDownloaded = await totalBytesDownloaded.value
401
+ let totalProgress = Progress(totalUnitCount: totalBytesToDownload, completedUnitCount: totalBytesDownloaded)
402
+ callback(totalProgress)
403
+ }
405
404
  }
406
405
  }
407
-
408
406
  try await group.waitForAll()
407
+ // Call the callback one last time to make sure the progress is at 100%
408
+ if let callback = callback {
409
+ let totalProgress = Progress(totalUnitCount: totalBytesToDownload, completedUnitCount: totalBytesToDownload)
410
+ callback(totalProgress)
411
+ }
409
412
  }
410
413
  }
411
414
 
@@ -413,7 +416,7 @@ import CommonCrypto
413
416
  // Create a temporary directory
414
417
  let temporaryDirectory = try createTemporaryDirectory()
415
418
  // Download the latest manifest
416
- let latestManifestFile = try await downloadBundleFile(baseUrl: url, href: manifestFileName, directory: temporaryDirectory)
419
+ let latestManifestFile = try await downloadBundleFile(baseUrl: url, href: manifestFileName, directory: temporaryDirectory, callback: nil)
417
420
  let latestManifest = try loadManifest(file: latestManifestFile)
418
421
  // Load the current manifest
419
422
  let currentManifest = try loadCurrentManifest()
@@ -427,18 +430,24 @@ import CommonCrypto
427
430
  itemsToDownload.append(contentsOf: latestManifest.items)
428
431
  }
429
432
  // Copy the files
430
- try self.copyCurrentBundleFiles(hrefs: itemsToCopy.map { $0.href }, toDirectory: temporaryDirectory)
433
+ try self.copyCurrentBundleFiles(filesToCopy: itemsToCopy, toDirectory: temporaryDirectory)
431
434
  // Download the files
432
- try await self.downloadBundleFiles(url: url, hrefs: itemsToDownload.map { $0.href }, directory: temporaryDirectory)
435
+ try await self.downloadBundleFiles(url: url, filesToDownload: itemsToDownload, directory: temporaryDirectory, callback: { progress in
436
+ let event = DownloadBundleProgressEvent(bundleId: bundleId, downloadedBytes: progress.completedUnitCount, totalBytes: progress.totalUnitCount)
437
+ self.notifyDownloadBundleProgressListeners(event)
438
+ })
433
439
  // Add the bundle
434
440
  try await addBundleOfTypeManifest(bundleId: bundleId, directory: temporaryDirectory)
435
441
  }
436
442
 
437
- private func downloadBundleOfTypeZip(bundleId: String, url: String, checksum: String?) async throws {
443
+ private func downloadBundleOfTypeZip(bundleId: String, url: String) async throws {
438
444
  let timestamp = String(Int(Date().timeIntervalSince1970))
439
445
  let temporaryZipFileUrl = self.cachesDirectoryUrl.appendingPathComponent(timestamp + ".zip")
440
446
  // Download the bundle
441
- try await downloadAndVerifyFile(url: url, file: temporaryZipFileUrl, checksum: checksum)
447
+ try await downloadAndVerifyFile(url: url, file: temporaryZipFileUrl, callback: { progress in
448
+ let event = DownloadBundleProgressEvent(bundleId: bundleId, downloadedBytes: progress.completedUnitCount, totalBytes: progress.totalUnitCount)
449
+ self.notifyDownloadBundleProgressListeners(event)
450
+ })
442
451
  // Add the bundle
443
452
  try await addBundleOfTypeZip(bundleId: bundleId, zipFile: temporaryZipFileUrl)
444
453
  }
@@ -455,7 +464,7 @@ import CommonCrypto
455
464
  parameters["osVersion"] = await UIDevice.current.systemVersion
456
465
  parameters["platform"] = "1"
457
466
  parameters["pluginVersion"] = LiveUpdatePlugin.version
458
- var urlComponents = URLComponents(string: "https://\(host)/v1/apps/\(config.appId ?? "")/bundles/latest")!
467
+ var urlComponents = URLComponents(string: "https://\(config.serverDomain)/v1/apps/\(config.appId ?? "")/bundles/latest")!
459
468
  urlComponents.queryItems = parameters.map { URLQueryItem(name: $0.key, value: $0.value) }
460
469
  let url = try urlComponents.asURL()
461
470
  CAPLog.print("[", LiveUpdatePlugin.tag, "] Fetching latest bundle: ", url)
@@ -517,12 +526,16 @@ import CommonCrypto
517
526
  return digest.map { String(format: "%02hhx", $0) }.joined()
518
527
  }
519
528
 
520
- /// - Returns: The current bundle ID (`public` for the built-in bundle) or `nil` if no view controller was found.
529
+ /// - Returns: The current bundle ID or `nil` if no view controller was found (should never happen) or the default bundle is in use.
521
530
  private func getCurrentBundleId() -> String? {
522
531
  guard let path = getCurrentCapacitorServerPath() else {
523
532
  return nil
524
533
  }
525
- return URL(fileURLWithPath: path).lastPathComponent
534
+ let bundleId = URL(fileURLWithPath: path).lastPathComponent
535
+ if bundleId == defaultWebAssetDir {
536
+ return nil
537
+ }
538
+ return bundleId
526
539
  }
527
540
 
528
541
  /// - Returns: The path to the current bundle directory or `nil` if no view controller was found.
@@ -538,21 +551,30 @@ import CommonCrypto
538
551
  return deviceId.lowercased()
539
552
  }
540
553
 
541
- /// - Returns: The next bundle ID (`public` for the built-in bundle).
542
- private func getNextBundleId() -> String {
554
+ /// - Returns: The next bundle ID or `nil` if the default bundle will be used.
555
+ private func getNextBundleId() -> String? {
543
556
  let path = getNextCapacitorServerPath()
544
- return URL(fileURLWithPath: path).lastPathComponent
557
+ let bundleId = URL(fileURLWithPath: path).lastPathComponent
558
+ if bundleId == defaultWebAssetDir {
559
+ return nil
560
+ }
561
+ return bundleId
545
562
  }
546
563
 
547
564
  /// - Returns: The absolute path to the next bundle directory.
548
565
  private func getNextCapacitorServerPath() -> String {
549
- let defaultCapacitorServerPath = buildCapacitorServerPathFor(bundleId: defaultWebAssetDir)
566
+ let defaultCapacitorServerPath = buildCapacitorServerPathFor(bundleId: nil)
550
567
  if let path = KeyValueStore.standard[self.defaultServerPathKey, as: String.self] {
551
568
  return path.isEmpty ? defaultCapacitorServerPath : path
552
569
  }
553
570
  return defaultCapacitorServerPath
554
571
  }
555
572
 
573
+ /// - Returns: The previous bundle ID or `nil` if the default bundle was used.
574
+ private func getPreviousBundleId() -> String? {
575
+ return preferences.getPreviousBundleId()
576
+ }
577
+
556
578
  private func getVersionCode() -> String {
557
579
  guard let appVersionCode = Bundle.main.infoDictionary?["CFBundleVersion"] as? String else {
558
580
  return ""
@@ -567,27 +589,24 @@ import CommonCrypto
567
589
  return appVersionName
568
590
  }
569
591
 
570
- private func hasBundle(bundleId: String) -> Bool {
592
+ private func hasBundleById(_ bundleId: String) -> Bool {
571
593
  let path = buildBundlePathFor(bundleId: bundleId)
572
594
  return FileManager.default.fileExists(atPath: path)
573
595
  }
574
596
 
575
597
  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 {
598
+ if let currentBundleId = getCurrentBundleId() {
599
+ let manifestFileUrl = buildBundleURLFor(bundleId: currentBundleId).appendingPathComponent(manifestFileName)
600
+ let manifestFileExists = FileManager.default.fileExists(atPath: manifestFileUrl.path)
601
+ if manifestFileExists {
583
602
  return try loadManifest(file: manifestFileUrl)
584
603
  } else {
585
604
  return nil
586
605
  }
587
606
  } else {
588
- let manifestFileUrl = buildBundleURLFor(bundleId: currentBundleId).appendingPathComponent(manifestFileName)
589
- let manifestFileExists = FileManager.default.fileExists(atPath: manifestFileUrl.path)
590
- if manifestFileExists {
607
+ let files = Bundle.main.urls(forResourcesWithExtension: nil, subdirectory: defaultWebAssetDir) ?? []
608
+ let manifestFileUrl = files.first { $0.lastPathComponent == manifestFileName }
609
+ if let manifestFileUrl = manifestFileUrl {
591
610
  return try loadManifest(file: manifestFileUrl)
592
611
  } else {
593
612
  return nil
@@ -602,21 +621,24 @@ import CommonCrypto
602
621
  return Manifest(items: items)
603
622
  }
604
623
 
624
+ private func notifyDownloadBundleProgressListeners(_ event: DownloadBundleProgressEvent) {
625
+ plugin.notifyDownloadBundleProgressListeners(event)
626
+ }
627
+
605
628
  private func rollback() {
606
- if getCurrentBundleId() == defaultWebAssetDir {
629
+ // Set the rollback flag
630
+ rollbackPerformed = true
631
+ // Set the new previous bundle ID
632
+ let currentBundleId = getCurrentBundleId()
633
+ setPreviousBundleId(bundleId: currentBundleId)
634
+ // Perform the rollback
635
+ if let _ = currentBundleId {
636
+ CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "App is not ready. Rolling back to default bundle.")
637
+ setNextBundleById(nil)
638
+ setCurrentBundleById(nil)
639
+ } else {
607
640
  CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "App is not ready. Default bundle is already in use.")
608
- return
609
641
  }
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
642
  }
621
643
 
622
644
  private func searchIndexHtmlFile(url: URL) -> URL? {
@@ -646,6 +668,12 @@ import CommonCrypto
646
668
  return nil
647
669
  }
648
670
 
671
+ /// - Parameter bundleId: The bundle ID to set as the current bundle. If `nil`, the default bundle will be used.
672
+ private func setCurrentBundleById(_ bundleId: String?) {
673
+ let path = buildCapacitorServerPathFor(bundleId: bundleId)
674
+ setCurrentCapacitorServerPath(path: path)
675
+ }
676
+
649
677
  private func setCurrentCapacitorServerPath(path: String) {
650
678
  guard let viewController = self.plugin.bridge?.viewController as? CAPBridgeViewController else {
651
679
  return
@@ -653,12 +681,8 @@ import CommonCrypto
653
681
  viewController.setServerBasePath(path: path)
654
682
  }
655
683
 
656
- private func setCurrentCapacitorServerPathToDefaultWebAssetDir() {
657
- let path = buildCapacitorServerPathFor(bundleId: defaultWebAssetDir)
658
- setCurrentCapacitorServerPath(path: path)
659
- }
660
-
661
- private func setNextBundle(bundleId: String) {
684
+ /// - Parameter bundleId: The bundle ID to set as the next bundle. If `nil`, the default bundle will be used.
685
+ private func setNextBundleById(_ bundleId: String?) {
662
686
  let path = buildCapacitorServerPathFor(bundleId: bundleId)
663
687
  setNextCapacitorServerPath(path: path)
664
688
  }
@@ -673,12 +697,15 @@ import CommonCrypto
673
697
  }
674
698
  }
675
699
 
676
- private func setNextCapacitorServerPathToDefaultWebAssetDir() {
677
- let path = buildCapacitorServerPathFor(bundleId: defaultWebAssetDir)
678
- setNextCapacitorServerPath(path: path)
700
+ /// - Parameter bundleId: The bundle ID to save as the previous bundle ID. If `nil`, the value will be removed.
701
+ private func setPreviousBundleId(bundleId: String?) {
702
+ preferences.setPreviousBundleId(bundleId)
679
703
  }
680
704
 
681
705
  private func startRollbackTimer() {
706
+ guard config.readyTimeout > 0 else {
707
+ return
708
+ }
682
709
  stopRollbackTimer()
683
710
  rollbackDispatchWorkItem = DispatchWorkItem { [weak self] in
684
711
  self?.rollback()
@@ -785,27 +812,6 @@ import CommonCrypto
785
812
  }
786
813
  return verificationResult
787
814
  }
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
815
  }
810
816
 
811
817
  extension DispatchQueue {
@@ -820,3 +826,10 @@ extension DispatchQueue {
820
826
  }
821
827
  }
822
828
  }
829
+
830
+ extension Progress {
831
+ convenience init(totalUnitCount: Int64, completedUnitCount: Int64) {
832
+ self.init(totalUnitCount: totalUnitCount)
833
+ self.completedUnitCount = completedUnitCount
834
+ }
835
+ }
@@ -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
  }
@@ -18,12 +18,16 @@ public class LiveUpdateHttpClient: NSObject {
18
18
  self.config = config
19
19
  }
20
20
 
21
- public func download(url: URL, destination: @escaping DownloadRequest.Destination) async throws -> AFDownloadResponse<Data> {
21
+ public func download(url: URL, destination: @escaping DownloadRequest.Destination, callback: ((Progress) -> Void)?) async throws -> AFDownloadResponse<Data> {
22
22
  var request = URLRequest(url: url)
23
23
  request.httpMethod = HTTPMethod.get.rawValue
24
24
  request.timeoutInterval = Double(config.httpTimeout) / 1000.0
25
25
  return try await withCheckedThrowingContinuation { continuation in
26
- AF.download(request, to: destination).responseData { response in
26
+ AF.download(request, to: destination).downloadProgress { progress in
27
+ if let callback = callback {
28
+ callback(progress)
29
+ }
30
+ }.responseData { response in
27
31
  continuation.resume(returning: response)
28
32
  }
29
33
  }