react-native-stallion 2.3.0-alpha.4 → 2.3.0-alpha.6

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.
Files changed (98) hide show
  1. package/android/build.gradle +25 -0
  2. package/android/src/main/cpp/CMakeLists.txt +19 -0
  3. package/android/src/main/cpp/stallion_signal_handler.cpp +91 -0
  4. package/android/src/main/java/com/stallion/Stallion.java +0 -2
  5. package/android/src/main/java/com/stallion/StallionModule.java +37 -1
  6. package/android/src/main/java/com/stallion/events/StallionEventConstants.java +1 -0
  7. package/android/src/main/java/com/stallion/events/StallionEventManager.java +23 -15
  8. package/android/src/main/java/com/stallion/networkmanager/StallionFileDownloader.java +8 -5
  9. package/android/src/main/java/com/stallion/networkmanager/StallionSyncHandler.java +18 -1
  10. package/android/src/main/java/com/stallion/storage/StallionMeta.java +52 -1
  11. package/android/src/main/java/com/stallion/storage/StallionStateManager.java +22 -1
  12. package/android/src/main/java/com/stallion/utils/StallionDeviceInfo.java +83 -0
  13. package/android/src/main/java/com/stallion/utils/StallionExceptionHandler.java +162 -29
  14. package/ios/Stallion.xcodeproj/project.pbxproj +6 -0
  15. package/ios/main/Stallion.swift +20 -0
  16. package/ios/main/StallionConstants.swift +1 -0
  17. package/ios/main/StallionDeviceInfo.swift +105 -0
  18. package/ios/main/StallionEventHandler.m +3 -1
  19. package/ios/main/StallionExceptionHandler.h +1 -0
  20. package/ios/main/StallionExceptionHandler.mm +379 -0
  21. package/ios/main/StallionFileDownloader.swift +9 -1
  22. package/ios/main/StallionMeta.h +11 -0
  23. package/ios/main/StallionMeta.m +76 -1
  24. package/ios/main/StallionSlotManager.m +2 -2
  25. package/ios/main/StallionStateManager.m +29 -0
  26. package/ios/main/StallionSyncHandler.swift +14 -2
  27. package/package.json +1 -1
  28. package/react-native-stallion.podspec +22 -0
  29. package/src/main/components/modules/listing/components/BundleCardInfoSection.js +1 -1
  30. package/src/main/components/modules/listing/components/styles/index.js +18 -13
  31. package/src/main/components/modules/listing/components/styles/index.js.map +1 -1
  32. package/src/main/components/modules/listing/index.js +15 -15
  33. package/src/main/components/modules/listing/index.js.map +1 -1
  34. package/src/main/components/modules/listing/styles.js +2 -1
  35. package/src/main/components/modules/listing/styles.js.map +1 -1
  36. package/src/main/components/modules/login/index.js +8 -5
  37. package/src/main/components/modules/login/index.js.map +1 -1
  38. package/src/main/components/modules/login/styles/index.js +10 -4
  39. package/src/main/components/modules/login/styles/index.js.map +1 -1
  40. package/src/main/components/modules/modal/StallionModal.js +2 -2
  41. package/src/main/components/modules/modal/StallionModal.js.map +1 -1
  42. package/src/main/components/modules/prod/prod.js +5 -4
  43. package/src/main/components/modules/prod/prod.js.map +1 -1
  44. package/src/main/components/modules/prod/styles/index.js +7 -4
  45. package/src/main/components/modules/prod/styles/index.js.map +1 -1
  46. package/src/main/constants/appConstants.js +1 -0
  47. package/src/main/constants/appConstants.js.map +1 -1
  48. package/src/main/state/actionCreators/useUpdateMetaActions.js +2 -2
  49. package/src/main/state/actionCreators/useUpdateMetaActions.js.map +1 -1
  50. package/src/main/state/index.js +5 -3
  51. package/src/main/state/index.js.map +1 -1
  52. package/src/main/state/reducers/updateMetaReducer.js +8 -0
  53. package/src/main/state/reducers/updateMetaReducer.js.map +1 -1
  54. package/src/main/state/useStallionEvents.js +27 -4
  55. package/src/main/state/useStallionEvents.js.map +1 -1
  56. package/src/main/utils/ErrorBoundary.js +45 -11
  57. package/src/main/utils/ErrorBoundary.js.map +1 -1
  58. package/src/main/utils/crashState.js +16 -0
  59. package/src/main/utils/crashState.js.map +1 -0
  60. package/src/main/utils/useStallionUpdate.js +2 -2
  61. package/src/main/utils/useStallionUpdate.js.map +1 -1
  62. package/src/main/utils/withStallion.js +4 -2
  63. package/src/main/utils/withStallion.js.map +1 -1
  64. package/src/types/updateMeta.types.js +1 -0
  65. package/src/types/updateMeta.types.js.map +1 -1
  66. package/types/main/components/modules/listing/components/styles/index.d.ts +9 -4
  67. package/types/main/components/modules/listing/components/styles/index.d.ts.map +1 -1
  68. package/types/main/components/modules/listing/index.d.ts.map +1 -1
  69. package/types/main/components/modules/listing/styles.d.ts +1 -0
  70. package/types/main/components/modules/listing/styles.d.ts.map +1 -1
  71. package/types/main/components/modules/login/index.d.ts.map +1 -1
  72. package/types/main/components/modules/login/styles/index.d.ts +6 -0
  73. package/types/main/components/modules/login/styles/index.d.ts.map +1 -1
  74. package/types/main/components/modules/prod/prod.d.ts.map +1 -1
  75. package/types/main/components/modules/prod/styles/index.d.ts +5 -2
  76. package/types/main/components/modules/prod/styles/index.d.ts.map +1 -1
  77. package/types/main/constants/appConstants.d.ts +2 -0
  78. package/types/main/constants/appConstants.d.ts.map +1 -1
  79. package/types/main/index.d.ts +1 -1
  80. package/types/main/state/actionCreators/useUpdateMetaActions.d.ts.map +1 -1
  81. package/types/main/state/index.d.ts +4 -1
  82. package/types/main/state/index.d.ts.map +1 -1
  83. package/types/main/state/reducers/updateMetaReducer.d.ts +1 -0
  84. package/types/main/state/reducers/updateMetaReducer.d.ts.map +1 -1
  85. package/types/main/state/useStallionEvents.d.ts +4 -2
  86. package/types/main/state/useStallionEvents.d.ts.map +1 -1
  87. package/types/main/utils/ErrorBoundary.d.ts +2 -1
  88. package/types/main/utils/ErrorBoundary.d.ts.map +1 -1
  89. package/types/main/utils/crashState.d.ts +4 -0
  90. package/types/main/utils/crashState.d.ts.map +1 -0
  91. package/types/main/utils/useStallionUpdate.d.ts.map +1 -1
  92. package/types/main/utils/withStallion.d.ts +2 -1
  93. package/types/main/utils/withStallion.d.ts.map +1 -1
  94. package/types/types/updateMeta.types.d.ts +7 -2
  95. package/types/types/updateMeta.types.d.ts.map +1 -1
  96. package/types/types/utils.types.d.ts +1 -1
  97. package/types/types/utils.types.d.ts.map +1 -1
  98. package/ios/main/StallionExceptionHandler.m +0 -184
