@swiftpatch/react-native 2.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 (182) hide show
  1. package/README.md +430 -0
  2. package/android/build.gradle +105 -0
  3. package/android/src/main/AndroidManifest.xml +6 -0
  4. package/android/src/main/java/com/swiftpatch/BundleManager.kt +107 -0
  5. package/android/src/main/java/com/swiftpatch/CrashDetector.kt +79 -0
  6. package/android/src/main/java/com/swiftpatch/CryptoVerifier.kt +69 -0
  7. package/android/src/main/java/com/swiftpatch/DownloadManager.kt +120 -0
  8. package/android/src/main/java/com/swiftpatch/EventQueue.kt +86 -0
  9. package/android/src/main/java/com/swiftpatch/FileUtils.kt +60 -0
  10. package/android/src/main/java/com/swiftpatch/PatchApplier.kt +60 -0
  11. package/android/src/main/java/com/swiftpatch/SignalCrashHandler.kt +84 -0
  12. package/android/src/main/java/com/swiftpatch/SlotManager.kt +299 -0
  13. package/android/src/main/java/com/swiftpatch/SwiftPatchModule.kt +630 -0
  14. package/android/src/main/java/com/swiftpatch/SwiftPatchPackage.kt +21 -0
  15. package/android/src/main/jni/CMakeLists.txt +12 -0
  16. package/android/src/main/jni/bspatch.c +188 -0
  17. package/android/src/main/jni/bspatch.h +57 -0
  18. package/android/src/main/jni/bspatch_jni.c +28 -0
  19. package/ios/Libraries/bspatch/bspatch.c +188 -0
  20. package/ios/Libraries/bspatch/bspatch.h +50 -0
  21. package/ios/Libraries/bspatch/module.modulemap +4 -0
  22. package/ios/SwiftPatch/BundleManager.swift +113 -0
  23. package/ios/SwiftPatch/CrashDetector.swift +71 -0
  24. package/ios/SwiftPatch/CryptoVerifier.swift +70 -0
  25. package/ios/SwiftPatch/DownloadManager.swift +125 -0
  26. package/ios/SwiftPatch/EventQueue.swift +116 -0
  27. package/ios/SwiftPatch/FileUtils.swift +38 -0
  28. package/ios/SwiftPatch/PatchApplier.swift +41 -0
  29. package/ios/SwiftPatch/SignalCrashHandler.swift +129 -0
  30. package/ios/SwiftPatch/SlotManager.swift +360 -0
  31. package/ios/SwiftPatch/SwiftPatchModule.m +56 -0
  32. package/ios/SwiftPatch/SwiftPatchModule.swift +621 -0
  33. package/lib/commonjs/SwiftPatchCore.js +140 -0
  34. package/lib/commonjs/SwiftPatchCore.js.map +1 -0
  35. package/lib/commonjs/SwiftPatchProvider.js +617 -0
  36. package/lib/commonjs/SwiftPatchProvider.js.map +1 -0
  37. package/lib/commonjs/constants.js +50 -0
  38. package/lib/commonjs/constants.js.map +1 -0
  39. package/lib/commonjs/core/Downloader.js +63 -0
  40. package/lib/commonjs/core/Downloader.js.map +1 -0
  41. package/lib/commonjs/core/Installer.js +46 -0
  42. package/lib/commonjs/core/Installer.js.map +1 -0
  43. package/lib/commonjs/core/Rollback.js +36 -0
  44. package/lib/commonjs/core/Rollback.js.map +1 -0
  45. package/lib/commonjs/core/UpdateChecker.js +57 -0
  46. package/lib/commonjs/core/UpdateChecker.js.map +1 -0
  47. package/lib/commonjs/core/Verifier.js +82 -0
  48. package/lib/commonjs/core/Verifier.js.map +1 -0
  49. package/lib/commonjs/core/index.js +41 -0
  50. package/lib/commonjs/core/index.js.map +1 -0
  51. package/lib/commonjs/index.js +154 -0
  52. package/lib/commonjs/index.js.map +1 -0
  53. package/lib/commonjs/modal/SwiftPatchModal.js +667 -0
  54. package/lib/commonjs/modal/SwiftPatchModal.js.map +1 -0
  55. package/lib/commonjs/modal/useSwiftPatchModal.js +26 -0
  56. package/lib/commonjs/modal/useSwiftPatchModal.js.map +1 -0
  57. package/lib/commonjs/native/NativeSwiftPatch.js +85 -0
  58. package/lib/commonjs/native/NativeSwiftPatch.js.map +1 -0
  59. package/lib/commonjs/native/NativeSwiftPatchSpec.js +15 -0
  60. package/lib/commonjs/native/NativeSwiftPatchSpec.js.map +1 -0
  61. package/lib/commonjs/package.json +1 -0
  62. package/lib/commonjs/types.js +126 -0
  63. package/lib/commonjs/types.js.map +1 -0
  64. package/lib/commonjs/useSwiftPatch.js +31 -0
  65. package/lib/commonjs/useSwiftPatch.js.map +1 -0
  66. package/lib/commonjs/utils/api.js +206 -0
  67. package/lib/commonjs/utils/api.js.map +1 -0
  68. package/lib/commonjs/utils/device.js +23 -0
  69. package/lib/commonjs/utils/device.js.map +1 -0
  70. package/lib/commonjs/utils/logger.js +30 -0
  71. package/lib/commonjs/utils/logger.js.map +1 -0
  72. package/lib/commonjs/utils/storage.js +31 -0
  73. package/lib/commonjs/utils/storage.js.map +1 -0
  74. package/lib/commonjs/withSwiftPatch.js +42 -0
  75. package/lib/commonjs/withSwiftPatch.js.map +1 -0
  76. package/lib/module/SwiftPatchCore.js +135 -0
  77. package/lib/module/SwiftPatchCore.js.map +1 -0
  78. package/lib/module/SwiftPatchProvider.js +611 -0
  79. package/lib/module/SwiftPatchProvider.js.map +1 -0
  80. package/lib/module/constants.js +46 -0
  81. package/lib/module/constants.js.map +1 -0
  82. package/lib/module/core/Downloader.js +57 -0
  83. package/lib/module/core/Downloader.js.map +1 -0
  84. package/lib/module/core/Installer.js +41 -0
  85. package/lib/module/core/Installer.js.map +1 -0
  86. package/lib/module/core/Rollback.js +31 -0
  87. package/lib/module/core/Rollback.js.map +1 -0
  88. package/lib/module/core/UpdateChecker.js +51 -0
  89. package/lib/module/core/UpdateChecker.js.map +1 -0
  90. package/lib/module/core/Verifier.js +76 -0
  91. package/lib/module/core/Verifier.js.map +1 -0
  92. package/lib/module/core/index.js +8 -0
  93. package/lib/module/core/index.js.map +1 -0
  94. package/lib/module/index.js +34 -0
  95. package/lib/module/index.js.map +1 -0
  96. package/lib/module/modal/SwiftPatchModal.js +661 -0
  97. package/lib/module/modal/SwiftPatchModal.js.map +1 -0
  98. package/lib/module/modal/useSwiftPatchModal.js +22 -0
  99. package/lib/module/modal/useSwiftPatchModal.js.map +1 -0
  100. package/lib/module/native/NativeSwiftPatch.js +78 -0
  101. package/lib/module/native/NativeSwiftPatch.js.map +1 -0
  102. package/lib/module/native/NativeSwiftPatchSpec.js +12 -0
  103. package/lib/module/native/NativeSwiftPatchSpec.js.map +1 -0
  104. package/lib/module/types.js +139 -0
  105. package/lib/module/types.js.map +1 -0
  106. package/lib/module/useSwiftPatch.js +26 -0
  107. package/lib/module/useSwiftPatch.js.map +1 -0
  108. package/lib/module/utils/api.js +197 -0
  109. package/lib/module/utils/api.js.map +1 -0
  110. package/lib/module/utils/device.js +18 -0
  111. package/lib/module/utils/device.js.map +1 -0
  112. package/lib/module/utils/logger.js +26 -0
  113. package/lib/module/utils/logger.js.map +1 -0
  114. package/lib/module/utils/storage.js +24 -0
  115. package/lib/module/utils/storage.js.map +1 -0
  116. package/lib/module/withSwiftPatch.js +37 -0
  117. package/lib/module/withSwiftPatch.js.map +1 -0
  118. package/lib/typescript/SwiftPatchCore.d.ts +64 -0
  119. package/lib/typescript/SwiftPatchCore.d.ts.map +1 -0
  120. package/lib/typescript/SwiftPatchProvider.d.ts +22 -0
  121. package/lib/typescript/SwiftPatchProvider.d.ts.map +1 -0
  122. package/lib/typescript/constants.d.ts +33 -0
  123. package/lib/typescript/constants.d.ts.map +1 -0
  124. package/lib/typescript/core/Downloader.d.ts +34 -0
  125. package/lib/typescript/core/Downloader.d.ts.map +1 -0
  126. package/lib/typescript/core/Installer.d.ts +25 -0
  127. package/lib/typescript/core/Installer.d.ts.map +1 -0
  128. package/lib/typescript/core/Rollback.d.ts +18 -0
  129. package/lib/typescript/core/Rollback.d.ts.map +1 -0
  130. package/lib/typescript/core/UpdateChecker.d.ts +27 -0
  131. package/lib/typescript/core/UpdateChecker.d.ts.map +1 -0
  132. package/lib/typescript/core/Verifier.d.ts +31 -0
  133. package/lib/typescript/core/Verifier.d.ts.map +1 -0
  134. package/lib/typescript/core/index.d.ts +8 -0
  135. package/lib/typescript/core/index.d.ts.map +1 -0
  136. package/lib/typescript/index.d.ts +13 -0
  137. package/lib/typescript/index.d.ts.map +1 -0
  138. package/lib/typescript/modal/SwiftPatchModal.d.ts +11 -0
  139. package/lib/typescript/modal/SwiftPatchModal.d.ts.map +1 -0
  140. package/lib/typescript/modal/useSwiftPatchModal.d.ts +7 -0
  141. package/lib/typescript/modal/useSwiftPatchModal.d.ts.map +1 -0
  142. package/lib/typescript/native/NativeSwiftPatch.d.ts +61 -0
  143. package/lib/typescript/native/NativeSwiftPatch.d.ts.map +1 -0
  144. package/lib/typescript/native/NativeSwiftPatchSpec.d.ts +67 -0
  145. package/lib/typescript/native/NativeSwiftPatchSpec.d.ts.map +1 -0
  146. package/lib/typescript/types.d.ts +266 -0
  147. package/lib/typescript/types.d.ts.map +1 -0
  148. package/lib/typescript/useSwiftPatch.d.ts +12 -0
  149. package/lib/typescript/useSwiftPatch.d.ts.map +1 -0
  150. package/lib/typescript/utils/api.d.ts +87 -0
  151. package/lib/typescript/utils/api.d.ts.map +1 -0
  152. package/lib/typescript/utils/device.d.ts +9 -0
  153. package/lib/typescript/utils/device.d.ts.map +1 -0
  154. package/lib/typescript/utils/logger.d.ts +8 -0
  155. package/lib/typescript/utils/logger.d.ts.map +1 -0
  156. package/lib/typescript/utils/storage.d.ts +14 -0
  157. package/lib/typescript/utils/storage.d.ts.map +1 -0
  158. package/lib/typescript/withSwiftPatch.d.ts +12 -0
  159. package/lib/typescript/withSwiftPatch.d.ts.map +1 -0
  160. package/package.json +99 -0
  161. package/react-native-swiftpatch.podspec +50 -0
  162. package/src/SwiftPatchCore.ts +148 -0
  163. package/src/SwiftPatchProvider.tsx +514 -0
  164. package/src/constants.ts +49 -0
  165. package/src/core/Downloader.ts +74 -0
  166. package/src/core/Installer.ts +38 -0
  167. package/src/core/Rollback.ts +28 -0
  168. package/src/core/UpdateChecker.ts +70 -0
  169. package/src/core/Verifier.ts +92 -0
  170. package/src/core/index.ts +11 -0
  171. package/src/index.ts +64 -0
  172. package/src/modal/SwiftPatchModal.tsx +657 -0
  173. package/src/modal/useSwiftPatchModal.ts +24 -0
  174. package/src/native/NativeSwiftPatch.ts +205 -0
  175. package/src/native/NativeSwiftPatchSpec.ts +139 -0
  176. package/src/types.ts +336 -0
  177. package/src/useSwiftPatch.ts +29 -0
  178. package/src/utils/api.ts +244 -0
  179. package/src/utils/device.ts +15 -0
  180. package/src/utils/logger.ts +29 -0
  181. package/src/utils/storage.ts +23 -0
  182. package/src/withSwiftPatch.tsx +41 -0
