@capgo/capacitor-updater 7.37.0 → 7.39.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.
@@ -51,10 +51,16 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
51
51
  CAPPluginMethod(name: "getNextBundle", returnType: CAPPluginReturnPromise),
52
52
  CAPPluginMethod(name: "getFailedUpdate", returnType: CAPPluginReturnPromise),
53
53
  CAPPluginMethod(name: "setShakeMenu", returnType: CAPPluginReturnPromise),
54
- CAPPluginMethod(name: "isShakeMenuEnabled", returnType: CAPPluginReturnPromise)
54
+ CAPPluginMethod(name: "isShakeMenuEnabled", returnType: CAPPluginReturnPromise),
55
+ // App Store update methods
56
+ CAPPluginMethod(name: "getAppUpdateInfo", returnType: CAPPluginReturnPromise),
57
+ CAPPluginMethod(name: "openAppStore", returnType: CAPPluginReturnPromise),
58
+ CAPPluginMethod(name: "performImmediateUpdate", returnType: CAPPluginReturnPromise),
59
+ CAPPluginMethod(name: "startFlexibleUpdate", returnType: CAPPluginReturnPromise),
60
+ CAPPluginMethod(name: "completeFlexibleUpdate", returnType: CAPPluginReturnPromise)
55
61
  ]
56
62
  public var implementation = CapgoUpdater()
57
- private let pluginVersion: String = "7.37.0"
63
+ private let pluginVersion: String = "7.39.0"
58
64
  static let updateUrlDefault = "https://plugin.capgo.app/updates"
59
65
  static let statsUrlDefault = "https://plugin.capgo.app/stats"
60
66
  static let channelUrlDefault = "https://plugin.capgo.app/channel_self"
@@ -399,6 +405,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
399
405
  return id.isEmpty ? nil : id
400
406
  })
401
407
  self.implementation.cleanupDownloadDirectories(allowedIds: allowedIds, threadToCheck: Thread.current)
408
+ self.implementation.cleanupOrphanedTempFolders(threadToCheck: Thread.current)
402
409
 
403
410
  // Check again before the expensive delta cache cleanup
404
411
  if Thread.current.isCancelled {
@@ -1659,4 +1666,208 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1659
1666
  implementation.appId = appId
1660
1667
  call.resolve()
1661
1668
  }