@@ -0,0 +1,379 @@
1
+ //
2
+ // StallionExceptionHandler.mm
3
+ // react-native-stallion
4
+ //
5
+ // Created by Thor963 on 29/01/25.
6
+ //
7
+
8
+ #import "StallionExceptionHandler.h"
9
+ #import "StallionStateManager.h"
10
+ #import "StallionEventHandler.h"
11
+ #import "StallionSlotManager.h"
12
+ #import "StallionMeta.h"
13
+ #import "StallionMetaConstants.h"
14
+ #import "StallionObjConstants.h"
15
+ #import "StallionFileManager.h"
16
+ #import <signal.h>
17
+ #import <unistd.h>
18
+ #import <fcntl.h>
19
+ #import <React/RCTLog.h>
20
+ #import <React/RCTUtils.h>
21
+ #include <atomic>
22
+
23
+ // Forward declarations
24
+ void handleException(NSException *exception);
25
+ void handleSignal(int signal, siginfo_t *info, void *context);
26
+ static void performStallionRollback(NSString *errorString);
27
+ static void setupSignalHandlers(void);
28
+ static void processNativeCrashMarkerIfPresent(void);
29
+
30
+ // Async-signal-safe crash marker storage
31
+ static char g_marker_path[512];
32
+ static char g_mount_marker_path[512];
33
+
34
+ @implementation StallionExceptionHandler
35
+
36
+ NSUncaughtExceptionHandler *_defaultExceptionHandler;
37
+ static std::atomic<bool> exceptionAlertDismissed(false);
38
+ static std::atomic<bool> exceptionAlertShowing(false);
39
+ static std::atomic<bool> rollbackPerformed(false);
40
+ static std::atomic<bool> hasProcessedNativeCrashMarker(false);
41
+ static struct sigaction _previousHandlers[32]; // Store previous handlers for chaining
42
+
43
+ + (void)initExceptionHandler {
44
+ static dispatch_once_t onceToken;
45
+ dispatch_once(&onceToken, ^{
46
+ // Reset rollback flag when initializing exception handler
47
+ [self resetRollbackFlag];
48
+
49
+ // Store and set Objective-C exception handler
50
+ if (!_defaultExceptionHandler) {
51
+ _defaultExceptionHandler = NSGetUncaughtExceptionHandler();
52
+ }
53
+ NSSetUncaughtExceptionHandler(&handleException);
54
+
55
+ // Setup signal handlers using sigaction with chaining
56
+ setupSignalHandlers();
57
+
58
+ // Initialize JavaScript exception handler
59
+ [self initJavaScriptExceptionHandler];
60
+
61
+ // Process any crash marker from previous session
62
+ processNativeCrashMarkerIfPresent();
63
+ });
64
+ }
65
+
66
+ + (void)initJavaScriptExceptionHandler {
67
+ // Handle fatal JavaScript errors that cause app crashes
68
+ RCTSetLogFunction(^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) {
69
+ // Rollback only for fatal errors that crash the app
70
+ if (level >= RCTLogLevelFatal) {
71
+ NSString *errorString = [NSString stringWithFormat:@"Fatal JS Error in %@:%@ - %@", fileName, lineNumber, message];
72
+ performStallionRollback(errorString);
73
+ }
74
+ });
75
+ }
76
+
77
+ + (void)resetRollbackFlag {
78
+ rollbackPerformed.store(false);
79
+ }
80
+
81
+ @end
82
+
83
+ #pragma mark - Shared Rollback Logic
84
+
85
+ static void performStallionRollback(NSString *errorString) {
86
+
87
+ StallionStateManager *stateManager = [StallionStateManager sharedInstance];
88
+ StallionMeta *meta = stateManager.stallionMeta;
89
+ BOOL isAutoRollback = !stateManager.isMounted;
90
+
91
+
92
+ // Only prevent multiple executions for auto rollback cases
93
+ // Launch crashes (when mounted) can continue to be registered
94
+ if (isAutoRollback) {
95
+ // Use compare-and-swap to atomically check and set the flag
96
+ bool expected = false;
97
+ if (!rollbackPerformed.compare_exchange_strong(expected, true)) {
98
+ // Flag was already true, skip duplicate rollback
99
+ return;
100
+ }
101
+ }
102
+
103
+ if (errorString.length > 900) {
104
+ errorString = [errorString substringToIndex:900];
105
+ }
106
+
107
+ if (meta.switchState == SwitchStateProd) {
108
+ NSString *currentHash = [meta getActiveReleaseHash] ?: @"";
109
+
110
+ [[StallionEventHandler sharedInstance] cacheEvent:StallionObjConstants.exception_prod_event
111
+ eventPayload:@{
112
+ @"meta": errorString,
113
+ StallionObjConstants.release_hash_key: currentHash,
114
+ StallionObjConstants.is_auto_rollback_key: isAutoRollback ? @"true" : @"false"
115
+ }];
116
+ if (isAutoRollback) {
117
+ [StallionSlotManager rollbackProdWithAutoRollback:YES errorString:errorString];
118
+ }
119
+
120
+ } else if (meta.switchState == SwitchStateStage) {
121
+ NSString *currentStageHash = meta.stageNewHash ?: @"";
122
+
123
+ // Emit exception event before rollback (consistent with PROD and Android)
124
+ [[StallionEventHandler sharedInstance] cacheEvent:StallionObjConstants.exception_stage_event
125
+ eventPayload:@{
126
+ @"meta": errorString,
127
+ StallionObjConstants.release_hash_key: currentStageHash,
128
+ StallionObjConstants.is_auto_rollback_key: isAutoRollback ? @"true" : @"false"
129
+ }];
130
+
131
+ if(isAutoRollback) {
132
+ [StallionSlotManager rollbackStage];
133
+ }
134
+
135
+ // Check if alert is already showing (atomic check to prevent duplicates)
136
+ bool expectedShowing = false;
137
+ if (exceptionAlertShowing.compare_exchange_strong(expectedShowing, true)) {
138
+ // We successfully claimed the right to show the alert
139
+ UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Stallion Exception Handler"
140
+ message:[NSString stringWithFormat:@"%@\n%@",
141
+ @"A crash occurred in the app. Build was rolled back. Check crash report below. Continue crash to invoke other exception handlers. \n \n",
142
+ errorString]
143
+ preferredStyle:UIAlertControllerStyleAlert];
144
+
145
+ [alert addAction:[UIAlertAction actionWithTitle:@"Continue Crash"
146
+ style:UIAlertActionStyleDefault
147
+ handler:^(UIAlertAction *action) {
148
+ // Set flag to true only when button is pressed
149
+ exceptionAlertDismissed.store(true);
150
+ }]];
151
+
152
+ UIApplication *app = [UIApplication sharedApplication];
153
+ UIViewController *rootViewController = app.delegate.window.rootViewController;
154
+
155
+ dispatch_async(dispatch_get_main_queue(), ^{
156
+ [rootViewController presentViewController:alert animated:YES completion:nil];
157
+ });
158
+
159
+ // Wait for user to press the button (exceptionAlertDismissed becomes true)
160
+ while (!exceptionAlertDismissed.load()) {
161
+ [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
162
+ }
163
+ }
164
+ }
165
+ }
166
+
167
+ #pragma mark - Signal Handler Setup
168
+
169
+ // Async-signal-safe function to check if mount marker exists
170
+ static int is_mounted_safe(void) {
171
+ int fd = open(g_mount_marker_path, O_RDONLY);
172
+ if (fd >= 0) {
173
+ close(fd);
174
+ return 1; // Mounted
175
+ }
176
+ return 0; // Not mounted
177
+ }
178
+
179
+ // Async-signal-safe JSON writing (minimal JSON for crash marker)
180
+ static void write_crash_marker_json_safe(int signal, int mounted) {
181
+ int fd = open(g_marker_path, O_CREAT | O_WRONLY | O_TRUNC, 0600);
182
+ if (fd >= 0) {
183
+ // Write JSON: {"signal":X,"isAutoRollback":true/false,"crashLog":"signal=X\n"}
184
+ // isAutoRollback = !mounted (auto rollback if not mounted)
185
+ int autoRollback = !mounted;
186
+ char json[512];
187
+ int len = snprintf(json, sizeof(json),
188
+ "{\"signal\":%d,\"isAutoRollback\":%s,\"crashLog\":\"signal=%d\\n\"}",
189
+ signal, autoRollback ? "true" : "false", signal);
190
+ if (len > 0 && len < (int)sizeof(json)) {
191
+ (void)write(fd, json, len);
192
+ }
193
+ close(fd);
194
+ }
195
+ }
196
+
197
+ static void setupSignalHandlers(void) {
198
+ // Initialize marker paths from StallionStateManager
199
+ StallionStateManager *stateManager = [StallionStateManager sharedInstance];
200
+ NSString *filesDir = stateManager.stallionConfig.filesDirectory;
201
+ if (filesDir) {
202
+ const char *path = [filesDir UTF8String];
203
+ snprintf(g_marker_path, sizeof(g_marker_path), "%s/%s", path, "stallion_crash.marker");
204
+ snprintf(g_mount_marker_path, sizeof(g_mount_marker_path), "%s/%s", path, "stallion_mount.marker");
205
+ }
206
+
207
+ struct sigaction action;
208
+ sigemptyset(&action.sa_mask);
209
+ action.sa_flags = SA_SIGINFO;
210
+ action.sa_sigaction = handleSignal;
211
+
212
+ // List of signals to catch - comprehensive coverage
213
+ int signals[] = {
214
+ SIGABRT, // Abort signal
215
+ SIGILL, // Illegal instruction
216
+ SIGSEGV, // Segmentation violation
217
+ SIGFPE, // Floating point exception
218
+ SIGBUS, // Bus error
219
+ SIGTRAP, // Trace/breakpoint trap
220
+ SIGPIPE, // Broken pipe
221
+ SIGSYS, // Bad system call
222
+ };
223
+
224
+ int signalCount = sizeof(signals) / sizeof(signals[0]);
225
+
226
+ for (int i = 0; i < signalCount; i++) {
227
+ int sig = signals[i];
228
+ // Store previous handler before installing ours (for chaining)
229
+ sigaction(sig, NULL, &_previousHandlers[sig]);
230
+ // Now install our handler
231
+ sigaction(sig, &action, NULL);
232
+ }
233
+ }
234
+
235
+ void handleException(NSException *exception) {
236
+ NSString *readableError = [exception reason] ?: @"Unknown exception";
237
+ NSString *name = [exception name] ?: @"Unknown";
238
+ NSString *callStack = [[exception callStackSymbols] componentsJoinedByString:@"\n"];
239
+ NSString *fullError = [NSString stringWithFormat:@"Exception: %@\nReason: %@\nStack:\n%@", name, readableError, callStack];
240
+
241
+ performStallionRollback(fullError);
242
+
243
+ // Chain to default exception handler if available
244
+ if (_defaultExceptionHandler) {
245
+ _defaultExceptionHandler(exception);
246
+ }
247
+ }
248
+
249
+ void handleSignal(int signalVal, siginfo_t *info, void *context) {
250
+
251
+ if (!rollbackPerformed.load()) {
252
+ // Async-signal-safe operations only
253
+ // Read mount state at crash time (async-signal-safe)
254
+ int mounted = is_mounted_safe();
255
+ // Write JSON marker with crash info and autoRollback flag
256
+ write_crash_marker_json_safe(signalVal, mounted);
257
+ }
258
+
259
+ // Chain to previous handler if it exists and is valid (bubble up)
260
+ if (signalVal >= 0 && signalVal < 32) {
261
+ struct sigaction *prev = &_previousHandlers[signalVal];
262
+ if (prev->sa_handler != SIG_DFL && prev->sa_handler != SIG_IGN && prev->sa_handler != NULL) {
263
+ // Prevent infinite loop - don't call ourselves
264
+ if (prev->sa_sigaction != handleSignal) {
265
+ if (prev->sa_flags & SA_SIGINFO) {
266
+ prev->sa_sigaction(signalVal, info, context);
267
+ } else if (prev->sa_handler != SIG_DFL && prev->sa_handler != SIG_IGN) {
268
+ prev->sa_handler(signalVal);
269
+ }
270
+ }
271
+ }
272
+ }
273
+
274
+ // Restore default and raise to proceed with crash
275
+ signal(signalVal, SIG_DFL);
276
+ raise(signalVal);
277
+ }
278
+
279
+ #pragma mark - Crash Marker Processing
280
+
281
+ static void processNativeCrashMarkerIfPresent(void) {
282
+ @try {
283
+ if (hasProcessedNativeCrashMarker.load()) {
284
+ return;
285
+ }
286
+
287
+ StallionStateManager *stateManager = [StallionStateManager sharedInstance];
288
+ NSString *filesDir = stateManager.stallionConfig.filesDirectory;
289
+ NSString *markerPath = [NSString stringWithFormat:@"%@/stallion_crash.marker", filesDir];
290
+
291
+ NSFileManager *fileManager = [NSFileManager defaultManager];
292
+ if ([fileManager fileExistsAtPath:markerPath]) {
293
+ NSError *error = nil;
294
+ NSString *jsonContent = [NSString stringWithContentsOfFile:markerPath
295
+ encoding:NSUTF8StringEncoding
296
+ error:&error];
297
+
298
+ if (jsonContent && !error) {
299
+ NSString *stackTraceString = @"";
300
+ BOOL isAutoRollback = NO;
301
+
302
+ @try {
303
+ // Parse JSON from previous crash
304
+ NSData *jsonData = [jsonContent dataUsingEncoding:NSUTF8StringEncoding];
305
+ NSDictionary *crashMarker = [NSJSONSerialization JSONObjectWithData:jsonData
306
+ options:0
307
+ error:&error];
308
+ if (crashMarker && !error) {
309
+ // Extract crash log and autoRollback flag from marker
310
+ stackTraceString = crashMarker[@"crashLog"] ?: @"";
311
+ // Use the autoRollback flag that was determined at crash time (previous session)
312
+ isAutoRollback = [crashMarker[@"isAutoRollback"] boolValue];
313
+ }
314
+ } @catch (NSException *e) {
315
+ // Fallback for old format (non-JSON)
316
+ stackTraceString = jsonContent;
317
+ // Default to true for old format (conservative approach)
318
+ isAutoRollback = YES;
319
+ }
320
+
321
+ if (stackTraceString.length > 900) {
322
+ stackTraceString = [stackTraceString substringToIndex:900];
323
+ }
324
+
325
+ StallionMeta *meta = stateManager.stallionMeta;
326
+ SwitchState switchState = meta.switchState;
327
+
328
+ if (switchState == SwitchStateProd) {
329
+ NSString *currentHash = [meta getActiveReleaseHash] ?: @"";
330
+ // Use isAutoRollback from previous crash, not current session state
331
+ @try {
332
+ [[StallionEventHandler sharedInstance] cacheEvent:StallionObjConstants.exception_prod_event
333
+ eventPayload:@{
334
+ @"meta": stackTraceString,
335
+ StallionObjConstants.release_hash_key: currentHash,
336
+ StallionObjConstants.is_auto_rollback_key: isAutoRollback ? @"true" : @"false"
337
+ }];
338
+ } @catch (NSException *e) { }
339
+
340
+ if (isAutoRollback) {
341
+ // Only prevent multiple executions for auto rollback cases
342
+ bool expected = false;
343
+ if (rollbackPerformed.compare_exchange_strong(expected, true)) {
344
+ @try {
345
+ [StallionSlotManager rollbackProdWithAutoRollback:YES errorString:stackTraceString];
346
+ } @catch (NSException *e) { }
347
+ }
348
+ }
349
+ } else if (switchState == SwitchStateStage) {
350
+ NSString *currentStageHash = meta.stageNewHash ?: @"";
351
+ // Use isAutoRollback from previous crash, not current session state
352
+ @try {
353
+ [[StallionEventHandler sharedInstance] cacheEvent:StallionObjConstants.exception_stage_event
354
+ eventPayload:@{
355
+ @"meta": stackTraceString,
356
+ StallionObjConstants.release_hash_key: currentStageHash,
357
+ StallionObjConstants.is_auto_rollback_key: isAutoRollback ? @"true" : @"false"
358
+ }];
359
+ } @catch (NSException *e) { }
360
+
361
+ if (isAutoRollback) {
362
+ // Only prevent multiple executions for auto rollback cases
363
+ bool expected = false;
364
+ if (rollbackPerformed.compare_exchange_strong(expected, true)) {
365
+ @try {
366
+ [StallionSlotManager rollbackStage];
367
+ } @catch (NSException *e) { }
368
+ }
369
+ }
370
+ }
371
+
372
+ // Delete marker
373
+ [StallionFileManager deleteFileOrFolderSilently:markerPath];
374
+ hasProcessedNativeCrashMarker.store(true);
375
+ }
376
+ }
377
+ } @catch (NSException *e) { }
378
+ }
379
+
@@ -17,6 +17,7 @@ class StallionFileDownloader: NSObject {
17
17
  private var _reject: RCTPromiseRejectBlock?
18
18
  private var _onProgress: ((Float) -> Void)?
19
19
  private var lastSentProgress: Float = 0
20
+ private var lastProgressEmitTime: TimeInterval = 0
20
21
  private var _downloadDirectory: String?
21
22
 
22
23
  private let queue = DispatchQueue(label: "com.stallion.networkmanager", qos: .background)
@@ -116,13 +117,20 @@ class StallionFileDownloader: NSObject {
116
117
  guard bytesRead == headerSize else { return false }
117
118
  return header == [0x50, 0x4B, 0x03, 0x04] // PKZIP magic number
118
119
  }
120
+
121
+ private func shouldEmitProgress(currentTime: TimeInterval) -> Bool {
122
+ return currentTime - self.lastProgressEmitTime >= StallionConstants.PROGRESS_THROTTLE_INTERVAL_MS
123
+ }
119
124
  }