@@ -0,0 +1,630 @@
1
+ package com.swiftpatch
2
+
3
+ import android.content.Context
4
+ import android.content.SharedPreferences
5
+ import com.facebook.react.bridge.*
6
+ import com.facebook.react.modules.core.DeviceEventManagerModule
7
+ import kotlinx.coroutines.*
8
+ import org.json.JSONArray
9
+ import java.io.File
10
+ import java.util.*
11
+
12
+ class SwiftPatchModule(reactContext: ReactApplicationContext) :
13
+ ReactContextBaseJavaModule(reactContext) {
14
+
15
+ companion object {
16
+ private const val TAG = "SwiftPatch"
17
+ private const val PREFS_NAME = "swiftpatch_prefs"
18
+
19
+ @JvmStatic
20
+ fun getJSBundleFile(context: Context): String? {
21
+ val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
22
+ val slotManager = SlotManager(context, prefs)
23
+
24
+ // Check for version change
25
+ if (slotManager.checkVersionChange()) {
26
+ return null
27
+ }
28
+
29
+ // Apply temp hash on launch
30
+ slotManager.applyTempHashOnLaunch()
31
+
32
+ // Get bundle from slot manager
33
+ return slotManager.getBundleFile()
34
+ }
35
+ }
36
+
37
+ private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
38
+ private val prefs: SharedPreferences =
39
+ reactContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
40
+ private val bundleManager = BundleManager(reactContext)
41
+ private val downloadManager = DownloadManager()
42
+ private val patchApplier = PatchApplier()
43
+ private val cryptoVerifier = CryptoVerifier()
44
+ private val slotManager = SlotManager(reactContext, prefs)
45
+ private val eventQueue = EventQueue(prefs)
46
+ private val crashDetector = CrashDetector(prefs)
47
+
48
+ private var config: SwiftPatchConfig? = null
49
+ private var isDebugMode = false
50
+
51
+ override fun getName(): String = "SwiftPatch"
52
+
53
+ override fun initialize() {
54
+ super.initialize()
55
+
56
+ // Install crash handler
57
+ val signalHandler = SignalCrashHandler.init(reactApplicationContext)
58
+ signalHandler.installHandler()
59
+
60
+ // Check for crash marker from previous run
61
+ signalHandler.checkForCrashMarker()?.let { crashInfo ->
62
+ if (signalHandler.shouldAutoRollback(crashInfo)) {
63
+ val result = slotManager.rollbackProd(isAutoRollback = true)
64
+ eventQueue.pushEvent(
65
+ type = SwiftPatchEventType.CRASH_DETECTED,
66
+ metadata = mapOf(
67
+ "exceptionType" to crashInfo.exceptionType,
68
+ "autoRollback" to true,
69
+ "rollbackResult" to result,
70
+ )
71
+ )
72
+ sendEvent("SwiftPatch:rollbackOccurred", Arguments.createMap().apply {
73
+ putString("reason", "Crash detected: ${crashInfo.exceptionType}, auto-rollback to $result")
74
+ })
75
+ } else {
76
+ eventQueue.pushEvent(
77
+ type = SwiftPatchEventType.CRASH_DETECTED,
78
+ metadata = mapOf(
79
+ "exceptionType" to crashInfo.exceptionType,
80
+ "autoRollback" to false,
81
+ )
82
+ )
83
+ }
84
+ }
85
+
86
+ // Also check timer-based crash detection (legacy/fallback)
87
+ crashDetector.checkForPendingRollback { reason ->
88
+ sendEvent("SwiftPatch:rollbackOccurred", Arguments.createMap().apply {
89
+ putString("reason", reason)
90
+ })
91
+ }
92
+
93
+ // Check for version change
94
+ if (slotManager.checkVersionChange()) {
95
+ eventQueue.pushEvent(type = SwiftPatchEventType.VERSION_CHANGED)
96
+ sendEvent("SwiftPatch:versionChanged", Arguments.createMap().apply {
97
+ putString("reason", "App binary version changed, reset to default bundle")
98
+ })
99
+ }
100
+ }
101
+
102
+ override fun onCatalystInstanceDestroy() {
103
+ super.onCatalystInstanceDestroy()
104
+ scope.cancel()
105
+ }
106
+
107
+ // ═══════════════════════════════════════════════════════════════
108
+ // INITIALIZATION
109
+ // ═══════════════════════════════════════════════════════════════
110
+
111
+ @ReactMethod
112
+ fun initialize(configMap: ReadableMap, promise: Promise) {
113
+ try {
114
+ config = SwiftPatchConfig(
115
+ deploymentKey = configMap.getString("deploymentKey")
116
+ ?: throw IllegalArgumentException("deploymentKey required"),
117
+ serverUrl = configMap.getString("serverUrl")
118
+ ?: "https://swiftpatch.hyperbrainlabs.com/api/v1",
119
+ publicKey = if (configMap.hasKey("publicKey") && !configMap.isNull("publicKey"))
120
+ configMap.getString("publicKey") else null
121
+ )
122
+
123
+ bundleManager.ensureDirectoriesExist()
124
+ slotManager.ensureSlotDirectories()
125
+
126
+ log("Initialized with deployment key: ${config?.deploymentKey?.take(8)}...")
127
+ promise.resolve(null)
128
+ } catch (e: Exception) {
129
+ promise.reject("INIT_ERROR", e.message, e)
130
+ }
131
+ }
132
+
133
+ @ReactMethod
134
+ fun setDebugMode(enabled: Boolean) {
135
+ isDebugMode = enabled
136
+ }
137
+
138
+ // ═══════════════════════════════════════════════════════════════
139
+ // MOUNT STATE
140
+ // ═══════════════════════════════════════════════════════════════
141
+
142
+ @ReactMethod
143
+ fun markMounted(promise: Promise) {
144
+ SignalCrashHandler.getInstance()?.markMounted()
145
+ promise.resolve(null)
146
+ }
147
+
148
+ @ReactMethod
149
+ fun markUnmounted(promise: Promise) {
150
+ SignalCrashHandler.getInstance()?.markUnmounted()
151
+ promise.resolve(null)
152
+ }
153
+
154
+ // ═══════════════════════════════════════════════════════════════
155
+ // BUNDLE INFORMATION
156
+ // ═══════════════════════════════════════════════════════════════
157
+
158
+ @ReactMethod
159
+ fun getCurrentBundle(promise: Promise) {
160
+ try {
161
+ val env = slotManager.currentEnvironment
162
+
163
+ if (env == EnvironmentMode.STAGING) {
164
+ val hash = slotManager.stageNewHash
165
+ if (hash != null) {
166
+ promise.resolve(Arguments.createMap().apply {
167
+ putString("hash", hash)
168
+ putString("version", "staging")
169
+ putString("installedAt", "")
170
+ putBoolean("isOriginal", false)
171
+ putString("slot", "NEW_SLOT")
172
+ putString("environment", "STAGE")
173
+ })
174
+ } else {
175
+ promise.resolve(Arguments.createMap().apply {
176
+ putString("hash", "original")
177
+ putString("version", "0.0.0")
178
+ putString("installedAt", "")
179
+ putBoolean("isOriginal", true)
180
+ putString("slot", "DEFAULT_SLOT")
181
+ putString("environment", "STAGE")
182
+ })
183
+ }
184
+ return
185
+ }
186
+
187
+ val slot = slotManager.currentProdSlot
188
+ val hash = when (slot) {
189
+ SlotState.NEW_SLOT -> slotManager.prodNewHash
190
+ SlotState.STABLE_SLOT -> slotManager.prodStableHash
191
+ SlotState.DEFAULT_SLOT -> null
192
+ }
193
+
194
+ if (hash != null) {
195
+ promise.resolve(Arguments.createMap().apply {
196
+ putString("hash", hash)
197
+ putString("version", prefs.getString("current_bundle_version", "unknown"))
198
+ putString("installedAt", prefs.getString("current_bundle_installed_at", ""))
199
+ putBoolean("isOriginal", false)
200
+ putString("slot", slot.value)
201
+ putString("environment", "PROD")
202
+ })
203
+ } else {
204
+ promise.resolve(Arguments.createMap().apply {
205
+ putString("hash", "original")
206
+ putString("version", "0.0.0")
207
+ putString("installedAt", "")
208
+ putBoolean("isOriginal", true)
209
+ putString("slot", "DEFAULT_SLOT")
210
+ putString("environment", "PROD")
211
+ })
212
+ }
213
+ } catch (e: Exception) {
214
+ promise.reject("GET_BUNDLE_ERROR", e.message, e)
215
+ }
216
+ }
217
+
218
+ @ReactMethod
219
+ fun getSlotMetadata(promise: Promise) {
220
+ try {
221
+ val metadata = slotManager.getMetadata()
222
+ promise.resolve(Arguments.makeNativeMap(metadata as Map<String, Any?>))
223
+ } catch (e: Exception) {
224
+ promise.reject("META_ERROR", e.message, e)
225
+ }
226
+ }
227
+
228
+ @ReactMethod
229
+ fun getDeviceId(promise: Promise) {
230
+ try {
231
+ var deviceId = prefs.getString("device_id", null)
232
+ if (deviceId == null) {
233
+ deviceId = UUID.randomUUID().toString()
234
+ prefs.edit().putString("device_id", deviceId).apply()
235
+ }
236
+ promise.resolve(deviceId)
237
+ } catch (e: Exception) {
238
+ promise.reject("DEVICE_ID_ERROR", e.message, e)
239
+ }
240
+ }
241
+
242
+ @ReactMethod
243
+ fun getAppVersion(promise: Promise) {
244
+ try {
245
+ val packageInfo = reactApplicationContext.packageManager
246
+ .getPackageInfo(reactApplicationContext.packageName, 0)
247
+ promise.resolve(packageInfo.versionName ?: "0.0.0")
248
+ } catch (e: Exception) {
249
+ promise.reject("APP_VERSION_ERROR", e.message, e)
250
+ }
251
+ }
252
+
253
+ // ═══════════════════════════════════════════════════════════════
254
+ // ENVIRONMENT SWITCHING
255
+ // ═══════════════════════════════════════════════════════════════
256
+
257
+ @ReactMethod
258
+ fun switchEnvironment(environment: String, promise: Promise) {
259
+ try {
260
+ val env = EnvironmentMode.from(environment)
261
+ slotManager.currentEnvironment = env
262
+ log("Switched environment to: $environment")
263
+ promise.resolve(Arguments.makeNativeMap(slotManager.getMetadata() as Map<String, Any?>))
264
+ } catch (e: Exception) {
265
+ promise.reject("ENV_ERROR", e.message, e)
266
+ }
267
+ }
268
+
269
+ // ═══════════════════════════════════════════════════════════════
270
+ // DOWNLOAD & INSTALL
271
+ // ═══════════════════════════════════════════════════════════════
272
+
273
+ @ReactMethod
274
+ fun downloadUpdate(
275
+ url: String,
276
+ expectedHash: String,
277
+ isPatch: Boolean,
278
+ signature: String?,
279
+ promise: Promise
280
+ ) {
281
+ scope.launch {
282
+ try {
283
+ log("Downloading update from: $url (isPatch: $isPatch)")
284
+ eventQueue.pushEvent(SwiftPatchEventType.DOWNLOAD_PROD_STARTED, expectedHash)
285
+
286
+ val downloadFile = bundleManager.getDownloadFile(expectedHash)
287
+
288
+ // Download with progress (supports resume via Range header)
289
+ downloadManager.download(url, downloadFile) { progress ->
290
+ sendEvent("SwiftPatch:downloadProgress", Arguments.createMap().apply {
291
+ putDouble("downloadedBytes", progress.downloadedBytes.toDouble())
292
+ putDouble("totalBytes", progress.totalBytes.toDouble())
293
+ putInt("percentage", progress.percentage)
294
+ })
295
+ }
296
+
297
+ log("Download complete, processing...")
298
+
299
+ // Decompress if needed
300
+ val decompressedFile = if (url.endsWith(".br")) {
301
+ val decompressed = bundleManager.getDecompressedFile(expectedHash)
302
+ FileUtils.decompressBrotli(downloadFile, decompressed)
303
+ downloadFile.delete()
304
+ decompressed
305
+ } else {
306
+ downloadFile
307
+ }
308
+
309
+ // Apply patch or use as full bundle
310
+ val finalBundle = if (isPatch) {
311
+ log("Applying patch...")
312
+ val currentHash = when (slotManager.currentProdSlot) {
313
+ SlotState.NEW_SLOT -> slotManager.prodNewHash
314
+ SlotState.STABLE_SLOT -> slotManager.prodStableHash
315
+ SlotState.DEFAULT_SLOT -> null
316
+ }
317
+ val currentBundleFile = if (currentHash != null) {
318
+ bundleManager.getBundleFile(currentHash)
319
+ } else {
320
+ bundleManager.extractOriginalBundle(reactApplicationContext)
321
+ }
322
+
323
+ val patchedFile = bundleManager.getBundleFile(expectedHash)
324
+ patchApplier.applyPatch(currentBundleFile, decompressedFile, patchedFile)
325
+ decompressedFile.delete()
326
+ patchedFile
327
+ } else {
328
+ val bundleFile = bundleManager.getBundleFile(expectedHash)
329
+ if (decompressedFile.absolutePath != bundleFile.absolutePath) {
330
+ decompressedFile.renameTo(bundleFile)
331
+ }
332
+ bundleFile
333
+ }
334
+
335
+ // Verify hash
336
+ log("Verifying hash...")
337
+ val actualHash = cryptoVerifier.sha256(finalBundle)
338
+ if (actualHash != expectedHash) {
339
+ finalBundle.delete()
340
+ eventQueue.pushEvent(SwiftPatchEventType.DOWNLOAD_PROD_FAILED, expectedHash, "Hash mismatch")
341
+ throw SecurityException("Hash mismatch: expected $expectedHash, got $actualHash")
342
+ }
343
+
344
+ // Verify signature
345
+ if (signature != null && config?.publicKey != null) {
346
+ log("Verifying signature...")
347
+ if (!cryptoVerifier.verifySignature(actualHash, signature, config!!.publicKey!!)) {
348
+ finalBundle.delete()
349
+ eventQueue.pushEvent(SwiftPatchEventType.DOWNLOAD_PROD_FAILED, expectedHash, "Signature invalid")
350
+ throw SecurityException("Signature verification failed")
351
+ }
352
+ }
353
+
354
+ // Set as temp hash (deferred apply)
355
+ slotManager.prodTempHash = expectedHash
356
+
357
+ eventQueue.pushEvent(SwiftPatchEventType.DOWNLOAD_PROD_COMPLETED, expectedHash)
358
+ log("Download and verification complete, tempHash set: $expectedHash")
359
+ withContext(Dispatchers.Main) { promise.resolve(null) }
360
+
361
+ } catch (e: Exception) {
362
+ log("Download failed: ${e.message}")
363
+ eventQueue.pushEvent(SwiftPatchEventType.DOWNLOAD_PROD_FAILED, expectedHash, e.message)
364
+ withContext(Dispatchers.Main) { promise.reject("DOWNLOAD_ERROR", e.message, e) }
365
+ }
366
+ }
367
+ }
368
+
369
+ @ReactMethod
370
+ fun downloadStageBundle(url: String, expectedHash: String, promise: Promise) {
371
+ scope.launch {
372
+ try {
373
+ eventQueue.pushEvent(SwiftPatchEventType.DOWNLOAD_STAGE_STARTED, expectedHash)
374
+
375
+ val downloadFile = bundleManager.getDownloadFile("stage_$expectedHash")
376
+
377
+ downloadManager.download(url, downloadFile) { progress ->
378
+ sendEvent("SwiftPatch:downloadProgress", Arguments.createMap().apply {
379
+ putDouble("downloadedBytes", progress.downloadedBytes.toDouble())
380
+ putDouble("totalBytes", progress.totalBytes.toDouble())
381
+ putInt("percentage", progress.percentage)
382
+ })
383
+ }
384
+
385
+ val stageDir = File(File(reactApplicationContext.filesDir, "swiftpatch/stage"), "new")
386
+ stageDir.mkdirs()
387
+ val destFile = File(stageDir, "$expectedHash.bundle")
388
+ destFile.delete()
389
+ downloadFile.renameTo(destFile)
390
+
391
+ slotManager.stageNewHash = expectedHash
392
+ eventQueue.pushEvent(SwiftPatchEventType.DOWNLOAD_STAGE_COMPLETED, expectedHash)
393
+ withContext(Dispatchers.Main) { promise.resolve(null) }
394
+
395
+ } catch (e: Exception) {
396
+ eventQueue.pushEvent(SwiftPatchEventType.DOWNLOAD_STAGE_FAILED, expectedHash, e.message)
397
+ withContext(Dispatchers.Main) { promise.reject("DOWNLOAD_ERROR", e.message, e) }
398
+ }
399
+ }
400
+ }
401
+
402
+ @ReactMethod
403
+ fun installUpdate(bundleHash: String, promise: Promise) {
404
+ try {
405
+ if (slotManager.prodTempHash == null) {
406
+ val pendingHash = prefs.getString("pending_bundle_hash", null)
407
+ if (pendingHash != bundleHash) {
408
+ throw IllegalStateException("No pending bundle found")
409
+ }
410
+ }
411
+
412
+ prefs.edit()
413
+ .putBoolean("pending_install_confirmation", true)
414
+ .putLong("install_timestamp", System.currentTimeMillis())
415
+ .apply()
416
+
417
+ log("Update marked for install: $bundleHash")
418
+ promise.resolve(null)
419
+ } catch (e: Exception) {
420
+ promise.reject("INSTALL_ERROR", e.message, e)
421
+ }
422
+ }
423
+
424
+ @ReactMethod
425
+ fun hasPendingInstall(promise: Promise) {
426
+ try {
427
+ val hasPending = slotManager.prodTempHash != null ||
428
+ prefs.getString("pending_bundle_hash", null) != null
429
+ promise.resolve(hasPending)
430
+ } catch (e: Exception) {
431
+ promise.reject("ERROR", e.message, e)
432
+ }
433
+ }
434
+
435
+ @ReactMethod
436
+ fun confirmInstall(promise: Promise) {
437
+ try {
438
+ prefs.edit()
439
+ .putBoolean("pending_install_confirmation", false)
440
+ .putInt("crash_count", 0)
441
+ .apply()
442
+
443
+ slotManager.cleanup()
444
+ log("Install confirmed, cleaned up old bundles")
445
+ promise.resolve(null)
446
+ } catch (e: Exception) {
447
+ promise.reject("ERROR", e.message, e)
448
+ }
449
+ }
450
+
451
+ // ═══════════════════════════════════════════════════════════════
452
+ // STABILIZATION
453
+ // ═══════════════════════════════════════════════════════════════
454
+
455
+ @ReactMethod
456
+ fun stabilize(promise: Promise) {
457
+ try {
458
+ val success = slotManager.stabilize()
459
+ if (success) {
460
+ eventQueue.pushEvent(SwiftPatchEventType.STABILIZE_PROD, slotManager.prodStableHash)
461
+ log("Bundle stabilized (promoted NEW → STABLE)")
462
+ promise.resolve(Arguments.makeNativeMap(slotManager.getMetadata() as Map<String, Any?>))
463
+ } else {
464
+ promise.reject("STABILIZE_ERROR", "No bundle in NEW slot to stabilize", null)
465
+ }
466
+ } catch (e: Exception) {
467
+ promise.reject("STABILIZE_ERROR", e.message, e)
468
+ }
469
+ }
470
+
471
+ // ═══════════════════════════════════════════════════════════════
472
+ // RESTART & ROLLBACK
473
+ // ═══════════════════════════════════════════════════════════════
474
+
475
+ @ReactMethod
476
+ fun restart() {
477
+ log("Restarting app...")
478
+ val intent = reactApplicationContext.packageManager
479
+ .getLaunchIntentForPackage(reactApplicationContext.packageName)
480
+ intent?.addFlags(
481
+ android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP or
482
+ android.content.Intent.FLAG_ACTIVITY_NEW_TASK or
483
+ android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK
484
+ )
485
+ reactApplicationContext.startActivity(intent)
486
+ android.os.Process.killProcess(android.os.Process.myPid())
487
+ }
488
+
489
+ @ReactMethod
490
+ fun rollback(promise: Promise) {
491
+ try {
492
+ val result = slotManager.rollbackProd(isAutoRollback = false)
493
+ prefs.edit()
494
+ .putBoolean("pending_install_confirmation", false)
495
+ .putInt("crash_count", 0)
496
+ .apply()
497
+ eventQueue.pushEvent(SwiftPatchEventType.ROLLBACK_PROD, metadata = mapOf("result" to result, "manual" to true))
498
+ log("Rollback complete: $result")
499
+ promise.resolve(result)
500
+ } catch (e: Exception) {
501
+ promise.reject("ROLLBACK_ERROR", e.message, e)
502
+ }
503
+ }
504
+
505
+ @ReactMethod
506
+ fun clearPendingUpdate(promise: Promise) {
507
+ try {
508
+ slotManager.prodTempHash = null
509
+
510
+ val pendingPath = prefs.getString("pending_bundle_path", null)
511
+ if (pendingPath != null) File(pendingPath).delete()
512
+
513
+ prefs.edit()
514
+ .remove("pending_bundle_hash")
515
+ .remove("pending_bundle_path")
516
+ .apply()
517
+
518
+ promise.resolve(null)
519
+ } catch (e: Exception) {
520
+ promise.reject("ERROR", e.message, e)
521
+ }
522
+ }
523
+
524
+ // ═══════════════════════════════════════════════════════════════
525
+ // EVENT QUEUE
526
+ // ═══════════════════════════════════════════════════════════════
527
+
528
+ @ReactMethod
529
+ fun popEvents(promise: Promise) {
530
+ try {
531
+ val events = eventQueue.popEvents()
532
+ val result = Arguments.createArray()
533
+ for (i in 0 until events.length()) {
534
+ result.pushMap(Arguments.makeNativeMap(jsonObjectToMap(events.getJSONObject(i))))
535
+ }
536
+ promise.resolve(result)
537
+ } catch (e: Exception) {
538
+ promise.reject("EVENT_ERROR", e.message, e)
539
+ }
540
+ }
541
+
542
+ @ReactMethod
543
+ fun acknowledgeEvents(eventIdsJson: String, promise: Promise) {
544
+ try {
545
+ val ids = JSONArray(eventIdsJson)
546
+ val idList = mutableListOf<String>()
547
+ for (i in 0 until ids.length()) {
548
+ idList.add(ids.getString(i))
549
+ }
550
+ eventQueue.acknowledgeEvents(idList)
551
+ promise.resolve(null)
552
+ } catch (e: Exception) {
553
+ promise.reject("EVENT_ERROR", e.message, e)
554
+ }
555
+ }
556
+
557
+ // ═══════════════════════════════════════════════════════════════
558
+ // LAUNCH TRACKING
559
+ // ═══════════════════════════════════════════════════════════════
560
+
561
+ @ReactMethod
562
+ fun recordSuccessfulLaunch(bundleHash: String, promise: Promise) {
563
+ try {
564
+ val key = "launch_count_$bundleHash"
565
+ val count = prefs.getInt(key, 0) + 1
566
+ prefs.edit().putInt(key, count).apply()
567
+
568
+ if (count == 1) {
569
+ eventQueue.pushEvent(SwiftPatchEventType.INSTALLED_PROD, bundleHash)
570
+ }
571
+
572
+ promise.resolve(count)
573
+ } catch (e: Exception) {
574
+ promise.reject("LAUNCH_ERROR", e.message, e)
575
+ }
576
+ }
577
+
578
+ @ReactMethod
579
+ fun getLaunchCount(bundleHash: String, promise: Promise) {
580
+ try {
581
+ promise.resolve(prefs.getInt("launch_count_$bundleHash", 0))
582
+ } catch (e: Exception) {
583
+ promise.reject("LAUNCH_ERROR", e.message, e)
584
+ }
585
+ }
586
+
587
+ // ═══════════════════════════════════════════════════════════════
588
+ // STATUS REPORTING
589
+ // ═══════════════════════════════════════════════════════════════
590
+
591
+ @ReactMethod
592
+ fun reportStatus(releaseId: String, status: String, promise: Promise) {
593
+ promise.resolve(null)
594
+ }
595
+
596
+ // ═══════════════════════════════════════════════════════════════
597
+ // HELPERS
598
+ // ═══════════════════════════════════════════════════════════════
599
+
600
+ private fun sendEvent(eventName: String, params: WritableMap) {
601
+ reactApplicationContext
602
+ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
603
+ .emit(eventName, params)
604
+ }
605
+
606
+ private fun log(message: String) {
607
+ if (isDebugMode) {
608
+ android.util.Log.d(TAG, message)
609
+ }
610
+ }
611
+
612
+ private fun jsonObjectToMap(json: org.json.JSONObject): Map<String, Any?> {
613
+ val map = mutableMapOf<String, Any?>()
614
+ json.keys().forEach { key ->
615
+ val value = json.opt(key)
616
+ map[key] = when (value) {
617
+ is org.json.JSONObject -> jsonObjectToMap(value)
618
+ org.json.JSONObject.NULL -> null
619
+ else -> value
620
+ }
621
+ }
622
+ return map
623
+ }
624
+ }
625
+
626
+ data class SwiftPatchConfig(
627
+ val deploymentKey: String,
628
+ val serverUrl: String,
629
+ val publicKey: String?
630
+ )
@@ -0,0 +1,21 @@
1
+ package com.swiftpatch
2
+
3
+ import com.facebook.react.ReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.uimanager.ViewManager
7
+
8
+ class SwiftPatchPackage : ReactPackage {
9
+
10
+ override fun createNativeModules(
11
+ reactContext: ReactApplicationContext
12
+ ): List<NativeModule> {
13
+ return listOf(SwiftPatchModule(reactContext))
14
+ }
15
+
16
+ override fun createViewManagers(
17
+ reactContext: ReactApplicationContext
18
+ ): List<ViewManager<*, *>> {
19
+ return emptyList()
20
+ }
21
+ }
@@ -0,0 +1,12 @@
1
+ cmake_minimum_required(VERSION 3.22)
2
+ project(bspatch)
3
+
4
+ add_library(bspatch SHARED
5
+ bspatch_jni.c
6
+ bspatch.c
7
+ )
8
+
9
+ target_include_directories(bspatch PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
10
+
11
+ find_library(log-lib log)
12
+ target_link_libraries(bspatch ${log-lib})