native-update 1.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 (184) hide show
  1. package/CapacitorNativeUpdate.podspec +18 -0
  2. package/LICENSE +21 -0
  3. package/Readme.md +451 -0
  4. package/android/build.gradle +92 -0
  5. package/android/gradle/wrapper/gradle-wrapper.properties +8 -0
  6. package/android/gradle.properties +17 -0
  7. package/android/proguard-rules.pro +29 -0
  8. package/android/settings.gradle +2 -0
  9. package/android/src/main/AndroidManifest.xml +34 -0
  10. package/android/src/main/java/com/aoneahsan/nativeupdate/AppReviewPlugin.kt +153 -0
  11. package/android/src/main/java/com/aoneahsan/nativeupdate/AppUpdatePlugin.kt +275 -0
  12. package/android/src/main/java/com/aoneahsan/nativeupdate/BackgroundNotificationManager.kt +390 -0
  13. package/android/src/main/java/com/aoneahsan/nativeupdate/BackgroundUpdateManager.kt +46 -0
  14. package/android/src/main/java/com/aoneahsan/nativeupdate/BackgroundUpdatePlugin.kt +333 -0
  15. package/android/src/main/java/com/aoneahsan/nativeupdate/BackgroundUpdateWorker.kt +251 -0
  16. package/android/src/main/java/com/aoneahsan/nativeupdate/CapacitorNativeUpdatePlugin.kt +265 -0
  17. package/android/src/main/java/com/aoneahsan/nativeupdate/LiveUpdatePlugin.kt +526 -0
  18. package/android/src/main/java/com/aoneahsan/nativeupdate/NotificationActionReceiver.kt +99 -0
  19. package/android/src/main/java/com/aoneahsan/nativeupdate/SecurityManager.kt +249 -0
  20. package/dist/esm/__tests__/bundle-manager.test.d.ts +1 -0
  21. package/dist/esm/__tests__/bundle-manager.test.js +123 -0
  22. package/dist/esm/__tests__/bundle-manager.test.js.map +1 -0
  23. package/dist/esm/__tests__/config.test.d.ts +1 -0
  24. package/dist/esm/__tests__/config.test.js +69 -0
  25. package/dist/esm/__tests__/config.test.js.map +1 -0
  26. package/dist/esm/__tests__/integration.test.d.ts +1 -0
  27. package/dist/esm/__tests__/integration.test.js +78 -0
  28. package/dist/esm/__tests__/integration.test.js.map +1 -0
  29. package/dist/esm/__tests__/security.test.d.ts +1 -0
  30. package/dist/esm/__tests__/security.test.js +54 -0
  31. package/dist/esm/__tests__/security.test.js.map +1 -0
  32. package/dist/esm/__tests__/version-manager.test.d.ts +1 -0
  33. package/dist/esm/__tests__/version-manager.test.js +45 -0
  34. package/dist/esm/__tests__/version-manager.test.js.map +1 -0
  35. package/dist/esm/app-review/app-review-manager.d.ts +24 -0
  36. package/dist/esm/app-review/app-review-manager.js +195 -0
  37. package/dist/esm/app-review/app-review-manager.js.map +1 -0
  38. package/dist/esm/app-review/index.d.ts +5 -0
  39. package/dist/esm/app-review/index.js +6 -0
  40. package/dist/esm/app-review/index.js.map +1 -0
  41. package/dist/esm/app-review/platform-review-handler.d.ts +20 -0
  42. package/dist/esm/app-review/platform-review-handler.js +138 -0
  43. package/dist/esm/app-review/platform-review-handler.js.map +1 -0
  44. package/dist/esm/app-review/review-conditions-checker.d.ts +22 -0
  45. package/dist/esm/app-review/review-conditions-checker.js +155 -0
  46. package/dist/esm/app-review/review-conditions-checker.js.map +1 -0
  47. package/dist/esm/app-review/review-rate-limiter.d.ts +23 -0
  48. package/dist/esm/app-review/review-rate-limiter.js +164 -0
  49. package/dist/esm/app-review/review-rate-limiter.js.map +1 -0
  50. package/dist/esm/app-review/types.d.ts +41 -0
  51. package/dist/esm/app-review/types.js +2 -0
  52. package/dist/esm/app-review/types.js.map +1 -0
  53. package/dist/esm/app-update/app-update-checker.d.ts +13 -0
  54. package/dist/esm/app-update/app-update-checker.js +104 -0
  55. package/dist/esm/app-update/app-update-checker.js.map +1 -0
  56. package/dist/esm/app-update/app-update-installer.d.ts +19 -0
  57. package/dist/esm/app-update/app-update-installer.js +123 -0
  58. package/dist/esm/app-update/app-update-installer.js.map +1 -0
  59. package/dist/esm/app-update/app-update-manager.d.ts +28 -0
  60. package/dist/esm/app-update/app-update-manager.js +199 -0
  61. package/dist/esm/app-update/app-update-manager.js.map +1 -0
  62. package/dist/esm/app-update/app-update-notifier.d.ts +14 -0
  63. package/dist/esm/app-update/app-update-notifier.js +100 -0
  64. package/dist/esm/app-update/app-update-notifier.js.map +1 -0
  65. package/dist/esm/app-update/index.d.ts +6 -0
  66. package/dist/esm/app-update/index.js +7 -0
  67. package/dist/esm/app-update/index.js.map +1 -0
  68. package/dist/esm/app-update/platform-app-update.d.ts +19 -0
  69. package/dist/esm/app-update/platform-app-update.js +129 -0
  70. package/dist/esm/app-update/platform-app-update.js.map +1 -0
  71. package/dist/esm/app-update/types.d.ts +58 -0
  72. package/dist/esm/app-update/types.js +12 -0
  73. package/dist/esm/app-update/types.js.map +1 -0
  74. package/dist/esm/background-update/background-scheduler.d.ts +17 -0
  75. package/dist/esm/background-update/background-scheduler.js +195 -0
  76. package/dist/esm/background-update/background-scheduler.js.map +1 -0
  77. package/dist/esm/background-update/index.d.ts +3 -0
  78. package/dist/esm/background-update/index.js +3 -0
  79. package/dist/esm/background-update/index.js.map +1 -0
  80. package/dist/esm/background-update/notification-manager.d.ts +29 -0
  81. package/dist/esm/background-update/notification-manager.js +89 -0
  82. package/dist/esm/background-update/notification-manager.js.map +1 -0
  83. package/dist/esm/core/analytics.d.ts +70 -0
  84. package/dist/esm/core/analytics.js +137 -0
  85. package/dist/esm/core/analytics.js.map +1 -0
  86. package/dist/esm/core/cache-manager.d.ts +72 -0
  87. package/dist/esm/core/cache-manager.js +275 -0
  88. package/dist/esm/core/cache-manager.js.map +1 -0
  89. package/dist/esm/core/config.d.ts +48 -0
  90. package/dist/esm/core/config.js +83 -0
  91. package/dist/esm/core/config.js.map +1 -0
  92. package/dist/esm/core/errors.d.ts +51 -0
  93. package/dist/esm/core/errors.js +80 -0
  94. package/dist/esm/core/errors.js.map +1 -0
  95. package/dist/esm/core/logger.d.ts +21 -0
  96. package/dist/esm/core/logger.js +109 -0
  97. package/dist/esm/core/logger.js.map +1 -0
  98. package/dist/esm/core/performance.d.ts +53 -0
  99. package/dist/esm/core/performance.js +140 -0
  100. package/dist/esm/core/performance.js.map +1 -0
  101. package/dist/esm/core/plugin-manager.d.ts +66 -0
  102. package/dist/esm/core/plugin-manager.js +148 -0
  103. package/dist/esm/core/plugin-manager.js.map +1 -0
  104. package/dist/esm/core/security.d.ts +93 -0
  105. package/dist/esm/core/security.js +315 -0
  106. package/dist/esm/core/security.js.map +1 -0
  107. package/dist/esm/definitions.d.ts +639 -0
  108. package/dist/esm/definitions.js +103 -0
  109. package/dist/esm/definitions.js.map +1 -0
  110. package/dist/esm/index.d.ts +12 -0
  111. package/dist/esm/index.js +16 -0
  112. package/dist/esm/index.js.map +1 -0
  113. package/dist/esm/live-update/bundle-manager.d.ts +94 -0
  114. package/dist/esm/live-update/bundle-manager.js +310 -0
  115. package/dist/esm/live-update/bundle-manager.js.map +1 -0
  116. package/dist/esm/live-update/certificate-pinning.d.ts +38 -0
  117. package/dist/esm/live-update/certificate-pinning.js +78 -0
  118. package/dist/esm/live-update/certificate-pinning.js.map +1 -0
  119. package/dist/esm/live-update/download-manager.d.ts +67 -0
  120. package/dist/esm/live-update/download-manager.js +319 -0
  121. package/dist/esm/live-update/download-manager.js.map +1 -0
  122. package/dist/esm/live-update/update-manager.d.ts +52 -0
  123. package/dist/esm/live-update/update-manager.js +294 -0
  124. package/dist/esm/live-update/update-manager.js.map +1 -0
  125. package/dist/esm/live-update/version-manager.d.ts +84 -0
  126. package/dist/esm/live-update/version-manager.js +335 -0
  127. package/dist/esm/live-update/version-manager.js.map +1 -0
  128. package/dist/esm/plugin.d.ts +6 -0
  129. package/dist/esm/plugin.js +283 -0
  130. package/dist/esm/plugin.js.map +1 -0
  131. package/dist/esm/security/crypto.d.ts +25 -0
  132. package/dist/esm/security/crypto.js +70 -0
  133. package/dist/esm/security/crypto.js.map +1 -0
  134. package/dist/esm/security/validator.d.ts +60 -0
  135. package/dist/esm/security/validator.js +143 -0
  136. package/dist/esm/security/validator.js.map +1 -0
  137. package/dist/esm/web.d.ts +74 -0
  138. package/dist/esm/web.js +595 -0
  139. package/dist/esm/web.js.map +1 -0
  140. package/dist/plugin.cjs.js +2 -0
  141. package/dist/plugin.cjs.js.map +1 -0
  142. package/dist/plugin.esm.js +2 -0
  143. package/dist/plugin.esm.js.map +1 -0
  144. package/dist/plugin.js +3 -0
  145. package/dist/plugin.js.map +1 -0
  146. package/docs/APP_REVIEW_GUIDE.md +768 -0
  147. package/docs/BUNDLE_SIGNING.md +264 -0
  148. package/docs/LIVE_UPDATES_GUIDE.md +650 -0
  149. package/docs/MIGRATION.md +192 -0
  150. package/docs/NATIVE_UPDATES_GUIDE.md +694 -0
  151. package/docs/QUICK_START.md +606 -0
  152. package/docs/README.md +111 -0
  153. package/docs/REMAINING_FEATURES.md +139 -0
  154. package/docs/api/app-review-api.md +259 -0
  155. package/docs/api/app-update-api.md +238 -0
  156. package/docs/api/events-api.md +451 -0
  157. package/docs/api/live-update-api.md +265 -0
  158. package/docs/background-updates.md +392 -0
  159. package/docs/examples/advanced-scenarios.md +410 -0
  160. package/docs/examples/basic-usage.md +185 -0
  161. package/docs/features/app-reviews.md +975 -0
  162. package/docs/features/app-updates.md +785 -0
  163. package/docs/features/live-updates.md +633 -0
  164. package/docs/getting-started/configuration.md +468 -0
  165. package/docs/getting-started/installation.md +209 -0
  166. package/docs/getting-started/quick-start.md +379 -0
  167. package/docs/guides/deployment-guide.md +333 -0
  168. package/docs/guides/migration-from-codepush.md +142 -0
  169. package/docs/guides/security-best-practices.md +1057 -0
  170. package/docs/guides/testing-guide.md +373 -0
  171. package/docs/production-readiness.md +478 -0
  172. package/docs/security/certificate-pinning.md +122 -0
  173. package/docs/server-requirements.md +147 -0
  174. package/ios/Plugin/AppReview/AppReviewPlugin.swift +158 -0
  175. package/ios/Plugin/AppUpdate/AppUpdatePlugin.swift +234 -0
  176. package/ios/Plugin/BackgroundUpdate/BackgroundNotificationManager.swift +329 -0
  177. package/ios/Plugin/BackgroundUpdate/BackgroundUpdatePlugin.swift +396 -0
  178. package/ios/Plugin/CapacitorNativeUpdatePlugin.m +45 -0
  179. package/ios/Plugin/CapacitorNativeUpdatePlugin.swift +190 -0
  180. package/ios/Plugin/Info.plist +43 -0
  181. package/ios/Plugin/LiveUpdate/LiveUpdatePlugin.swift +689 -0
  182. package/ios/Plugin/LiveUpdate/WebViewConfiguration.swift +45 -0
  183. package/ios/Plugin/Security/SecurityManager.swift +289 -0
  184. package/package.json +90 -0
