@otakit/capacitor-updater 1.2.0 → 2.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.
@@ -23,8 +23,6 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
23
23
  CAPPluginMethod(name: "download", returnType: CAPPluginReturnPromise),
24
24
  CAPPluginMethod(name: "apply", returnType: CAPPluginReturnPromise),
25
25
  CAPPluginMethod(name: "debugGetState", returnType: CAPPluginReturnPromise),
26
- CAPPluginMethod(name: "debugCheck", returnType: CAPPluginReturnPromise),
27
- CAPPluginMethod(name: "debugDownload", returnType: CAPPluginReturnPromise),
28
26
  CAPPluginMethod(name: "notifyAppReady", returnType: CAPPluginReturnPromise),
29
27
  CAPPluginMethod(name: "debugReset", returnType: CAPPluginReturnPromise),
30
28
  CAPPluginMethod(name: "debugListBundles", returnType: CAPPluginReturnPromise),
@@ -41,6 +39,7 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
41
39
  private var allowInsecureUrls = false
42
40
  private var updateMode: UpdateMode = .nextLaunch
43
41
  private var updateUrl = UpdaterPlugin.defaultUpdateURL
42
+ private var cdnUrl = UpdaterPlugin.defaultCdnURL
44
43
  private var appId: String?
45
44
  private var channel: String?
46
45
  private var runtimeVersion: String?
@@ -51,15 +50,22 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
51
50
  private var checkInProgress = false
52
51
  private var foregroundObserver: NSObjectProtocol?
53
52
  private static let defaultUpdateURL = "https://www.otakit.app/api/v1"
53
+ private static let defaultCdnURL = "https://cdn.otakit.app"
54
54
  private static let apiPathSuffix = "/api/v1"
55
55
  private static let lastCheckTimestampKey = "otakit_last_check_timestamp"
56
56
 
57
57
  public override func load() {
58
58
  let envUpdateUrl = ProcessInfo.processInfo.environment["OTAKIT_SERVER_URL"]
59
+ let envCdnUrl = ProcessInfo.processInfo.environment["OTAKIT_CDN_URL"]
59
60
  updateUrl = resolveUpdateUrl(
60
61
  configured: getConfig().getString("serverUrl"),
61
62
  env: envUpdateUrl
62
63
  )
64
+ cdnUrl = resolveCdnUrl(
65
+ configured: getConfig().getString("cdnUrl"),
66
+ env: envCdnUrl,
67
+ resolvedUpdateUrl: updateUrl
68
+ )
63
69
  appId = getConfig().getString("appId")
64
70
  channel = trimToNil(getConfig().getString("channel"))
65
71
  runtimeVersion = trimToNil(getConfig().getString("runtimeVersion"))
@@ -88,7 +94,7 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
88
94
  manifestKeys = [(kid: "_invalid_", key: Data())]
89
95
  }
90
96
 
91
- if manifestKeys.isEmpty && HostedManifestKeys.matchesManagedServer(updateUrl) {
97
+ if manifestKeys.isEmpty && HostedManifestKeys.matchesManagedManifestURL(cdnUrl) {
92
98
  manifestKeys = HostedManifestKeys.defaults
93
99
  }
94
100
 
@@ -154,7 +160,11 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
154
160
  return
155
161
  }
156
162
 
157
- if shouldThrottleCheck() { return }
163
+ if updateMode != .immediate && shouldThrottleCheck() { return }
164
+ }
165
+
166
+ if trigger == .launch && updateMode != .immediate && shouldThrottleCheck() {
167
+ return
158
168
  }
159
169
 
160
170
  // Acquire in-flight guard (submit-time)
@@ -166,7 +176,6 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
166
176
  defer { releaseCheckInProgress() }