1669
+
1670
+ // MARK: - App Store Update Methods
1671
+
1672
+ /// AppUpdateAvailability enum values matching TypeScript definitions
1673
+ private enum AppUpdateAvailability: Int {
1674
+ case unknown = 0
1675
+ case updateNotAvailable = 1
1676
+ case updateAvailable = 2
1677
+ case updateInProgress = 3
1678
+ }
1679
+
1680
+ @objc func getAppUpdateInfo(_ call: CAPPluginCall) {
1681
+ let country = call.getString("country", "US")
1682
+ let bundleId = implementation.appId
1683
+
1684
+ logger.info("Getting App Store update info for \(bundleId) in country \(country)")
1685
+
1686
+ DispatchQueue.global(qos: .background).async {
1687
+ let urlString = "https://itunes.apple.com/lookup?bundleId=\(bundleId)&country=\(country)"
1688
+ guard let url = URL(string: urlString) else {
1689
+ call.reject("Invalid URL for App Store lookup")
1690
+ return
1691
+ }
1692
+
1693
+ let task = URLSession.shared.dataTask(with: url) { data, response, error in
1694
+ if let error = error {
1695
+ self.logger.error("App Store lookup failed: \(error.localizedDescription)")
1696
+ call.reject("App Store lookup failed: \(error.localizedDescription)")
1697
+ return
1698
+ }
1699
+
1700
+ guard let data = data else {
1701
+ call.reject("No data received from App Store")
1702
+ return
1703
+ }
1704
+
1705
+ do {
1706
+ guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
1707
+ let resultCount = json["resultCount"] as? Int else {
1708
+ call.reject("Invalid response from App Store")
1709
+ return
1710
+ }
1711
+
1712
+ let currentVersionName = Bundle.main.versionName ?? "0.0.0"
1713
+ let currentVersionCode = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "0"
1714
+
1715
+ var result: [String: Any] = [
1716
+ "currentVersionName": currentVersionName,
1717
+ "currentVersionCode": currentVersionCode,
1718
+ "updateAvailability": AppUpdateAvailability.unknown.rawValue
1719
+ ]
1720
+
1721
+ if resultCount > 0,
1722
+ let results = json["results"] as? [[String: Any]],
1723
+ let appInfo = results.first {
1724
+
1725
+ let availableVersion = appInfo["version"] as? String
1726
+ let releaseDate = appInfo["currentVersionReleaseDate"] as? String
1727
+ let minimumOsVersion = appInfo["minimumOsVersion"] as? String
1728
+
1729
+ result["availableVersionName"] = availableVersion
1730
+ result["availableVersionCode"] = availableVersion // iOS doesn't have separate version code
1731
+ result["availableVersionReleaseDate"] = releaseDate
1732
+ result["minimumOsVersion"] = minimumOsVersion
1733
+
1734
+ // Determine update availability by comparing versions
1735
+ if let availableVersion = availableVersion {
1736
+ do {
1737
+ let currentVer = try Version(currentVersionName)
1738
+ let availableVer = try Version(availableVersion)
1739
+ if availableVer > currentVer {
1740
+ result["updateAvailability"] = AppUpdateAvailability.updateAvailable.rawValue
1741
+ } else {
1742
+ result["updateAvailability"] = AppUpdateAvailability.updateNotAvailable.rawValue
1743
+ }
1744
+ } catch {
1745
+ // If version parsing fails, do string comparison
1746
+ if availableVersion != currentVersionName {
1747
+ result["updateAvailability"] = AppUpdateAvailability.updateAvailable.rawValue
1748
+ } else {
1749
+ result["updateAvailability"] = AppUpdateAvailability.updateNotAvailable.rawValue
1750
+ }
1751
+ }
1752
+ } else {
1753
+ result["updateAvailability"] = AppUpdateAvailability.updateNotAvailable.rawValue
1754
+ }
1755
+
1756
+ // iOS doesn't support in-app updates like Android
1757
+ result["immediateUpdateAllowed"] = false
1758
+ result["flexibleUpdateAllowed"] = false
1759
+ } else {
1760
+ // App not found in App Store (maybe not published yet)
1761
+ result["updateAvailability"] = AppUpdateAvailability.updateNotAvailable.rawValue
1762
+ self.logger.info("App not found in App Store for bundleId: \(bundleId)")
1763
+ }
1764
+
1765
+ call.resolve(result)
1766
+ } catch {
1767
+ self.logger.error("Failed to parse App Store response: \(error.localizedDescription)")
1768
+ call.reject("Failed to parse App Store response: \(error.localizedDescription)")
1769
+ }
1770
+ }
1771
+ task.resume()
1772
+ }
1773
+ }
1774
+
1775
+ @objc func openAppStore(_ call: CAPPluginCall) {
1776
+ let appId = call.getString("appId")
1777
+
1778
+ if let appId = appId {
1779
+ // Open App Store with provided app ID
1780
+ let urlString = "https://apps.apple.com/app/id\(appId)"
1781
+ guard let url = URL(string: urlString) else {
1782
+ call.reject("Invalid App Store URL")
1783
+ return
1784
+ }
1785
+ DispatchQueue.main.async {
1786
+ UIApplication.shared.open(url) { success in
1787
+ if success {
1788
+ call.resolve()
1789
+ } else {
1790
+ call.reject("Failed to open App Store")
1791
+ }
1792
+ }
1793
+ }
1794
+ } else {
1795
+ // Look up app ID using bundle identifier
1796
+ let bundleId = implementation.appId
1797
+ let lookupUrl = "https://itunes.apple.com/lookup?bundleId=\(bundleId)"
1798
+
1799
+ DispatchQueue.global(qos: .background).async {
1800
+ guard let url = URL(string: lookupUrl) else {
1801
+ call.reject("Invalid lookup URL")
1802
+ return
1803
+ }
1804
+
1805
+ let task = URLSession.shared.dataTask(with: url) { data, _, error in
1806
+ if let error = error {
1807
+ call.reject("Failed to lookup app: \(error.localizedDescription)")
1808
+ return
1809
+ }
1810
+
1811
+ guard let data = data,
1812
+ let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
1813
+ let results = json["results"] as? [[String: Any]],
1814
+ let appInfo = results.first,
1815
+ let trackId = appInfo["trackId"] as? Int else {
1816
+ // If lookup fails, try opening the generic App Store app page using bundle ID
1817
+ let fallbackUrlString = "https://apps.apple.com/app/\(bundleId)"
1818
+ guard let fallbackUrl = URL(string: fallbackUrlString) else {
1819
+ call.reject("Failed to find app in App Store and fallback URL is invalid")
1820
+ return
1821
+ }
1822
+ DispatchQueue.main.async {
1823
+ UIApplication.shared.open(fallbackUrl) { success in
1824
+ if success {
1825
+ call.resolve()
1826
+ } else {
1827
+ call.reject("Failed to open App Store")
1828
+ }
1829
+ }
1830
+ }
1831
+ return
1832
+ }
1833
+
1834
+ let appStoreUrl = "https://apps.apple.com/app/id\(trackId)"
1835
+ guard let url = URL(string: appStoreUrl) else {
1836
+ call.reject("Invalid App Store URL")
1837
+ return
1838
+ }
1839
+
1840
+ DispatchQueue.main.async {
1841
+ UIApplication.shared.open(url) { success in
1842
+ if success {
1843
+ call.resolve()
1844
+ } else {
1845
+ call.reject("Failed to open App Store")
1846
+ }
1847
+ }
1848
+ }
1849
+ }
1850
+ task.resume()
1851
+ }
1852
+ }
1853
+ }
1854
+
1855
+ @objc func performImmediateUpdate(_ call: CAPPluginCall) {
1856
+ // iOS doesn't support in-app updates like Android's Play Store
1857
+ // Redirect users to the App Store instead
1858
+ logger.warn("performImmediateUpdate is not supported on iOS. Use openAppStore() instead.")
1859
+ call.reject("In-app updates are not supported on iOS. Use openAppStore() to direct users to the App Store.", "NOT_SUPPORTED")
1860
+ }
1861
+
1862
+ @objc func startFlexibleUpdate(_ call: CAPPluginCall) {
1863
+ // iOS doesn't support flexible in-app updates
1864
+ logger.warn("startFlexibleUpdate is not supported on iOS. Use openAppStore() instead.")
1865
+ call.reject("Flexible updates are not supported on iOS. Use openAppStore() to direct users to the App Store.", "NOT_SUPPORTED")
1866
+ }
1867
+
1868
+ @objc func completeFlexibleUpdate(_ call: CAPPluginCall) {
1869
+ // iOS doesn't support flexible in-app updates
1870
+ logger.warn("completeFlexibleUpdate is not supported on iOS.")
1871
+ call.reject("Flexible updates are not supported on iOS.", "NOT_SUPPORTED")
1872
+ }
1662
1873
  }
