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