167
177
  do {
168
178
  let result = try await performCheckAndDownload(channel: nil, emitEvents: true)
169
- recordCheckTimestamp()
170
179
  if result != nil {
171
180
  activateStagedBundleForReload()
172
181
  reloadWebView()
@@ -184,7 +193,6 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
184
193
  defer { releaseCheckInProgress() }
185
194
  do {
186
195
  let result = try await performCheckAndDownload(channel: nil, emitEvents: true)
187
- recordCheckTimestamp()
188
196
  if result != nil {
189
197
  activateStagedBundleForReload()
190
198
  reloadWebView()
@@ -282,27 +290,8 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
282
290
  call.resolve(payload)
283
291
  }
284
292
 
285
- @objc func debugCheck(_ call: CAPPluginCall) {
286
- let requestedChannel = call.getString("channel")
287
- let targetChannel = resolveTargetChannel(requestedChannel)
288
- Task {
289
- do {
290
- let latest = try await fetchLatest(channel: targetChannel)
291
- if let latest {
292
- let staged = findMatchingStagedBundle(latest: latest, targetChannel: targetChannel)
293
- call.resolve(manifestToDictionary(latest, downloaded: staged != nil))
294
- } else {
295
- call.resolve()
296
- }
297
- } catch {
298
- call.reject("debugCheck failed: \(error.localizedDescription)")
299
- }
300
- }
301
- }
302
-
303
293
  @objc func check(_ call: CAPPluginCall) {
304
- // Respect throttle — return staged bundle as a LatestVersion if available, else null
305
- if shouldThrottleCheck() || !claimCheckInProgress() {
294
+ if !claimCheckInProgress() {
306
295
  if let stagedId = store.getStagedBundleId(),
307
296
  let staged = store.getBundle(id: stagedId) {
308
297
  var payload: [String: Any] = [
@@ -329,9 +318,11 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
329
318
  defer { releaseCheckInProgress() }
330
319
  do {
331
320
  let latest = try await fetchLatest(channel: targetChannel)
332
- // check() does NOT record timestamp — only download() and automatic paths do.
333
- // This keeps check() -> download() working without the throttle blocking download().
334
321
  if let latest {
322
+ if isCurrentBundleLatest(latest: latest, targetChannel: targetChannel) {
323
+ call.resolve()
324
+ return
325
+ }
335
326
  let staged = findMatchingStagedBundle(latest: latest, targetChannel: targetChannel)
336
327
  call.resolve(manifestToDictionary(latest, downloaded: staged != nil))
337
328
  } else {
@@ -343,28 +334,8 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
343
334
  }
344
335
  }
345
336
 
346
- @objc func debugDownload(_ call: CAPPluginCall) {
347
- let requestedChannel = call.getString("channel")
348
- Task {
349
- do {
350
- let bundle = try await performCheckAndDownload(
351
- channel: requestedChannel,
352
- emitEvents: true
353
- )
354
- if let bundle {
355
- call.resolve(bundle.toDictionary())
356
- } else {
357
- call.resolve()
358
- }
359
- } catch {
360
- call.reject("debugDownload failed: \(error.localizedDescription)")
361
- }
362
- }
363
- }
364
-
365
337
  @objc func download(_ call: CAPPluginCall) {
366
- // Respect throttle — return staged info if available, else null
367
- if shouldThrottleCheck() || !claimCheckInProgress() {
338
+ if !claimCheckInProgress() {
368
339
  if let stagedId = store.getStagedBundleId(),
369
340
  let staged = store.getBundle(id: stagedId) {
370
341
  call.resolve(staged.toDictionary())
@@ -377,7 +348,6 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
377
348
  defer { releaseCheckInProgress() }
378
349
  do {
379
350
  let bundle = try await performCheckAndDownload(channel: nil, emitEvents: true)
380
- recordCheckTimestamp()
381
351
  if let bundle {
382
352
  call.resolve(bundle.toDictionary())
383
353
  } else {
@@ -504,15 +474,11 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
504
474
  throw NSError(domain: "OtaKit", code: 1, userInfo: [NSLocalizedDescriptionKey: "Missing appId in plugin config"])
505
475
  }
506
476
 
507
- let current = store.getCurrentBundle()
508
477
  return try await ManifestClient.fetchLatest(
509
- updateUrl: updateUrl,
478
+ cdnUrl: cdnUrl,
510
479
  appId: appId,
511
480
  channel: channel,
512
- currentVersion: current.version,
513
- currentReleaseId: current.releaseId,
514
481
  runtimeVersion: runtimeVersion,
515
- platform: "ios",
516
482
  allowInsecureUrls: allowInsecureUrls,
517
483
  manifestKeys: manifestKeys.map {
518
484
  ManifestKey(kid: $0.kid, derData: $0.key)
@@ -542,6 +508,13 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
542
508
  )
543
509
  }
544
510
 
511
+ if isCurrentBundleLatest(latest: manifest, targetChannel: targetChannel) {
512
+ if emitEvents {
513
+ notifyListeners("noUpdateAvailable", data: [:])
514
+ }
515
+ return nil
516
+ }
517
+
545
518
  let staged = findMatchingStagedBundle(latest: manifest, targetChannel: targetChannel)
546
519
  if emitEvents {
547
520
  notifyListeners("updateAvailable", data: manifestToDictionary(manifest, downloaded: staged != nil))
@@ -939,6 +912,42 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
939
912
  return payload
940
913
  }
941
914
 
915
+ private func isCurrentBundleLatest(
916
+ latest: LatestManifest,
917
+ targetChannel: String?
918
+ ) -> Bool {
919
+ doesBundleMatchLatest(store.getCurrentBundle(), latest: latest, targetChannel: targetChannel)
920
+ }
921
+
922
+ private func doesBundleMatchLatest(
923
+ _ bundle: BundleInfo,
924
+ latest: LatestManifest,
925
+ targetChannel: String?
926
+ ) -> Bool {
927
+ if trimToNil(bundle.channel) != targetChannel {
928
+ return false
929
+ }
930
+
931
+ if trimToNil(bundle.runtimeVersion) != trimToNil(latest.runtimeVersion) {
932
+ return false
933
+ }
934
+
935
+ if let latestReleaseId = latest.releaseId,
936
+ let bundleReleaseId = bundle.releaseId,
937
+ latestReleaseId == bundleReleaseId {
938
+ return true
939
+ }
940
+
941
+ if !latest.sha256.isEmpty,
942
+ let bundleSha = bundle.sha256,
943
+ !bundleSha.isEmpty,
944
+ latest.sha256 == bundleSha {
945
+ return true
946
+ }
947
+
948
+ return latest.version == bundle.version
949
+ }
950
+
942
951
  private func findMatchingStagedBundle(
943
952
  latest: LatestManifest,
944
953
  targetChannel: String?
@@ -959,18 +968,7 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
959
968
  return nil
960
969
  }
961
970
 
962
- if let latestReleaseId = latest.releaseId,
963
- let stagedReleaseId = staged.releaseId,
964
- latestReleaseId == stagedReleaseId {
965
- return staged
966
- }
967
-
968
- if latest.version == staged.version,
969
- latest.sha256 == staged.sha256 {
970
- return staged
971
- }
972
-
973
- return nil
971
+ return doesBundleMatchLatest(staged, latest: latest, targetChannel: targetChannel) ? staged : nil
974
972
  }
975
973
 
976
974
  private func pruneIncompatibleBundles() {
@@ -1077,6 +1075,25 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1077
1075
  return UpdaterPlugin.defaultUpdateURL
1078
1076
  }
1079
1077
 
1078
+ private func resolveCdnUrl(configured: String?, env: String?, resolvedUpdateUrl: String) -> String {
1079
+ let configuredValue = configured?.trimmingCharacters(in: .whitespacesAndNewlines)
1080
+ if let configuredValue, !configuredValue.isEmpty {
1081
+ return normalizeCdnUrl(configuredValue)
1082
+ }
1083
+
1084
+ let envValue = env?.trimmingCharacters(in: .whitespacesAndNewlines)
1085
+ if let envValue, !envValue.isEmpty {
1086
+ return normalizeCdnUrl(envValue)
1087
+ }
1088
+
1089
+ if resolvedUpdateUrl.lowercased() != UpdaterPlugin.defaultUpdateURL.lowercased(),
1090
+ resolvedUpdateUrl.lowercased().hasSuffix(UpdaterPlugin.apiPathSuffix) {
1091
+ return String(resolvedUpdateUrl.dropLast(UpdaterPlugin.apiPathSuffix.count))
1092
+ }
1093
+
1094
+ return UpdaterPlugin.defaultCdnURL
1095
+ }
1096
+
1080
1097
  private func normalizeUpdateUrl(_ raw: String) -> String {
1081
1098
  let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
1082
1099
  let withoutTrailingSlash = trimmed.replacingOccurrences(
@@ -1092,6 +1109,12 @@ public class UpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1092
1109
  return withoutTrailingSlash + UpdaterPlugin.apiPathSuffix
1093
1110
  }
1094
1111
 
1112
+ private func normalizeCdnUrl(_ raw: String) -> String {
1113
+ raw
1114
+ .trimmingCharacters(in: .whitespacesAndNewlines)
1115
+ .replacingOccurrences(of: "/+$", with: "", options: .regularExpression)
1116
+ }
1117
+
1095
1118
  private func getFreeDiskSpace() -> Int64 {
1096
1119
  do {
1097
1120
  let attributes = try fileManager.attributesOfFileSystem(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@otakit/capacitor-updater",
3
- "version": "1.2.0",
3
+ "version": "2.0.0",
4
4
  "description": "Capacitor plugin for OTA updates",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",