120
125
 
121
126
  extension StallionFileDownloader: URLSessionDownloadDelegate {
122
127
  func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
123
128
  let progress = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)
124
- if progress - self.lastSentProgress > StallionConstants.PROGRESS_EVENT_THRESHOLD {
129
+ let currentTime = Date().timeIntervalSince1970
130
+
131
+ if shouldEmitProgress(currentTime: currentTime) {
125
132
  self.lastSentProgress = progress
133
+ self.lastProgressEmitTime = currentTime
126
134
  self._onProgress?(progress)
127
135
  }
128
136
  }
@@ -20,10 +20,21 @@
20
20
  @property (nonatomic, copy) NSString *prodNewHash;
21
21
  @property (nonatomic, copy) NSString *prodStableHash;
22
22
  @property (nonatomic, copy) NSString *lastRolledBackHash;
23
+ @property (nonatomic, assign) NSTimeInterval lastRolledBackAt;
24
+ @property (nonatomic, assign) NSInteger successfulLaunchCount;
25
+ @property (nonatomic, copy) NSString *lastSuccessfulLaunchHash;
26
+
27
+ + (NSInteger)maxSuccessLaunchThreshold;
28
+ + (NSTimeInterval)lastRolledBackTTL;
23
29
 
24
30
  - (void)reset;
