@capgo/capacitor-updater 8.45.1 → 8.45.8

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.
@@ -72,7 +72,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
72
72
  CAPPluginMethod(name: "completeFlexibleUpdate", returnType: CAPPluginReturnPromise)
73
73
  ]
74
74
  public var implementation = CapgoUpdater()
75
- private let pluginVersion: String = "8.45.1"
75
+ private let pluginVersion: String = "8.45.8"
76
76
  static let updateUrlDefault = "https://plugin.capgo.app/updates"
77
77
  static let statsUrlDefault = "https://plugin.capgo.app/stats"
78
78
  static let channelUrlDefault = "https://plugin.capgo.app/channel_self"
@@ -436,7 +436,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
436
436
 
437
437
  let previous = UserDefaults.standard.string(forKey: "LatestNativeBuildVersion") ?? UserDefaults.standard.string(forKey: "LatestVersionNative") ?? "0"
438
438
  if previous != "0" && self.currentBuildVersion != previous {
439
- _ = self._reset(toLastSuccessful: false)
439
+ _ = self._reset(toLastSuccessful: false, usePendingBundle: false)
440
440
  let res = self.implementation.list()
441
441
  for version in res {
442
442
  // Check if thread was cancelled
@@ -655,47 +655,77 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
655
655
  }
656
656
  }
657
657
 
