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,689 @@
1
+ import Foundation
2
+ import Capacitor
3
+ import CommonCrypto
4
+
5
+ class LiveUpdatePlugin {
6
+ private weak var plugin: CAPPlugin?
7
+ private var config: [String: Any]?
8
+ private var progressListener: (([String: Any]) -> Void)?
9
+ private var stateChangeListener: (([String: Any]) -> Void)?
10
+ private var session: URLSession
11
+ private var downloadTask: URLSessionDownloadTask?
12
+ private let securityManager = SecurityManager()
13
+
14
+ init(plugin: CAPPlugin) {
15
+ self.plugin = plugin
16
+
17
+ // Create initial session with default configuration
18
+ self.session = createURLSession()
19
+ }
20
+
21
+ private func createURLSession() -> URLSession {
22
+ let config = URLSessionConfiguration.default
23
+ config.timeoutIntervalForRequest = 30
24
+ config.timeoutIntervalForResource = 300
25
+ config.tlsMinimumSupportedProtocolVersion = .TLSv12
26
+
27
+ // Configure certificate pinning if available
28
+ if let securityConfig = self.config?["security"] as? [String: Any],
29
+ let certificatePinning = securityConfig["certificatePinning"] as? [String: Any],
30
+ certificatePinning["enabled"] as? Bool == true {
31
+
32
+ return URLSession(configuration: config, delegate: CertificatePinningDelegate(securityManager: securityManager), delegateQueue: nil)
33
+ }
34
+
35
+ return URLSession(configuration: config)
36
+ }
37
+
38
+ func configure(_ config: [String: Any]) throws {
39
+ self.config = config
40
+
41
+ // Configure security manager
42
+ try securityManager.configure(config)
43
+
44
+ // Recreate URLSession with new configuration
45
+ self.session = createURLSession()
46
+
47
+ // Validate configuration
48
+ if let serverUrl = config["serverUrl"] as? String,
49
+ !serverUrl.hasPrefix("https://") {
50
+ let enforceHttps = (config["security"] as? [String: Any])?["enforceHttps"] as? Bool ?? true
51
+ if enforceHttps {
52
+ throw NSError(domain: "LiveUpdatePlugin", code: 1, userInfo: [
53
+ NSLocalizedDescriptionKey: "Server URL must use HTTPS"
54
+ ])
55
+ }
56
+ }
57
+ }
58
+
59
+ func setProgressListener(_ listener: @escaping ([String: Any]) -> Void) {
60
+ self.progressListener = listener
61
+ }
62
+
63
+ func setStateChangeListener(_ listener: @escaping ([String: Any]) -> Void) {
64
+ self.stateChangeListener = listener
65
+ }
66
+
67
+ func sync(_ call: CAPPluginCall) {
68
+ Task {
69
+ do {
70
+ let channel = call.getString("channel") ?? config?["channel"] as? String ?? "production"
71
+ guard let serverUrl = config?["serverUrl"] as? String else {
72
+ call.reject("Server URL not configured")
73
+ return
74
+ }
75
+
76
+ // Check for updates
77
+ if let latestVersion = try await checkForUpdates(serverUrl: serverUrl, channel: channel) {
78
+ call.resolve([
79
+ "status": "UPDATE_AVAILABLE",
80
+ "version": latestVersion["version"] ?? "",
81
+ "description": latestVersion["description"] ?? ""
82
+ ])
83
+ } else {
84
+ call.resolve([
85
+ "status": "UP_TO_DATE",
86
+ "version": getCurrentVersion()
87
+ ])
88
+ }
89
+ } catch {
90
+ call.resolve([
91
+ "status": "ERROR",
92
+ "error": [
93
+ "code": "NETWORK_ERROR",
94
+ "message": error.localizedDescription
95
+ ]
96
+ ])
97
+ }
98
+ }
99
+ }
100
+
101
+ func download(_ call: CAPPluginCall) {
102
+ guard let url = call.getString("url") else {
103
+ call.reject("URL is required")
104
+ return
105
+ }
106
+
107
+ guard let version = call.getString("version") else {
108
+ call.reject("Version is required")
109
+ return
110
+ }
111
+
112
+ guard let checksum = call.getString("checksum") else {
113
+ call.reject("Checksum is required")
114
+ return
115
+ }
116
+
117
+ // Validate URL
118
+ if !url.hasPrefix("https://") {
119
+ let enforceHttps = (config?["security"] as? [String: Any])?["enforceHttps"] as? Bool ?? true
120
+ if enforceHttps {
121
+ call.reject("INSECURE_URL", "Download URL must use HTTPS")
122
+ return
123
+ }
124
+ }
125
+
126
+ Task {
127
+ do {
128
+ let bundleId = "bundle-\(Date().timeIntervalSince1970)"
129
+ let downloadDir = getUpdatesDirectory().appendingPathComponent(bundleId)
130
+ try FileManager.default.createDirectory(at: downloadDir, withIntermediateDirectories: true)
131
+
132
+ // Start download
133
+ let downloadedFile = try await downloadBundle(from: url, to: downloadDir, bundleId: bundleId)
134
+
135
+ // Verify checksum
136
+ let calculatedChecksum = try calculateChecksum(for: downloadedFile)
137
+ if calculatedChecksum != checksum {
138
+ try FileManager.default.removeItem(at: downloadedFile)
139
+ call.reject("CHECKSUM_ERROR", "Bundle checksum validation failed")
140
+ return
141
+ }
142
+
143
+ // Verify signature if provided
144
+ if let signature = call.getString("signature"),
145
+ let publicKey = (config?["security"] as? [String: Any])?["publicKey"] as? String,
146
+ (config?["security"] as? [String: Any])?["enableSignatureValidation"] as? Bool == true {
147
+
148
+ let fileData = try Data(contentsOf: downloadedFile)
149
+ let securityManager = SecurityManager()
150
+
151
+ if !securityManager.verifySignature(data: fileData, signature: signature, publicKeyString: publicKey) {
152
+ try FileManager.default.removeItem(at: downloadedFile)
153
+ call.reject("SIGNATURE_ERROR", "Bundle signature validation failed")
154
+ return
155
+ }
156
+ }
157
+
158
+ // Create bundle info
159
+ let bundleInfo: [String: Any] = [
160
+ "bundleId": bundleId,
161
+ "version": version,
162
+ "path": downloadedFile.path,
163
+ "downloadTime": Date().timeIntervalSince1970 * 1000,
164
+ "size": try FileManager.default.attributesOfItem(atPath: downloadedFile.path)[.size] ?? 0,
165
+ "status": "READY",
166
+ "checksum": checksum,
167
+ "verified": true
168
+ ]
169
+
170
+ // Save bundle info
171
+ saveBundleInfo(bundleInfo)
172
+
173
+ // Extract the bundle
174
+ try extractAndApplyBundle(downloadedFile, bundleId: bundleId)
175
+
176
+ // Notify state change
177
+ stateChangeListener?([
178
+ "status": "READY",
179
+ "bundleId": bundleId,
180
+ "version": version
181
+ ])
182
+
183
+ call.resolve(bundleInfo)
184
+ } catch {
185
+ call.reject("DOWNLOAD_ERROR", error.localizedDescription)
186
+ }
187
+ }
188
+ }
189
+
190
+ func set(_ call: CAPPluginCall) {
191
+ guard let bundleId = call.getString("bundleId") else {
192
+ call.reject("Bundle ID is required")
193
+ return
194
+ }
195
+
196
+ setActiveBundle(bundleId)
197
+ call.resolve()
198
+ }
199
+
200
+ func reload(_ call: CAPPluginCall) {
201
+ // In iOS, we need to reload the WebView with the new bundle path
202
+ DispatchQueue.main.async { [weak self] in
203
+ guard let self = self,
204
+ let webView = self.plugin?.webView else {
205
+ return
206
+ }
207
+
208
+ // Get the active bundle path
209
+ if let activeBundleId = UserDefaults.standard.string(forKey: "native_update_active_bundle"),
210
+ let bundles = UserDefaults.standard.dictionary(forKey: "native_update_bundles"),
211
+ let bundleInfo = bundles[activeBundleId] as? [String: Any],
212
+ let extractedPath = bundleInfo["extractedPath"] as? String {
213
+
214
+ // Load from extracted bundle path
215
+ let indexPath = URL(fileURLWithPath: extractedPath).appendingPathComponent("index.html")
216
+ webView.load(URLRequest(url: indexPath))
217
+ } else {
218
+ // Fallback to regular reload
219
+ webView.reload()
220
+ }
221
+ }
222
+ call.resolve()
223
+ }
224
+
225
+ func reset(_ call: CAPPluginCall) {
226
+ clearAllBundles()
227
+ call.resolve()
228
+ }
229
+
230
+ func current(_ call: CAPPluginCall) {
231
+ let currentBundle = getCurrentBundleInfo()
232
+ call.resolve(currentBundle)
233
+ }
234
+
235
+ func list(_ call: CAPPluginCall) {
236
+ let bundles = getAllBundles()
237
+ call.resolve(["bundles": bundles])
238
+ }
239
+
240
+ func delete(_ call: CAPPluginCall) {
241
+ if let bundleId = call.getString("bundleId") {
242
+ deleteBundle(bundleId)
243
+ } else if let keepVersions = call.getInt("keepVersions") {
244
+ cleanupOldBundles(keepVersions: keepVersions)
245
+ }
246
+ call.resolve()
247
+ }
248
+
249
+ func notifyAppReady(_ call: CAPPluginCall) {
250
+ markBundleAsVerified()
251
+ call.resolve()
252
+ }
253
+
254
+ func getLatest(_ call: CAPPluginCall) {
255
+ Task {
256
+ do {
257
+ guard let serverUrl = config?["serverUrl"] as? String else {
258
+ call.reject("Server URL not configured")
259
+ return
260
+ }
261
+
262
+ let channel = config?["channel"] as? String ?? "production"
263
+
264
+ if let latestVersion = try await checkForUpdates(serverUrl: serverUrl, channel: channel) {
265
+ call.resolve([
266
+ "available": true,
267
+ "version": latestVersion["version"] ?? "",
268
+ "url": latestVersion["url"] ?? "",
269
+ "notes": latestVersion["notes"] ?? ""
270
+ ])
271
+ } else {
272
+ call.resolve(["available": false])
273
+ }
274
+ } catch {
275
+ call.reject("NETWORK_ERROR", error.localizedDescription)
276
+ }
277
+ }
278
+ }
279
+
280
+ func setChannel(_ call: CAPPluginCall) {
281
+ guard let channel = call.getString("channel") else {
282
+ call.reject("Channel is required")
283
+ return
284
+ }
285
+
286
+ config?["channel"] = channel
287
+ call.resolve()
288
+ }
289
+
290
+ func setUpdateUrl(_ call: CAPPluginCall) {
291
+ guard let url = call.getString("url") else {
292
+ call.reject("URL is required")
293
+ return
294
+ }
295
+
296
+ if !url.hasPrefix("https://") {
297
+ let enforceHttps = (config?["security"] as? [String: Any])?["enforceHttps"] as? Bool ?? true
298
+ if enforceHttps {
299
+ call.reject("INSECURE_URL", "Update URL must use HTTPS")
300
+ return
301
+ }
302
+ }
303
+
304
+ config?["serverUrl"] = url
305
+ call.resolve()
306
+ }
307
+
308
+ func validateUpdate(_ call: CAPPluginCall) {
309
+ guard let bundlePath = call.getString("bundlePath") else {
310
+ call.reject("Bundle path is required")
311
+ return
312
+ }
313
+
314
+ guard let checksum = call.getString("checksum") else {
315
+ call.reject("Checksum is required")
316
+ return
317
+ }
318
+
319
+ do {
320
+ let url = URL(fileURLWithPath: bundlePath)
321
+ let calculatedChecksum = try calculateChecksum(for: url)
322
+ let isValid = calculatedChecksum == checksum
323
+
324
+ call.resolve([
325
+ "isValid": isValid,
326
+ "details": [
327
+ "checksumValid": isValid
328
+ ]
329
+ ])
330
+ } catch {
331
+ call.reject("VALIDATION_ERROR", error.localizedDescription)
332
+ }
333
+ }
334
+
335
+ // MARK: - Async Methods for Background Updates
336
+
337
+ func getLatestVersionAsync() async -> LatestVersion? {
338
+ do {
339
+ guard let serverUrl = config?["serverUrl"] as? String else {
340
+ NSLog("Server URL not configured")
341
+ return nil
342
+ }
343
+
344
+ let channel = config?["channel"] as? String ?? "production"
345
+
346
+ if let latestVersion = try await checkForUpdates(serverUrl: serverUrl, channel: channel) {
347
+ return LatestVersion(
348
+ available: true,
349
+ version: latestVersion["version"] as? String
350
+ )
351
+ } else {
352
+ return LatestVersion(
353
+ available: false,
354
+ version: nil
355
+ )
356
+ }
357
+ } catch {
358
+ NSLog("Failed to check live update: \(error.localizedDescription)")
359
+ return nil
360
+ }
361
+ }
362
+
363
+ // MARK: - Private Methods
364
+
365
+ private func checkForUpdates(serverUrl: String, channel: String) async throws -> [String: Any]? {
366
+ guard let url = URL(string: "\(serverUrl)/check?channel=\(channel)") else {
367
+ throw NSError(domain: "LiveUpdatePlugin", code: 2, userInfo: [
368
+ NSLocalizedDescriptionKey: "Invalid server URL"
369
+ ])
370
+ }
371
+
372
+ let (data, _) = try await session.data(from: url)
373
+
374
+ if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
375
+ let available = json["available"] as? Bool,
376
+ available {
377
+ return json
378
+ }
379
+
380
+ return nil
381
+ }
382
+
383
+ private func downloadBundle(from urlString: String, to directory: URL, bundleId: String) async throws -> URL {
384
+ guard let url = URL(string: urlString) else {
385
+ throw NSError(domain: "LiveUpdatePlugin", code: 3, userInfo: [
386
+ NSLocalizedDescriptionKey: "Invalid download URL"
387
+ ])
388
+ }
389
+
390
+ let destinationURL = directory.appendingPathComponent("bundle.zip")
391
+
392
+ return try await withCheckedThrowingContinuation { continuation in
393
+ let task = session.downloadTask(with: url) { [weak self] tempURL, response, error in
394
+ if let error = error {
395
+ continuation.resume(throwing: error)
396
+ return
397
+ }
398
+
399
+ guard let tempURL = tempURL else {
400
+ continuation.resume(throwing: NSError(domain: "LiveUpdatePlugin", code: 4, userInfo: [
401
+ NSLocalizedDescriptionKey: "Download failed"
402
+ ]))
403
+ return
404
+ }
405
+
406
+ do {
407
+ try FileManager.default.moveItem(at: tempURL, to: destinationURL)
408
+ continuation.resume(returning: destinationURL)
409
+ } catch {
410
+ continuation.resume(throwing: error)
411
+ }
412
+ }
413
+
414
+ // Track progress
415
+ let observation = task.progress.observe(\.fractionCompleted) { [weak self] progress, _ in
416
+ let percent = Int(progress.fractionCompleted * 100)
417
+ self?.progressListener?([
418
+ "percent": percent,
419
+ "bytesDownloaded": progress.completedUnitCount,
420
+ "totalBytes": progress.totalUnitCount,
421
+ "bundleId": bundleId
422
+ ])
423
+ }
424
+
425
+ task.resume()
426
+ self.downloadTask = task
427
+ }
428
+ }
429
+
430
+ private func calculateChecksum(for url: URL) throws -> String {
431
+ let data = try Data(contentsOf: url)
432
+ var digest = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
433
+
434
+ data.withUnsafeBytes { bytes in
435
+ _ = CC_SHA256(bytes.baseAddress, CC_LONG(data.count), &digest)
436
+ }
437
+
438
+ return digest.map { String(format: "%02x", $0) }.joined()
439
+ }
440
+
441
+ private func getUpdatesDirectory() -> URL {
442
+ let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
443
+ return documentsDirectory.appendingPathComponent("updates")
444
+ }
445
+
446
+ private func getCurrentVersion() -> String {
447
+ if let bundle = getCurrentBundleInfo(),
448
+ let version = bundle["version"] as? String {
449
+ return version
450
+ }
451
+ return Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0"
452
+ }
453
+
454
+ private func saveBundleInfo(_ bundleInfo: [String: Any]) {
455
+ guard let bundleId = bundleInfo["bundleId"] as? String else { return }
456
+
457
+ let defaults = UserDefaults.standard
458
+ var bundles = defaults.dictionary(forKey: "native_update_bundles") ?? [:]
459
+ bundles[bundleId] = bundleInfo
460
+ defaults.set(bundles, forKey: "native_update_bundles")
461
+ }
462
+
463
+ private func setActiveBundle(_ bundleId: String) {
464
+ UserDefaults.standard.set(bundleId, forKey: "native_update_active_bundle")
465
+ }
466
+
467
+ private func clearAllBundles() {
468
+ let updateDir = getUpdatesDirectory()
469
+ try? FileManager.default.removeItem(at: updateDir)
470
+
471
+ UserDefaults.standard.removeObject(forKey: "native_update_bundles")
472
+ UserDefaults.standard.removeObject(forKey: "native_update_active_bundle")
473
+ }
474
+
475
+ private func getCurrentBundleInfo() -> [String: Any] {
476
+ let defaults = UserDefaults.standard
477
+
478
+ if let activeBundleId = defaults.string(forKey: "native_update_active_bundle"),
479
+ let bundles = defaults.dictionary(forKey: "native_update_bundles"),
480
+ let bundleInfo = bundles[activeBundleId] as? [String: Any] {
481
+ return bundleInfo
482
+ }
483
+
484
+ // Return default bundle
485
+ return [
486
+ "bundleId": "default",
487
+ "version": getCurrentVersion(),
488
+ "path": "/",
489
+ "downloadTime": Date().timeIntervalSince1970 * 1000,
490
+ "size": 0,
491
+ "status": "ACTIVE",
492
+ "checksum": "",
493
+ "verified": true
494
+ ]
495
+ }
496
+
497
+ private func getAllBundles() -> [[String: Any]] {
498
+ let defaults = UserDefaults.standard
499
+ let bundles = defaults.dictionary(forKey: "native_update_bundles") ?? [:]
500
+ return bundles.values.compactMap { $0 as? [String: Any] }
501
+ }
502
+
503
+ private func deleteBundle(_ bundleId: String) {
504
+ // Delete bundle files
505
+ let bundleDir = getUpdatesDirectory().appendingPathComponent(bundleId)
506
+ try? FileManager.default.removeItem(at: bundleDir)
507
+
508
+ // Remove from UserDefaults
509
+ let defaults = UserDefaults.standard
510
+ var bundles = defaults.dictionary(forKey: "native_update_bundles") ?? [:]
511
+ bundles.removeValue(forKey: bundleId)
512
+ defaults.set(bundles, forKey: "native_update_bundles")
513
+ }
514
+
515
+ private func cleanupOldBundles(keepVersions: Int) {
516
+ let bundles = getAllBundles().sorted { bundle1, bundle2 in
517
+ let time1 = bundle1["downloadTime"] as? Double ?? 0
518
+ let time2 = bundle2["downloadTime"] as? Double ?? 0
519
+ return time1 > time2
520
+ }
521
+
522
+ if bundles.count > keepVersions {
523
+ let bundlesToDelete = bundles.dropFirst(keepVersions)
524
+ for bundle in bundlesToDelete {
525
+ if let bundleId = bundle["bundleId"] as? String {
526
+ deleteBundle(bundleId)
527
+ }
528
+ }
529
+ }
530
+ }
531
+
532
+ private func markBundleAsVerified() {
533
+ UserDefaults.standard.set(true, forKey: "native_update_current_bundle_verified")
534
+ }
535
+
536
+ // MARK: - Bundle Extraction and WebView Configuration
537
+
538
+ private func extractAndApplyBundle(_ bundleUrl: URL, bundleId: String) throws {
539
+ let extractedPath = getUpdatesDirectory().appendingPathComponent(bundleId).appendingPathComponent("www")
540
+
541
+ // Extract the zip bundle
542
+ try extractZipBundle(from: bundleUrl, to: extractedPath)
543
+
544
+ // Update bundle info with extracted path
545
+ var bundleInfo = getAllBundles().first { $0["bundleId"] as? String == bundleId } ?? [:]
546
+ bundleInfo["extractedPath"] = extractedPath.path
547
+ bundleInfo["status"] = "READY"
548
+ saveBundleInfo(bundleInfo)
549
+
550
+ // Configure WebView to use new path
551
+ configureWebViewPath(extractedPath.path)
552
+ }
553
+
554
+ private func extractZipBundle(from zipUrl: URL, to destinationUrl: URL) throws {
555
+ // Create destination directory
556
+ try FileManager.default.createDirectory(at: destinationUrl, withIntermediateDirectories: true)
557
+
558
+ // Use system unzip command
559
+ let process = Process()
560
+ process.executableURL = URL(fileURLWithPath: "/usr/bin/unzip")
561
+ process.arguments = ["-o", zipUrl.path, "-d", destinationUrl.path]
562
+
563
+ try process.run()
564
+ process.waitUntilExit()
565
+
566
+ if process.terminationStatus != 0 {
567
+ throw NSError(domain: "LiveUpdatePlugin", code: 5, userInfo: [
568
+ NSLocalizedDescriptionKey: "Failed to extract bundle"
569
+ ])
570
+ }
571
+ }
572
+
573
+ private func configureWebViewPath(_ path: String) {
574
+ // Store the active bundle path
575
+ UserDefaults.standard.set(path, forKey: "native_update_webview_path")
576
+
577
+ // The actual WebView path configuration happens in the main plugin
578
+ // when the WebView is loaded or reloaded
579
+ }
580
+ }
581
+
582
+ // MARK: - Data Models
583
+
584
+ struct LatestVersion {
585
+ let available: Bool
586
+ let version: String?
587
+
588
+ func toDictionary() -> [String: Any] {
589
+ var obj: [String: Any] = [
590
+ "available": available
591
+ ]
592
+
593
+ if let version = version {
594
+ obj["version"] = version
595
+ }
596
+
597
+ return obj
598
+ }
599
+ }
600
+
601
+ // MARK: - Certificate Pinning Delegate
602
+
603
+ class CertificatePinningDelegate: NSObject, URLSessionDelegate {
604
+ private let securityManager: SecurityManager
605
+
606
+ init(securityManager: SecurityManager) {
607
+ self.securityManager = securityManager
608
+ super.init()
609
+ }
610
+
611
+ func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
612
+ guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
613
+ let serverTrust = challenge.protectionSpace.serverTrust else {
614
+ completionHandler(.performDefaultHandling, nil)
615
+ return
616
+ }
617
+
618
+ // Get certificate pins from security manager configuration
619
+ let certificatePins = getCertificatePins(for: challenge.protectionSpace.host)
620
+
621
+ if certificatePins.isEmpty {
622
+ // No pins configured for this host, use default handling
623
+ completionHandler(.performDefaultHandling, nil)
624
+ return
625
+ }
626
+
627
+ // Evaluate server trust
628
+ var error: CFError?
629
+ let isValid = SecTrustEvaluateWithError(serverTrust, &error)
630
+
631
+ if !isValid {
632
+ print("Certificate validation failed: \(error?.localizedDescription ?? "Unknown error")")
633
+ completionHandler(.cancelAuthenticationChallenge, nil)
634
+ return
635
+ }
636
+
637
+ // Check certificate pinning
638
+ if validateCertificatePins(serverTrust: serverTrust, expectedPins: certificatePins) {
639
+ let credential = URLCredential(trust: serverTrust)
640
+ completionHandler(.useCredential, credential)
641
+ } else {
642
+ print("Certificate pinning validation failed for host: \(challenge.protectionSpace.host)")
643
+ completionHandler(.cancelAuthenticationChallenge, nil)
644
+ }
645
+ }
646
+
647
+ private func getCertificatePins(for host: String) -> [String] {
648
+ // Get pins from security manager configuration
649
+ guard let securityInfo = securityManager.getSecurityInfo(),
650
+ let certificatePinning = securityInfo["certificatePinning"] as? [String: Any],
651
+ certificatePinning["enabled"] as? Bool == true,
652
+ let hostPins = certificatePinning["pins"] as? [String: [String]] else {
653
+ return []
654
+ }
655
+
656
+ // Return pins for the specific host
657
+ return hostPins[host] ?? []
658
+ }
659
+
660
+ private func validateCertificatePins(serverTrust: SecTrust, expectedPins: [String]) -> Bool {
661
+ let certificateCount = SecTrustGetCertificateCount(serverTrust)
662
+
663
+ for i in 0..<certificateCount {
664
+ guard let certificate = SecTrustGetCertificateAtIndex(serverTrust, i) else { continue }
665
+
666
+ let certificateData = SecCertificateCopyData(certificate) as Data
667
+ let hash = certificateData.sha256()
668
+ let pin = "sha256/" + hash.base64EncodedString()
669
+
670
+ if expectedPins.contains(pin) {
671
+ return true
672
+ }
673
+ }
674
+
675
+ return false
676
+ }
677
+ }
678
+
679
+ // MARK: - Data Extension for SHA256
680
+
681
+ extension Data {
682
+ func sha256() -> Data {
683
+ var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
684
+ self.withUnsafeBytes { bytes in
685
+ _ = CC_SHA256(bytes.baseAddress, CC_LONG(self.count), &hash)
686
+ }
687
+ return Data(hash)
688
+ }
689
+ }