25
31
  - (NSDictionary *)toDictionary;
32
+ - (NSString *)getHashAtCurrentProdSlot;
26
33
  - (NSString *)getActiveReleaseHash;
34
+ - (NSString *)getLastRolledBackHash;
35
+ - (void)setLastRolledBackHashWithTimestamp:(NSString *)lastRolledBackHash;
36
+ - (void)markSuccessfulLaunch:(NSString *)releaseHash;
37
+ - (NSInteger)getSuccessfulLaunchCount:(NSString *)releaseHash;
27
38
  + (instancetype)fromDictionary:(NSDictionary *)dict;
28
39
 
29
40
  @end
@@ -9,6 +9,14 @@
9
9
 
10
10
  @implementation StallionMeta
11
11
 
12
+ + (NSInteger)maxSuccessLaunchThreshold {
13
+ return 3;
14
+ }
15
+
16
+ + (NSTimeInterval)lastRolledBackTTL {
17
+ return 6 * 60 * 60; // 6 hours in seconds
18
+ }
19
+
12
20
  - (instancetype)init {
13
21
  self = [super init];
14
22
  if (self) [self reset];
@@ -26,6 +34,9 @@
26
34
  self.prodNewHash = @"";
27
35
  self.prodStableHash = @"";
28
36
  self.lastRolledBackHash = @"";
37
+ self.lastRolledBackAt = 0.0;
38
+ self.successfulLaunchCount = 0;
39
+ self.lastSuccessfulLaunchHash = @"";
29
40
  }
