@capawesome/capacitor-live-update 6.7.1 → 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 -132
  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 -148
  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 +9 -5
  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,40 +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
- reset()
39
- } else {
40
- startRollbackTimer()
41
- }
42
- saveCurrentBundleVersion()
43
- }
35
+ // Start the rollback timer to rollback to the default bundle
36
+ // if the app is not ready after a certain time
37
+ startRollbackTimer()
44
38
  }
45
39
 
46
40
  @objc public func deleteBundle(_ options: DeleteBundleOptions, completion: @escaping (Error?) -> Void) {
47
41
  let bundleId = options.getBundleId()
48
42
 
49
- if !hasBundle(bundleId: bundleId) {
43
+ if !hasBundleById(bundleId) {
50
44
  let error = CustomError.bundleNotFound
51
45
  completion(error)
52
46
  return
53
47
  }
54
48
 
55
49
  do {
56
- try deleteBundle(bundleId: bundleId)
50
+ try deleteBundleById(bundleId)
57
51
  completion(nil)
58
52
  } catch {
59
53
  completion(error)
@@ -63,11 +57,10 @@ import CommonCrypto
63
57
  @objc public func downloadBundle(_ options: DownloadBundleOptions) async throws {
64
58
  let artifactType = options.getArtifactType()
65
59
  let bundleId = options.getBundleId()
66
- let checksum = options.getChecksum()
67
60
  let url = options.getUrl()
68
61
 
69
62
  // Check if the bundle already exists
70
- if hasBundle(bundleId: bundleId) {
63
+ if hasBundleById(bundleId) {
71
64
  throw CustomError.bundleAlreadyExists
72
65
  }
73
66
 
@@ -75,22 +68,13 @@ import CommonCrypto
75
68
  if artifactType == .manifest {
76
69
  try await downloadBundleOfTypeManifest(bundleId: bundleId, url: url)
77
70
  } else {
78
- try await downloadBundleOfTypeZip(bundleId: bundleId, url: url, checksum: checksum)
71
+ try await downloadBundleOfTypeZip(bundleId: bundleId, url: url)
79
72
  }
80
73
  }
81
74
 
82
75
  @objc public func fetchLatestBundle(_ options: FetchLatestBundleOptions) async throws -> FetchLatestBundleResult {
83
76
  let response: GetLatestBundleResponse? = try await self.fetchLatestBundle(options)
84
- return FetchLatestBundleResult(artifactType: response?.artifactType, bundleId: response?.bundleId, downloadUrl: response?.url)
85
- }
86
-
87
- @objc public func getBundle(completion: @escaping (Result?, Error?) -> Void) {
88
- var bundleId = getCurrentBundleId()
89
- if bundleId == defaultWebAssetDir {
90
- bundleId = nil
91
- }
92
- let result = GetBundleResult(bundleId: bundleId)
93
- completion(result, nil)
77
+ return FetchLatestBundleResult(artifactType: response?.artifactType, bundleId: response?.bundleId, customProperties: response?.customProperties, downloadUrl: response?.url)
94
78
  }
95
79
 
96
80
  @objc public func getBundles(completion: @escaping (Result?, Error?) -> Void) {
@@ -106,10 +90,7 @@ import CommonCrypto
106
90
  }
107
91
 
108
92
  @objc public func getCurrentBundle(completion: @escaping (Result?, Error?) -> Void) {
109
- var bundleId = getCurrentBundleId()
110
- if bundleId == defaultWebAssetDir {
111
- bundleId = nil
112
- }
93
+ let bundleId = getCurrentBundleId()
113
94
  let result = GetCurrentBundleResult(bundleId: bundleId)
114
95
  completion(result, nil)
115
96
  }
@@ -128,10 +109,7 @@ import CommonCrypto
128
109
  }
129
110
 
130
111
  @objc public func getNextBundle(completion: @escaping (Result?, Error?) -> Void) {
131
- var bundleId: String? = getNextBundleId()
132
- if bundleId == defaultWebAssetDir {
133
- bundleId = nil
134
- }
112
+ let bundleId: String? = getNextBundleId()
135
113
  let result = GetNextBundleResult(bundleId: bundleId)
136
114
  completion(result, nil)
137
115
  }
@@ -148,12 +126,27 @@ import CommonCrypto
148
126
  completion(result, nil)
149
127
  }
150
128
 
151
- @objc public func ready() {
129
+ @objc public func ready(completion: @escaping (Result?, Error?) -> Void) {
152
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
153
135
  stopRollbackTimer()
136
+ // Delete unused bundles
154
137
  if config.autoDeleteBundles {
155
138
  deleteUnusedBundles()
156
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
157
150
  }
158
151
 
159
152
  @objc public func reload() {
@@ -163,21 +156,7 @@ import CommonCrypto
163
156
  }
164
157
 
165
158
  @objc public func reset() {
166
- self.setNextCapacitorServerPathToDefaultWebAssetDir()
167
- }
168
-
169
- @objc public func setBundle(_ options: SetBundleOptions, completion: @escaping (Error?) -> Void) {
170
- let bundleId = options.getBundleId()
171
-
172
- // Check if the bundle already exists
173
- if !hasBundle(bundleId: bundleId) {
174
- let error = CustomError.bundleNotFound
175
- completion(error)
176
- return
177
- }
178
-
179
- setNextBundle(bundleId: bundleId)
180
- completion(nil)
159
+ self.setNextBundleById(nil)
181
160
  }
182
161
 
183
162
  @objc public func setChannel(_ options: SetChannelOptions, completion: @escaping (Error?) -> Void) {
@@ -198,8 +177,8 @@ import CommonCrypto
198
177
  let bundleId = options.getBundleId()
199
178
 
200
179
  if let bundleId = bundleId {
201
- if hasBundle(bundleId: bundleId) {
202
- setNextBundle(bundleId: bundleId)
180
+ if hasBundleById(bundleId) {
181
+ setNextBundleById(bundleId)
203
182
  } else {
204
183
  let error = CustomError.bundleNotFound
205
184
  completion(error)
@@ -224,12 +203,12 @@ import CommonCrypto
224
203
  let latestBundleId = response.bundleId
225
204
  let downloadUrl = response.url
226
205
  // Check if bundle already exists
227
- if hasBundle(bundleId: latestBundleId) {
206
+ if hasBundleById(latestBundleId) {
228
207
  var nextBundleId: String?
229
208
  let currentBundleId = self.getCurrentBundleId()
230
209
  if latestBundleId != currentBundleId {
231
210
  // Set the next bundle
232
- setNextBundle(bundleId: latestBundleId)
211
+ setNextBundleById(latestBundleId)
233
212
  nextBundleId = latestBundleId
234
213
  }
235
214
  return SyncResult(nextBundleId: nextBundleId)
@@ -238,10 +217,10 @@ import CommonCrypto
238
217
  if artifactType == .manifest {
239
218
  try await downloadBundleOfTypeManifest(bundleId: latestBundleId, url: downloadUrl)
240
219
  } else {
241
- try await downloadBundleOfTypeZip(bundleId: latestBundleId, url: downloadUrl, checksum: nil)
220
+ try await downloadBundleOfTypeZip(bundleId: latestBundleId, url: downloadUrl)
242
221
  }
243
222
  // Set the next bundle
244
- setNextBundle(bundleId: latestBundleId)
223
+ setNextBundleById(latestBundleId)
245
224
  return SyncResult(nextBundleId: latestBundleId)
246
225
  }
247
226
 
@@ -270,12 +249,12 @@ import CommonCrypto
270
249
  try self.addBundle(bundleId: bundleId, directory: unzippedDirectory)
271
250
  }
272
251
 
273
- private func buildCapacitorServerPathFor(bundleId: String) -> String {
252
+ private func buildCapacitorServerPathFor(bundleId: String?) -> String {
274
253
  let path: String
275
- if bundleId == "public" {
276
- path = Bundle.main.bundleURL.appendingPathComponent(bundleId).path
277
- } else {
254
+ if let bundleId = bundleId {
278
255
  path = buildBundlePathFor(bundleId: bundleId)
256
+ } else {
257
+ path = Bundle.main.bundleURL.appendingPathComponent(defaultWebAssetDir).path
279
258
  }
280
259
  return path
281
260
  }
@@ -289,31 +268,28 @@ import CommonCrypto
289
268
  return url
290
269
  }
291
270
 
292
- private func copyCurrentBundleFile(href: String, toDirectory: URL) throws {
293
- guard let currentBundleId = getCurrentBundleId() else {
294
- return
295
- }
296
-
297
- 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)
298
274
  let parentDirectory = destination.deletingLastPathComponent()
299
275
  try FileManager.default.createDirectory(at: parentDirectory, withIntermediateDirectories: true, attributes: nil)
300
276
 
301
277
  let sourceURL: URL
302
- if currentBundleId == defaultWebAssetDir {
303
- 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 {
304
282
  return
305
283
  }
306
284
  sourceURL = file
307
- } else {
308
- sourceURL = buildBundleURLFor(bundleId: currentBundleId).appendingPathComponent(href)
309
285
  }
310
286
 
311
287
  try FileManager.default.copyItem(at: sourceURL, to: destination)
312
288
  }
313
289
 
314
- private func copyCurrentBundleFiles(hrefs: [String], toDirectory: URL) throws {
315
- for href in hrefs {
316
- 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)
317
293
  }
318
294
  }
319
295
 
@@ -335,14 +311,14 @@ import CommonCrypto
335
311
  return temporaryDirectory
336
312
  }
337
313
 
338
- private func deleteBundle(bundleId: String) throws {
314
+ private func deleteBundleById(_ bundleId: String) throws {
339
315
  // Delete the bundle directory
340
316
  let path = buildBundlePathFor(bundleId: bundleId)
341
317
  try FileManager.default.removeItem(atPath: path)
342
318
  // Reset the next bundle if it is the deleted bundle
343
319
  let nextBundleId = getNextBundleId()
344
320
  if bundleId == nextBundleId {
345
- setNextCapacitorServerPathToDefaultWebAssetDir()
321
+ setNextBundleById(nil)
346
322
  }
347
323
  }
348
324
 
@@ -354,7 +330,7 @@ import CommonCrypto
354
330
  for bundleId in bundleIds {
355
331
  if bundleId != currentBundleId && bundleId != nextBundleId {
356
332
  do {
357
- try deleteBundle(bundleId: bundleId)
333
+ try deleteBundleById(bundleId)
358
334
  } catch {
359
335
  CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "Failed to delete bundle with id: \(bundleId)")
360
336
  }
@@ -362,12 +338,12 @@ import CommonCrypto
362
338
  }
363
339
  }
364
340
 
365
- private func downloadAndVerifyFile(url: String, file: URL, checksum: String?) async throws {
341
+ private func downloadAndVerifyFile(url: String, file: URL, callback: ((Progress) -> Void)?) async throws {
366
342
  let destination: DownloadRequest.Destination = { _, _ in
367
343
  return (file, [.createIntermediateDirectories])
368
344
  }
369
345
  let urlComponents = URLComponents(string: url)!
370
- 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)
371
347
  if let error = result.error {
372
348
  CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "Failed to download file: \(error)")
373
349
  if let urlError = error.underlyingError as? URLError {
@@ -380,31 +356,59 @@ import CommonCrypto
380
356
  guard let response = result.response else {
381
357
  throw CustomError.unknown
382
358
  }
383
- let checksum = checksum == nil ? LiveUpdateHttpClient.getChecksumFromResponse(response: response) : checksum
359
+ let checksum = LiveUpdateHttpClient.getChecksumFromResponse(response: response)
384
360
  let signature = LiveUpdateHttpClient.getSignatureFromResponse(response: response)
385
361
  try verifyFile(url: file, checksum: checksum, signature: signature)
386
362
  }
387
363
 
388
- 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 {
389
365
  let fileURL = directory.appendingPathComponent(href)
390
366
  var parameters = [String: String]()
391
367
  parameters["href"] = href
392
368
  var urlComponents = URLComponents(string: baseUrl)!
393
369
  urlComponents.queryItems = parameters.map { URLQueryItem(name: $0.key, value: $0.value) }
394
370
  let url = urlComponents.string!
395
- try await self.downloadAndVerifyFile(url: url, file: fileURL, checksum: nil)
371
+ try await self.downloadAndVerifyFile(url: url, file: fileURL, callback: callback)
396
372
  return fileURL
397
373
  }
398
374
 
399
- 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()
400
384
  try await withThrowingTaskGroup(of: Void.self) { group in
401
- for href in hrefs {
385
+ for fileToDownload in filesToDownload {
402
386
  group.addTask {
403
- _ = 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
+ }
404
404
  }
405
405
  }
406
-
407
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
+ }
408
412
  }
409
413
  }
410
414
 
@@ -412,7 +416,7 @@ import CommonCrypto
412
416
  // Create a temporary directory
413
417
  let temporaryDirectory = try createTemporaryDirectory()
414
418
  // Download the latest manifest
415
- 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)
416
420
  let latestManifest = try loadManifest(file: latestManifestFile)
417
421
  // Load the current manifest
418
422
  let currentManifest = try loadCurrentManifest()
@@ -426,18 +430,24 @@ import CommonCrypto
426
430
  itemsToDownload.append(contentsOf: latestManifest.items)
427
431
  }
428
432
  // Copy the files
429
- try self.copyCurrentBundleFiles(hrefs: itemsToCopy.map { $0.href }, toDirectory: temporaryDirectory)
433
+ try self.copyCurrentBundleFiles(filesToCopy: itemsToCopy, toDirectory: temporaryDirectory)
430
434
  // Download the files
431
- 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
+ })
432
439
  // Add the bundle
433
440
  try await addBundleOfTypeManifest(bundleId: bundleId, directory: temporaryDirectory)
434
441
  }
