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,526 @@
1
+ package com.aoneahsan.nativeupdate
2
+
3
+ import android.app.Activity
4
+ import android.content.Context
5
+ import com.getcapacitor.JSObject
6
+ import com.getcapacitor.PluginCall
7
+ import kotlinx.coroutines.*
8
+ import okhttp3.*
9
+ import okhttp3.tls.HandshakeCertificates
10
+ import java.io.File
11
+ import java.io.FileOutputStream
12
+ import java.security.MessageDigest
13
+ import java.util.concurrent.TimeUnit
14
+ import javax.net.ssl.X509TrustManager
15
+
16
+ class LiveUpdatePlugin(
17
+ private val activity: Activity,
18
+ private val context: Context
19
+ ) {
20
+ private var config: JSObject? = null
21
+ private var progressListener: ((JSObject) -> Unit)? = null
22
+ private var stateChangeListener: ((JSObject) -> Unit)? = null
23
+ private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
24
+ private lateinit var okHttpClient: OkHttpClient
25
+ private val securityManager = SecurityManager(context)
26
+
27
+ init {
28
+ // Initialize OkHttp with default settings
29
+ okHttpClient = createOkHttpClient()
30
+ }
31
+
32
+ private fun createOkHttpClient(): OkHttpClient {
33
+ val builder = OkHttpClient.Builder()
34
+ .connectTimeout(30, TimeUnit.SECONDS)
35
+ .readTimeout(30, TimeUnit.SECONDS)
36
+ .writeTimeout(30, TimeUnit.SECONDS)
37
+
38
+ // Configure certificate pinning if config is available
39
+ if (config != null) {
40
+ val certificatePinner = securityManager.getCertificatePinner()
41
+ if (certificatePinner != null) {
42
+ builder.certificatePinner(certificatePinner)
43
+ }
44
+ }
45
+
46
+ return builder.build()
47
+ }
48
+
49
+ fun configure(config: JSObject) {
50
+ this.config = config
51
+
52
+ // Configure security manager
53
+ securityManager.configure(config)
54
+
55
+ // Recreate OkHttpClient with new configuration
56
+ okHttpClient = createOkHttpClient()
57
+
58
+ // Validate configuration
59
+ val serverUrl = config.getString("serverUrl")
60
+ if (serverUrl != null && !serverUrl.startsWith("https://")) {
61
+ val enforceHttps = config.getJSObject("security")?.getBool("enforceHttps") ?: true
62
+ if (enforceHttps) {
63
+ throw IllegalArgumentException("Server URL must use HTTPS")
64
+ }
65
+ }
66
+ }
67
+
68
+ fun setProgressListener(listener: (JSObject) -> Unit) {
69
+ progressListener = listener
70
+ }
71
+
72
+ fun setStateChangeListener(listener: (JSObject) -> Unit) {
73
+ stateChangeListener = listener
74
+ }
75
+
76
+ fun sync(call: PluginCall) {
77
+ scope.launch {
78
+ try {
79
+ val channel = call.getString("channel") ?: config?.getString("channel") ?: "production"
80
+ val serverUrl = config?.getString("serverUrl") ?: run {
81
+ call.reject("Server URL not configured")
82
+ return@launch
83
+ }
84
+
85
+ // Check for updates
86
+ val latestVersion = checkForUpdates(serverUrl, channel)
87
+
88
+ if (latestVersion != null) {
89
+ val result = JSObject()
90
+ result.put("status", "UPDATE_AVAILABLE")
91
+ result.put("version", latestVersion.getString("version"))
92
+ result.put("description", latestVersion.getString("description"))
93
+ call.resolve(result)
94
+ } else {
95
+ val result = JSObject()
96
+ result.put("status", "UP_TO_DATE")
97
+ result.put("version", getCurrentVersion())
98
+ call.resolve(result)
99
+ }
100
+ } catch (e: Exception) {
101
+ val error = JSObject()
102
+ error.put("code", "NETWORK_ERROR")
103
+ error.put("message", e.message)
104
+
105
+ val result = JSObject()
106
+ result.put("status", "ERROR")
107
+ result.put("error", error)
108
+ call.resolve(result)
109
+ }
110
+ }
111
+ }
112
+
113
+ fun download(call: PluginCall) {
114
+ scope.launch {
115
+ try {
116
+ val url = call.getString("url") ?: run {
117
+ call.reject("URL is required")
118
+ return@launch
119
+ }
120
+
121
+ val version = call.getString("version") ?: run {
122
+ call.reject("Version is required")
123
+ return@launch
124
+ }
125
+
126
+ val checksum = call.getString("checksum") ?: run {
127
+ call.reject("Checksum is required")
128
+ return@launch
129
+ }
130
+
131
+ // Validate URL
132
+ if (!url.startsWith("https://") && config?.getJSObject("security")?.getBool("enforceHttps") != false) {
133
+ call.reject("INSECURE_URL", "Download URL must use HTTPS")
134
+ return@launch
135
+ }
136
+
137
+ val bundleId = "bundle-${System.currentTimeMillis()}"
138
+ val downloadDir = File(context.filesDir, "updates/$bundleId")
139
+ downloadDir.mkdirs()
140
+
141
+ // Start download
142
+ val downloadedFile = downloadBundle(url, downloadDir, bundleId)
143
+
144
+ // Verify checksum
145
+ if (!verifyChecksum(downloadedFile, checksum)) {
146
+ downloadedFile.delete()
147
+ call.reject("CHECKSUM_ERROR", "Bundle checksum validation failed")
148
+ return@launch
149
+ }
150
+
151
+ // Verify signature if provided
152
+ val signature = call.getString("signature")
153
+ if (signature != null) {
154
+ val securityConfig = config?.getJSObject("security")
155
+ val publicKey = securityConfig?.getString("publicKey")
156
+ val enableSignatureValidation = securityConfig?.getBoolean("enableSignatureValidation", false) ?: false
157
+
158
+ if (enableSignatureValidation && publicKey != null) {
159
+ val fileBytes = downloadedFile.readBytes()
160
+ if (!securityManager.verifySignature(fileBytes, signature, publicKey)) {
161
+ downloadedFile.delete()
162
+ call.reject("SIGNATURE_ERROR", "Bundle signature validation failed")
163
+ return@launch
164
+ }
165
+ }
166
+ }
167
+
168
+ // Create bundle info
169
+ val bundleInfo = JSObject()
170
+ bundleInfo.put("bundleId", bundleId)
171
+ bundleInfo.put("version", version)
172
+ bundleInfo.put("path", downloadedFile.absolutePath)
173
+ bundleInfo.put("downloadTime", System.currentTimeMillis())
174
+ bundleInfo.put("size", downloadedFile.length())
175
+ bundleInfo.put("status", "READY")
176
+ bundleInfo.put("checksum", checksum)
177
+ bundleInfo.put("verified", true)
178
+
179
+ // Save bundle info
180
+ saveBundleInfo(bundleInfo)
181
+
182
+ // Notify state change
183
+ stateChangeListener?.invoke(JSObject().apply {
184
+ put("status", "READY")
185
+ put("bundleId", bundleId)
186
+ put("version", version)
187
+ })
188
+
189
+ call.resolve(bundleInfo)
190
+ } catch (e: Exception) {
191
+ call.reject("DOWNLOAD_ERROR", e.message)
192
+ }
193
+ }
194
+ }
195
+
196
+ fun set(call: PluginCall) {
197
+ val bundleId = call.getString("bundleId") ?: run {
198
+ call.reject("Bundle ID is required")
199
+ return
200
+ }
201
+
202
+ // Set active bundle
203
+ setActiveBundle(bundleId)
204
+ call.resolve()
205
+ }
206
+
207
+ fun reload(call: PluginCall) {
208
+ // In Android, we need to restart the activity or reload the WebView
209
+ activity.runOnUiThread {
210
+ activity.recreate()
211
+ }
212
+ call.resolve()
213
+ }
214
+
215
+ fun reset(call: PluginCall) {
216
+ // Reset to original bundle
217
+ clearAllBundles()
218
+ call.resolve()
219
+ }
220
+
221
+ fun current(call: PluginCall) {
222
+ val currentBundle = getCurrentBundleInfo()
223
+ call.resolve(currentBundle)
224
+ }
225
+
226
+ fun list(call: PluginCall) {
227
+ val bundles = getAllBundles()
228
+ val result = JSObject()
229
+ result.put("bundles", bundles)
230
+ call.resolve(result)
231
+ }
232
+
233
+ fun delete(call: PluginCall) {
234
+ val bundleId = call.getString("bundleId")
235
+ if (bundleId != null) {
236
+ deleteBundle(bundleId)
237
+ } else {
238
+ val keepVersions = call.getInt("keepVersions")
239
+ if (keepVersions != null) {
240
+ cleanupOldBundles(keepVersions)
241
+ }
242
+ }
243
+ call.resolve()
244
+ }
245
+
246
+ fun notifyAppReady(call: PluginCall) {
247
+ // Mark current bundle as verified
248
+ markBundleAsVerified()
249
+ call.resolve()
250
+ }
251
+
252
+ fun getLatest(call: PluginCall) {
253
+ scope.launch {
254
+ try {
255
+ val serverUrl = config?.getString("serverUrl") ?: run {
256
+ call.reject("Server URL not configured")
257
+ return@launch
258
+ }
259
+
260
+ val channel = config?.getString("channel") ?: "production"
261
+ val latestVersion = checkForUpdates(serverUrl, channel)
262
+
263
+ if (latestVersion != null) {
264
+ val result = JSObject()
265
+ result.put("available", true)
266
+ result.put("version", latestVersion.getString("version"))
267
+ result.put("url", latestVersion.getString("url"))
268
+ result.put("notes", latestVersion.getString("notes"))
269
+ call.resolve(result)
270
+ } else {
271
+ val result = JSObject()
272
+ result.put("available", false)
273
+ call.resolve(result)
274
+ }
275
+ } catch (e: Exception) {
276
+ call.reject("NETWORK_ERROR", e.message)
277
+ }
278
+ }
279
+ }
280
+
281
+ fun setChannel(call: PluginCall) {
282
+ val channel = call.getString("channel") ?: run {
283
+ call.reject("Channel is required")
284
+ return
285
+ }
286
+
287
+ config?.put("channel", channel)
288
+ call.resolve()
289
+ }
290
+
291
+ fun setUpdateUrl(call: PluginCall) {
292
+ val url = call.getString("url") ?: run {
293
+ call.reject("URL is required")
294
+ return
295
+ }
296
+
297
+ if (!url.startsWith("https://") && config?.getJSObject("security")?.getBool("enforceHttps") != false) {
298
+ call.reject("INSECURE_URL", "Update URL must use HTTPS")
299
+ return
300
+ }
301
+
302
+ config?.put("serverUrl", url)
303
+ call.resolve()
304
+ }
305
+
306
+ fun validateUpdate(call: PluginCall) {
307
+ scope.launch {
308
+ try {
309
+ val bundlePath = call.getString("bundlePath") ?: run {
310
+ call.reject("Bundle path is required")
311
+ return@launch
312
+ }
313
+
314
+ val checksum = call.getString("checksum") ?: run {
315
+ call.reject("Checksum is required")
316
+ return@launch
317
+ }
318
+
319
+ val file = File(bundlePath)
320
+ val checksumValid = verifyChecksum(file, checksum)
321
+
322
+ val result = JSObject()
323
+ result.put("isValid", checksumValid)
324
+
325
+ val details = JSObject()
326
+ details.put("checksumValid", checksumValid)
327
+ result.put("details", details)
328
+
329
+ call.resolve(result)
330
+ } catch (e: Exception) {
331
+ call.reject("VALIDATION_ERROR", e.message)
332
+ }
333
+ }
334
+ }
335
+
336
+ // Private helper methods
337
+
338
+ private suspend fun checkForUpdates(serverUrl: String, channel: String): JSObject? {
339
+ return withContext(Dispatchers.IO) {
340
+ try {
341
+ val request = Request.Builder()
342
+ .url("$serverUrl/check?channel=$channel")
343
+ .build()
344
+
345
+ val response = okHttpClient.newCall(request).execute()
346
+ if (response.isSuccessful) {
347
+ val body = response.body?.string()
348
+ // Parse JSON response
349
+ body?.let { JSObject(it) }
350
+ } else {
351
+ null
352
+ }
353
+ } catch (e: Exception) {
354
+ null
355
+ }
356
+ }
357
+ }
358
+
359
+ private suspend fun downloadBundle(url: String, downloadDir: File, bundleId: String): File {
360
+ return withContext(Dispatchers.IO) {
361
+ val request = Request.Builder()
362
+ .url(url)
363
+ .build()
364
+
365
+ val response = okHttpClient.newCall(request).execute()
366
+ if (!response.isSuccessful) {
367
+ throw Exception("Download failed: ${response.code}")
368
+ }
369
+
370
+ val file = File(downloadDir, "bundle.zip")
371
+ val totalBytes = response.body?.contentLength() ?: -1
372
+ var downloadedBytes = 0L
373
+
374
+ response.body?.byteStream()?.use { input ->
375
+ FileOutputStream(file).use { output ->
376
+ val buffer = ByteArray(8192)
377
+ var read: Int
378
+
379
+ while (input.read(buffer).also { read = it } != -1) {
380
+ output.write(buffer, 0, read)
381
+ downloadedBytes += read
382
+
383
+ // Report progress
384
+ if (totalBytes > 0) {
385
+ val percent = ((downloadedBytes * 100) / totalBytes).toInt()
386
+ progressListener?.invoke(JSObject().apply {
387
+ put("percent", percent)
388
+ put("bytesDownloaded", downloadedBytes)
389
+ put("totalBytes", totalBytes)
390
+ put("bundleId", bundleId)
391
+ })
392
+ }
393
+ }
394
+ }
395
+ }
396
+
397
+ file
398
+ }
399
+ }
400
+
401
+ private fun verifyChecksum(file: File, expectedChecksum: String): Boolean {
402
+ val digest = MessageDigest.getInstance("SHA-256")
403
+ file.inputStream().use { input ->
404
+ val buffer = ByteArray(8192)
405
+ var read: Int
406
+ while (input.read(buffer).also { read = it } != -1) {
407
+ digest.update(buffer, 0, read)
408
+ }
409
+ }
410
+
411
+ val calculatedChecksum = digest.digest().joinToString("") { "%02x".format(it) }
412
+ return calculatedChecksum == expectedChecksum
413
+ }
414
+
415
+ private fun getCurrentVersion(): String {
416
+ // Get current bundle version or app version
417
+ return "1.0.0"
418
+ }
419
+
420
+ private fun saveBundleInfo(bundleInfo: JSObject) {
421
+ // Save bundle info to SharedPreferences or database
422
+ val prefs = context.getSharedPreferences("native_update_bundles", Context.MODE_PRIVATE)
423
+ val bundleId = bundleInfo.getString("bundleId")
424
+ prefs.edit().putString(bundleId, bundleInfo.toString()).apply()
425
+ }
426
+
427
+ private fun setActiveBundle(bundleId: String) {
428
+ val prefs = context.getSharedPreferences("native_update", Context.MODE_PRIVATE)
429
+ prefs.edit().putString("active_bundle", bundleId).apply()
430
+ }
431
+
432
+ private fun clearAllBundles() {
433
+ val updateDir = File(context.filesDir, "updates")
434
+ updateDir.deleteRecursively()
435
+
436
+ val prefs = context.getSharedPreferences("native_update_bundles", Context.MODE_PRIVATE)
437
+ prefs.edit().clear().apply()
438
+
439
+ val activePrefs = context.getSharedPreferences("native_update", Context.MODE_PRIVATE)
440
+ activePrefs.edit().remove("active_bundle").apply()
441
+ }
442
+
443
+ private fun getCurrentBundleInfo(): JSObject {
444
+ val prefs = context.getSharedPreferences("native_update", Context.MODE_PRIVATE)
445
+ val activeBundleId = prefs.getString("active_bundle", null)
446
+
447
+ if (activeBundleId != null) {
448
+ val bundlePrefs = context.getSharedPreferences("native_update_bundles", Context.MODE_PRIVATE)
449
+ val bundleData = bundlePrefs.getString(activeBundleId, null)
450
+ if (bundleData != null) {
451
+ return JSObject(bundleData)
452
+ }
453
+ }
454
+
455
+ // Return default bundle
456
+ return JSObject().apply {
457
+ put("bundleId", "default")
458
+ put("version", "1.0.0")
459
+ put("path", "/")
460
+ put("downloadTime", System.currentTimeMillis())
461
+ put("size", 0)
462
+ put("status", "ACTIVE")
463
+ put("checksum", "")
464
+ put("verified", true)
465
+ }
466
+ }
467
+
468
+ private fun getAllBundles(): List<JSObject> {
469
+ val prefs = context.getSharedPreferences("native_update_bundles", Context.MODE_PRIVATE)
470
+ return prefs.all.mapNotNull { entry ->
471
+ try {
472
+ JSObject(entry.value as String)
473
+ } catch (e: Exception) {
474
+ null
475
+ }
476
+ }
477
+ }
478
+
479
+ private fun deleteBundle(bundleId: String) {
480
+ // Delete bundle files
481
+ val bundleDir = File(context.filesDir, "updates/$bundleId")
482
+ bundleDir.deleteRecursively()
483
+
484
+ // Remove from preferences
485
+ val prefs = context.getSharedPreferences("native_update_bundles", Context.MODE_PRIVATE)
486
+ prefs.edit().remove(bundleId).apply()
487
+ }
488
+
489
+ private fun cleanupOldBundles(keepVersions: Int) {
490
+ val bundles = getAllBundles().sortedByDescending { it.getLong("downloadTime") }
491
+ if (bundles.size > keepVersions) {
492
+ bundles.drop(keepVersions).forEach { bundle ->
493
+ bundle.getString("bundleId")?.let { deleteBundle(it) }
494
+ }
495
+ }
496
+ }
497
+
498
+ private fun markBundleAsVerified() {
499
+ val prefs = context.getSharedPreferences("native_update", Context.MODE_PRIVATE)
500
+ prefs.edit().putBoolean("current_bundle_verified", true).apply()
501
+ }
502
+
503
+ // Async method for background update checks
504
+ suspend fun getLatestVersionAsync(): LatestVersion? {
505
+ return try {
506
+ val serverUrl = config?.getString("serverUrl") ?: return null
507
+ val channel = config?.getString("channel") ?: "production"
508
+
509
+ val latestVersion = checkForUpdates(serverUrl, channel)
510
+
511
+ if (latestVersion != null) {
512
+ LatestVersion(
513
+ available = true,
514
+ version = latestVersion.getString("version")
515
+ )
516
+ } else {
517
+ LatestVersion(
518
+ available = false,
519
+ version = null
520
+ )
521
+ }
522
+ } catch (e: Exception) {
523
+ null
524
+ }
525
+ }
526
+ }
@@ -0,0 +1,99 @@
1
+ package com.aoneahsan.nativeupdate
2
+
3
+ import android.content.BroadcastReceiver
4
+ import android.content.Context
5
+ import android.content.Intent
6
+ import android.util.Log
7
+ import androidx.work.ExistingWorkPolicy
8
+ import androidx.work.OneTimeWorkRequestBuilder
9
+ import androidx.work.WorkManager
10
+ import androidx.work.workDataOf
11
+
12
+ /**
13
+ * Handles notification actions for background updates
14
+ */
15
+ class NotificationActionReceiver : BroadcastReceiver() {
16
+
17
+ companion object {
18
+ const val ACTION_INSTALL_NOW = "com.aoneahsan.nativeupdate.ACTION_INSTALL_NOW"
19
+ const val ACTION_INSTALL_LATER = "com.aoneahsan.nativeupdate.ACTION_INSTALL_LATER"
20
+ const val ACTION_DISMISS = "com.aoneahsan.nativeupdate.ACTION_DISMISS"
21
+
22
+ const val EXTRA_UPDATE_TYPE = "update_type"
23
+ const val EXTRA_BUNDLE_ID = "bundle_id"
24
+ const val EXTRA_VERSION = "version"
25
+
26
+ private const val TAG = "NotificationActionReceiver"
27
+ }
28
+
29
+ override fun onReceive(context: Context, intent: Intent) {
30
+ val action = intent.action ?: return
31
+
32
+ Log.d(TAG, "Received action: $action")
33
+
34
+ when (action) {
35
+ ACTION_INSTALL_NOW -> handleInstallNow(context, intent)
36
+ ACTION_INSTALL_LATER -> handleInstallLater(context, intent)
37
+ ACTION_DISMISS -> handleDismiss(context, intent)
38
+ }
39
+
40
+ // Cancel the notification
41
+ val notificationManager = BackgroundNotificationManager(context)
42
+ notificationManager.cancelUpdateNotification()
43
+ }
44
+
45
+ private fun handleInstallNow(context: Context, intent: Intent) {
46
+ val updateType = intent.getStringExtra(EXTRA_UPDATE_TYPE) ?: return
47
+
48
+ when (updateType) {
49
+ "live_update" -> {
50
+ val bundleId = intent.getStringExtra(EXTRA_BUNDLE_ID) ?: return
51
+ // Trigger immediate installation through WorkManager
52
+ val workRequest = OneTimeWorkRequestBuilder<BackgroundUpdateWorker>()
53
+ .setInputData(workDataOf(
54
+ "action" to "install_bundle",
55
+ "bundle_id" to bundleId
56
+ ))
57
+ .build()
58
+
59
+ WorkManager.getInstance(context).enqueueUniqueWork(
60
+ "install_update_$bundleId",
61
+ ExistingWorkPolicy.REPLACE,
62
+ workRequest
63
+ )
64
+
65
+ Log.d(TAG, "Scheduled immediate installation for bundle: $bundleId")
66
+ }
67
+ "app_update" -> {
68
+ // Open Play Store for app update
69
+ try {
70
+ val packageName = context.packageName
71
+ val playStoreIntent = Intent(Intent.ACTION_VIEW).apply {
72
+ data = android.net.Uri.parse("market://details?id=$packageName")
73
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
74
+ }
75
+ context.startActivity(playStoreIntent)
76
+ } catch (e: Exception) {
77
+ Log.e(TAG, "Failed to open Play Store", e)
78
+ }
79
+ }
80
+ }
81
+ }
82
+
83
+ private fun handleInstallLater(context: Context, intent: Intent) {
84
+ // Store preference to install later
85
+ val prefs = context.getSharedPreferences("capacitor_native_update", Context.MODE_PRIVATE)
86
+ prefs.edit().apply {
87
+ putBoolean("pending_update", true)
88
+ putLong("deferred_until", System.currentTimeMillis() + 24 * 60 * 60 * 1000) // 24 hours
89
+ apply()
90
+ }
91
+
92
+ Log.d(TAG, "Update deferred for 24 hours")
93
+ }
94
+
95
+ private fun handleDismiss(context: Context, intent: Intent) {
96
+ // Just dismiss the notification, already handled in onReceive
97
+ Log.d(TAG, "Update notification dismissed")
98
+ }
99
+ }