30
41
 
31
42
  - (NSDictionary *)toDictionary {
@@ -43,7 +54,10 @@
43
54
  @"stableHash": self.prodStableHash ?: @"",
44
55
  @"currentSlot": [StallionMetaConstants stringFromSlotState:self.currentProdSlot] ?: @""
45
56
  },
46
- @"lastRolledBackHash": self.lastRolledBackHash ?: @""
57
+ @"lastRolledBackHash": self.lastRolledBackHash ?: @"",
58
+ @"lastRolledBackAt": @(self.lastRolledBackAt),
59
+ @"successfulLaunchCount": @(self.successfulLaunchCount),
60
+ @"lastSuccessfulLaunchHash": self.lastSuccessfulLaunchHash ?: @""
47
61
  };
48
62
  } @catch (NSException *exception) {
49
63
  NSLog(@"Error in toDictionary: %@", exception.reason);
@@ -87,6 +101,9 @@
87
101
  meta.currentProdSlot = [StallionMetaConstants slotStateFromString:prodSlot[@"currentSlot"] ?: @"default_slot"];
88
102
 
89
103
  meta.lastRolledBackHash = dict[@"lastRolledBackHash"] ?: @"";
104
+ meta.lastRolledBackAt = [dict[@"lastRolledBackAt"] doubleValue];
105
+ meta.successfulLaunchCount = [dict[@"successfulLaunchCount"] integerValue];
106
+ meta.lastSuccessfulLaunchHash = dict[@"lastSuccessfulLaunchHash"] ?: @"";
90
107
 
91
108
  return meta;
92
109
  } @catch (NSException *exception) {
@@ -95,4 +112,62 @@
95
112
  }