435
442
 
436
- private func downloadBundleOfTypeZip(bundleId: String, url: String, checksum: String?) async throws {
443
+ private func downloadBundleOfTypeZip(bundleId: String, url: String) async throws {
437
444
  let timestamp = String(Int(Date().timeIntervalSince1970))
438
445
  let temporaryZipFileUrl = self.cachesDirectoryUrl.appendingPathComponent(timestamp + ".zip")
439
446
  // Download the bundle
440
- 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
+ })
441
451
  // Add the bundle
442
452
  try await addBundleOfTypeZip(bundleId: bundleId, zipFile: temporaryZipFileUrl)
443
453
  }
@@ -454,7 +464,7 @@ import CommonCrypto
454
464
  parameters["osVersion"] = await UIDevice.current.systemVersion
455
465
  parameters["platform"] = "1"
456
466
  parameters["pluginVersion"] = LiveUpdatePlugin.version
457
- 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")!
458
468
  urlComponents.queryItems = parameters.map { URLQueryItem(name: $0.key, value: $0.value) }
459
469
  let url = try urlComponents.asURL()
460
470
  CAPLog.print("[", LiveUpdatePlugin.tag, "] Fetching latest bundle: ", url)
@@ -516,12 +526,16 @@ import CommonCrypto
516
526
  return digest.map { String(format: "%02hhx", $0) }.joined()
