@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,360 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Represents the three slots for bundle management
|
|
4
|
+
enum SlotState: String {
|
|
5
|
+
case defaultSlot = "DEFAULT_SLOT"
|
|
6
|
+
case stableSlot = "STABLE_SLOT"
|
|
7
|
+
case newSlot = "NEW_SLOT"
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/// Represents the environment mode
|
|
11
|
+
enum EnvironmentMode: String {
|
|
12
|
+
case production = "PROD"
|
|
13
|
+
case staging = "STAGE"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/// Manages the dual-slot architecture for safe OTA deployments.
|
|
17
|
+
/// Supports: DEFAULT_SLOT (app store), STABLE_SLOT (promoted), NEW_SLOT (latest OTA)
|
|
18
|
+
class SlotManager {
|
|
19
|
+
|
|
20
|
+
private let userDefaults: UserDefaults
|
|
21
|
+
private let prefsKey: String
|
|
22
|
+
private let fileManager = FileManager.default
|
|
23
|
+
|
|
24
|
+
private var baseDir: URL {
|
|
25
|
+
let documentsDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
|
26
|
+
return documentsDir.appendingPathComponent("swiftpatch")
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private var prodDir: URL { baseDir.appendingPathComponent("prod") }
|
|
30
|
+
private var prodNewDir: URL { prodDir.appendingPathComponent("new") }
|
|
31
|
+
private var prodStableDir: URL { prodDir.appendingPathComponent("stable") }
|
|
32
|
+
private var stageDir: URL { baseDir.appendingPathComponent("stage") }
|
|
33
|
+
private var stageNewDir: URL { stageDir.appendingPathComponent("new") }
|
|
34
|
+
|
|
35
|
+
init(userDefaults: UserDefaults, prefsKey: String) {
|
|
36
|
+
self.userDefaults = userDefaults
|
|
37
|
+
self.prefsKey = prefsKey
|
|
38
|
+
ensureSlotDirectories()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// MARK: - Directory Management
|
|
42
|
+
|
|
43
|
+
func ensureSlotDirectories() {
|
|
44
|
+
try? fileManager.createDirectory(at: prodNewDir, withIntermediateDirectories: true)
|
|
45
|
+
try? fileManager.createDirectory(at: prodStableDir, withIntermediateDirectories: true)
|
|
46
|
+
try? fileManager.createDirectory(at: stageNewDir, withIntermediateDirectories: true)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// MARK: - Current State
|
|
50
|
+
|
|
51
|
+
/// Get the current environment mode
|
|
52
|
+
var currentEnvironment: EnvironmentMode {
|
|
53
|
+
get {
|
|
54
|
+
let raw = userDefaults.string(forKey: "\(prefsKey)_environment") ?? "PROD"
|
|
55
|
+
return EnvironmentMode(rawValue: raw) ?? .production
|
|
56
|
+
}
|
|
57
|
+
set {
|
|
58
|
+
userDefaults.set(newValue.rawValue, forKey: "\(prefsKey)_environment")
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/// Get the current production slot
|
|
63
|
+
var currentProdSlot: SlotState {
|
|
64
|
+
get {
|
|
65
|
+
let raw = userDefaults.string(forKey: "\(prefsKey)_prod_current_slot") ?? "DEFAULT_SLOT"
|
|
66
|
+
return SlotState(rawValue: raw) ?? .defaultSlot
|
|
67
|
+
}
|
|
68
|
+
set {
|
|
69
|
+
userDefaults.set(newValue.rawValue, forKey: "\(prefsKey)_prod_current_slot")
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/// Get/set production slot hashes
|
|
74
|
+
var prodNewHash: String? {
|
|
75
|
+
get { userDefaults.string(forKey: "\(prefsKey)_prod_new_hash") }
|
|
76
|
+
set { userDefaults.set(newValue, forKey: "\(prefsKey)_prod_new_hash") }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
var prodStableHash: String? {
|
|
80
|
+
get { userDefaults.string(forKey: "\(prefsKey)_prod_stable_hash") }
|
|
81
|
+
set { userDefaults.set(newValue, forKey: "\(prefsKey)_prod_stable_hash") }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
var prodTempHash: String? {
|
|
85
|
+
get { userDefaults.string(forKey: "\(prefsKey)_prod_temp_hash") }
|
|
86
|
+
set { userDefaults.set(newValue, forKey: "\(prefsKey)_prod_temp_hash") }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/// Stage slot hash
|
|
90
|
+
var stageNewHash: String? {
|
|
91
|
+
get { userDefaults.string(forKey: "\(prefsKey)_stage_new_hash") }
|
|
92
|
+
set { userDefaults.set(newValue, forKey: "\(prefsKey)_stage_new_hash") }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// MARK: - Bundle Path Resolution
|
|
96
|
+
|
|
97
|
+
/// Get the correct JS bundle URL based on current slot state
|
|
98
|
+
func getBundleURL() -> URL? {
|
|
99
|
+
let env = currentEnvironment
|
|
100
|
+
|
|
101
|
+
if env == .staging {
|
|
102
|
+
// In staging, use stage new slot if available
|
|
103
|
+
if let hash = stageNewHash {
|
|
104
|
+
let path = stageNewDir.appendingPathComponent("\(hash).bundle")
|
|
105
|
+
if fileManager.fileExists(atPath: path.path) {
|
|
106
|
+
return path
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return nil // Fall back to default
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Production mode
|
|
113
|
+
switch currentProdSlot {
|
|
114
|
+
case .newSlot:
|
|
115
|
+
if let hash = prodNewHash {
|
|
116
|
+
let path = prodNewDir.appendingPathComponent("\(hash).bundle")
|
|
117
|
+
if fileManager.fileExists(atPath: path.path) {
|
|
118
|
+
return path
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// Fallthrough to stable
|
|
122
|
+
if let hash = prodStableHash {
|
|
123
|
+
let path = prodStableDir.appendingPathComponent("\(hash).bundle")
|
|
124
|
+
if fileManager.fileExists(atPath: path.path) {
|
|
125
|
+
currentProdSlot = .stableSlot
|
|
126
|
+
return path
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
currentProdSlot = .defaultSlot
|
|
130
|
+
return nil
|
|
131
|
+
|
|
132
|
+
case .stableSlot:
|
|
133
|
+
if let hash = prodStableHash {
|
|
134
|
+
let path = prodStableDir.appendingPathComponent("\(hash).bundle")
|
|
135
|
+
if fileManager.fileExists(atPath: path.path) {
|
|
136
|
+
return path
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
currentProdSlot = .defaultSlot
|
|
140
|
+
return nil
|
|
141
|
+
|
|
142
|
+
case .defaultSlot:
|
|
143
|
+
return nil // Use app store bundle
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// MARK: - Temp Hash (Deferred Apply)
|
|
148
|
+
|
|
149
|
+
/// Apply temp hash on restart. Called during app launch.
|
|
150
|
+
/// Moves tempHash → newHash and updates slot.
|
|
151
|
+
func applyTempHashOnLaunch() {
|
|
152
|
+
guard let tempHash = prodTempHash else { return }
|
|
153
|
+
|
|
154
|
+
// Move bundle from bundles dir to prod/new dir
|
|
155
|
+
let bundlesDir = baseDir.appendingPathComponent("bundles")
|
|
156
|
+
let sourcePath = bundlesDir.appendingPathComponent("\(tempHash).bundle")
|
|
157
|
+
let destPath = prodNewDir.appendingPathComponent("\(tempHash).bundle")
|
|
158
|
+
|
|
159
|
+
if fileManager.fileExists(atPath: sourcePath.path) {
|
|
160
|
+
try? fileManager.removeItem(at: destPath)
|
|
161
|
+
try? fileManager.moveItem(at: sourcePath, to: destPath)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
prodNewHash = tempHash
|
|
165
|
+
prodTempHash = nil
|
|
166
|
+
currentProdSlot = .newSlot
|
|
167
|
+
|
|
168
|
+
userDefaults.set(
|
|
169
|
+
ISO8601DateFormatter().string(from: Date()),
|
|
170
|
+
forKey: "\(prefsKey)_current_bundle_installed_at"
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// MARK: - Stabilization (NEW → STABLE)
|
|
175
|
+
|
|
176
|
+
/// Promote the current NEW slot bundle to STABLE slot.
|
|
177
|
+
/// This creates a known-good fallback point.
|
|
178
|
+
func stabilize() -> Bool {
|
|
179
|
+
guard let newHash = prodNewHash else { return false }
|
|
180
|
+
|
|
181
|
+
let sourcePath = prodNewDir.appendingPathComponent("\(newHash).bundle")
|
|
182
|
+
let destPath = prodStableDir.appendingPathComponent("\(newHash).bundle")
|
|
183
|
+
|
|
184
|
+
guard fileManager.fileExists(atPath: sourcePath.path) else { return false }
|
|
185
|
+
|
|
186
|
+
// Remove old stable bundle
|
|
187
|
+
if let oldStableHash = prodStableHash {
|
|
188
|
+
let oldPath = prodStableDir.appendingPathComponent("\(oldStableHash).bundle")
|
|
189
|
+
try? fileManager.removeItem(at: oldPath)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Copy new bundle to stable slot
|
|
193
|
+
try? fileManager.removeItem(at: destPath)
|
|
194
|
+
do {
|
|
195
|
+
try fileManager.copyItem(at: sourcePath, to: destPath)
|
|
196
|
+
} catch {
|
|
197
|
+
return false
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
prodStableHash = newHash
|
|
201
|
+
prodNewHash = nil
|
|
202
|
+
currentProdSlot = .stableSlot
|
|
203
|
+
|
|
204
|
+
return true
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// MARK: - Rollback
|
|
208
|
+
|
|
209
|
+
/// Rollback production: NEW → STABLE → DEFAULT
|
|
210
|
+
func rollbackProd(isAutoRollback: Bool) -> String {
|
|
211
|
+
let currentSlot = currentProdSlot
|
|
212
|
+
|
|
213
|
+
switch currentSlot {
|
|
214
|
+
case .newSlot:
|
|
215
|
+
// Clear new slot
|
|
216
|
+
if let hash = prodNewHash {
|
|
217
|
+
let path = prodNewDir.appendingPathComponent("\(hash).bundle")
|
|
218
|
+
try? fileManager.removeItem(at: path)
|
|
219
|
+
}
|
|
220
|
+
prodNewHash = nil
|
|
221
|
+
prodTempHash = nil
|
|
222
|
+
|
|
223
|
+
// Fall to stable if available
|
|
224
|
+
if prodStableHash != nil {
|
|
225
|
+
currentProdSlot = .stableSlot
|
|
226
|
+
return "ROLLED_BACK_TO_STABLE"
|
|
227
|
+
} else {
|
|
228
|
+
currentProdSlot = .defaultSlot
|
|
229
|
+
return "ROLLED_BACK_TO_DEFAULT"
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
case .stableSlot:
|
|
233
|
+
// Clear stable slot
|
|
234
|
+
if let hash = prodStableHash {
|
|
235
|
+
let path = prodStableDir.appendingPathComponent("\(hash).bundle")
|
|
236
|
+
try? fileManager.removeItem(at: path)
|
|
237
|
+
}
|
|
238
|
+
prodStableHash = nil
|
|
239
|
+
currentProdSlot = .defaultSlot
|
|
240
|
+
return "ROLLED_BACK_TO_DEFAULT"
|
|
241
|
+
|
|
242
|
+
case .defaultSlot:
|
|
243
|
+
return "ALREADY_AT_DEFAULT"
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/// Rollback staging: NEW → DEFAULT
|
|
248
|
+
func rollbackStage() {
|
|
249
|
+
if let hash = stageNewHash {
|
|
250
|
+
let path = stageNewDir.appendingPathComponent("\(hash).bundle")
|
|
251
|
+
try? fileManager.removeItem(at: path)
|
|
252
|
+
}
|
|
253
|
+
stageNewHash = nil
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// MARK: - Version Change Detection
|
|
257
|
+
|
|
258
|
+
/// Check if app binary version changed (app store update).
|
|
259
|
+
/// If so, wipe all OTA bundles and reset to default.
|
|
260
|
+
func checkVersionChange() -> Bool {
|
|
261
|
+
let currentAppVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0.0"
|
|
262
|
+
let lastKnownVersion = userDefaults.string(forKey: "\(prefsKey)_last_known_app_version")
|
|
263
|
+
|
|
264
|
+
if let lastVersion = lastKnownVersion, lastVersion != currentAppVersion {
|
|
265
|
+
// App binary was updated via App Store - wipe all OTA bundles
|
|
266
|
+
fallbackToDefault()
|
|
267
|
+
userDefaults.set(currentAppVersion, forKey: "\(prefsKey)_last_known_app_version")
|
|
268
|
+
return true
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if lastKnownVersion == nil {
|
|
272
|
+
// First launch, just store the version
|
|
273
|
+
userDefaults.set(currentAppVersion, forKey: "\(prefsKey)_last_known_app_version")
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return false
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/// Wipe all OTA bundles and reset to default slot
|
|
280
|
+
func fallbackToDefault() {
|
|
281
|
+
// Clear all hashes
|
|
282
|
+
prodNewHash = nil
|
|
283
|
+
prodStableHash = nil
|
|
284
|
+
prodTempHash = nil
|
|
285
|
+
stageNewHash = nil
|
|
286
|
+
currentProdSlot = .defaultSlot
|
|
287
|
+
|
|
288
|
+
// Clear legacy keys
|
|
289
|
+
userDefaults.removeObject(forKey: "\(prefsKey)_current_bundle_hash")
|
|
290
|
+
userDefaults.removeObject(forKey: "\(prefsKey)_current_bundle_path")
|
|
291
|
+
userDefaults.removeObject(forKey: "\(prefsKey)_previous_bundle_hash")
|
|
292
|
+
userDefaults.removeObject(forKey: "\(prefsKey)_previous_bundle_path")
|
|
293
|
+
userDefaults.removeObject(forKey: "\(prefsKey)_pending_bundle_hash")
|
|
294
|
+
userDefaults.removeObject(forKey: "\(prefsKey)_pending_bundle_path")
|
|
295
|
+
userDefaults.removeObject(forKey: "\(prefsKey)_pending_install_confirmation")
|
|
296
|
+
userDefaults.set(0, forKey: "\(prefsKey)_crash_count")
|
|
297
|
+
|
|
298
|
+
// Delete all bundle directories
|
|
299
|
+
try? fileManager.removeItem(at: prodNewDir)
|
|
300
|
+
try? fileManager.removeItem(at: prodStableDir)
|
|
301
|
+
try? fileManager.removeItem(at: stageNewDir)
|
|
302
|
+
|
|
303
|
+
// Recreate
|
|
304
|
+
ensureSlotDirectories()
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// MARK: - Metadata
|
|
308
|
+
|
|
309
|
+
/// Get full slot metadata as dictionary (for JS bridge)
|
|
310
|
+
func getMetadata() -> [String: Any] {
|
|
311
|
+
return [
|
|
312
|
+
"environment": currentEnvironment.rawValue,
|
|
313
|
+
"prod": [
|
|
314
|
+
"currentSlot": currentProdSlot.rawValue,
|
|
315
|
+
"newHash": prodNewHash as Any,
|
|
316
|
+
"stableHash": prodStableHash as Any,
|
|
317
|
+
"tempHash": prodTempHash as Any,
|
|
318
|
+
],
|
|
319
|
+
"stage": [
|
|
320
|
+
"newHash": stageNewHash as Any,
|
|
321
|
+
]
|
|
322
|
+
]
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// MARK: - Cleanup
|
|
326
|
+
|
|
327
|
+
/// Clean up unused bundles from all slots
|
|
328
|
+
func cleanup() {
|
|
329
|
+
let keepHashes = Set([prodNewHash, prodStableHash, stageNewHash].compactMap { $0 })
|
|
330
|
+
|
|
331
|
+
// Clean prod/new
|
|
332
|
+
cleanDirectory(prodNewDir, keeping: keepHashes)
|
|
333
|
+
// Clean prod/stable
|
|
334
|
+
cleanDirectory(prodStableDir, keeping: keepHashes)
|
|
335
|
+
// Clean stage/new
|
|
336
|
+
cleanDirectory(stageNewDir, keeping: keepHashes)
|
|
337
|
+
|
|
338
|
+
// Clean legacy bundles dir
|
|
339
|
+
let bundlesDir = baseDir.appendingPathComponent("bundles")
|
|
340
|
+
cleanDirectory(bundlesDir, keeping: keepHashes)
|
|
341
|
+
|
|
342
|
+
// Clean temp
|
|
343
|
+
let tempDir = baseDir.appendingPathComponent("temp")
|
|
344
|
+
if let files = try? fileManager.contentsOfDirectory(at: tempDir, includingPropertiesForKeys: nil) {
|
|
345
|
+
for file in files {
|
|
346
|
+
try? fileManager.removeItem(at: file)
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
private func cleanDirectory(_ dir: URL, keeping keepHashes: Set<String>) {
|
|
352
|
+
guard let files = try? fileManager.contentsOfDirectory(at: dir, includingPropertiesForKeys: nil) else { return }
|
|
353
|
+
for file in files {
|
|
354
|
+
let fileHash = file.deletingPathExtension().lastPathComponent
|
|
355
|
+
if !keepHashes.contains(fileHash) {
|
|
356
|
+
try? fileManager.removeItem(at: file)
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#import <React/RCTBridgeModule.h>
|
|
2
|
+
#import <React/RCTEventEmitter.h>
|
|
3
|
+
|
|
4
|
+
@interface RCT_EXTERN_MODULE(SwiftPatch, RCTEventEmitter)
|
|
5
|
+
|
|
6
|
+
RCT_EXTERN_METHOD(initialize:(NSDictionary *)config
|
|
7
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
8
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
9
|
+
|
|
10
|
+
RCT_EXTERN_METHOD(setDebugMode:(BOOL)enabled)
|
|
11
|
+
|
|
12
|
+
RCT_EXTERN_METHOD(getCurrentBundle:(RCTPromiseResolveBlock)resolve
|
|
13
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
14
|
+
|
|
15
|
+
RCT_EXTERN_METHOD(getDeviceId:(RCTPromiseResolveBlock)resolve
|
|
16
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
17
|
+
|
|
18
|
+
RCT_EXTERN_METHOD(getAppVersion:(RCTPromiseResolveBlock)resolve
|
|
19
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
20
|
+
|
|
21
|
+
RCT_EXTERN_METHOD(downloadUpdate:(NSString *)url
|
|
22
|
+
expectedHash:(NSString *)expectedHash
|
|
23
|
+
isPatch:(BOOL)isPatch
|
|
24
|
+
signature:(NSString *)signature
|
|
25
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
26
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
27
|
+
|
|
28
|
+
RCT_EXTERN_METHOD(installUpdate:(NSString *)bundleHash
|
|
29
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
30
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
31
|
+
|
|
32
|
+
RCT_EXTERN_METHOD(hasPendingInstall:(RCTPromiseResolveBlock)resolve
|
|
33
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
34
|
+
|
|
35
|
+
RCT_EXTERN_METHOD(confirmInstall:(RCTPromiseResolveBlock)resolve
|
|
36
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
37
|
+
|
|
38
|
+
RCT_EXTERN_METHOD(restart)
|
|
39
|
+
|
|
40
|
+
RCT_EXTERN_METHOD(rollback:(RCTPromiseResolveBlock)resolve
|
|
41
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
42
|
+
|
|
43
|
+
RCT_EXTERN_METHOD(clearPendingUpdate:(RCTPromiseResolveBlock)resolve
|
|
44
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
45
|
+
|
|
46
|
+
RCT_EXTERN_METHOD(reportStatus:(NSString *)releaseId
|
|
47
|
+
status:(NSString *)status
|
|
48
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
49
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
50
|
+
|
|
51
|
+
+ (BOOL)requiresMainQueueSetup
|
|
52
|
+
{
|
|
53
|
+
return YES;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@end
|