96
113
  }
97
114
 
115
+ - (NSString *)getHashAtCurrentProdSlot {
116
+ switch (self.currentProdSlot) {
117
+ case SlotStateNewSlot:
118
+ return self.prodNewHash;
119
+ case SlotStateStableSlot:
120
+ return self.prodStableHash;
121
+ default:
122
+ return @"";
123
+ }
124
+ }
125
+
126
+ - (NSString *)getLastRolledBackHash {
127
+ [self enforceLastRolledBackExpiry];
128
+ return self.lastRolledBackHash;
129
+ }
130
+
131
+ - (void)setLastRolledBackHashWithTimestamp:(NSString *)lastRolledBackHash {
132
+ NSString *hashValue = lastRolledBackHash ?: @"";
133
+ self.lastRolledBackHash = hashValue;
134
+ self.lastRolledBackAt = [hashValue isEqualToString:@""] ? 0.0 : [[NSDate date] timeIntervalSince1970];
135
+ }
136
+
137
+ - (void)enforceLastRolledBackExpiry {
138
+ if (!self.lastRolledBackHash || [self.lastRolledBackHash isEqualToString:@""]) {
139
+ return;
140
+ }
141
+ if (self.lastRolledBackAt <= 0.0) {
142
+ return;
143
+ }
144
+ NSTimeInterval now = [[NSDate date] timeIntervalSince1970];
145
+ if (now - self.lastRolledBackAt >= [StallionMeta lastRolledBackTTL]) {
146
+ self.lastRolledBackHash = @"";
147
+ self.lastRolledBackAt = 0.0;
148
+ }
149
+ }
150
+
151
+ - (void)markSuccessfulLaunch:(NSString *)releaseHash {
152
+ if (!releaseHash || [releaseHash isEqualToString:@""]) {
153
+ return;
154
+ }
155
+ if (![releaseHash isEqualToString:self.lastSuccessfulLaunchHash]) {
156
+ self.successfulLaunchCount = 0;
157
+ self.lastSuccessfulLaunchHash = releaseHash;
158
+ }
159
+ if (self.successfulLaunchCount < 3) { // MAX_SUCCESS_LAUNCH_THRESHOLD
160
+ self.successfulLaunchCount += 1;
161
+ }
162
+ }
163
+
164
+ - (NSInteger)getSuccessfulLaunchCount:(NSString *)releaseHash {
165
+ NSString *currentHash = releaseHash ?: @"";
166
+ if (![currentHash isEqualToString:self.lastSuccessfulLaunchHash]) {
167
+ return 0;
168
+ } else {
169
+ return self.successfulLaunchCount;
170
+ }
171
+ }
172
+
98
173
  @end