@@ -0,0 +1,147 @@
1
+ # Backend Server Requirements
2
+
3
+ This document outlines the server infrastructure you need to build to use this plugin.
4
+
5
+ ## ⚠️ No Server Included
6
+
7
+ This plugin does **not** include a backend server. You must build your own server that implements these requirements.
8
+
9
+ ## Required API Endpoints
10
+
11
+ ### 1. Version Check Endpoint
12
+ ```
13
+ GET /api/v1/updates/check
14
+ ```
15
+
16
+ **Query Parameters:**
17
+ - `appId`: Your application ID
18
+ - `currentVersion`: Current bundle version
19
+ - `platform`: ios | android | web
20
+ - `channel`: production | staging | development
21
+
22
+ **Response:**
23
+ ```json
24
+ {
25
+ "updateAvailable": true,
26
+ "version": "1.2.3",
27
+ "minAppVersion": "2.0.0",
28
+ "releaseNotes": "Bug fixes and improvements",
29
+ "downloadUrl": "https://cdn.example.com/bundles/1.2.3.zip",
30
+ "signature": "base64-encoded-signature",
31
+ "checksum": "sha256-hash",
32
+ "size": 1234567,
33
+ "mandatory": false
34
+ }
35
+ ```
36
+
37
+ ### 2. Bundle Download Endpoint
38
+ ```
39
+ GET /api/v1/bundles/{version}
40
+ ```
41
+
42
+ **Requirements:**
43
+ - Serve bundle files with proper MIME types
44
+ - Support resume/partial downloads
45
+ - Include security headers
46
+ - CDN integration recommended
47
+
48
+ ### 3. Update Confirmation Endpoint
49
+ ```
50
+ POST /api/v1/updates/confirm
51
+ ```
52
+
53
+ **Body:**
54
+ ```json
55
+ {
56
+ "appId": "com.example.app",
57
+ "version": "1.2.3",
58
+ "platform": "ios",
59
+ "success": true,
60
+ "deviceId": "unique-device-id"
61
+ }
62
+ ```
63
+
64
+ ## Security Requirements
65
+
66
+ ### Bundle Signing
67
+ - Generate RSA or ECDSA key pairs
68
+ - Sign bundles with private key
69
+ - Include public key in app
70
+ - Verify signatures before applying updates
71
+
72
+ ### HTTPS Requirements
73
+ - All endpoints must use HTTPS
74
+ - Consider certificate pinning
75
+ - Implement rate limiting
76
+ - Add authentication tokens
77
+
78
+ ## Storage Infrastructure
79
+
80
+ ### Bundle Storage
81
+ - Store bundle files securely
82
+ - Implement versioning system
83
+ - Clean up old versions
84
+ - Monitor storage usage
85
+
86
+ ### CDN Configuration
87
+ - Geographic distribution
88
+ - Caching strategies
89
+ - Bandwidth optimization
90
+ - Fallback mechanisms
91
+
92
+ ## Example Server Structure
93
+
94
+ ```
95
+ update-server/
96
+ ├── src/
97
+ │ ├── api/
98
+ │ │ ├── updates.js
99
+ │ │ ├── bundles.js
100
+ │ │ └── analytics.js
101
+ │ ├── services/
102
+ │ │ ├── signing.js
103
+ │ │ ├── storage.js
104
+ │ │ └── version.js
105
+ │ ├── middleware/
106
+ │ │ ├── auth.js
107
+ │ │ ├── security.js
108
+ │ │ └── logging.js
109
+ │ └── config/
110
+ │ └── index.js
111
+ ├── bundles/
112
+ │ └── [stored bundle files]
113
+ ├── keys/
114
+ │ ├── private.pem
115
+ │ └── public.pem
116
+ └── package.json
117
+ ```
118
+
119
+ ## Minimum Implementation Checklist
120
+
121
+ - [ ] Version check API endpoint
122
+ - [ ] Bundle download endpoint
123
+ - [ ] Bundle signing implementation
124
+ - [ ] HTTPS configuration
125
+ - [ ] Basic authentication
126
+ - [ ] Error handling
127
+ - [ ] Logging system
128
+ - [ ] Health check endpoint
129
+
130
+ ## Recommended Features
131
+
132
+ - [ ] Analytics tracking
133
+ - [ ] A/B testing support
134
+ - [ ] Rollback capabilities
135
+ - [ ] Multi-tenant support
136
+ - [ ] Admin dashboard
137
+ - [ ] Monitoring/alerting
138
+ - [ ] Automated testing
139
+
140
+ ## Getting Help
141
+
142
+ Since no server is provided, you'll need to:
143
+ 1. Build your own implementation
144
+ 2. Use a third-party service
145
+ 3. Hire developers familiar with update servers
146
+
147
+ Consider looking at open-source examples or commercial solutions for inspiration.
@@ -0,0 +1,158 @@
1
+ import Foundation
2
+ import Capacitor
3
+ import StoreKit
4
+
5
+ class AppReviewPlugin {
6
+ private weak var plugin: CAPPlugin?
7
+ private var config: [String: Any]?
8
+ private let userDefaults = UserDefaults.standard
9
+
10
+ private struct Keys {
11
+ static let installDate = "native_update_review_install_date"
12
+ static let lastReviewRequest = "native_update_review_last_request"
13
+ static let launchCount = "native_update_review_launch_count"
14
+ static let reviewShownCount = "native_update_review_shown_count"
15
+ }
16
+
17
+ init(plugin: CAPPlugin) {
18
+ self.plugin = plugin
19
+
20
+ // Initialize install date if not set
21
+ if userDefaults.object(forKey: Keys.installDate) == nil {
22
+ userDefaults.set(Date(), forKey: Keys.installDate)
23
+ }
24
+
25
+ // Increment launch count
26
+ incrementLaunchCount()
27
+ }
28
+
29
+ func configure(_ config: [String: Any]) throws {
30
+ self.config = config
31
+ }
32
+
33
+ func requestReview(_ call: CAPPluginCall) {
34
+ // First check if we can request a review
35
+ let (allowed, reason) = checkCanRequestReview()
36
+
37
+ if !allowed {
38
+ call.resolve([
39
+ "shown": false,
40
+ "error": reason ?? "Review conditions not met"
41
+ ])
42
+ return
43
+ }
44
+
45
+ // Request review using StoreKit
46
+ DispatchQueue.main.async { [weak self] in
47
+ if #available(iOS 14.0, *) {
48
+ if let windowScene = UIApplication.shared.connectedScenes
49
+ .first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene {
50
+ SKStoreReviewController.requestReview(in: windowScene)
51
+ self?.updateReviewRequestTime()
52
+
53
+ call.resolve([
54
+ "shown": true
55
+ ])
56
+ } else {
57
+ call.resolve([
58
+ "shown": false,
59
+ "error": "No active window scene found"
60
+ ])
61
+ }
62
+ } else {
63
+ // Fallback for iOS 13 and earlier
64
+ SKStoreReviewController.requestReview()
65
+ self?.updateReviewRequestTime()
66
+
67
+ call.resolve([
68
+ "shown": true
69
+ ])
70
+ }
71
+ }
72
+ }
73
+
74
+ func canRequestReview(_ call: CAPPluginCall) {
75
+ let (allowed, reason) = checkCanRequestReview()
76
+
77
+ var result: [String: Any] = ["allowed": allowed]
78
+ if !allowed, let reason = reason {
79
+ result["reason"] = reason
80
+ }
81
+
82
+ call.resolve(result)
83
+ }
84
+
85
+ // MARK: - Private Methods
86
+
87
+ private func checkCanRequestReview() -> (Bool, String?) {
88
+ // Check if debug mode is enabled
89
+ let debugMode = config?["debugMode"] as? Bool ?? false
90
+ if debugMode {
91
+ return (true, nil)
92
+ }
93
+
94
+ let now = Date()
95
+ let installDate = userDefaults.object(forKey: Keys.installDate) as? Date ?? now
96
+ let lastReviewRequest = userDefaults.object(forKey: Keys.lastReviewRequest) as? Date
97
+ let launchCount = userDefaults.integer(forKey: Keys.launchCount)
98
+ let shownCount = userDefaults.integer(forKey: Keys.reviewShownCount)
99
+
100
+ // Check minimum days since install
101
+ let minDaysSinceInstall = config?["minimumDaysSinceInstall"] as? Int ?? 7
102
+ let daysSinceInstall = Calendar.current.dateComponents([.day], from: installDate, to: now).day ?? 0
103
+ if daysSinceInstall < minDaysSinceInstall {
104
+ return (false, "Not enough days since install")
105
+ }
106
+
107
+ // Check minimum days since last prompt
108
+ if let lastRequest = lastReviewRequest {
109
+ let minDaysSinceLastPrompt = config?["minimumDaysSinceLastPrompt"] as? Int ?? 90
110
+ let daysSinceLastPrompt = Calendar.current.dateComponents([.day], from: lastRequest, to: now).day ?? 0
111
+ if daysSinceLastPrompt < minDaysSinceLastPrompt {
112
+ return (false, "Too soon since last review request")
113
+ }
114
+ }
115
+
116
+ // Check minimum launch count
117
+ let minLaunchCount = config?["minimumLaunchCount"] as? Int ?? 3
118
+ if launchCount < minLaunchCount {
119
+ return (false, "Not enough app launches")
120
+ }
121
+
122
+ // iOS limits to 3 review requests per year
123
+ if shownCount >= 3 {
124
+ // Check if it's been a year since the first request
125
+ if let firstRequestDate = userDefaults.object(forKey: "native_update_review_first_request") as? Date {
126
+ let daysSinceFirst = Calendar.current.dateComponents([.day], from: firstRequestDate, to: now).day ?? 0
127
+ if daysSinceFirst < 365 {
128
+ return (false, "Review quota exceeded")
129
+ } else {
130
+ // Reset count after a year
131
+ userDefaults.set(0, forKey: Keys.reviewShownCount)
132
+ userDefaults.removeObject(forKey: "native_update_review_first_request")
133
+ }
134
+ }
135
+ }
136
+
137
+ return (true, nil)
138
+ }
139
+
140
+ private func incrementLaunchCount() {
141
+ let currentCount = userDefaults.integer(forKey: Keys.launchCount)
142
+ userDefaults.set(currentCount + 1, forKey: Keys.launchCount)
143
+ }
144
+
145
+ private func updateReviewRequestTime() {
146
+ let now = Date()
147
+ userDefaults.set(now, forKey: Keys.lastReviewRequest)
148
+
149
+ // Track first request for yearly quota
150
+ if userDefaults.object(forKey: "native_update_review_first_request") == nil {
151
+ userDefaults.set(now, forKey: "native_update_review_first_request")
152
+ }
153
+
154
+ // Increment shown count
155
+ let shownCount = userDefaults.integer(forKey: Keys.reviewShownCount)
156
+ userDefaults.set(shownCount + 1, forKey: Keys.reviewShownCount)
157
+ }
158
+ }
@@ -0,0 +1,234 @@
1
+ import Foundation
2
+ import Capacitor
3
+ import UIKit
4
+
5
+ class AppUpdatePlugin {
6
+ private weak var plugin: CAPPlugin?
7
+ private var config: [String: Any]?
8
+ private let iTunesLookupURL = "https://itunes.apple.com/lookup"
9
+
10
+ init(plugin: CAPPlugin) {
11
+ self.plugin = plugin
12
+ }
13
+
14
+ func configure(_ config: [String: Any]) throws {
15
+ self.config = config
16
+ }
17
+
18
+ func getAppUpdateInfo(_ call: CAPPluginCall) {
19
+ Task {
20
+ do {
21
+ let bundleId = Bundle.main.bundleIdentifier ?? ""
22
+ let currentVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0.0"
23
+
24
+ // Check iTunes for app info
25
+ let appInfo = try await checkAppStoreVersion(bundleId: bundleId)
26
+
27
+ let storeVersion = appInfo["version"] as? String ?? "0.0.0"
28
+ let updateAvailable = isVersionNewer(storeVersion, than: currentVersion)
29
+
30
+ var result: [String: Any] = [
31
+ "updateAvailable": updateAvailable,
32
+ "currentVersion": currentVersion
33
+ ]
34
+
35
+ if updateAvailable {
36
+ result["availableVersion"] = storeVersion
37
+
38
+ // Calculate staleness in days
39
+ if let releaseDate = appInfo["currentVersionReleaseDate"] as? String {
40
+ let formatter = ISO8601DateFormatter()
41
+ if let date = formatter.date(from: releaseDate) {
42
+ let days = Calendar.current.dateComponents([.day], from: date, to: Date()).day ?? 0
43
+ result["clientVersionStalenessDays"] = days
44
+ }
45
+ }
46
+ }
47
+
48
+ // iOS doesn't have the same update types as Android
49
+ result["immediateUpdateAllowed"] = false
50
+ result["flexibleUpdateAllowed"] = false
51
+
52
+ // Check minimum version requirement
53
+ if let minimumVersion = config?["minimumVersion"] as? String {
54
+ let needsUpdate = isVersionNewer(minimumVersion, than: currentVersion)
55
+ if needsUpdate {
56
+ result["updatePriority"] = 5 // High priority
57
+ }
58
+ }
59
+
60
+ call.resolve(result)
61
+ } catch {
62
+ call.reject("UPDATE_CHECK_FAILED", error.localizedDescription)
63
+ }
64
+ }
65
+ }
66
+
67
+ func performImmediateUpdate(_ call: CAPPluginCall) {
68
+ // iOS doesn't support in-app updates like Android
69
+ // We'll open the App Store instead
70
+ openAppStore(call)
71
+ }
72
+
73
+ func startFlexibleUpdate(_ call: CAPPluginCall) {
74
+ // iOS doesn't support flexible updates
75
+ call.reject("PLATFORM_NOT_SUPPORTED", "Flexible updates are not supported on iOS")
76
+ }
77
+
78
+ func completeFlexibleUpdate(_ call: CAPPluginCall) {
79
+ // iOS doesn't support flexible updates
80
+ call.reject("PLATFORM_NOT_SUPPORTED", "Flexible updates are not supported on iOS")
81
+ }
82
+
83
+ func openAppStore(_ call: CAPPluginCall) {
84
+ let appId = call.getString("appId") ?? getAppStoreId()
85
+
86
+ guard let appId = appId else {
87
+ call.reject("APP_ID_REQUIRED", "App ID is required to open App Store")
88
+ return
89
+ }
90
+
91
+ let urlString = "itms-apps://apple.com/app/id\(appId)"
92
+
93
+ DispatchQueue.main.async {
94
+ if let url = URL(string: urlString),
95
+ UIApplication.shared.canOpenURL(url) {
96
+ UIApplication.shared.open(url, options: [:]) { success in
97
+ if success {
98
+ call.resolve()
99
+ } else {
100
+ // Fallback to web URL
101
+ if let webUrl = URL(string: "https://apps.apple.com/app/id\(appId)") {
102
+ UIApplication.shared.open(webUrl, options: [:]) { webSuccess in
103
+ if webSuccess {
104
+ call.resolve()
105
+ } else {
106
+ call.reject("OPEN_STORE_FAILED", "Failed to open App Store")
107
+ }
108
+ }
109
+ } else {
110
+ call.reject("OPEN_STORE_FAILED", "Failed to open App Store")
111
+ }
112
+ }
113
+ }
114
+ } else {
115
+ call.reject("INVALID_URL", "Invalid App Store URL")
116
+ }
117
+ }
118
+ }
119
+
120
+ // MARK: - Async Methods for Background Updates
121
+
122
+ func getAppUpdateInfoAsync() async -> AppUpdateInfo? {
123
+ do {
124
+ let bundleId = Bundle.main.bundleIdentifier ?? ""
125
+ let currentVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0.0"
126
+
127
+ // Check iTunes for app info
128
+ let appInfo = try await checkAppStoreVersion(bundleId: bundleId)
129
+
130
+ let storeVersion = appInfo["version"] as? String ?? "0.0.0"
131
+ let updateAvailable = isVersionNewer(storeVersion, than: currentVersion)
132
+
133
+ return AppUpdateInfo(
134
+ updateAvailable: updateAvailable,
135
+ currentVersion: currentVersion,
136
+ availableVersion: updateAvailable ? storeVersion : nil
137
+ )
138
+ } catch {
139
+ NSLog("Failed to check app update: \(error.localizedDescription)")
140
+ return nil
141
+ }
142
+ }
143
+
144
+ // MARK: - Private Methods
145
+
146
+ private func checkAppStoreVersion(bundleId: String) async throws -> [String: Any] {
147
+ guard let countryCode = Locale.current.regionCode else {
148
+ throw NSError(domain: "AppUpdatePlugin", code: 1, userInfo: [
149
+ NSLocalizedDescriptionKey: "Could not determine country code"
150
+ ])
151
+ }
152
+
153
+ var components = URLComponents(string: iTunesLookupURL)!
154
+ components.queryItems = [
155
+ URLQueryItem(name: "bundleId", value: bundleId),
156
+ URLQueryItem(name: "country", value: countryCode)
157
+ ]
158
+
159
+ guard let url = components.url else {
160
+ throw NSError(domain: "AppUpdatePlugin", code: 2, userInfo: [
161
+ NSLocalizedDescriptionKey: "Invalid iTunes lookup URL"
162
+ ])
163
+ }
164
+
165
+ let (data, _) = try await URLSession.shared.data(from: url)
166
+
167
+ guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
168
+ let results = json["results"] as? [[String: Any]],
169
+ let appInfo = results.first else {
170
+ throw NSError(domain: "AppUpdatePlugin", code: 3, userInfo: [
171
+ NSLocalizedDescriptionKey: "No app found in App Store"
172
+ ])
173
+ }
174
+
175
+ return appInfo
176
+ }
177
+
178
+ private func isVersionNewer(_ version1: String, than version2: String) -> Bool {
179
+ let v1Components = version1.split(separator: ".").compactMap { Int($0) }
180
+ let v2Components = version2.split(separator: ".").compactMap { Int($0) }
181
+
182
+ let maxCount = max(v1Components.count, v2Components.count)
183
+
184
+ for i in 0..<maxCount {
185
+ let v1 = i < v1Components.count ? v1Components[i] : 0
186
+ let v2 = i < v2Components.count ? v2Components[i] : 0
187
+
188
+ if v1 > v2 {
189
+ return true
190
+ } else if v1 < v2 {
191
+ return false
192
+ }
193
+ }
194
+
195
+ return false
196
+ }
197
+
198
+ private func getAppStoreId() -> String? {
199
+ // Try to get from config first
200
+ if let storeUrl = (config?["storeUrl"] as? [String: Any])?["ios"] as? String {
201
+ // Extract ID from URL like https://apps.apple.com/app/id123456789
202
+ let pattern = #"id(\d+)"#
203
+ if let regex = try? NSRegularExpression(pattern: pattern),
204
+ let match = regex.firstMatch(in: storeUrl, range: NSRange(storeUrl.startIndex..., in: storeUrl)) {
205
+ let range = Range(match.range(at: 1), in: storeUrl)!
206
+ return String(storeUrl[range])
207
+ }
208
+ }
209
+
210
+ // Try to get from Info.plist
211
+ return Bundle.main.infoDictionary?["AppStoreId"] as? String
212
+ }
213
+ }
214
+
215
+ // MARK: - Data Models
216
+
217
+ struct AppUpdateInfo {
218
+ let updateAvailable: Bool
219
+ let currentVersion: String
220
+ let availableVersion: String?
221
+
222
+ func toDictionary() -> [String: Any] {
223
+ var obj: [String: Any] = [
224
+ "updateAvailable": updateAvailable,
225
+ "currentVersion": currentVersion
226
+ ]
227
+
228
+ if let availableVersion = availableVersion {
229
+ obj["availableVersion"] = availableVersion
230
+ }
231
+
232
+ return obj
233
+ }
234
+ }