658
- public func _reload() -> Bool {
659
- guard let bridge = self.bridge else { return false }
660
- self.semaphoreUp()
658
+ private func currentReloadDestination() -> URL {
661
659
  let id = self.implementation.getCurrentBundleId()
662
- let dest: URL
663
660
  if BundleInfo.ID_BUILTIN == id {
664
- dest = Bundle.main.resourceURL!.appendingPathComponent("public")
661
+ return Bundle.main.resourceURL!.appendingPathComponent("public")
665
662
  } else {
666
- dest = self.implementation.getBundleDirectory(id: id)
663
+ return self.implementation.getBundleDirectory(id: id)
667
664
  }
665
+ }
666
+
667
+ private func applyCurrentBundleToBridge(_ bridge: CAPBridgeProtocol) -> Bool {
668
+ let id = self.implementation.getCurrentBundleId()
669
+ let dest = self.currentReloadDestination()
668
670
  logger.info("Reloading \(id)")
669
671
 
670
- let performReload: () -> Bool = {
671
- guard let vc = bridge.viewController as? CAPBridgeViewController else {
672
- self.logger.error("Cannot get viewController")
673
- return false
674
- }
675
- guard let capBridge = vc.bridge else {
676
- self.logger.error("Cannot get capBridge")
677
- return false
678
- }
679
- if self.keepUrlPathAfterReload {
680
- if let currentURL = vc.webView?.url {
681
- capBridge.setServerBasePath(dest.path)
682
- var urlComponents = URLComponents(url: capBridge.config.serverURL, resolvingAgainstBaseURL: false)!
683
- urlComponents.path = currentURL.path
684
- urlComponents.query = currentURL.query
685
- urlComponents.fragment = currentURL.fragment
686
- if let finalUrl = urlComponents.url {
687
- _ = vc.webView?.load(URLRequest(url: finalUrl))
688
- } else {
689
- self.logger.error("Unable to build final URL when keeping path after reload; falling back to base path")
690
- vc.setServerBasePath(path: dest.path)
691
- }
672
+ guard let vc = bridge.viewController as? CAPBridgeViewController else {
673
+ self.logger.error("Cannot get viewController")
674
+ return false
675
+ }
676
+ guard let capBridge = vc.bridge else {
677
+ self.logger.error("Cannot get capBridge")
678
+ return false
679
+ }
680
+ if self.keepUrlPathAfterReload {
681
+ if let currentURL = vc.webView?.url {
682
+ capBridge.setServerBasePath(dest.path)
683
+ var urlComponents = URLComponents(url: capBridge.config.serverURL, resolvingAgainstBaseURL: false)!
684
+ urlComponents.path = currentURL.path
685
+ urlComponents.query = currentURL.query
686
+ urlComponents.fragment = currentURL.fragment
687
+ if let finalUrl = urlComponents.url {
688
+ _ = vc.webView?.load(URLRequest(url: finalUrl))
692
689
  } else {
693
- self.logger.error("vc.webView?.url is null? Falling back to base path reload.")
690
+ self.logger.error("Unable to build final URL when keeping path after reload; falling back to base path")
694
691
  vc.setServerBasePath(path: dest.path)
695
692
  }
696
693
  } else {
694
+ self.logger.error("vc.webView?.url is null? Falling back to base path reload.")
697
695
  vc.setServerBasePath(path: dest.path)
698
696
  }
697
+ } else {
698
+ vc.setServerBasePath(path: dest.path)
699
+ }
700
+ return true
701
+ }
702
+
703
+ func restoreLiveBundleStateAfterFailedReload() {
704
+ guard let bridge = self.bridge else {
705
+ return
706
+ }
707
+
708
+ let restoreLiveState = {
709
+ _ = self.applyCurrentBundleToBridge(bridge)
710
+ }
711
+
712
+ if Thread.isMainThread {
713
+ restoreLiveState()
714
+ } else {
715
+ DispatchQueue.main.sync {
716
+ restoreLiveState()
717
+ }
718
+ }
719
+ }
720
+
721
+ public func _reload() -> Bool {
722
+ guard let bridge = self.bridge else { return false }
723
+ self.semaphoreUp()
724
+
725
+ let performReload: () -> Bool = {
726
+ guard self.applyCurrentBundleToBridge(bridge) else {
727
+ return false
728
+ }
699
729
  self.checkAppReady()
700
730
  self.notifyListeners("appReloaded", data: [:])
701
731
  return true
@@ -713,6 +743,38 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
713
743
  }
714
744
 
715
745
  @objc func reload(_ call: CAPPluginCall) {
746
+ let current: BundleInfo = self.implementation.getCurrentBundle()
747
+ let next: BundleInfo? = self.implementation.getNextBundle()
748
+
749
+ if let next = next, !next.isErrorStatus(), next.getId() != current.getId() {
750
+ let previousState = self.implementation.captureResetState()
751
+ let previousBundleName = self.implementation.getCurrentBundle().getVersionName()
752
+ logger.info("Applying pending bundle before reload: \(next.toString())")
753
+ let didApplyPendingBundle: Bool
754
+ if next.isBuiltin() {
755
+ self.implementation.prepareResetStateForTransition()
756
+ didApplyPendingBundle = true
757
+ } else {
758
+ didApplyPendingBundle = self.implementation.stagePendingReload(bundle: next)
759
+ }
760
+ if didApplyPendingBundle && self._reload() {
761
+ if next.isBuiltin() {
762
+ self.implementation.finalizeResetTransition(previousBundleName: previousBundleName, isInternal: false)
763
+ } else {
764
+ self.implementation.finalizePendingReload(bundle: next, previousBundleName: previousBundleName)
765
+ }
766
+ self.notifyBundleSet(next)
767
+ _ = self.implementation.setNextBundle(next: Optional<String>.none)
768
+ call.resolve()
769
+ return
770
+ }
771
+ self.implementation.restoreResetState(previousState)
772
+ self.restoreLiveBundleStateAfterFailedReload()
773
+ logger.error("Reload failed after applying pending bundle: \(next.toString())")
774
+ call.reject("Reload failed after applying pending bundle")
775
+ return
776
+ }
777
+
716
778
  if self._reload() {
717
779
  call.resolve()
718
780
  } else {
@@ -936,36 +998,90 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
936
998
  call.resolve()
937
999
  }
938
1000
 
939
- @objc func _reset(toLastSuccessful: Bool) -> Bool {
940
- guard let bridge = self.bridge else { return false }
1001
+ @objc func _reset(toLastSuccessful: Bool, usePendingBundle: Bool) -> Bool {
1002
+ self.performReset(toLastSuccessful: toLastSuccessful, usePendingBundle: usePendingBundle, isInternal: false)
1003
+ }
1004
+
1005
+ func performReset(toLastSuccessful: Bool, usePendingBundle: Bool, isInternal: Bool) -> Bool {
1006
+ guard self.canPerformResetTransition() else { return false }
1007
+
1008
+ let fallback: BundleInfo = self.implementation.getFallbackBundle()
1009
+ let pending: BundleInfo? = self.implementation.getNextBundle()
1010
+ let previousState = self.implementation.captureResetState()
1011
+ let previousBundleName = self.implementation.getCurrentBundle().getVersionName()
941
1012
 
942
- if (bridge.viewController as? CAPBridgeViewController) != nil {
943
- let fallback: BundleInfo = self.implementation.getFallbackBundle()
1013
+ if usePendingBundle {
1014
+ guard let pending = pending, !pending.isErrorStatus() else {
1015
+ logger.error("No pending bundle available to reset to")
1016
+ return false
1017
+ }
1018
+ guard self.implementation.canSet(bundle: pending) else {
1019
+ logger.error("Pending bundle is not installable")
1020
+ return false
1021
+ }
1022
+ self.implementation.prepareResetStateForTransition()
1023
+ logger.info("Resetting to pending bundle: \(pending.toString())")
1024
+ let didApplyPendingBundle: Bool
1025
+ if pending.isBuiltin() {
1026
+ didApplyPendingBundle = true
1027
+ } else {
1028
+ didApplyPendingBundle = self.implementation.set(bundle: pending)
1029
+ }
1030
+ if didApplyPendingBundle && self._reload() {
1031
+ self.implementation.finalizeResetTransition(previousBundleName: previousBundleName, isInternal: isInternal)
1032
+ self.notifyBundleSet(pending)
1033
+ _ = self.implementation.setNextBundle(next: Optional<String>.none)
1034
+ return true
1035
+ }
1036
+ self.implementation.restoreResetState(previousState)
1037
+ self.restoreLiveBundleStateAfterFailedReload()
1038
+ return false
1039
+ }
944
1040
 
945
- // If developer wants to reset to the last successful bundle, and that bundle is not
946
- // the built-in bundle, set it as the bundle to use and reload.
947
- if toLastSuccessful && !fallback.isBuiltin() {
1041
+ // If developer wants to reset to the last successful bundle, and that bundle is not
1042
+ // the built-in bundle, set it as the bundle to use and reload.
1043
+ if toLastSuccessful && !fallback.isBuiltin() {
1044
+ if self.implementation.canSet(bundle: fallback) {
1045
+ self.implementation.prepareResetStateForTransition()
948
1046
  logger.info("Resetting to: \(fallback.toString())")
949
1047
  if self.implementation.set(bundle: fallback) && self._reload() {
1048
+ self.implementation.finalizeResetTransition(previousBundleName: previousBundleName, isInternal: isInternal)
950
1049
  self.notifyBundleSet(fallback)
951
1050
  return true
952
1051
  }
953
- return false
1052
+ if !isInternal {
1053
+ self.implementation.restoreResetState(previousState)
1054
+ self.restoreLiveBundleStateAfterFailedReload()
1055
+ return false
1056
+ }
1057
+ logger.warn("Fallback reload failed during internal reset, resetting to builtin instead")
1058
+ } else {
1059
+ logger.warn("Fallback bundle is not installable, resetting to builtin instead")
954
1060
  }
955
-
956
- logger.info("Resetting to builtin version")
957
-
958
- // Otherwise, reset back to the built-in bundle and reload.
959
- self.implementation.reset()
960
- return self._reload()
961
1061
  }
962
1062
 
1063
+ self.implementation.prepareResetStateForTransition()
1064
+ logger.info("Resetting to builtin version")
1065
+ if self._reload() {
1066
+ self.implementation.finalizeResetTransition(previousBundleName: previousBundleName, isInternal: isInternal)
1067
+ return true
1068
+ }
1069
+ if !isInternal {
1070
+ self.implementation.restoreResetState(previousState)
1071
+ self.restoreLiveBundleStateAfterFailedReload()
1072
+ }
963
1073
  return false
964
1074
  }
965
1075
 
1076
+ func canPerformResetTransition() -> Bool {
1077
+ guard let bridge = self.bridge else { return false }
1078
+ return (bridge.viewController as? CAPBridgeViewController) != nil
1079
+ }
1080
+
966
1081
  @objc func reset(_ call: CAPPluginCall) {
967
1082
  let toLastSuccessful = call.getBool("toLastSuccessful") ?? false
968
- if self._reset(toLastSuccessful: toLastSuccessful) {
1083
+ let usePendingBundle = call.getBool("usePendingBundle") ?? false
1084
+ if self._reset(toLastSuccessful: toLastSuccessful, usePendingBundle: usePendingBundle) {
969
1085
  call.resolve()
970
1086
  } else {
971
1087
  logger.error("Reset failed")
@@ -1084,7 +1200,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1084
1200
  self.persistLastFailedBundle(current)
1085
1201
  self.implementation.sendStats(action: "update_fail", versionName: current.getVersionName())
1086
1202
  self.implementation.setError(bundle: current)
1087
- _ = self._reset(toLastSuccessful: true)
1203
+ _ = self.performReset(toLastSuccessful: true, usePendingBundle: false, isInternal: true)
1088
1204
  if self.autoDeleteFailed && !current.isBuiltin() {
1089
1205
  logger.info("Deleting failing bundle: \(current.toString())")
1090
1206
  let res = self.implementation.delete(id: current.getId(), removeInfo: false)
@@ -1609,7 +1725,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1609
1725
  let directUpdateAllowed = plannedDirectUpdate && !self.autoSplashscreenTimedOut
1610
1726
  if directUpdateAllowed {
1611
1727
  self.logger.info("Direct update to builtin version")
1612
- _ = self._reset(toLastSuccessful: false)
1728
+ _ = self._reset(toLastSuccessful: false, usePendingBundle: false)
1613
1729
  self.endBackGroundTaskWithNotif(
1614
1730
  msg: "Updated to builtin version",
1615
1731
  latestVersionName: res.version,
@@ -54,10 +54,31 @@ import UIKit
54
54
  private var statsFlushTimer: Timer?
55
55
  private static let statsFlushInterval: TimeInterval = 1.0
56
56
 
57
+ private static func sanitizeHeaderValue(_ value: String) -> String {
58
+ if value.isEmpty {
59
+ return "unknown"
60
+ }
61
+
62
+ let filteredScalars = value.unicodeScalars.filter { scalar in
63
+ let cp = scalar.value
64
+ let isVisibleAscii = (0x20...0x7E).contains(cp)
65
+ let isIso88591 = (0xA0...0xFF).contains(cp)
66
+ return isVisibleAscii || isIso88591
67
+ }
68
+
69
+ let sanitized = String(String.UnicodeScalarView(filteredScalars)).trimmingCharacters(in: .whitespacesAndNewlines)
70
+ return sanitized.isEmpty ? "unknown" : sanitized
71
+ }
72
+
73
+ static func buildUserAgent(appId: String, pluginVersion: String, versionOs: String) -> String {
74
+ let safePluginVersion = sanitizeHeaderValue(pluginVersion)
75
+ let safeAppId = sanitizeHeaderValue(appId)
76
+ let safeVersionOs = sanitizeHeaderValue(versionOs)
77
+ return "CapacitorUpdater/\(safePluginVersion) (\(safeAppId)) ios/\(safeVersionOs)"
78
+ }
79
+
57
80
  private var userAgent: String {
58
- let safePluginVersion = pluginVersion.isEmpty ? "unknown" : pluginVersion
59
- let safeAppId = appId.isEmpty ? "unknown" : appId
60
- return "CapacitorUpdater/\(safePluginVersion) (\(safeAppId)) ios/\(versionOs)"
81
+ CapgoUpdater.buildUserAgent(appId: appId, pluginVersion: pluginVersion, versionOs: versionOs)
61
82
  }
62
83
 
63
84
  private lazy var alamofireSession: Session = {
@@ -1459,6 +1480,52 @@ import UIKit
1459
1480
  return libraryDir.appendingPathComponent(self.bundleDirectory).appendingPathComponent(id)
1460
1481
  }
1461
1482
 
1483
+ struct ResetState {
1484
+ let currentBundlePath: String
1485
+ let fallbackBundleId: String
1486
+ let nextBundleId: String?
1487
+ }
1488
+
1489
+ func captureResetState() -> ResetState {
1490
+ ResetState(
1491
+ currentBundlePath: UserDefaults.standard.string(forKey: self.CAP_SERVER_PATH) ?? self.DEFAULT_FOLDER,
1492
+ fallbackBundleId: UserDefaults.standard.string(forKey: self.FALLBACK_VERSION) ?? BundleInfo.ID_BUILTIN,
1493
+ nextBundleId: UserDefaults.standard.string(forKey: self.NEXT_VERSION)
1494
+ )
1495
+ }
1496
+
1497
+ func restoreResetState(_ state: ResetState) {
1498
+ let currentBundlePath = state.currentBundlePath.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
1499
+ ? self.DEFAULT_FOLDER
1500
+ : state.currentBundlePath
1501
+ let fallbackBundleId = state.fallbackBundleId.isEmpty ? BundleInfo.ID_BUILTIN : state.fallbackBundleId
1502
+
1503
+ self.setCurrentBundle(bundle: currentBundlePath)
1504
+ UserDefaults.standard.set(fallbackBundleId, forKey: self.FALLBACK_VERSION)
1505
+ if let nextBundleId = state.nextBundleId, !nextBundleId.isEmpty {
1506
+ UserDefaults.standard.set(nextBundleId, forKey: self.NEXT_VERSION)
1507
+ } else {
1508
+ UserDefaults.standard.removeObject(forKey: self.NEXT_VERSION)
1509
+ }
1510
+ UserDefaults.standard.synchronize()
1511
+ }
1512
+
1513
+ func prepareResetStateForTransition() {
1514
+ self.setCurrentBundle(bundle: "")
1515
+ self.setFallbackBundle(fallback: Optional<BundleInfo>.none)
1516
+ _ = self.setNextBundle(next: Optional<String>.none)
1517
+ }
1518
+
1519
+ func finalizeResetTransition(previousBundleName: String, isInternal: Bool) {
1520
+ if !isInternal {
1521
+ self.sendStats(action: "reset", versionName: self.getCurrentBundle().getVersionName(), oldVersionName: previousBundleName)
1522
+ }
1523
+ }
1524
+
1525
+ func canSet(bundle: BundleInfo) -> Bool {
1526
+ bundle.isBuiltin() || self.bundleExists(id: bundle.getId())
1527
+ }
1528
+
1462
1529
  public func set(bundle: BundleInfo) -> Bool {
1463
1530
  return self.set(id: bundle.getId())
1464
1531
  }
@@ -1496,6 +1563,21 @@ import UIKit
1496
1563
  return false
1497
1564
  }
1498
1565
 
1566
+ func stagePendingReload(bundle: BundleInfo) -> Bool {
1567
+ guard !bundle.isBuiltin(), bundleExists(id: bundle.getId()) else {
1568
+ return false
1569
+ }
1570
+ self.setCurrentBundle(bundle: self.getBundleDirectory(id: bundle.getId()).path)
1571
+ return true
1572
+ }
1573
+
1574
+ func finalizePendingReload(bundle: BundleInfo, previousBundleName: String) {
1575
+ guard !bundle.isBuiltin() else {
1576
+ return
1577
+ }
1578
+ self.sendStats(action: "set", versionName: bundle.getVersionName(), oldVersionName: previousBundleName)
1579
+ }
1580
+
1499
1581
  public func autoReset() {
1500
1582
  let currentBundle: BundleInfo = self.getCurrentBundle()
1501
1583
  if !currentBundle.isBuiltin() && !self.bundleExists(id: currentBundle.getId()) {
@@ -1521,12 +1603,8 @@ import UIKit
1521
1603
  public func reset(isInternal: Bool) {
1522
1604
  logger.info("reset: \(isInternal)")
1523
1605
  let currentBundleName = self.getCurrentBundle().getVersionName()
1524
- self.setCurrentBundle(bundle: "")
1525
- self.setFallbackBundle(fallback: Optional<BundleInfo>.none)
1526
- _ = self.setNextBundle(next: Optional<String>.none)
1527
- if !isInternal {
1528
- self.sendStats(action: "reset", versionName: self.getCurrentBundle().getVersionName(), oldVersionName: currentBundleName)
1529
- }
1606
+ self.prepareResetStateForTransition()
1607
+ self.finalizeResetTransition(previousBundleName: currentBundleName, isInternal: isInternal)
1530
1608
  }
1531
1609
 
1532
1610
  public func setSuccess(bundle: BundleInfo, autoDeletePrevious: Bool) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-updater",
3
- "version": "8.45.1",
3
+ "version": "8.45.8",
4
4
  "license": "MPL-2.0",
5
5
  "description": "Live update for capacitor apps",
6
6
  "main": "dist/plugin.cjs.js",
@@ -41,23 +41,25 @@
41
41
  "native"
42
42
  ],
43
43
  "scripts": {
44
- "verify": "npm run verify:ios && npm run verify:android && npm run verify:web",
44
+ "verify": "bun run verify:ios && bun run verify:android && bun run verify:web",
45
45
  "verify:ios": "xcodebuild -scheme CapgoCapacitorUpdater -destination generic/platform=iOS",
46
46
  "verify:android": "cd android && ./gradlew clean build test && cd ..",
47
- "verify:web": "npm run build",
48
- "test": "npm run test:ios && npm run test:android",
47
+ "verify:web": "bun run build",
48
+ "test": "bun run test:ios && bun run test:android",
49
49
  "test:ios": "./scripts/test-ios.sh",
50
50
  "test:android": "cd android && ./gradlew test && cd ..",
51
- "lint": "npm run eslint && npm run prettier -- --check && npm run swiftlint -- lint",
52
- "fmt": "npm run eslint -- --fix && npm run prettier -- --write && npm run swiftlint -- --fix --format",
51
+ "test:maestro:android": "./scripts/test-maestro-android.sh",
52
+ "test:maestro:ios": "./scripts/test-maestro-ios.sh",
53
+ "lint": "bun run eslint && bun run prettier -- --check && bun run swiftlint -- lint",
54
+ "fmt": "bun run eslint -- --fix && bun run prettier -- --write && bun run swiftlint -- --fix --format",
53
55
  "eslint": "eslint . --ext .ts",
54
56
  "prettier": "prettier-pretty-check \"**/*.{css,html,ts,js,java}\" --plugin=prettier-plugin-java",
55
57
  "swiftlint": "node-swiftlint",
56
58
  "docgen": "node scripts/generate-docs.js",
57
- "build": "npm run clean && npm run docgen && tsc && rollup -c rollup.config.mjs",
59
+ "build": "bun run clean && bun run docgen && tsc && rollup -c rollup.config.mjs",
58
60
  "clean": "rimraf ./dist",
59
61
  "watch": "tsc --watch",
60
- "prepublishOnly": "npm run build",
62
+ "prepublishOnly": "bun run build",
61
63
  "check:wiring": "node scripts/check-capacitor-plugin-wiring.mjs"
62
64
  },
63
65
  "devDependencies": {