@@ -74,7 +74,7 @@
74
74
  NSString *newSlotPath = [NSString stringWithFormat:@"%@/%@/%@", baseFolderPath, StallionObjConstants.prod_directory, StallionObjConstants.new_folder_slot];
75
75
  NSString *stableSlotPath = [NSString stringWithFormat:@"%@/%@/%@", baseFolderPath, StallionObjConstants.prod_directory, StallionObjConstants.stable_folder_slot];
76
76
 
77
- [StallionFileManager copyFileOrDirectoryFrom:newSlotPath to:stableSlotPath];
77
+ [StallionFileManager moveFileFrom:newSlotPath to:stableSlotPath];
78
78
 
79
79
  NSString *newReleaseHash = stateManager.stallionMeta.prodNewHash;
80
80
  [stateManager.stallionMeta setProdStableHash:newReleaseHash];
@@ -96,7 +96,7 @@
96
96
 
97
97
  if (isAutoRollback) {
98
98
  StallionStateManager *stateManager = [StallionStateManager sharedInstance];
99
- [stateManager.stallionMeta setLastRolledBackHash:rolledBackReleaseHash];
99
+ [stateManager.stallionMeta setLastRolledBackHashWithTimestamp:rolledBackReleaseHash];
100
100
  [stateManager syncStallionMeta];
101
101
  }
102
102
 
@@ -44,6 +44,9 @@ static StallionStateManager *_instance = nil;
44
44
  _isMounted = NO;
45
45
  _pendingReleaseUrl = @"";
46
46
  _pendingReleaseHash = @"";
47
+
48
+ // Reset mount state on initialization (ensures mount marker file is deleted for new session)
49
+ [self setIsMounted:NO];
47
50
  }
48
51
  return self;
49
52
  }
