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
|
@@ -1,12 +1,340 @@
|
|
|
1
1
|
#import "InstantpayCodePush.h"
|
|
2
|
+
#import <React/RCTReloadCommand.h>
|
|
3
|
+
#import <React/RCTLog.h>
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
-
|
|
5
|
-
|
|
5
|
+
#if __has_include("InstantpayCodePush/InstantpayCodePush-Swift.h")
|
|
6
|
+
#import "InstantpayCodePush/InstantpayCodePush-Swift.h"
|
|
7
|
+
#else
|
|
8
|
+
#import "InstantpayCodePush-Swift.h"
|
|
9
|
+
#endif
|
|
6
10
|
|
|
7
|
-
|
|
11
|
+
// Define Notification names used for observing Swift Core
|
|
12
|
+
NSNotificationName const IpayCodePushDownloadProgressUpdateNotification = @"IpayCodePushDownloadProgressUpdate";
|
|
13
|
+
NSNotificationName const IpayCodePushDownloadDidFinishNotification = @"IpayCodePushDownloadDidFinish";
|
|
14
|
+
|
|
15
|
+
@implementation InstantpayCodePush {
|
|
16
|
+
bool hasListeners;
|
|
17
|
+
// Keep track of tasks ONLY for removing observers when this ObjC instance is invalidated
|
|
18
|
+
NSMutableSet<NSURLSessionTask *> *observedTasks; // Changed to NSURLSessionTask for broader compatibility if needed
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
+ (BOOL)requiresMainQueueSetup {
|
|
22
|
+
return YES;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
- (instancetype)init {
|
|
26
|
+
self = [super init];
|
|
27
|
+
if (self) {
|
|
28
|
+
observedTasks = [NSMutableSet set];
|
|
29
|
+
|
|
30
|
+
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
31
|
+
selector:@selector(handleDownloadProgress:)
|
|
32
|
+
name:IpayCodePushDownloadProgressUpdateNotification
|
|
33
|
+
object:nil];
|
|
34
|
+
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
35
|
+
selector:@selector(handleDownloadCompletion:)
|
|
36
|
+
name:IpayCodePushDownloadDidFinishNotification
|
|
37
|
+
object:nil];
|
|
38
|
+
|
|
39
|
+
_lastUpdateTime = 0;
|
|
40
|
+
}
|
|
41
|
+
return self;
|
|
8
42
|
}
|
|
9
43
|
|
|
44
|
+
// Clean up observers when module is invalidated or deallocated
|
|
45
|
+
- (void)invalidate {
|
|
46
|
+
RCTLogInfo(@"[InstantpayCodePush.mm] invalidate called, removing observers.");
|
|
47
|
+
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
|
48
|
+
// Swift side should handle KVO observer removal for its tasks
|
|
49
|
+
[super invalidate];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
- (void)dealloc {
|
|
53
|
+
RCTLogInfo(@"[InstantpayCodePush.mm] dealloc called, removing observers.");
|
|
54
|
+
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
//RCT_EXPORT_MODULE();
|
|
58
|
+
|
|
59
|
+
#pragma mark - Singleton Instance
|
|
60
|
+
|
|
61
|
+
// Static singleton IpayCodePushImpl getter
|
|
62
|
+
+ (IpayCodePushImpl *)sharedImpl {
|
|
63
|
+
static IpayCodePushImpl *_sharedImpl = nil;
|
|
64
|
+
static dispatch_once_t onceToken;
|
|
65
|
+
dispatch_once(&onceToken, ^{
|
|
66
|
+
_sharedImpl = [[IpayCodePushImpl alloc] init];
|
|
67
|
+
});
|
|
68
|
+
return _sharedImpl;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
#pragma mark - React Native Constants (Keep getMinBundleId, delegate others)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
// Returns the minimum bundle ID string, either from Info.plist or generated from build timestamp
|
|
75
|
+
|
|
76
|
+
- (NSString *)getMinBundleId {
|
|
77
|
+
static NSString *uuid = nil;
|
|
78
|
+
static dispatch_once_t onceToken;
|
|
79
|
+
|
|
80
|
+
dispatch_once(&onceToken, ^{
|
|
81
|
+
#if DEBUG
|
|
82
|
+
uuid = @"00000000-0000-0000-0000-000000000000";
|
|
83
|
+
#else
|
|
84
|
+
// Step 1: Try to read IPAY_CODE_PUSH_BUILD_TIMESTAMP from Info.plist
|
|
85
|
+
NSDictionary *infoDictionary = [[NSBundle mainBundle] infoDictionary];
|
|
86
|
+
NSString *customValue = infoDictionary[@"IPAY_CODE_PUSH_BUILD_TIMESTAMP"];
|
|
87
|
+
|
|
88
|
+
// Step 2: If custom value exists and is not empty
|
|
89
|
+
if (customValue && customValue.length > 0 && ![customValue isEqualToString:@"$(IPAY_CODE_PUSH_BUILD_TIMESTAMP)"]) {
|
|
90
|
+
// Check if it's a timestamp (pure digits) or UUID
|
|
91
|
+
NSCharacterSet *nonDigits = [[NSCharacterSet decimalDigitCharacterSet] invertedSet];
|
|
92
|
+
BOOL isTimestamp = ([customValue rangeOfCharacterFromSet:nonDigits].location == NSNotFound);
|
|
93
|
+
|
|
94
|
+
if (isTimestamp) {
|
|
95
|
+
// Convert timestamp (milliseconds) to UUID v7
|
|
96
|
+
uint64_t timestampMs = [customValue longLongValue];
|
|
97
|
+
uuid = [self generateUUIDv7FromTimestamp:timestampMs];
|
|
98
|
+
RCTLogInfo(@"[InstantpayCodePush.mm] Using timestamp %@ as MIN_BUNDLE_ID: %@", customValue, uuid);
|
|
99
|
+
} else {
|
|
100
|
+
// Use as UUID directly
|
|
101
|
+
uuid = customValue;
|
|
102
|
+
RCTLogInfo(@"[InstantpayCodePush.mm] Using custom MIN_BUNDLE_ID from Info.plist: %@", uuid);
|
|
103
|
+
}
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Step 3: Fallback to default logic (26-hour subtraction)
|
|
108
|
+
RCTLogInfo(@"[InstantpayCodePush.mm] No custom MIN_BUNDLE_ID found, using default calculation");
|
|
109
|
+
|
|
110
|
+
NSString *compileDateStr = [NSString stringWithFormat:@"%s %s", __DATE__, __TIME__];
|
|
111
|
+
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
|
|
112
|
+
|
|
113
|
+
// Parse __DATE__ __TIME__ as UTC to ensure consistent timezone handling across all build environments
|
|
114
|
+
[formatter setTimeZone:[NSTimeZone timeZoneWithName:@"UTC"]];
|
|
115
|
+
[formatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]];
|
|
116
|
+
[formatter setDateFormat:@"MMM d yyyy HH:mm:ss"]; // Correct format for __DATE__ __TIME__
|
|
117
|
+
NSDate *buildDate = [formatter dateFromString:compileDateStr];
|
|
118
|
+
|
|
119
|
+
if (!buildDate) {
|
|
120
|
+
RCTLogWarn(@"[InstantpayCodePush.mm] Could not parse build date: %@", compileDateStr);
|
|
121
|
+
uuid = @"00000000-0000-0000-0000-000000000000";
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Subtract 26 hours (93600 seconds) to ensure MIN_BUNDLE_ID is always in the past
|
|
126
|
+
// This guarantees that uuidv7-based bundleIds (generated at runtime) will always be newer than MIN_BUNDLE_ID
|
|
127
|
+
// Why 26 hours? Global timezone range spans from UTC-12 to UTC+14 (total 26 hours)
|
|
128
|
+
// By subtracting 26 hours, MIN_BUNDLE_ID becomes a safe "past timestamp" regardless of build timezone
|
|
129
|
+
// Example: Build at 15:00 in any timezone → parse as 15:00 UTC → subtract 26h → 13:00 UTC (previous day)
|
|
130
|
+
|
|
131
|
+
NSTimeInterval adjustedTimestamp = [buildDate timeIntervalSince1970] - 93600.0;
|
|
132
|
+
uint64_t buildTimestampMs = (uint64_t)(adjustedTimestamp * 1000.0);
|
|
133
|
+
|
|
134
|
+
uuid = [self generateUUIDv7FromTimestamp:buildTimestampMs];
|
|
135
|
+
|
|
136
|
+
#endif
|
|
137
|
+
});
|
|
138
|
+
return uuid;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Helper method: Generate UUID v7 from timestamp (milliseconds)
|
|
142
|
+
- (NSString *)generateUUIDv7FromTimestamp:(uint64_t)timestampMs {
|
|
143
|
+
unsigned char bytes[16];
|
|
144
|
+
|
|
145
|
+
// UUID v7 format: timestamp_ms (48 bits) + ver (4 bits) + random (12 bits) + variant (2 bits) + random (62 bits)
|
|
146
|
+
bytes[0] = (timestampMs >> 40) & 0xFF;
|
|
147
|
+
bytes[1] = (timestampMs >> 32) & 0xFF;
|
|
148
|
+
bytes[2] = (timestampMs >> 24) & 0xFF;
|
|
149
|
+
bytes[3] = (timestampMs >> 16) & 0xFF;
|
|
150
|
+
bytes[4] = (timestampMs >> 8) & 0xFF;
|
|
151
|
+
bytes[5] = timestampMs & 0xFF;
|
|
152
|
+
|
|
153
|
+
// Version 7
|
|
154
|
+
bytes[6] = 0x70;
|
|
155
|
+
bytes[7] = 0x00;
|
|
156
|
+
|
|
157
|
+
// Variant bits (10xxxxxx)
|
|
158
|
+
bytes[8] = 0x80;
|
|
159
|
+
bytes[9] = 0x00;
|
|
160
|
+
|
|
161
|
+
// Remaining bytes (zeros for deterministic MIN_BUNDLE_ID)
|
|
162
|
+
for (int i = 10; i < 16; i++) {
|
|
163
|
+
bytes[i] = 0x00;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return [NSString stringWithFormat:
|
|
167
|
+
@"%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x",
|
|
168
|
+
bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
|
|
169
|
+
bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15]];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
- (NSDictionary *)_buildConstantsDictionary {
|
|
173
|
+
return @{
|
|
174
|
+
@"MIN_BUNDLE_ID": [self getMinBundleId] ?: [NSNull null],
|
|
175
|
+
@"APP_VERSION": [IpayCodePushImpl appVersion] ?: [NSNull null],
|
|
176
|
+
@"CHANNEL": [[InstantpayCodePush sharedImpl] getChannel] ?: [NSNull null],
|
|
177
|
+
@"FINGERPRINT_HASH": [[InstantpayCodePush sharedImpl] getFingerprintHash] ?: [NSNull null]
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Get bundleURL with default bundle using singleton
|
|
182
|
+
+ (NSURL *)bundleURL {
|
|
183
|
+
return [[InstantpayCodePush sharedImpl] bundleURLWithBundle:[NSBundle mainBundle]];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Get bundleURL with specific bundle using singleton
|
|
187
|
+
+ (NSURL *)bundleURLWithBundle:(NSBundle *)bundle {
|
|
188
|
+
return [[InstantpayCodePush sharedImpl] bundleURLWithBundle:bundle];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Get bundleURL with default bundle using instance impl
|
|
192
|
+
- (NSURL *)bundleURL {
|
|
193
|
+
return [[InstantpayCodePush sharedImpl] bundleURLWithBundle:[NSBundle mainBundle]];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Get bundleURL with specific bundle using instance impl
|
|
197
|
+
- (NSURL *)bundleURLWithBundle:(NSBundle *)bundle {
|
|
198
|
+
return [[InstantpayCodePush sharedImpl] bundleURLWithBundle:bundle];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
#pragma mark - Progress Updates & Event Emitting (Keep in ObjC Wrapper)
|
|
202
|
+
|
|
203
|
+
- (void)handleDownloadProgress:(NSNotification *)notification {
|
|
204
|
+
if (!hasListeners) return;
|
|
205
|
+
|
|
206
|
+
NSDictionary *userInfo = notification.userInfo;
|
|
207
|
+
NSNumber *progressNum = userInfo[@"progress"];
|
|
208
|
+
|
|
209
|
+
if (progressNum) {
|
|
210
|
+
double progress = [progressNum doubleValue];
|
|
211
|
+
NSTimeInterval currentTime = [[NSDate date] timeIntervalSince1970] * 1000;
|
|
212
|
+
if ((currentTime - self.lastUpdateTime) >= 100 || progress >= 1.0) {
|
|
213
|
+
self.lastUpdateTime = currentTime;
|
|
214
|
+
[self sendEvent:@"onProgress" body:@{@"progress": @(progress)}];
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
- (void)handleDownloadCompletion:(NSNotification *)notification {
|
|
220
|
+
NSURLSessionTask *task = notification.object; // Task that finished
|
|
221
|
+
RCTLogInfo(@"[InstantpayCodePush.mm] Received download completion notification for task: %@", task.originalRequest.URL);
|
|
222
|
+
// Swift side handles KVO observer removal internally now when task finishes.
|
|
223
|
+
// No specific cleanup needed here based on this notification anymore.
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
#pragma mark - React Native Events (Keep as is)
|
|
227
|
+
|
|
228
|
+
- (NSArray<NSString *> *)supportedEvents {
|
|
229
|
+
return @[@"onProgress"];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
- (void)startObserving {
|
|
233
|
+
hasListeners = YES;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
- (void)stopObserving {
|
|
237
|
+
hasListeners = NO;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
- (void)sendEvent:(NSString *)name body:(id)body {
|
|
241
|
+
if (hasListeners) {
|
|
242
|
+
[self sendEventWithName:name body:body];
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
#pragma mark - React Native Exports
|
|
247
|
+
|
|
248
|
+
- (void)reload:(RCTPromiseResolveBlock)resolve
|
|
249
|
+
reject:(RCTPromiseRejectBlock)reject {
|
|
250
|
+
RCTLogInfo(@"[InstantpayCodePush.mm] IpayCodePush requested a reload");
|
|
251
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
252
|
+
@try {
|
|
253
|
+
IpayCodePushImpl *impl = [InstantpayCodePush sharedImpl];
|
|
254
|
+
NSURL *bundleURL = [impl bundleURLWithBundle:[NSBundle mainBundle]];
|
|
255
|
+
RCTLogInfo(@"[InstantpayCodePush.mm] Reloading with bundle URL: %@", bundleURL);
|
|
256
|
+
if (bundleURL && super.bridge) {
|
|
257
|
+
[super.bridge setValue:bundleURL forKey:@"bundleURL"];
|
|
258
|
+
} else if (!super.bridge) {
|
|
259
|
+
RCTLogWarn(@"[InstantpayCodePush.mm] Bridge is nil, cannot set bundleURL for reload.");
|
|
260
|
+
}
|
|
261
|
+
RCTTriggerReloadCommandListeners(@"IpayCodePush requested a reload");
|
|
262
|
+
resolve(nil);
|
|
263
|
+
} @catch (NSError *error) {
|
|
264
|
+
RCTLogError(@"[InstantpayCodePush.mm] Failed to reload: %@", error);
|
|
265
|
+
reject(@"RELOAD_ERROR", error.description, error);
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
- (void)updateBundle:(JS::NativeInstantpayCodePush::UpdateBundleParams &)params
|
|
271
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
272
|
+
reject:(RCTPromiseRejectBlock)reject {
|
|
273
|
+
NSLog(@"[InstantpayCodePush.mm] updateBundle called.");
|
|
274
|
+
NSMutableDictionary *paramDict = [NSMutableDictionary dictionary];
|
|
275
|
+
if (params.bundleId()) {
|
|
276
|
+
paramDict[@"bundleId"] = params.bundleId();
|
|
277
|
+
}
|
|
278
|
+
if (params.fileUrl()) {
|
|
279
|
+
paramDict[@"fileUrl"] = params.fileUrl();
|
|
280
|
+
}
|
|
281
|
+
if (params.fileHash()) {
|
|
282
|
+
paramDict[@"fileHash"] = params.fileHash();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
IpayCodePushImpl *impl = [InstantpayCodePush sharedImpl];
|
|
286
|
+
[impl updateBundle:paramDict resolver:resolve rejecter:reject];
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
- (NSDictionary *)notifyAppReady:(JS::NativeInstantpayCodePush::SpecNotifyAppReadyParams &)params {
|
|
290
|
+
NSString *bundleId = nil;
|
|
291
|
+
if (params.bundleId()) {
|
|
292
|
+
bundleId = params.bundleId();
|
|
293
|
+
}
|
|
294
|
+
NSLog(@"[InstantpayCodePush.mm] notifyAppReady called with bundleId: %@", bundleId);
|
|
295
|
+
IpayCodePushImpl *impl = [InstantpayCodePush sharedImpl];
|
|
296
|
+
return [impl notifyAppReadyWithBundleId:bundleId];
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
- (NSArray<NSString *> *)getCrashHistory {
|
|
300
|
+
NSLog(@"[InstantpayCodePush.mm] getCrashHistory called");
|
|
301
|
+
IpayCodePushImpl *impl = [InstantpayCodePush sharedImpl];
|
|
302
|
+
NSArray<NSString *> *crashHistory = [impl getCrashHistory];
|
|
303
|
+
return crashHistory ?: @[];
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
- (NSNumber *)clearCrashHistory {
|
|
307
|
+
NSLog(@"[InstantpayCodePush.mm] clearCrashHistory called");
|
|
308
|
+
IpayCodePushImpl *impl = [InstantpayCodePush sharedImpl];
|
|
309
|
+
BOOL result = [impl clearCrashHistory];
|
|
310
|
+
return @(result);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
- (NSString *)getBaseURL {
|
|
314
|
+
NSLog(@"[InstantpayCodePush.mm] getBaseURL called");
|
|
315
|
+
IpayCodePushImpl *impl = [InstantpayCodePush sharedImpl];
|
|
316
|
+
NSString *baseURL = [impl getBaseURL];
|
|
317
|
+
return baseURL ?: @"";
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
- (facebook::react::ModuleConstants<JS::NativeInstantpayCodePush::Constants::Builder>)constantsToExport {
|
|
321
|
+
return [self getConstants];
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
- (facebook::react::ModuleConstants<JS::NativeInstantpayCodePush::Constants::Builder>)getConstants {
|
|
325
|
+
IpayCodePushImpl *impl = [InstantpayCodePush sharedImpl];
|
|
326
|
+
return facebook::react::typedConstants<JS::NativeInstantpayCodePush::Constants::Builder>({
|
|
327
|
+
.MIN_BUNDLE_ID = [self getMinBundleId],
|
|
328
|
+
.APP_VERSION = [IpayCodePushImpl appVersion],
|
|
329
|
+
.CHANNEL = [impl getChannel],
|
|
330
|
+
.FINGERPRINT_HASH = [impl getFingerprintHash],
|
|
331
|
+
.KEYSTORE_PUBLIC_KEY = nil
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
#pragma mark - Turbo Module Support (Keep as is)
|
|
336
|
+
|
|
337
|
+
|
|
10
338
|
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
|
|
11
339
|
(const facebook::react::ObjCTurboModule::InitParams &)params
|
|
12
340
|
{
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
//
|
|
2
|
+
// IpayCodePushHelper.swift
|
|
3
|
+
// InstantpayCodePush
|
|
4
|
+
//
|
|
5
|
+
// Created by Dhananjay kumar on 06/02/26.
|
|
6
|
+
//
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
@objcMembers public class IpayCodePushHelper {
|
|
10
|
+
|
|
11
|
+
private init() {}
|
|
12
|
+
|
|
13
|
+
static let MAIN_LOG_TAG: String = "*IpayCodePush -> ";
|
|
14
|
+
|
|
15
|
+
let WARNING_LOG = "WARNING_LOG"
|
|
16
|
+
|
|
17
|
+
let ERROR_LOG = "ERROR_LOG"
|
|
18
|
+
|
|
19
|
+
static func logPrint(classTag: String, log: String?) -> Void {
|
|
20
|
+
if(log == nil){
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let fullTagName = "\(MAIN_LOG_TAG) \(classTag)"
|
|
25
|
+
|
|
26
|
+
if let isEnableLog: Bool = Bundle.main.object(forInfoDictionaryKey: "IpayCodePush_Log") as! Bool? {
|
|
27
|
+
if(isEnableLog){
|
|
28
|
+
let logger = Logger(subsystem: "com.instantpay.ipayCodePush", category: "IpayCodePushHelper")
|
|
29
|
+
logger.info("\(fullTagName)")
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
static func logPrint(logType: String, classTag: String, log: String?){
|
|
35
|
+
if(log == nil){
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let fullTagName = "\(MAIN_LOG_TAG) \(classTag)"
|
|
40
|
+
|
|
41
|
+
if let isEnableLog: Bool = Bundle.main.object(forInfoDictionaryKey: "IpayCodePush_Log") as! Bool? {
|
|
42
|
+
if(isEnableLog){
|
|
43
|
+
let logger = Logger(subsystem: "com.instantpay.ipayCodePush", category: "IpayCodePushHelper")
|
|
44
|
+
if(logType == "WARNING_LOG"){
|
|
45
|
+
logger.warning("\(fullTagName)")
|
|
46
|
+
}
|
|
47
|
+
else if(logType == "ERROR_LOG"){
|
|
48
|
+
logger.error("\(fullTagName)")
|
|
49
|
+
}
|
|
50
|
+
else{
|
|
51
|
+
logger.info("\(fullTagName)")
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
}
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
//
|
|
2
|
+
// IpayCodePushImpl.swift
|
|
3
|
+
// InstantpayCodePush
|
|
4
|
+
//
|
|
5
|
+
// Created by Dhananjay kumar on 06/02/26.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
import Foundation
|
|
9
|
+
import React
|
|
10
|
+
|
|
11
|
+
@objcMembers public class IpayCodePushImpl: NSObject {
|
|
12
|
+
|
|
13
|
+
private let bundleStorage: BundleStorageService
|
|
14
|
+
private let preferences: PreferencesService
|
|
15
|
+
|
|
16
|
+
let CLASS_TAG = "*IpayCodePushImpl"
|
|
17
|
+
|
|
18
|
+
private static let DEFAULT_CHANNEL = "production"
|
|
19
|
+
|
|
20
|
+
// MARK: - Initialization
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Convenience initializer that creates and configures all dependencies.
|
|
24
|
+
*/
|
|
25
|
+
public convenience override init() {
|
|
26
|
+
let fileSystem = FileManagerService()
|
|
27
|
+
let isolationKey = IpayCodePushImpl.getIsolationKey()
|
|
28
|
+
let preferences = VersionedPreferencesService()
|
|
29
|
+
let downloadService = URLSessionDownloadService()
|
|
30
|
+
let decompressService = DecompressService()
|
|
31
|
+
|
|
32
|
+
let bundleStorage = BundleFileStorageService(
|
|
33
|
+
fileSystem: fileSystem,
|
|
34
|
+
downloadService: downloadService,
|
|
35
|
+
decompressService: decompressService,
|
|
36
|
+
preferences: preferences,
|
|
37
|
+
isolationKey: isolationKey
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
self.init(bundleStorage: bundleStorage, preferences: preferences)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Primary initializer with dependency injection.
|
|
45
|
+
* @param bundleStorage Service for bundle storage operations
|
|
46
|
+
* @param preferences Service for preference storage
|
|
47
|
+
*/
|
|
48
|
+
internal init(bundleStorage: BundleStorageService, preferences: PreferencesService) {
|
|
49
|
+
self.bundleStorage = bundleStorage
|
|
50
|
+
self.preferences = preferences
|
|
51
|
+
super.init()
|
|
52
|
+
|
|
53
|
+
// Configure preferences with isolation key
|
|
54
|
+
let isolationKey = IpayCodePushImpl.getIsolationKey()
|
|
55
|
+
(preferences as? VersionedPreferencesService)?.configure(isolationKey: isolationKey)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// MARK: - Static Properties
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Returns the app version from main bundle info.
|
|
62
|
+
*/
|
|
63
|
+
public static var appVersion: String? {
|
|
64
|
+
return Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Returns the app version from main bundle info.
|
|
69
|
+
*/
|
|
70
|
+
public static var appChannel: String {
|
|
71
|
+
return Bundle.main.object(forInfoDictionaryKey: "IPAY_CODE_PUSH_CHANNEL") as? String ?? Self.DEFAULT_CHANNEL
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Gets the complete isolation key for preferences storage.
|
|
76
|
+
* @return The isolation key in format: ipaycodepush_{fingerprintOrVersion}_{channel}_
|
|
77
|
+
*/
|
|
78
|
+
public static func getIsolationKey() -> String {
|
|
79
|
+
// Get fingerprint hash from Info.plist
|
|
80
|
+
let fingerprintHash = Bundle.main.object(forInfoDictionaryKey: "IPAY_CODE_PUSH_FINGERPRINT_HASH") as? String
|
|
81
|
+
|
|
82
|
+
// Get app version and channel
|
|
83
|
+
let appVersion = self.appVersion ?? "unknown"
|
|
84
|
+
let appChannel = self.appChannel
|
|
85
|
+
|
|
86
|
+
// Include both fingerprint hash and app version for complete isolation
|
|
87
|
+
let baseKey: String
|
|
88
|
+
if let hash = fingerprintHash, !hash.isEmpty {
|
|
89
|
+
baseKey = "\(hash)_\(appVersion)"
|
|
90
|
+
} else {
|
|
91
|
+
baseKey = appVersion
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return "ipaycodepush_\(baseKey)_\(appChannel)_"
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// MARK: - Channel Management
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Gets the current update channel.
|
|
101
|
+
* @return The channel name or nil if not set
|
|
102
|
+
*/
|
|
103
|
+
public func getChannel() -> String {
|
|
104
|
+
return Bundle.main.object(forInfoDictionaryKey: "IPAY_CODE_PUSH_CHANNEL") as? String ?? Self.DEFAULT_CHANNEL
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Gets the current fingerprint hash.
|
|
109
|
+
* @return The fingerprint hash or nil if not set
|
|
110
|
+
*/
|
|
111
|
+
public func getFingerprintHash() -> String? {
|
|
112
|
+
return Bundle.main.object(forInfoDictionaryKey: "IPAY_CODE_PUSH_FINGERPRINT_HASH") as? String
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// MARK: - Bundle URL Management
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Gets the URL to the bundle file.
|
|
119
|
+
* @param bundle instance to lookup the JavaScript bundle resource. Defaults to Bundle.main.
|
|
120
|
+
* @return URL to the bundle or nil
|
|
121
|
+
*/
|
|
122
|
+
public func bundleURL(bundle: Bundle = Bundle.main) -> URL? {
|
|
123
|
+
return bundleStorage.getBundleURL(bundle: bundle)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// MARK: - Bundle Update
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Updates the bundle from JavaScript bridge.
|
|
130
|
+
* This method acts as the primary error boundary for all bundle operations.
|
|
131
|
+
* @param params Dictionary with bundleId and fileUrl parameters
|
|
132
|
+
* @param resolve Promise resolve callback
|
|
133
|
+
* @param reject Promise reject callback
|
|
134
|
+
*/
|
|
135
|
+
public func updateBundle(
|
|
136
|
+
_ params: NSDictionary?,
|
|
137
|
+
resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
138
|
+
rejecter reject: @escaping RCTPromiseRejectBlock
|
|
139
|
+
) {
|
|
140
|
+
|
|
141
|
+
do {
|
|
142
|
+
// Validate parameters (this runs on calling thread - typically JS thread)
|
|
143
|
+
guard let data = params else {
|
|
144
|
+
let error = NSError(domain: "IpayCodePush", code: 0,
|
|
145
|
+
userInfo: [NSLocalizedDescriptionKey: "Missing or invalid parameters for updateBundle"])
|
|
146
|
+
reject("UNKNOWN_ERROR", error.localizedDescription, error)
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
guard let bundleId = data["bundleId"] as? String, !bundleId.isEmpty else {
|
|
151
|
+
let error = NSError(domain: "IpayCodePush", code: 0,
|
|
152
|
+
userInfo: [NSLocalizedDescriptionKey: "Missing or empty 'bundleId'"])
|
|
153
|
+
reject("MISSING_BUNDLE_ID", error.localizedDescription, error)
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
let fileUrlString = data["fileUrl"] as? String ?? ""
|
|
158
|
+
|
|
159
|
+
var fileUrl: URL? = nil
|
|
160
|
+
if !fileUrlString.isEmpty {
|
|
161
|
+
guard let url = URL(string: fileUrlString) else {
|
|
162
|
+
let error = NSError(domain: "IpayCodePush", code: 0,
|
|
163
|
+
userInfo: [NSLocalizedDescriptionKey: "Invalid 'fileUrl' provided: \(fileUrlString)"])
|
|
164
|
+
reject("INVALID_FILE_URL", error.localizedDescription, error)
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
fileUrl = url
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Extract fileHash if provided
|
|
171
|
+
let fileHash = data["fileHash"] as? String
|
|
172
|
+
|
|
173
|
+
// Extract progress callback if provided
|
|
174
|
+
let progressCallback = data["progressCallback"] as? RCTResponseSenderBlock
|
|
175
|
+
|
|
176
|
+
IpayCodePushHelper.logPrint(classTag: CLASS_TAG, log: "updateBundle called with bundleId: \(bundleId), fileUrl: \(fileUrl?.absoluteString ?? "nil"), fileHash: \(fileHash ?? "nil")")
|
|
177
|
+
|
|
178
|
+
// Heavy work is delegated to bundle storage service with safe error handling
|
|
179
|
+
bundleStorage.updateBundle(bundleId: bundleId, fileUrl: fileUrl, fileHash: fileHash, progressHandler: { progress in
|
|
180
|
+
// Call JS progress callback if provided
|
|
181
|
+
if let callback = progressCallback {
|
|
182
|
+
DispatchQueue.main.async {
|
|
183
|
+
callback([progress])
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}) { [weak self] result in
|
|
187
|
+
guard self != nil else {
|
|
188
|
+
let error = NSError(domain: "IpayCodePush", code: 0,
|
|
189
|
+
userInfo: [NSLocalizedDescriptionKey: "Internal error: self deallocated during update"])
|
|
190
|
+
DispatchQueue.main.async {
|
|
191
|
+
reject("SELF_DEALLOCATED", error.localizedDescription, error)
|
|
192
|
+
}
|
|
193
|
+
return
|
|
194
|
+
}
|
|
195
|
+
// Return results on main thread for React Native bridge
|
|
196
|
+
DispatchQueue.main.async {
|
|
197
|
+
switch result {
|
|
198
|
+
case .success:
|
|
199
|
+
IpayCodePushHelper.logPrint(classTag: self!.CLASS_TAG, log: "Update successful for \(bundleId). Resolving promise.")
|
|
200
|
+
resolve(true)
|
|
201
|
+
case .failure(let error):
|
|
202
|
+
IpayCodePushHelper.logPrint(classTag: self!.CLASS_TAG, log: "Update failed for \(bundleId) - Error: \(error)")
|
|
203
|
+
|
|
204
|
+
let normalizedCode = IpayCodePushImpl.normalizeErrorCode(from: error)
|
|
205
|
+
let nsError = error as NSError
|
|
206
|
+
reject(normalizedCode, nsError.localizedDescription, nsError)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
} catch let error {
|
|
211
|
+
// Main error boundary - catch and convert all errors to JS rejection
|
|
212
|
+
let nsError = error as NSError
|
|
213
|
+
IpayCodePushHelper.logPrint(classTag: CLASS_TAG, log: "Error in updateBundleFromJS - Domain: \(nsError.domain), Code: \(nsError.code), Description: \(nsError.localizedDescription)")
|
|
214
|
+
|
|
215
|
+
reject("UNKNOWN_ERROR", nsError.localizedDescription, nsError)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Normalizes native errors to a small, predictable set of JS-facing error codes.
|
|
221
|
+
* Rare or platform-specific codes are collapsed to UNKNOWN_ERROR to reduce surface area.
|
|
222
|
+
*/
|
|
223
|
+
private static func normalizeErrorCode(from error: Error) -> String {
|
|
224
|
+
let baseCode: String
|
|
225
|
+
|
|
226
|
+
if let storageError = error as? BundleStorageError {
|
|
227
|
+
// Collapse signature sub-errors into a single public code
|
|
228
|
+
if case .signatureVerificationFailed = storageError {
|
|
229
|
+
baseCode = "SIGNATURE_VERIFICATION_FAILED"
|
|
230
|
+
} else {
|
|
231
|
+
baseCode = storageError.errorCodeString
|
|
232
|
+
}
|
|
233
|
+
} else if error is SignatureVerificationError {
|
|
234
|
+
baseCode = "SIGNATURE_VERIFICATION_FAILED"
|
|
235
|
+
} else {
|
|
236
|
+
baseCode = "UNKNOWN_ERROR"
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return userFacingErrorCodes.contains(baseCode) ? baseCode : "UNKNOWN_ERROR"
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Error codes we intentionally expose to JS callers.
|
|
243
|
+
private static let userFacingErrorCodes: Set<String> = [
|
|
244
|
+
"MISSING_BUNDLE_ID",
|
|
245
|
+
"INVALID_FILE_URL",
|
|
246
|
+
"DIRECTORY_CREATION_FAILED",
|
|
247
|
+
"DOWNLOAD_FAILED",
|
|
248
|
+
"INCOMPLETE_DOWNLOAD",
|
|
249
|
+
"EXTRACTION_FORMAT_ERROR",
|
|
250
|
+
"INVALID_BUNDLE",
|
|
251
|
+
"INSUFFICIENT_DISK_SPACE",
|
|
252
|
+
"SIGNATURE_VERIFICATION_FAILED",
|
|
253
|
+
"MOVE_OPERATION_FAILED",
|
|
254
|
+
"BUNDLE_IN_CRASHED_HISTORY",
|
|
255
|
+
"SELF_DEALLOCATED",
|
|
256
|
+
"UNKNOWN_ERROR",
|
|
257
|
+
]
|
|
258
|
+
|
|
259
|
+
// MARK: - Rollback Support
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Notifies the system that the app has successfully started with the given bundle.
|
|
263
|
+
* If the bundle matches the staging bundle, it promotes to stable.
|
|
264
|
+
* @param bundleId The ID of the currently running bundle
|
|
265
|
+
* @return true if promotion was successful or no action was needed
|
|
266
|
+
*/
|
|
267
|
+
public func notifyAppReady(bundleId: String) -> [String: Any] {
|
|
268
|
+
return bundleStorage.notifyAppReady(bundleId: bundleId)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Gets the crashed bundle history.
|
|
273
|
+
* @return Array of crashed bundle IDs
|
|
274
|
+
*/
|
|
275
|
+
public func getCrashHistory() -> [String] {
|
|
276
|
+
return bundleStorage.getCrashHistory().bundles.map { $0.bundleId }
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Clears the crashed bundle history.
|
|
281
|
+
* @return true if clearing was successful
|
|
282
|
+
*/
|
|
283
|
+
public func clearCrashHistory() -> Bool {
|
|
284
|
+
return bundleStorage.clearCrashHistory()
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Gets the base URL for the current active bundle directory.
|
|
289
|
+
* Returns the file:// URL to the bundle directory with trailing slash.
|
|
290
|
+
* This is used for Expo DOM components to construct full asset paths.
|
|
291
|
+
* @return Base URL string (e.g., "file:///var/.../bundle-store/abc123/") or empty string
|
|
292
|
+
*/
|
|
293
|
+
public func getBaseURL() -> String {
|
|
294
|
+
return bundleStorage.getBaseURL()
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
}
|