517
527
  }
518
528
 
519
- /// - 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.
520
530
  private func getCurrentBundleId() -> String? {
521
531
  guard let path = getCurrentCapacitorServerPath() else {
522
532
  return nil
523
533
  }
524
- return URL(fileURLWithPath: path).lastPathComponent
534
+ let bundleId = URL(fileURLWithPath: path).lastPathComponent
535
+ if bundleId == defaultWebAssetDir {
536
+ return nil
537
+ }
538
+ return bundleId
525
539
  }
526
540
 
527
541
  /// - Returns: The path to the current bundle directory or `nil` if no view controller was found.
@@ -537,21 +551,30 @@ import CommonCrypto
537
551
  return deviceId.lowercased()
538
552
  }
539
553
 
540
- /// - Returns: The next bundle ID (`public` for the built-in bundle).
541
- private func getNextBundleId() -> String {
554
+ /// - Returns: The next bundle ID or `nil` if the default bundle will be used.
555
+ private func getNextBundleId() -> String? {
542
556
  let path = getNextCapacitorServerPath()
543
- return URL(fileURLWithPath: path).lastPathComponent
557
+ let bundleId = URL(fileURLWithPath: path).lastPathComponent
558
+ if bundleId == defaultWebAssetDir {
559
+ return nil
560
+ }
561
+ return bundleId
544
562
  }
545
563
 
546
564
  /// - Returns: The absolute path to the next bundle directory.
547
565
  private func getNextCapacitorServerPath() -> String {
548
- let defaultCapacitorServerPath = buildCapacitorServerPathFor(bundleId: defaultWebAssetDir)
566
+ let defaultCapacitorServerPath = buildCapacitorServerPathFor(bundleId: nil)
549
567
  if let path = KeyValueStore.standard[self.defaultServerPathKey, as: String.self] {
550
568
  return path.isEmpty ? defaultCapacitorServerPath : path
551
569
  }
552
570
  return defaultCapacitorServerPath
553
571
  }
554
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
+
555
578
  private func getVersionCode() -> String {
556
579
  guard let appVersionCode = Bundle.main.infoDictionary?["CFBundleVersion"] as? String else {
557
580
  return ""
@@ -566,27 +589,24 @@ import CommonCrypto
566
589
  return appVersionName
567
590
  }
568
591
 
569
- private func hasBundle(bundleId: String) -> Bool {
592
+ private func hasBundleById(_ bundleId: String) -> Bool {
570
593
  let path = buildBundlePathFor(bundleId: bundleId)
571
594
  return FileManager.default.fileExists(atPath: path)
572
595
  }
573
596
 
574
597
  private func loadCurrentManifest() throws -> Manifest? {
575
- guard let currentBundleId = getCurrentBundleId() else {
576
- return nil
577
- }
578
- if currentBundleId == defaultWebAssetDir {
579
- let files = Bundle.main.urls(forResourcesWithExtension: nil, subdirectory: defaultWebAssetDir) ?? []
580
- let manifestFileUrl = files.first { $0.lastPathComponent == manifestFileName }
581
- 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 {
582
602
  return try loadManifest(file: manifestFileUrl)
583
603
  } else {
584
604
  return nil
585
605
  }
586
606
  } else {
587
- let manifestFileUrl = buildBundleURLFor(bundleId: currentBundleId).appendingPathComponent(manifestFileName)
588
- let manifestFileExists = FileManager.default.fileExists(atPath: manifestFileUrl.path)
589
- 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 {
590
610
  return try loadManifest(file: manifestFileUrl)
591
611
  } else {
592
612
  return nil
@@ -601,21 +621,24 @@ import CommonCrypto
601
621
  return Manifest(items: items)
602
622
  }
603
623
 
604
- private func rollback() {
605
- if getCurrentBundleId() == defaultWebAssetDir {
606
- CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "App is not ready. Default bundle is already in use.")
607
- return
608
- }
609
- CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "App is not ready. Rolling back to default bundle.")
610
- setNextCapacitorServerPathToDefaultWebAssetDir()
611
- setCurrentCapacitorServerPathToDefaultWebAssetDir()
624
+ private func notifyDownloadBundleProgressListeners(_ event: DownloadBundleProgressEvent) {
625
+ plugin.notifyDownloadBundleProgressListeners(event)
612
626
  }
613
627
 
614
- private func saveCurrentBundleVersion() {
615
- guard let currentBundleVersion = Bundle.main.infoDictionary?["CFBundleVersion"] as? String else {
616
- return
628
+ private func rollback() {
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 {
640
+ CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "App is not ready. Default bundle is already in use.")
617
641
  }
618
- preferences.setLastBundleVersion(currentBundleVersion)
619
642
  }
620
643
 
621
644
  private func searchIndexHtmlFile(url: URL) -> URL? {
@@ -645,6 +668,12 @@ import CommonCrypto
645
668
  return nil
646
669
  }
647
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
+
648
677
  private func setCurrentCapacitorServerPath(path: String) {
649
678
  guard let viewController = self.plugin.bridge?.viewController as? CAPBridgeViewController else {
650
679
  return
@@ -652,12 +681,8 @@ import CommonCrypto
652
681
  viewController.setServerBasePath(path: path)
653
682
  }
654
683
 
655
- private func setCurrentCapacitorServerPathToDefaultWebAssetDir() {
656
- let path = buildCapacitorServerPathFor(bundleId: defaultWebAssetDir)
657
- setCurrentCapacitorServerPath(path: path)
658
- }
659
-
660
- 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?) {
661
686
  let path = buildCapacitorServerPathFor(bundleId: bundleId)
662
687
  setNextCapacitorServerPath(path: path)
663
688
  }
@@ -672,12 +697,15 @@ import CommonCrypto
672
697
  }
673
698
  }
674
699
 
675
- private func setNextCapacitorServerPathToDefaultWebAssetDir() {
676
- let path = buildCapacitorServerPathFor(bundleId: defaultWebAssetDir)
677
- 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)
678
703
  }
679
704
 
680
705
  private func startRollbackTimer() {
706
+ guard config.readyTimeout > 0 else {
707
+ return
708
+ }
681
709
  stopRollbackTimer()
682
710
  rollbackDispatchWorkItem = DispatchWorkItem { [weak self] in
683
711
  self?.rollback()
@@ -784,22 +812,6 @@ import CommonCrypto
784
812
  }
785
813
  return verificationResult
786
814
  }
787
-
788
- private func wasUpdated() -> Bool {
789
- guard let lastBundleVersionString = preferences.getLastBundleVersion() else {
790
- return false
791
- }
792
- guard let lastBundleVersion = Int(lastBundleVersionString) else {
793
- return false
794
- }
795
- guard let currentBundleVersionString = Bundle.main.infoDictionary?["CFBundleVersion"] as? String else {
796
- return false
797
- }
798
- guard let currentBundleVersion = Int(currentBundleVersionString) else {
799
- return false
800
- }
801
- return currentBundleVersion > lastBundleVersion
802
- }
803
815
  }
804
816
 
805
817
  extension DispatchQueue {
@@ -814,3 +826,10 @@ extension DispatchQueue {
814
826
  }
815
827
  }
816
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
  }