@@ -26,6 +26,7 @@ import UIKit
26
26
  private let FALLBACK_VERSION: String = "pastVersion"
27
27
  private let NEXT_VERSION: String = "nextVersion"
28
28
  private var unzipPercent = 0
29
+ private let TEMP_UNZIP_PREFIX: String = "capgo_unzip_"
29
30
 
30
31
  // Add this line to declare cacheFolder
31
32
  private let cacheFolder: URL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!.appendingPathComponent("capgo_downloads")
@@ -274,7 +275,7 @@ import UIKit
274
275
  private func saveDownloaded(sourceZip: URL, id: String, base: URL, notify: Bool) throws {
275
276
  try prepareFolder(source: base)
276
277
  let destPersist: URL = base.appendingPathComponent(id)
277
- let destUnZip: URL = libraryDir.appendingPathComponent(randomString(length: 10))
278
+ let destUnZip: URL = libraryDir.appendingPathComponent(TEMP_UNZIP_PREFIX + randomString(length: 10))
278
279
 
279
280
  self.unzipPercent = 0
280
281
  self.notifyDownload(id: id, percent: 75)
@@ -1103,6 +1104,43 @@ import UIKit
1103
1104
  }
1104
1105
  }
1105
1106
 
1107
+ public func cleanupOrphanedTempFolders(threadToCheck: Thread?) {
1108
+ let fileManager = FileManager.default
1109
+
1110
+ do {
1111
+ let contents = try fileManager.contentsOfDirectory(at: libraryDir, includingPropertiesForKeys: [.isDirectoryKey], options: [.skipsHiddenFiles])
1112
+
1113
+ for url in contents {
1114
+ // Check if thread was cancelled
1115
+ if let thread = threadToCheck, thread.isCancelled {
1116
+ logger.warn("cleanupOrphanedTempFolders was cancelled")
1117
+ return
1118
+ }
1119
+
1120
+ let resourceValues = try url.resourceValues(forKeys: [.isDirectoryKey])
1121
+ if resourceValues.isDirectory != true {
1122
+ continue
1123
+ }
1124
+
1125
+ let folderName = url.lastPathComponent
1126
+
1127
+ // Only delete folders with the temp unzip prefix
1128
+ if !folderName.hasPrefix(TEMP_UNZIP_PREFIX) {
1129
+ continue
1130
+ }
1131
+
1132
+ do {
1133
+ try fileManager.removeItem(at: url)
1134
+ logger.info("Deleted orphaned temp unzip folder: \(folderName)")
1135
+ } catch {
1136
+ logger.error("Failed to delete orphaned temp folder: \(folderName) \(error.localizedDescription)")
1137
+ }
1138
+ }
1139
+ } catch {
1140
+ logger.error("Failed to enumerate library directory for temp folder cleanup: \(error.localizedDescription)")
1141
+ }
1142
+ }
1143
+
1106
1144
  public func getBundleDirectory(id: String) -> URL {
1107
1145
  return libraryDir.appendingPathComponent(self.bundleDirectory).appendingPathComponent(id)
1108
1146
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-updater",
3
- "version": "7.37.0",
3
+ "version": "7.39.0",
4
4
  "license": "MPL-2.0",
5
5
  "description": "Live update for capacitor apps",
6
6
  "main": "dist/plugin.cjs.js",