@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.
- package/README.md +430 -0
- package/android/build.gradle +105 -0
- package/android/src/main/AndroidManifest.xml +6 -0
- package/android/src/main/java/com/swiftpatch/BundleManager.kt +107 -0
- package/android/src/main/java/com/swiftpatch/CrashDetector.kt +79 -0
- package/android/src/main/java/com/swiftpatch/CryptoVerifier.kt +69 -0
- package/android/src/main/java/com/swiftpatch/DownloadManager.kt +120 -0
- package/android/src/main/java/com/swiftpatch/EventQueue.kt +86 -0
- package/android/src/main/java/com/swiftpatch/FileUtils.kt +60 -0
- package/android/src/main/java/com/swiftpatch/PatchApplier.kt +60 -0
- package/android/src/main/java/com/swiftpatch/SignalCrashHandler.kt +84 -0
- package/android/src/main/java/com/swiftpatch/SlotManager.kt +299 -0
- package/android/src/main/java/com/swiftpatch/SwiftPatchModule.kt +630 -0
- package/android/src/main/java/com/swiftpatch/SwiftPatchPackage.kt +21 -0
- package/android/src/main/jni/CMakeLists.txt +12 -0
- package/android/src/main/jni/bspatch.c +188 -0
- package/android/src/main/jni/bspatch.h +57 -0
- package/android/src/main/jni/bspatch_jni.c +28 -0
- package/ios/Libraries/bspatch/bspatch.c +188 -0
- package/ios/Libraries/bspatch/bspatch.h +50 -0
- package/ios/Libraries/bspatch/module.modulemap +4 -0
- package/ios/SwiftPatch/BundleManager.swift +113 -0
- package/ios/SwiftPatch/CrashDetector.swift +71 -0
- package/ios/SwiftPatch/CryptoVerifier.swift +70 -0
- package/ios/SwiftPatch/DownloadManager.swift +125 -0
- package/ios/SwiftPatch/EventQueue.swift +116 -0
- package/ios/SwiftPatch/FileUtils.swift +38 -0
- package/ios/SwiftPatch/PatchApplier.swift +41 -0
- package/ios/SwiftPatch/SignalCrashHandler.swift +129 -0
- package/ios/SwiftPatch/SlotManager.swift +360 -0
- package/ios/SwiftPatch/SwiftPatchModule.m +56 -0
- package/ios/SwiftPatch/SwiftPatchModule.swift +621 -0
- package/lib/commonjs/SwiftPatchCore.js +140 -0
- package/lib/commonjs/SwiftPatchCore.js.map +1 -0
- package/lib/commonjs/SwiftPatchProvider.js +617 -0
- package/lib/commonjs/SwiftPatchProvider.js.map +1 -0
- package/lib/commonjs/constants.js +50 -0
- package/lib/commonjs/constants.js.map +1 -0
- package/lib/commonjs/core/Downloader.js +63 -0
- package/lib/commonjs/core/Downloader.js.map +1 -0
- package/lib/commonjs/core/Installer.js +46 -0
- package/lib/commonjs/core/Installer.js.map +1 -0
- package/lib/commonjs/core/Rollback.js +36 -0
- package/lib/commonjs/core/Rollback.js.map +1 -0
- package/lib/commonjs/core/UpdateChecker.js +57 -0
- package/lib/commonjs/core/UpdateChecker.js.map +1 -0
- package/lib/commonjs/core/Verifier.js +82 -0
- package/lib/commonjs/core/Verifier.js.map +1 -0
- package/lib/commonjs/core/index.js +41 -0
- package/lib/commonjs/core/index.js.map +1 -0
- package/lib/commonjs/index.js +154 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/modal/SwiftPatchModal.js +667 -0
- package/lib/commonjs/modal/SwiftPatchModal.js.map +1 -0
- package/lib/commonjs/modal/useSwiftPatchModal.js +26 -0
- package/lib/commonjs/modal/useSwiftPatchModal.js.map +1 -0
- package/lib/commonjs/native/NativeSwiftPatch.js +85 -0
- package/lib/commonjs/native/NativeSwiftPatch.js.map +1 -0
- package/lib/commonjs/native/NativeSwiftPatchSpec.js +15 -0
- package/lib/commonjs/native/NativeSwiftPatchSpec.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/types.js +126 -0
- package/lib/commonjs/types.js.map +1 -0
- package/lib/commonjs/useSwiftPatch.js +31 -0
- package/lib/commonjs/useSwiftPatch.js.map +1 -0
- package/lib/commonjs/utils/api.js +206 -0
- package/lib/commonjs/utils/api.js.map +1 -0
- package/lib/commonjs/utils/device.js +23 -0
- package/lib/commonjs/utils/device.js.map +1 -0
- package/lib/commonjs/utils/logger.js +30 -0
- package/lib/commonjs/utils/logger.js.map +1 -0
- package/lib/commonjs/utils/storage.js +31 -0
- package/lib/commonjs/utils/storage.js.map +1 -0
- package/lib/commonjs/withSwiftPatch.js +42 -0
- package/lib/commonjs/withSwiftPatch.js.map +1 -0
- package/lib/module/SwiftPatchCore.js +135 -0
- package/lib/module/SwiftPatchCore.js.map +1 -0
- package/lib/module/SwiftPatchProvider.js +611 -0
- package/lib/module/SwiftPatchProvider.js.map +1 -0
- package/lib/module/constants.js +46 -0
- package/lib/module/constants.js.map +1 -0
- package/lib/module/core/Downloader.js +57 -0
- package/lib/module/core/Downloader.js.map +1 -0
- package/lib/module/core/Installer.js +41 -0
- package/lib/module/core/Installer.js.map +1 -0
- package/lib/module/core/Rollback.js +31 -0
- package/lib/module/core/Rollback.js.map +1 -0
- package/lib/module/core/UpdateChecker.js +51 -0
- package/lib/module/core/UpdateChecker.js.map +1 -0
- package/lib/module/core/Verifier.js +76 -0
- package/lib/module/core/Verifier.js.map +1 -0
- package/lib/module/core/index.js +8 -0
- package/lib/module/core/index.js.map +1 -0
- package/lib/module/index.js +34 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/modal/SwiftPatchModal.js +661 -0
- package/lib/module/modal/SwiftPatchModal.js.map +1 -0
- package/lib/module/modal/useSwiftPatchModal.js +22 -0
- package/lib/module/modal/useSwiftPatchModal.js.map +1 -0
- package/lib/module/native/NativeSwiftPatch.js +78 -0
- package/lib/module/native/NativeSwiftPatch.js.map +1 -0
- package/lib/module/native/NativeSwiftPatchSpec.js +12 -0
- package/lib/module/native/NativeSwiftPatchSpec.js.map +1 -0
- package/lib/module/types.js +139 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/useSwiftPatch.js +26 -0
- package/lib/module/useSwiftPatch.js.map +1 -0
- package/lib/module/utils/api.js +197 -0
- package/lib/module/utils/api.js.map +1 -0
- package/lib/module/utils/device.js +18 -0
- package/lib/module/utils/device.js.map +1 -0
- package/lib/module/utils/logger.js +26 -0
- package/lib/module/utils/logger.js.map +1 -0
- package/lib/module/utils/storage.js +24 -0
- package/lib/module/utils/storage.js.map +1 -0
- package/lib/module/withSwiftPatch.js +37 -0
- package/lib/module/withSwiftPatch.js.map +1 -0
- package/lib/typescript/SwiftPatchCore.d.ts +64 -0
- package/lib/typescript/SwiftPatchCore.d.ts.map +1 -0
- package/lib/typescript/SwiftPatchProvider.d.ts +22 -0
- package/lib/typescript/SwiftPatchProvider.d.ts.map +1 -0
- package/lib/typescript/constants.d.ts +33 -0
- package/lib/typescript/constants.d.ts.map +1 -0
- package/lib/typescript/core/Downloader.d.ts +34 -0
- package/lib/typescript/core/Downloader.d.ts.map +1 -0
- package/lib/typescript/core/Installer.d.ts +25 -0
- package/lib/typescript/core/Installer.d.ts.map +1 -0
- package/lib/typescript/core/Rollback.d.ts +18 -0
- package/lib/typescript/core/Rollback.d.ts.map +1 -0
- package/lib/typescript/core/UpdateChecker.d.ts +27 -0
- package/lib/typescript/core/UpdateChecker.d.ts.map +1 -0
- package/lib/typescript/core/Verifier.d.ts +31 -0
- package/lib/typescript/core/Verifier.d.ts.map +1 -0
- package/lib/typescript/core/index.d.ts +8 -0
- package/lib/typescript/core/index.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +13 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/modal/SwiftPatchModal.d.ts +11 -0
- package/lib/typescript/modal/SwiftPatchModal.d.ts.map +1 -0
- package/lib/typescript/modal/useSwiftPatchModal.d.ts +7 -0
- package/lib/typescript/modal/useSwiftPatchModal.d.ts.map +1 -0
- package/lib/typescript/native/NativeSwiftPatch.d.ts +61 -0
- package/lib/typescript/native/NativeSwiftPatch.d.ts.map +1 -0
- package/lib/typescript/native/NativeSwiftPatchSpec.d.ts +67 -0
- package/lib/typescript/native/NativeSwiftPatchSpec.d.ts.map +1 -0
- package/lib/typescript/types.d.ts +266 -0
- package/lib/typescript/types.d.ts.map +1 -0
- package/lib/typescript/useSwiftPatch.d.ts +12 -0
- package/lib/typescript/useSwiftPatch.d.ts.map +1 -0
- package/lib/typescript/utils/api.d.ts +87 -0
- package/lib/typescript/utils/api.d.ts.map +1 -0
- package/lib/typescript/utils/device.d.ts +9 -0
- package/lib/typescript/utils/device.d.ts.map +1 -0
- package/lib/typescript/utils/logger.d.ts +8 -0
- package/lib/typescript/utils/logger.d.ts.map +1 -0
- package/lib/typescript/utils/storage.d.ts +14 -0
- package/lib/typescript/utils/storage.d.ts.map +1 -0
- package/lib/typescript/withSwiftPatch.d.ts +12 -0
- package/lib/typescript/withSwiftPatch.d.ts.map +1 -0
- package/package.json +99 -0
- package/react-native-swiftpatch.podspec +50 -0
- package/src/SwiftPatchCore.ts +148 -0
- package/src/SwiftPatchProvider.tsx +514 -0
- package/src/constants.ts +49 -0
- package/src/core/Downloader.ts +74 -0
- package/src/core/Installer.ts +38 -0
- package/src/core/Rollback.ts +28 -0
- package/src/core/UpdateChecker.ts +70 -0
- package/src/core/Verifier.ts +92 -0
- package/src/core/index.ts +11 -0
- package/src/index.ts +64 -0
- package/src/modal/SwiftPatchModal.tsx +657 -0
- package/src/modal/useSwiftPatchModal.ts +24 -0
- package/src/native/NativeSwiftPatch.ts +205 -0
- package/src/native/NativeSwiftPatchSpec.ts +139 -0
- package/src/types.ts +336 -0
- package/src/useSwiftPatch.ts +29 -0
- package/src/utils/api.ts +244 -0
- package/src/utils/device.ts +15 -0
- package/src/utils/logger.ts +29 -0
- package/src/utils/storage.ts +23 -0
- 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
|
+
}
|