@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,621 @@
1
+ import Foundation
2
+ import React
3
+
4
+ @objc(SwiftPatch)
5
+ class SwiftPatchModule: RCTEventEmitter {
6
+
7
+ // MARK: - Static Properties
8
+
9
+ private static let prefsKey = "swiftpatch_prefs"
10
+ private static var userDefaults: UserDefaults {
11
+ UserDefaults.standard
12
+ }
13
+
14
+ /// Static SlotManager for getJSBundleFile (called before module init)
15
+ private static var staticSlotManager: SlotManager?
16
+
17
+ /// Get the JS bundle file path (called from AppDelegate)
18
+ @objc static func getJSBundleFile() -> URL? {
19
+ let sm = getOrCreateSlotManager()
20
+
21
+ // Check for version change first
22
+ let versionChanged = sm.checkVersionChange()
23
+ if versionChanged {
24
+ return nil // Fall back to default bundle
25
+ }
26
+
27
+ // Apply temp hash if pending
28
+ sm.applyTempHashOnLaunch()
29
+
30
+ // Get bundle URL from slot manager
31
+ return sm.getBundleURL()
32
+ }
33
+
34
+ private static func getOrCreateSlotManager() -> SlotManager {
35
+ if staticSlotManager == nil {
36
+ staticSlotManager = SlotManager(userDefaults: userDefaults, prefsKey: prefsKey)
37
+ }
38
+ return staticSlotManager!
39
+ }
40
+
41
+ // MARK: - Instance Properties
42
+
43
+ private var config: SwiftPatchConfigModel?
44
+ private var isDebugMode = false
45
+
46
+ private lazy var spBundleManager = BundleManager()
47
+ private lazy var downloadManager = SPDownloadManager()
48
+ private lazy var patchApplier = PatchApplier()
49
+ private lazy var cryptoVerifier = CryptoVerifier()
50
+
51
+ private lazy var slotManager: SlotManager = {
52
+ let sm = Self.getOrCreateSlotManager()
53
+ return sm
54
+ }()
55
+
56
+ private lazy var eventQueue: EventQueue = {
57
+ EventQueue(userDefaults: Self.userDefaults, prefsKey: Self.prefsKey)
58
+ }()
59
+
60
+ private lazy var crashDetector: CrashDetector = {
61
+ CrashDetector(userDefaults: Self.userDefaults, prefsKey: Self.prefsKey)
62
+ }()
63
+
64
+ // MARK: - RCTEventEmitter
65
+
66
+ override func supportedEvents() -> [String]! {
67
+ return [
68
+ "SwiftPatch:downloadProgress",
69
+ "SwiftPatch:installComplete",
70
+ "SwiftPatch:rollbackOccurred",
71
+ "SwiftPatch:error",
72
+ "SwiftPatch:event",
73
+ "SwiftPatch:versionChanged",
74
+ ]
75
+ }
76
+
77
+ override static func requiresMainQueueSetup() -> Bool {
78
+ return true
79
+ }
80
+
81
+ // MARK: - Initialization
82
+
83
+ override init() {
84
+ super.init()
85
+
86
+ // Install signal-level crash handlers
87
+ SignalCrashHandler.shared.installSignalHandlers()
88
+
89
+ // Check for crash marker from previous run
90
+ if let crashInfo = SignalCrashHandler.shared.checkForCrashMarker() {
91
+ if SignalCrashHandler.shared.shouldAutoRollback(crashInfo: crashInfo) {
92
+ // App crashed before mounting - auto rollback
93
+ let result = slotManager.rollbackProd(isAutoRollback: true)
94
+ eventQueue.pushEvent(
95
+ type: .crashDetected,
96
+ metadata: [
97
+ "signalName": crashInfo.signalName,
98
+ "signalNumber": crashInfo.signalNumber,
99
+ "autoRollback": true,
100
+ "rollbackResult": result,
101
+ ]
102
+ )
103
+ sendEvent(
104
+ withName: "SwiftPatch:rollbackOccurred",
105
+ body: ["reason": "Native crash detected: \(crashInfo.signalName), auto-rollback to \(result)"]
106
+ )
107
+ } else {
108
+ // App crashed after mounting - log but don't auto-rollback
109
+ eventQueue.pushEvent(
110
+ type: .crashDetected,
111
+ metadata: [
112
+ "signalName": crashInfo.signalName,
113
+ "signalNumber": crashInfo.signalNumber,
114
+ "autoRollback": false,
115
+ ]
116
+ )
117
+ }
118
+ }
119
+
120
+ // Also check timer-based crash detection (legacy/fallback)
121
+ crashDetector.checkForPendingRollback { [weak self] reason in
122
+ self?.sendEvent(
123
+ withName: "SwiftPatch:rollbackOccurred",
124
+ body: ["reason": reason]
125
+ )
126
+ }
127
+
128
+ // Check for version change
129
+ if slotManager.checkVersionChange() {
130
+ eventQueue.pushEvent(type: .versionChanged)
131
+ sendEvent(
132
+ withName: "SwiftPatch:versionChanged",
133
+ body: ["reason": "App binary version changed, reset to default bundle"]
134
+ )
135
+ }
136
+ }
137
+
138
+ @objc
139
+ func initialize(_ configDict: NSDictionary,
140
+ resolver resolve: @escaping RCTPromiseResolveBlock,
141
+ rejecter reject: @escaping RCTPromiseRejectBlock) {
142
+ guard let deploymentKey = configDict["deploymentKey"] as? String else {
143
+ reject("INIT_ERROR", "deploymentKey is required", nil)
144
+ return
145
+ }
146
+
147
+ config = SwiftPatchConfigModel(
148
+ deploymentKey: deploymentKey,
149
+ serverUrl: configDict["serverUrl"] as? String ?? "https://swiftpatch.hyperbrainlabs.com/api/v1",
150
+ publicKey: configDict["publicKey"] as? String
151
+ )
152
+
153
+ spBundleManager.ensureDirectoriesExist()
154
+ slotManager.ensureSlotDirectories()
155
+
156
+ log("Initialized with deployment key: \(String(deploymentKey.prefix(8)))...")
157
+ resolve(nil)
158
+ }
159
+
160
+ @objc
161
+ func setDebugMode(_ enabled: Bool) {
162
+ isDebugMode = enabled
163
+ }
164
+
165
+ // MARK: - Mount State (for signal crash handler)
166
+
167
+ @objc
168
+ func markMounted(_ resolve: @escaping RCTPromiseResolveBlock,
169
+ rejecter reject: @escaping RCTPromiseRejectBlock) {
170
+ SignalCrashHandler.shared.markMounted()
171
+ resolve(nil)
172
+ }
173
+
174
+ @objc
175
+ func markUnmounted(_ resolve: @escaping RCTPromiseResolveBlock,
176
+ rejecter reject: @escaping RCTPromiseRejectBlock) {
177
+ SignalCrashHandler.shared.markUnmounted()
178
+ resolve(nil)
179
+ }
180
+
181
+ // MARK: - Bundle Information
182
+
183
+ @objc
184
+ func getCurrentBundle(_ resolve: @escaping RCTPromiseResolveBlock,
185
+ rejecter reject: @escaping RCTPromiseRejectBlock) {
186
+ let env = slotManager.currentEnvironment
187
+
188
+ if env == .staging {
189
+ if let hash = slotManager.stageNewHash {
190
+ resolve([
191
+ "hash": hash,
192
+ "version": "staging",
193
+ "installedAt": "",
194
+ "isOriginal": false,
195
+ "slot": "NEW_SLOT",
196
+ "environment": "STAGE",
197
+ ] as [String: Any])
198
+ } else {
199
+ resolve([
200
+ "hash": "original",
201
+ "version": "0.0.0",
202
+ "installedAt": "",
203
+ "isOriginal": true,
204
+ "slot": "DEFAULT_SLOT",
205
+ "environment": "STAGE",
206
+ ] as [String: Any])
207
+ }
208
+ return
209
+ }
210
+
211
+ // Production mode
212
+ let slot = slotManager.currentProdSlot
213
+ var hash: String?
214
+ switch slot {
215
+ case .newSlot: hash = slotManager.prodNewHash
216
+ case .stableSlot: hash = slotManager.prodStableHash
217
+ case .defaultSlot: hash = nil
218
+ }
219
+
220
+ if let hash = hash {
221
+ let installedAt = Self.userDefaults.string(forKey: "\(Self.prefsKey)_current_bundle_installed_at") ?? ""
222
+ resolve([
223
+ "hash": hash,
224
+ "version": Self.userDefaults.string(forKey: "\(Self.prefsKey)_current_bundle_version") ?? "unknown",
225
+ "installedAt": installedAt,
226
+ "isOriginal": false,
227
+ "slot": slot.rawValue,
228
+ "environment": "PROD",
229
+ ] as [String: Any])
230
+ } else {
231
+ resolve([
232
+ "hash": "original",
233
+ "version": "0.0.0",
234
+ "installedAt": "",
235
+ "isOriginal": true,
236
+ "slot": "DEFAULT_SLOT",
237
+ "environment": "PROD",
238
+ ] as [String: Any])
239
+ }
240
+ }
241
+
242
+ @objc
243
+ func getSlotMetadata(_ resolve: @escaping RCTPromiseResolveBlock,
244
+ rejecter reject: @escaping RCTPromiseRejectBlock) {
245
+ resolve(slotManager.getMetadata())
246
+ }
247
+
248
+ @objc
249
+ func getDeviceId(_ resolve: @escaping RCTPromiseResolveBlock,
250
+ rejecter reject: @escaping RCTPromiseRejectBlock) {
251
+ var deviceId = Self.userDefaults.string(forKey: "\(Self.prefsKey)_device_id")
252
+ if deviceId == nil {
253
+ deviceId = UUID().uuidString
254
+ Self.userDefaults.set(deviceId, forKey: "\(Self.prefsKey)_device_id")
255
+ }
256
+ resolve(deviceId)
257
+ }
258
+
259
+ @objc
260
+ func getAppVersion(_ resolve: @escaping RCTPromiseResolveBlock,
261
+ rejecter reject: @escaping RCTPromiseRejectBlock) {
262
+ let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0.0"
263
+ resolve(version)
264
+ }
265
+
266
+ // MARK: - Environment Switching
267
+
268
+ @objc
269
+ func switchEnvironment(_ environment: String,
270
+ resolver resolve: @escaping RCTPromiseResolveBlock,
271
+ rejecter reject: @escaping RCTPromiseRejectBlock) {
272
+ guard let env = EnvironmentMode(rawValue: environment) else {
273
+ reject("ENV_ERROR", "Invalid environment: \(environment). Use 'PROD' or 'STAGE'.", nil)
274
+ return
275
+ }
276
+ slotManager.currentEnvironment = env
277
+ log("Switched environment to: \(environment)")
278
+ resolve(slotManager.getMetadata())
279
+ }
280
+
281
+ // MARK: - Download & Install
282
+
283
+ @objc
284
+ func downloadUpdate(_ url: String,
285
+ expectedHash: String,
286
+ isPatch: Bool,
287
+ signature: String?,
288
+ resolver resolve: @escaping RCTPromiseResolveBlock,
289
+ rejecter reject: @escaping RCTPromiseRejectBlock) {
290
+
291
+ DispatchQueue.global(qos: .userInitiated).async { [weak self] in
292
+ guard let self = self else { return }
293
+
294
+ do {
295
+ self.log("Downloading update from: \(url) (isPatch: \(isPatch))")
296
+ self.eventQueue.pushEvent(type: .downloadProdStarted, releaseHash: expectedHash)
297
+
298
+ let downloadFile = self.spBundleManager.getDownloadFile(hash: expectedHash)
299
+
300
+ // Download with progress
301
+ try self.downloadManager.download(
302
+ urlString: url,
303
+ to: downloadFile
304
+ ) { [weak self] progress in
305
+ self?.sendEvent(withName: "SwiftPatch:downloadProgress", body: [
306
+ "downloadedBytes": progress.downloadedBytes,
307
+ "totalBytes": progress.totalBytes,
308
+ "percentage": progress.percentage
309
+ ])
310
+ }
311
+
312
+ self.log("Download complete, processing...")
313
+
314
+ // Decompress if brotli
315
+ let decompressedFile: URL
316
+ if url.hasSuffix(".br") {
317
+ decompressedFile = self.spBundleManager.getDecompressedFile(hash: expectedHash)
318
+ try SPFileUtils.decompressGzip(from: downloadFile, to: decompressedFile)
319
+ try FileManager.default.removeItem(at: downloadFile)
320
+ } else {
321
+ decompressedFile = downloadFile
322
+ }
323
+
324
+ // Apply patch or use as full bundle
325
+ let finalBundle: URL
326
+ if isPatch {
327
+ self.log("Applying patch...")
328
+ let currentHash: String?
329
+ switch self.slotManager.currentProdSlot {
330
+ case .newSlot: currentHash = self.slotManager.prodNewHash
331
+ case .stableSlot: currentHash = self.slotManager.prodStableHash
332
+ case .defaultSlot: currentHash = nil
333
+ }
334
+
335
+ let currentBundleFile: URL
336
+ if let hash = currentHash {
337
+ currentBundleFile = self.spBundleManager.getBundleFile(hash: hash)
338
+ } else {
339
+ currentBundleFile = try self.spBundleManager.extractOriginalBundle()
340
+ }
341
+
342
+ finalBundle = self.spBundleManager.getBundleFile(hash: expectedHash)
343
+ try self.patchApplier.applyPatch(
344
+ oldFile: currentBundleFile,
345
+ patchFile: decompressedFile,
346
+ newFile: finalBundle
347
+ )
348
+ try? FileManager.default.removeItem(at: decompressedFile)
349
+ } else {
350
+ finalBundle = self.spBundleManager.getBundleFile(hash: expectedHash)
351
+ if decompressedFile != finalBundle {
352
+ try? FileManager.default.removeItem(at: finalBundle)
353
+ try FileManager.default.moveItem(at: decompressedFile, to: finalBundle)
354
+ }
355
+ }
356
+
357
+ // Verify hash
358
+ self.log("Verifying hash...")
359
+ let actualHash = try self.cryptoVerifier.sha256(fileURL: finalBundle)
360
+ guard actualHash == expectedHash else {
361
+ try? FileManager.default.removeItem(at: finalBundle)
362
+ self.eventQueue.pushEvent(type: .downloadProdFailed, releaseHash: expectedHash, errorMessage: "Hash mismatch")
363
+ throw SwiftPatchError.hashMismatch(expected: expectedHash, actual: actualHash)
364
+ }
365
+
366
+ // Verify signature
367
+ if let signature = signature, let publicKey = self.config?.publicKey {
368
+ self.log("Verifying signature...")
369
+ guard self.cryptoVerifier.verifySignature(
370
+ data: actualHash,
371
+ signature: signature,
372
+ publicKey: publicKey
373
+ ) else {
374
+ try? FileManager.default.removeItem(at: finalBundle)
375
+ self.eventQueue.pushEvent(type: .downloadProdFailed, releaseHash: expectedHash, errorMessage: "Signature invalid")
376
+ throw SwiftPatchError.signatureInvalid
377
+ }
378
+ }
379
+
380
+ // Set as temp hash (deferred apply)
381
+ self.slotManager.prodTempHash = expectedHash
382
+
383
+ self.eventQueue.pushEvent(type: .downloadProdCompleted, releaseHash: expectedHash)
384
+ self.log("Download and verification complete, tempHash set: \(expectedHash)")
385
+ resolve(nil)
386
+
387
+ } catch {
388
+ self.log("Download failed: \(error.localizedDescription)")
389
+ self.eventQueue.pushEvent(type: .downloadProdFailed, releaseHash: expectedHash, errorMessage: error.localizedDescription)
390
+ reject("DOWNLOAD_ERROR", error.localizedDescription, error)
391
+ }
392
+ }
393
+ }
394
+
395
+ /// Download a stage bundle (for testing)
396
+ @objc
397
+ func downloadStageBundle(_ url: String,
398
+ expectedHash: String,
399
+ resolver resolve: @escaping RCTPromiseResolveBlock,
400
+ rejecter reject: @escaping RCTPromiseRejectBlock) {
401
+
402
+ DispatchQueue.global(qos: .userInitiated).async { [weak self] in
403
+ guard let self = self else { return }
404
+
405
+ do {
406
+ self.eventQueue.pushEvent(type: .downloadStageStarted, releaseHash: expectedHash)
407
+
408
+ let downloadFile = self.spBundleManager.getDownloadFile(hash: "stage_\(expectedHash)")
409
+
410
+ try self.downloadManager.download(urlString: url, to: downloadFile) { [weak self] progress in
411
+ self?.sendEvent(withName: "SwiftPatch:downloadProgress", body: [
412
+ "downloadedBytes": progress.downloadedBytes,
413
+ "totalBytes": progress.totalBytes,
414
+ "percentage": progress.percentage
415
+ ])
416
+ }
417
+
418
+ // Move to stage directory
419
+ let stageDir = self.spBundleManager.baseDir.appendingPathComponent("stage/new")
420
+ try? FileManager.default.createDirectory(at: stageDir, withIntermediateDirectories: true)
421
+ let destPath = stageDir.appendingPathComponent("\(expectedHash).bundle")
422
+ try? FileManager.default.removeItem(at: destPath)
423
+ try FileManager.default.moveItem(at: downloadFile, to: destPath)
424
+
425
+ self.slotManager.stageNewHash = expectedHash
426
+ self.eventQueue.pushEvent(type: .downloadStageCompleted, releaseHash: expectedHash)
427
+ resolve(nil)
428
+
429
+ } catch {
430
+ self.eventQueue.pushEvent(type: .downloadStageFailed, releaseHash: expectedHash, errorMessage: error.localizedDescription)
431
+ reject("DOWNLOAD_ERROR", error.localizedDescription, error)
432
+ }
433
+ }
434
+ }
435
+
436
+ @objc
437
+ func installUpdate(_ bundleHash: String,
438
+ resolver resolve: @escaping RCTPromiseResolveBlock,
439
+ rejecter reject: @escaping RCTPromiseRejectBlock) {
440
+ // With the new temp hash system, install just means "apply temp on next restart"
441
+ // The temp hash is already set during download.
442
+ // This method is kept for backward compatibility and explicit install trigger.
443
+
444
+ if slotManager.prodTempHash == nil {
445
+ // Check legacy pending system
446
+ guard let pendingHash = Self.userDefaults.string(
447
+ forKey: "\(Self.prefsKey)_pending_bundle_hash"
448
+ ), pendingHash == bundleHash else {
449
+ reject("INSTALL_ERROR", "No pending bundle found", nil)
450
+ return
451
+ }
452
+ }
453
+
454
+ Self.userDefaults.set(true, forKey: "\(Self.prefsKey)_pending_install_confirmation")
455
+ Self.userDefaults.set(Date().timeIntervalSince1970, forKey: "\(Self.prefsKey)_install_timestamp")
456
+
457
+ log("Update marked for install: \(bundleHash)")
458
+ resolve(nil)
459
+ }
460
+
461
+ @objc
462
+ func hasPendingInstall(_ resolve: @escaping RCTPromiseResolveBlock,
463
+ rejecter reject: @escaping RCTPromiseRejectBlock) {
464
+ let hasPending = slotManager.prodTempHash != nil ||
465
+ Self.userDefaults.string(forKey: "\(Self.prefsKey)_pending_bundle_hash") != nil
466
+ resolve(hasPending)
467
+ }
468
+
469
+ @objc
470
+ func confirmInstall(_ resolve: @escaping RCTPromiseResolveBlock,
471
+ rejecter reject: @escaping RCTPromiseRejectBlock) {
472
+ Self.userDefaults.set(false, forKey: "\(Self.prefsKey)_pending_install_confirmation")
473
+ Self.userDefaults.set(0, forKey: "\(Self.prefsKey)_crash_count")
474
+ slotManager.cleanup()
475
+ log("Install confirmed")
476
+ resolve(nil)
477
+ }
478
+
479
+ // MARK: - Stabilization
480
+
481
+ @objc
482
+ func stabilize(_ resolve: @escaping RCTPromiseResolveBlock,
483
+ rejecter reject: @escaping RCTPromiseRejectBlock) {
484
+ let success = slotManager.stabilize()
485
+ if success {
486
+ eventQueue.pushEvent(type: .stabilizeProd, releaseHash: slotManager.prodStableHash)
487
+ log("Bundle stabilized (promoted NEW → STABLE)")
488
+ resolve(slotManager.getMetadata())
489
+ } else {
490
+ reject("STABILIZE_ERROR", "No bundle in NEW slot to stabilize", nil)
491
+ }
492
+ }
493
+
494
+ // MARK: - Restart & Rollback
495
+
496
+ @objc
497
+ func restart() {
498
+ log("Restarting app...")
499
+ DispatchQueue.main.async {
500
+ if let bridge = RCTBridge.current() {
501
+ bridge.reload()
502
+ }
503
+ }
504
+ }
505
+
506
+ @objc
507
+ func rollback(_ resolve: @escaping RCTPromiseResolveBlock,
508
+ rejecter reject: @escaping RCTPromiseRejectBlock) {
509
+ let result = slotManager.rollbackProd(isAutoRollback: false)
510
+ Self.userDefaults.set(false, forKey: "\(Self.prefsKey)_pending_install_confirmation")
511
+ Self.userDefaults.set(0, forKey: "\(Self.prefsKey)_crash_count")
512
+ eventQueue.pushEvent(type: .rollbackProd, metadata: ["result": result, "manual": true])
513
+ log("Rollback complete: \(result)")
514
+ resolve(result)
515
+ }
516
+
517
+ @objc
518
+ func clearPendingUpdate(_ resolve: @escaping RCTPromiseResolveBlock,
519
+ rejecter reject: @escaping RCTPromiseRejectBlock) {
520
+ slotManager.prodTempHash = nil
521
+
522
+ if let pendingPath = Self.userDefaults.string(forKey: "\(Self.prefsKey)_pending_bundle_path") {
523
+ try? FileManager.default.removeItem(atPath: pendingPath)
524
+ }
525
+ Self.userDefaults.removeObject(forKey: "\(Self.prefsKey)_pending_bundle_hash")
526
+ Self.userDefaults.removeObject(forKey: "\(Self.prefsKey)_pending_bundle_path")
527
+
528
+ resolve(nil)
529
+ }
530
+
531
+ // MARK: - Event Queue (JS Bridge)
532
+
533
+ @objc
534
+ func popEvents(_ resolve: @escaping RCTPromiseResolveBlock,
535
+ rejecter reject: @escaping RCTPromiseRejectBlock) {
536
+ let events = eventQueue.popEvents()
537
+ resolve(events)
538
+ }
539
+
540
+ @objc
541
+ func acknowledgeEvents(_ eventIdsJson: String,
542
+ resolver resolve: @escaping RCTPromiseResolveBlock,
543
+ rejecter reject: @escaping RCTPromiseRejectBlock) {
544
+ guard let data = eventIdsJson.data(using: .utf8),
545
+ let ids = try? JSONSerialization.jsonObject(with: data) as? [String] else {
546
+ reject("EVENT_ERROR", "Invalid event IDs JSON", nil)
547
+ return
548
+ }
549
+ eventQueue.acknowledgeEvents(eventIds: ids)
550
+ resolve(nil)
551
+ }
552
+
553
+ // MARK: - Launch Success Tracking
554
+
555
+ @objc
556
+ func recordSuccessfulLaunch(_ bundleHash: String,
557
+ resolver resolve: @escaping RCTPromiseResolveBlock,
558
+ rejecter reject: @escaping RCTPromiseRejectBlock) {
559
+ let key = "\(Self.prefsKey)_launch_count_\(bundleHash)"
560
+ let count = Self.userDefaults.integer(forKey: key) + 1
561
+ Self.userDefaults.set(count, forKey: key)
562
+
563
+ if count == 1 {
564
+ // First successful launch of this hash
565
+ eventQueue.pushEvent(type: .installedProd, releaseHash: bundleHash)
566
+ }
567
+
568
+ resolve(count)
569
+ }
570
+
571
+ @objc
572
+ func getLaunchCount(_ bundleHash: String,
573
+ resolver resolve: @escaping RCTPromiseResolveBlock,
574
+ rejecter reject: @escaping RCTPromiseRejectBlock) {
575
+ let key = "\(Self.prefsKey)_launch_count_\(bundleHash)"
576
+ resolve(Self.userDefaults.integer(forKey: key))
577
+ }
578
+
579
+ // MARK: - Status Reporting
580
+
581
+ @objc
582
+ func reportStatus(_ releaseId: String,
583
+ status: String,
584
+ resolver resolve: @escaping RCTPromiseResolveBlock,
585
+ rejecter reject: @escaping RCTPromiseRejectBlock) {
586
+ resolve(nil)
587
+ }
588
+
589
+ // MARK: - Helpers
590
+
591
+ private func log(_ message: String) {
592
+ if isDebugMode {
593
+ print("[SwiftPatch] \(message)")
594
+ }
595
+ }
596
+ }
597
+
598
+ // MARK: - Supporting Types
599
+
600
+ struct SwiftPatchConfigModel {
601
+ let deploymentKey: String
602
+ let serverUrl: String
603
+ let publicKey: String?
604
+ }
605
+
606
+ enum SwiftPatchError: LocalizedError {
607
+ case hashMismatch(expected: String, actual: String)
608
+ case signatureInvalid
609
+ case patchFailed
610
+
611
+ var errorDescription: String? {
612
+ switch self {
613
+ case .hashMismatch(let expected, let actual):
614
+ return "Hash mismatch: expected \(expected), got \(actual)"
615
+ case .signatureInvalid:
616
+ return "Signature verification failed"
617
+ case .patchFailed:
618
+ return "Failed to apply patch"
619
+ }
620
+ }
621
+ }