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.
@@ -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 */
@@ -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 : NSObject <NativeInstantpayCodePushSpec>
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