@@ -70,6 +73,32 @@ static StallionStateManager *_instance = nil;
70
73
  [self syncStallionMeta];
71
74
  }
72
75
 
76
+ - (void)setIsMounted:(BOOL)isMounted {
77
+ _isMounted = isMounted;
78
+ // Write mount state to a simple file that signal handler can read (async-signal-safe)
79
+ NSString *filesDir = self.stallionConfig.filesDirectory;
80
+ NSString *mountMarkerPath = [NSString stringWithFormat:@"%@/stallion_mount.marker", filesDir];
81
+ NSFileManager *fileManager = [NSFileManager defaultManager];
82
+
83
+ if (isMounted) {
84
+ // Create file to indicate mounted (file existence = mounted)
85
+ @try {
86
+ [fileManager createFileAtPath:mountMarkerPath contents:nil attributes:nil];
87
+ } @catch (NSException *e) {
88
+ // Silently ignore errors
89
+ }
90
+ } else {
91
+ // Delete file to indicate not mounted (no file = not mounted)
92
+ @try {
93
+ if ([fileManager fileExistsAtPath:mountMarkerPath]) {
94
+ [fileManager removeItemAtPath:mountMarkerPath error:nil];
95
+ }
96
+ } @catch (NSException *e) {
97
+ // Silently ignore errors
98
+ }
99
+ }
100
+ }
101
+
73
102
  #pragma mark - Local Storage Methods
74
103
 
75
104
  - (NSString *)getStringForKey:(NSString *)key defaultValue:(NSString *)defaultValue {
@@ -46,6 +46,7 @@ class StallionSyncHandler {
46
46
  "platform": StallionConstants.PlatformValue,
47
47
  "projectId": projectId,
48
48
  "appliedBundleHash": appliedBundleHash,
49
+ "deviceMeta": StallionDeviceInfo.getDeviceMetaJson(config)
49
50
  ]
50
51
 
51
52
  // Make API call using URLSession
@@ -151,7 +152,7 @@ class StallionSyncHandler {
151
152
  !newReleaseHash.isEmpty else { return }
152
153
 
153
154
  let stateManager = StallionStateManager.sharedInstance()
154
- let lastRolledBackHash = stateManager?.stallionMeta?.lastRolledBackHash ?? ""
155
+ let lastRolledBackHash = stateManager?.stallionMeta?.getLastRolledBackHash() ?? ""
155
156
  let lastUnverifiedHash = stateManager?.stallionConfig?.lastUnverifiedHash ?? ""
156
157
 
157
158
  if newReleaseHash != lastRolledBackHash && newReleaseHash != lastUnverifiedHash {
@@ -190,7 +191,7 @@ class StallionSyncHandler {
190
191
  url: fromUrl,
191
192
  downloadDirectory: downloadPath,
192
193
  onProgress: { progress in
193
- // Handle progress updates if necessary
194
+ emitDownloadProgress(releaseHash: newReleaseHash, progress: progress)
194
195
  },
195
196
  resolve: { _ in
196
197
  completeDownload()
@@ -278,5 +279,16 @@ class StallionSyncHandler {
278
279
  shouldCache: true
279
280
  )
280
281
  }
282
+
283
+ private static func emitDownloadProgress(releaseHash: String, progress: Float) {
284
+ let progressPayload: NSDictionary = [
285
+ "releaseHash": releaseHash,
286
+ "progress": "\(progress)"
287
+ ]
288
+ Stallion.sendEventToRn(eventName: StallionConstants.NativeEventTypesProd.DOWNLOAD_PROGRESS_PROD,
289
+ eventBody: progressPayload,
290
+ shouldCache: false
291
+ )
292
+ }
281
293
  }
282
294
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-stallion",
3
- "version": "2.3.0-alpha.4",
3
+ "version": "2.3.0-alpha.6",
4
4
  "description": "Offical React Native SDK for Stallion",
5
5
  "main": "index",
6
6
  "types": "types/index.d.ts",
@@ -3,6 +3,28 @@ require "json"
3
3
  package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4
4
  folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32'
5
5
 
6
+ # Generate StallionVersion.h during pod install
7
+ version_header_path = File.join(__dir__, "ios/main/StallionVersion.h")
8
+ version_header_content = <<~HEADER
9
+ //
10
+ // StallionVersion.h
11
+ // react-native-stallion
12
+ //
13
+ // Auto-generated from package.json during pod install
14
+ // Do not edit this file manually
15
+ //
16
+
17
+ #ifndef StallionVersion_h
18
+ #define StallionVersion_h
19
+
20
+ #define STALLION_SDK_VERSION @"#{package["version"]}"
21
+
22
+ #endif /* StallionVersion_h */
23
+ HEADER
24
+
25
+ File.write(version_header_path, version_header_content)
26
+ puts "Stallion: Generated StallionVersion.h with version #{package["version"]}"
27
+
6
28
  Pod::Spec.new do |s|
7
29
  s.name = "react-native-stallion"
8
30
  s.version = package["version"]