react-native-instantpay-code-push 1.1.8 → 1.2.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/InstantpayCodePush.podspec +5 -1
- package/ios/BundleFileStorageService.swift +1269 -0
- package/ios/BundleMetadata.swift +208 -0
- package/ios/DecompressService.swift +116 -0
- package/ios/FileManagerService.swift +104 -0
- package/ios/HashUtils.swift +73 -0
- package/ios/InstantpayCodePush-Bridging-Header.h +16 -0
- package/ios/InstantpayCodePush.h +39 -1
- package/ios/InstantpayCodePush.mm +332 -4
- package/ios/IpayCodePushHelper.swift +57 -0
- package/ios/IpayCodePushImpl.swift +297 -0
- package/ios/NotificationExtension.swift +13 -0
- package/ios/SignatureVerifier.swift +358 -0
- package/ios/URLSessionDownloadService.swift +251 -0
- package/ios/VersionedPreferencesService.swift +93 -0
- package/ios/ZipDecompressionStrategy.swift +175 -0
- package/package.json +1 -1
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
//
|
|
2
|
+
// BundleMetadata.swift
|
|
3
|
+
// InstantpayCodePush
|
|
4
|
+
//
|
|
5
|
+
// Created by Dhananjay kumar on 09/02/26.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
import Foundation
|
|
9
|
+
|
|
10
|
+
// MARK: - BundleMetadata
|
|
11
|
+
|
|
12
|
+
/// Bundle metadata for managing stable/staging bundles and verification state
|
|
13
|
+
|
|
14
|
+
public struct BundleMetadata: Codable {
|
|
15
|
+
|
|
16
|
+
static let schemaVersion = "metadata-v1"
|
|
17
|
+
static let metadataFilename = "metadata.json"
|
|
18
|
+
static let CLASS_TAG = "*BundleMetadata"
|
|
19
|
+
let CLASS_TAG = "*BundleMetadata"
|
|
20
|
+
|
|
21
|
+
let schema: String
|
|
22
|
+
var isolationKey: String?
|
|
23
|
+
var stableBundleId: String?
|
|
24
|
+
var stagingBundleId: String?
|
|
25
|
+
var verificationPending: Bool
|
|
26
|
+
var verificationAttemptedAt: Double?
|
|
27
|
+
var stagingExecutionCount: Int?
|
|
28
|
+
var updatedAt: Double
|
|
29
|
+
|
|
30
|
+
enum CodingKeys: String, CodingKey {
|
|
31
|
+
case schema
|
|
32
|
+
case isolationKey = "isolation_key"
|
|
33
|
+
case stableBundleId = "stable_bundle_id"
|
|
34
|
+
case stagingBundleId = "staging_bundle_id"
|
|
35
|
+
case verificationPending = "verification_pending"
|
|
36
|
+
case verificationAttemptedAt = "verification_attempted_at"
|
|
37
|
+
case stagingExecutionCount = "staging_execution_count"
|
|
38
|
+
case updatedAt = "updated_at"
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
init(
|
|
42
|
+
schema: String = BundleMetadata.schemaVersion,
|
|
43
|
+
isolationKey: String? = nil,
|
|
44
|
+
stableBundleId: String? = nil,
|
|
45
|
+
stagingBundleId: String? = nil,
|
|
46
|
+
verificationPending: Bool = false,
|
|
47
|
+
verificationAttemptedAt: Double? = nil,
|
|
48
|
+
stagingExecutionCount: Int? = nil,
|
|
49
|
+
updatedAt: Double = Date().timeIntervalSince1970 * 1000
|
|
50
|
+
) {
|
|
51
|
+
self.schema = schema
|
|
52
|
+
self.isolationKey = isolationKey
|
|
53
|
+
self.stableBundleId = stableBundleId
|
|
54
|
+
self.stagingBundleId = stagingBundleId
|
|
55
|
+
self.verificationPending = verificationPending
|
|
56
|
+
self.verificationAttemptedAt = verificationAttemptedAt
|
|
57
|
+
self.stagingExecutionCount = stagingExecutionCount
|
|
58
|
+
self.updatedAt = updatedAt
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
static func load(from file: URL, expectedIsolationKey: String) -> BundleMetadata? {
|
|
62
|
+
guard FileManager.default.fileExists(atPath: file.path) else {
|
|
63
|
+
IpayCodePushHelper.logPrint(classTag: CLASS_TAG, log: "Metadata file does not exist: \(file.path)")
|
|
64
|
+
return nil
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
do {
|
|
68
|
+
let data = try Data(contentsOf: file)
|
|
69
|
+
let decoder = JSONDecoder()
|
|
70
|
+
let metadata = try decoder.decode(BundleMetadata.self, from: data)
|
|
71
|
+
|
|
72
|
+
// Validate isolation key
|
|
73
|
+
if let metadataKey = metadata.isolationKey {
|
|
74
|
+
if metadataKey != expectedIsolationKey {
|
|
75
|
+
IpayCodePushHelper.logPrint(classTag: CLASS_TAG, log: "Isolation key mismatch: expected=\(expectedIsolationKey), got=\(metadataKey)")
|
|
76
|
+
return nil
|
|
77
|
+
}
|
|
78
|
+
} else {
|
|
79
|
+
IpayCodePushHelper.logPrint(classTag: CLASS_TAG, log: "Missing isolation key in metadata, treating as invalid")
|
|
80
|
+
return nil
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return metadata
|
|
84
|
+
} catch {
|
|
85
|
+
IpayCodePushHelper.logPrint(classTag: CLASS_TAG, log: "Failed to load metadata from file: \(error)")
|
|
86
|
+
return nil
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
func save(to file: URL) -> Bool {
|
|
91
|
+
do {
|
|
92
|
+
let encoder = JSONEncoder()
|
|
93
|
+
encoder.outputFormatting = .prettyPrinted
|
|
94
|
+
let data = try encoder.encode(self)
|
|
95
|
+
|
|
96
|
+
// Create directory if needed
|
|
97
|
+
let directory = file.deletingLastPathComponent()
|
|
98
|
+
if !FileManager.default.fileExists(atPath: directory.path) {
|
|
99
|
+
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
try data.write(to: file)
|
|
103
|
+
IpayCodePushHelper.logPrint(classTag: CLASS_TAG, log: "Saved metadata to file: \(file.path)")
|
|
104
|
+
return true
|
|
105
|
+
} catch {
|
|
106
|
+
IpayCodePushHelper.logPrint(classTag: CLASS_TAG, log: "Failed to save metadata to file: \(error)")
|
|
107
|
+
return false
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// MARK: - CrashedBundleEntry
|
|
113
|
+
|
|
114
|
+
/// Entry for a crashed bundle in history
|
|
115
|
+
public struct CrashedBundleEntry: Codable {
|
|
116
|
+
let bundleId: String
|
|
117
|
+
var crashedAt: Double
|
|
118
|
+
var crashCount: Int
|
|
119
|
+
|
|
120
|
+
init(bundleId: String, crashedAt: Double = Date().timeIntervalSince1970 * 1000, crashCount: Int = 1) {
|
|
121
|
+
self.bundleId = bundleId
|
|
122
|
+
self.crashedAt = crashedAt
|
|
123
|
+
self.crashCount = crashCount
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// MARK: - CrashedHistory
|
|
128
|
+
|
|
129
|
+
/// History of crashed bundles
|
|
130
|
+
public struct CrashedHistory: Codable {
|
|
131
|
+
|
|
132
|
+
static let defaultMaxHistorySize = 10
|
|
133
|
+
static let crashedHistoryFilename = "crashed-history.json"
|
|
134
|
+
static let CLASS_TAG = "*BundleMetadata->CrashedHistory"
|
|
135
|
+
var CLASS_TAG = "*BundleMetadata->CrashedHistory"
|
|
136
|
+
|
|
137
|
+
var bundles: [CrashedBundleEntry]
|
|
138
|
+
var maxHistorySize: Int
|
|
139
|
+
|
|
140
|
+
init(bundles: [CrashedBundleEntry] = [], maxHistorySize: Int = CrashedHistory.defaultMaxHistorySize) {
|
|
141
|
+
self.bundles = bundles
|
|
142
|
+
self.maxHistorySize = maxHistorySize
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
static func load(from file: URL) -> CrashedHistory {
|
|
146
|
+
guard FileManager.default.fileExists(atPath: file.path) else {
|
|
147
|
+
IpayCodePushHelper.logPrint(classTag: CLASS_TAG, log: "Crashed history file does not exist, returning empty history")
|
|
148
|
+
return CrashedHistory()
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
do {
|
|
152
|
+
let data = try Data(contentsOf: file)
|
|
153
|
+
let decoder = JSONDecoder()
|
|
154
|
+
let history = try decoder.decode(CrashedHistory.self, from: data)
|
|
155
|
+
return history
|
|
156
|
+
} catch {
|
|
157
|
+
IpayCodePushHelper.logPrint(classTag: CLASS_TAG, log: "Failed to load crashed history from file: \(error)")
|
|
158
|
+
return CrashedHistory()
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
func save(to file: URL) -> Bool {
|
|
163
|
+
do {
|
|
164
|
+
let encoder = JSONEncoder()
|
|
165
|
+
encoder.outputFormatting = .prettyPrinted
|
|
166
|
+
let data = try encoder.encode(self)
|
|
167
|
+
|
|
168
|
+
// Create directory if needed
|
|
169
|
+
let directory = file.deletingLastPathComponent()
|
|
170
|
+
if !FileManager.default.fileExists(atPath: directory.path) {
|
|
171
|
+
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
try data.write(to: file)
|
|
175
|
+
IpayCodePushHelper.logPrint(classTag: CLASS_TAG, log: "Saved crashed history to file: \(file.path)")
|
|
176
|
+
return true
|
|
177
|
+
} catch {
|
|
178
|
+
IpayCodePushHelper.logPrint(classTag: CLASS_TAG, log: "Failed to save crashed history to file: \(error)")
|
|
179
|
+
return false
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
func contains(_ bundleId: String) -> Bool {
|
|
184
|
+
return bundles.contains { $0.bundleId == bundleId }
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
mutating func addEntry(_ bundleId: String) {
|
|
188
|
+
if let index = bundles.firstIndex(where: { $0.bundleId == bundleId }) {
|
|
189
|
+
// Update existing entry
|
|
190
|
+
bundles[index].crashedAt = Date().timeIntervalSince1970 * 1000
|
|
191
|
+
bundles[index].crashCount += 1
|
|
192
|
+
} else {
|
|
193
|
+
// Add new entry
|
|
194
|
+
bundles.append(CrashedBundleEntry(bundleId: bundleId))
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Trim to max size (keep most recent)
|
|
198
|
+
if bundles.count > maxHistorySize {
|
|
199
|
+
bundles.sort { $0.crashedAt < $1.crashedAt }
|
|
200
|
+
bundles = Array(bundles.suffix(maxHistorySize))
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
mutating func clear() {
|
|
205
|
+
bundles.removeAll()
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
//
|
|
2
|
+
// DecompressService.swift
|
|
3
|
+
// InstantpayCodePush
|
|
4
|
+
//
|
|
5
|
+
// Created by Dhananjay kumar on 09/02/26.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
import Foundation
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Protocol for decompression strategies
|
|
12
|
+
*/
|
|
13
|
+
protocol DecompressionStrategy {
|
|
14
|
+
/**
|
|
15
|
+
* Validates if a file can be decompressed by this strategy
|
|
16
|
+
* @param file Path to the file to validate
|
|
17
|
+
* @return true if the file is valid for this strategy
|
|
18
|
+
*/
|
|
19
|
+
func isValid(file: String) -> Bool
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Decompresses a file to the destination directory
|
|
23
|
+
* @param file Path to the compressed file
|
|
24
|
+
* @param destination Path to the destination directory
|
|
25
|
+
* @param progressHandler Callback for progress updates (0.0 - 1.0)
|
|
26
|
+
* @throws Error if decompression fails
|
|
27
|
+
*/
|
|
28
|
+
func decompress(file: String, to destination: String, progressHandler: @escaping (Double) -> Void) throws
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Unified decompression service that uses Strategy pattern to handle multiple compression formats.
|
|
33
|
+
* Automatically detects format by trying each strategy's validation and delegates to appropriate decompression strategy.
|
|
34
|
+
*/
|
|
35
|
+
class DecompressService {
|
|
36
|
+
/// Array of available strategies in order of detection priority
|
|
37
|
+
private let strategies: [DecompressionStrategy]
|
|
38
|
+
let CLASS_TAG = "DecompressService"
|
|
39
|
+
|
|
40
|
+
init() {
|
|
41
|
+
// Order matters: Try ZIP first (clear magic bytes), then TAR.GZ (GZIP magic bytes), then TAR.BR (fallback)
|
|
42
|
+
self.strategies = [
|
|
43
|
+
ZipDecompressionStrategy(),
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Extracts a compressed file to the destination directory.
|
|
49
|
+
* Automatically detects compression format by trying each strategy's validation.
|
|
50
|
+
* @param file Path to the compressed file
|
|
51
|
+
* @param destination Path to the destination directory
|
|
52
|
+
* @param progressHandler Callback for progress updates (0.0 - 1.0)
|
|
53
|
+
* @throws Error if decompression fails or no valid strategy found
|
|
54
|
+
*/
|
|
55
|
+
func unzip(file: String, to destination: String, progressHandler: @escaping (Double) -> Void) throws {
|
|
56
|
+
// Collect file information for better error messages
|
|
57
|
+
let fileURL = URL(fileURLWithPath: file)
|
|
58
|
+
let fileName = fileURL.lastPathComponent
|
|
59
|
+
let fileSize = (try? FileManager.default.attributesOfItem(atPath: file)[.size] as? UInt64) ?? 0
|
|
60
|
+
|
|
61
|
+
// Try each strategy's validation
|
|
62
|
+
for strategy in strategies {
|
|
63
|
+
if strategy.isValid(file: file) {
|
|
64
|
+
IpayCodePushHelper.logPrint(classTag: CLASS_TAG, log: "Using strategy for \(fileName)")
|
|
65
|
+
try strategy.decompress(file: file, to: destination, progressHandler: progressHandler)
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// No valid strategy found - provide detailed error message
|
|
71
|
+
let errorMessage = """
|
|
72
|
+
Failed to decompress file: \(fileName) (\(fileSize) bytes)
|
|
73
|
+
|
|
74
|
+
Tried strategies: ZIP (magic bytes 0x504B0304)
|
|
75
|
+
|
|
76
|
+
Supported formats:
|
|
77
|
+
- ZIP archives (.zip)
|
|
78
|
+
|
|
79
|
+
Please verify the file is not corrupted and matches one of the supported formats.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
IpayCodePushHelper.logPrint(classTag: CLASS_TAG, log: "\(errorMessage)")
|
|
83
|
+
throw NSError(
|
|
84
|
+
domain: "IpayCodePush",
|
|
85
|
+
code: 1,
|
|
86
|
+
userInfo: [NSLocalizedDescriptionKey: errorMessage]
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Extracts a compressed file to the destination directory (without progress tracking).
|
|
92
|
+
* @param file Path to the compressed file
|
|
93
|
+
* @param destination Path to the destination directory
|
|
94
|
+
* @throws Error if decompression fails or no valid strategy found
|
|
95
|
+
*/
|
|
96
|
+
func unzip(file: String, to destination: String) throws {
|
|
97
|
+
try unzip(file: file, to: destination, progressHandler: { _ in })
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Validates if a file is a valid compressed archive.
|
|
102
|
+
* @param file Path to the file to validate
|
|
103
|
+
* @return true if the file is valid for any strategy
|
|
104
|
+
*/
|
|
105
|
+
func isValid(file: String) -> Bool {
|
|
106
|
+
for strategy in strategies {
|
|
107
|
+
if strategy.isValid(file: file) {
|
|
108
|
+
return true
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
IpayCodePushHelper.logPrint(classTag: CLASS_TAG, log: "No valid strategy found for file: \(file)")
|
|
113
|
+
|
|
114
|
+
return false
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
//
|
|
2
|
+
// FileManagerService.swift
|
|
3
|
+
// InstantpayCodePush
|
|
4
|
+
//
|
|
5
|
+
// Created by Dhananjay kumar on 09/02/26.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
import Foundation
|
|
9
|
+
|
|
10
|
+
// MARK: - File System Service
|
|
11
|
+
|
|
12
|
+
enum FileSystemError: Error {
|
|
13
|
+
case createDirectoryFailed(String)
|
|
14
|
+
case fileOperationFailed(String, Error)
|
|
15
|
+
case fileNotFound(String)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
protocol FileSystemService {
|
|
19
|
+
func fileExists(atPath path: String) -> Bool
|
|
20
|
+
func createDirectory(atPath path: String) -> Bool
|
|
21
|
+
func removeItem(atPath path: String) throws
|
|
22
|
+
func moveItem(atPath srcPath: String, toPath dstPath: String) throws
|
|
23
|
+
func copyItem(atPath srcPath: String, toPath dstPath: String) throws
|
|
24
|
+
func contentsOfDirectory(atPath path: String) throws -> [String]
|
|
25
|
+
func attributesOfItem(atPath path: String) throws -> [FileAttributeKey: Any]
|
|
26
|
+
func documentsPath() -> String
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
class FileManagerService: FileSystemService {
|
|
30
|
+
|
|
31
|
+
let CLASS_TAG = "*FileSystemService"
|
|
32
|
+
|
|
33
|
+
private let fileManager = FileManager.default
|
|
34
|
+
|
|
35
|
+
func fileExists(atPath path: String) -> Bool {
|
|
36
|
+
return fileManager.fileExists(atPath: path)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
func createDirectory(atPath path: String) -> Bool {
|
|
40
|
+
do {
|
|
41
|
+
try fileManager.createDirectory(atPath: path, withIntermediateDirectories: true, attributes: nil)
|
|
42
|
+
return true
|
|
43
|
+
} catch let error {
|
|
44
|
+
IpayCodePushHelper.logPrint(classTag: CLASS_TAG, log: "Failed to create directory at \(path): \(error)")
|
|
45
|
+
return false
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
func removeItem(atPath path: String) throws {
|
|
50
|
+
do {
|
|
51
|
+
try fileManager.removeItem(atPath: path)
|
|
52
|
+
} catch let error {
|
|
53
|
+
IpayCodePushHelper.logPrint(classTag: CLASS_TAG, log: "Failed to remove item at \(path): \(error)")
|
|
54
|
+
throw FileSystemError.fileOperationFailed(path, error)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
func moveItem(atPath srcPath: String, toPath dstPath: String) throws {
|
|
59
|
+
do {
|
|
60
|
+
try fileManager.moveItem(atPath: srcPath, toPath: dstPath)
|
|
61
|
+
} catch let error {
|
|
62
|
+
IpayCodePushHelper.logPrint(classTag: CLASS_TAG, log: "Failed to move item from \(srcPath) to \(dstPath): \(error)")
|
|
63
|
+
throw FileSystemError.fileOperationFailed(srcPath, error)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
func copyItem(atPath srcPath: String, toPath dstPath: String) throws {
|
|
68
|
+
do {
|
|
69
|
+
try fileManager.copyItem(atPath: srcPath, toPath: dstPath)
|
|
70
|
+
} catch let error {
|
|
71
|
+
IpayCodePushHelper.logPrint(classTag: CLASS_TAG, log: "Failed to copy item from \(srcPath) to \(dstPath): \(error)")
|
|
72
|
+
throw FileSystemError.fileOperationFailed(srcPath, error)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
func contentsOfDirectory(atPath path: String) throws -> [String] {
|
|
77
|
+
do {
|
|
78
|
+
return try fileManager.contentsOfDirectory(atPath: path)
|
|
79
|
+
} catch let error {
|
|
80
|
+
IpayCodePushHelper.logPrint(classTag: CLASS_TAG, log: "Failed to get directory contents at \(path): \(error)")
|
|
81
|
+
throw FileSystemError.fileOperationFailed(path, error)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
func attributesOfItem(atPath path: String) throws -> [FileAttributeKey: Any] {
|
|
86
|
+
do {
|
|
87
|
+
return try fileManager.attributesOfItem(atPath: path)
|
|
88
|
+
} catch let error {
|
|
89
|
+
IpayCodePushHelper.logPrint(classTag: CLASS_TAG, log: "Failed to get attributes for \(path): \(error)")
|
|
90
|
+
throw FileSystemError.fileOperationFailed(path, error)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
func documentsPath() -> String {
|
|
95
|
+
#if os(tvOS)
|
|
96
|
+
// tvOS doesn't have persistent Documents directory, use Caches instead
|
|
97
|
+
return NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true)[0]
|
|
98
|
+
#else
|
|
99
|
+
return NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
|
|
100
|
+
#endif
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
//
|
|
2
|
+
// HashUtils.swift
|
|
3
|
+
// InstantpayCodePush
|
|
4
|
+
//
|
|
5
|
+
// Created by Dhananjay kumar on 06/02/26.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
import Foundation
|
|
9
|
+
import CryptoKit
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Utility class for file hash operations
|
|
13
|
+
*/
|
|
14
|
+
class HashUtils {
|
|
15
|
+
|
|
16
|
+
public static let CLASS_TAG = "*HashUtils"
|
|
17
|
+
|
|
18
|
+
/// Buffer size for file reading operations (64KB for optimal I/O performance)
|
|
19
|
+
private static let BUFFER_SIZE = 65536
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Calculates SHA256 hash of a file
|
|
23
|
+
* @param fileURL URL of the file to hash
|
|
24
|
+
* @return Hex string of the hash (lowercase), or nil if error occurs
|
|
25
|
+
*/
|
|
26
|
+
static func calculateSHA256(fileURL: URL) -> String? {
|
|
27
|
+
guard let fileHandle = try? FileHandle(forReadingFrom: fileURL) else {
|
|
28
|
+
IpayCodePushHelper.logPrint(classTag: CLASS_TAG, log: "Failed to open file: \(fileURL.path)")
|
|
29
|
+
return nil
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
defer {
|
|
33
|
+
try? fileHandle.close()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
var hasher = SHA256()
|
|
37
|
+
|
|
38
|
+
// Read file in chunks with autoreleasepool for memory efficiency
|
|
39
|
+
while autoreleasepool(invoking: {
|
|
40
|
+
let data = fileHandle.readData(ofLength: BUFFER_SIZE)
|
|
41
|
+
if data.count > 0 {
|
|
42
|
+
hasher.update(data: data)
|
|
43
|
+
return true
|
|
44
|
+
} else {
|
|
45
|
+
return false
|
|
46
|
+
}
|
|
47
|
+
}) { }
|
|
48
|
+
|
|
49
|
+
let digest = hasher.finalize()
|
|
50
|
+
return digest.map { String(format: "%02x", $0) }.joined()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Verifies file hash
|
|
55
|
+
* @param fileURL URL of the file to verify
|
|
56
|
+
* @param expectedHash Expected SHA256 hash (hex string, case-insensitive)
|
|
57
|
+
* @return true if hash matches, false otherwise
|
|
58
|
+
*/
|
|
59
|
+
static func verifyHash(fileURL: URL, expectedHash: String) -> Bool {
|
|
60
|
+
guard let actualHash = calculateSHA256(fileURL: fileURL) else {
|
|
61
|
+
IpayCodePushHelper.logPrint(classTag: CLASS_TAG, log: "Failed to calculate hash")
|
|
62
|
+
return false
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let matches = actualHash.caseInsensitiveCompare(expectedHash) == .orderedSame
|
|
66
|
+
|
|
67
|
+
if !matches {
|
|
68
|
+
IpayCodePushHelper.logPrint(classTag: CLASS_TAG, log: "Hash mismatch - Expected: \(expectedHash), Actual: \(actualHash)")
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return matches
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//
|
|
2
|
+
// InstantpayCodePush-Bridging-Header.h
|
|
3
|
+
// InstantpayCodePush
|
|
4
|
+
//
|
|
5
|
+
// Created by Dhananjay kumar on 06/02/26.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
#ifndef InstantpayCodePush_Bridging_Header_h
|
|
9
|
+
#define InstantpayCodePush_Bridging_Header_h
|
|
10
|
+
|
|
11
|
+
#import "React/RCTBridgeModule.h"
|
|
12
|
+
#import "React/RCTEventEmitter.h"
|
|
13
|
+
#import "React/RCTUtils.h" // Needed for RCTPromiseResolveBlock/RejectBlock in Swift
|
|
14
|
+
#import <SSZipArchive/SSZipArchive.h>
|
|
15
|
+
|
|
16
|
+
#endif /* InstantpayCodePush_Bridging_Header_h */
|
package/ios/InstantpayCodePush.h
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
#import <InstantpayCodePushSpec/InstantpayCodePushSpec.h>
|
|
2
|
+
#import <React/RCTEventEmitter.h>
|
|
3
|
+
#import <React/RCTBundleURLProvider.h>
|
|
2
4
|
|
|
3
|
-
@interface InstantpayCodePush :
|
|
5
|
+
@interface InstantpayCodePush : RCTEventEmitter <NativeInstantpayCodePushSpec>
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Returns the currently active bundle URL from the default (static) instance.
|
|
9
|
+
* Callable from Objective-C (e.g., AppDelegate).
|
|
10
|
+
* This is implemented in InstantpayCodePush.mm and calls the Swift static method.
|
|
11
|
+
*/
|
|
12
|
+
+ (NSURL *)bundleURL;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Returns the currently active bundle URL with specific bundle from the default (static) instance.
|
|
16
|
+
* Callable from Objective-C (e.g., AppDelegate).
|
|
17
|
+
* This is implemented in InstantpayCodePush.mm and calls the Swift static method.
|
|
18
|
+
*/
|
|
19
|
+
+ (NSURL *)bundleURLWithBundle:(NSBundle *)bundle NS_SWIFT_NAME(bundleURL(bundle:));
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Returns the bundle URL for this specific instance.
|
|
23
|
+
* @return The bundle URL for this instance
|
|
24
|
+
*/
|
|
25
|
+
- (NSURL *)bundleURL;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Returns the bundle URL with specific bundle for this specific instance.
|
|
29
|
+
* @return The bundle URL for this instance
|
|
30
|
+
*/
|
|
31
|
+
- (NSURL *)bundleURLWithBundle:(NSBundle *)bundle NS_SWIFT_NAME(bundleURL(bundle:));
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 다운로드 진행 상황 업데이트 시간을 추적하는 속성
|
|
35
|
+
*/
|
|
36
|
+
@property (nonatomic, assign) NSTimeInterval lastUpdateTime;
|
|
37
|
+
|
|
38
|
+
// No need to declare the exported methods (reload, etc.) here
|
|
39
|
+
// as RCT_EXPORT_METHOD handles their exposure to JavaScript.
|
|
40
|
+
// We also don't need to declare supportedEvents or requiresMainQueueSetup here
|
|
41
|
+
// as they are implemented in the .mm file (calling Swift).
|
|
4
42
|
|
|
5
43
|
@end
|