@rejourneyco/react-native 1.0.2 → 1.0.4

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 (43) hide show
  1. package/android/src/main/java/com/rejourney/RejourneyModuleImpl.kt +38 -363
  2. package/android/src/main/java/com/rejourney/capture/CaptureEngine.kt +11 -113
  3. package/android/src/main/java/com/rejourney/capture/SegmentUploader.kt +1 -15
  4. package/android/src/main/java/com/rejourney/capture/VideoEncoder.kt +1 -61
  5. package/android/src/main/java/com/rejourney/capture/ViewHierarchyScanner.kt +3 -1
  6. package/android/src/main/java/com/rejourney/lifecycle/SessionLifecycleService.kt +1 -22
  7. package/android/src/main/java/com/rejourney/network/DeviceAuthManager.kt +14 -27
  8. package/android/src/main/java/com/rejourney/network/NetworkMonitor.kt +0 -2
  9. package/android/src/main/java/com/rejourney/network/UploadManager.kt +7 -93
  10. package/android/src/main/java/com/rejourney/network/UploadWorker.kt +5 -41
  11. package/android/src/main/java/com/rejourney/privacy/PrivacyMask.kt +2 -58
  12. package/android/src/main/java/com/rejourney/touch/TouchInterceptor.kt +4 -4
  13. package/android/src/main/java/com/rejourney/utils/EventBuffer.kt +36 -7
  14. package/ios/Capture/RJCaptureEngine.m +9 -61
  15. package/ios/Capture/RJViewHierarchyScanner.m +68 -51
  16. package/ios/Core/RJLifecycleManager.m +0 -14
  17. package/ios/Core/Rejourney.mm +24 -37
  18. package/ios/Network/RJDeviceAuthManager.m +0 -2
  19. package/ios/Network/RJUploadManager.h +8 -0
  20. package/ios/Network/RJUploadManager.m +45 -0
  21. package/ios/Privacy/RJPrivacyMask.m +5 -31
  22. package/ios/Rejourney.h +0 -14
  23. package/ios/Touch/RJTouchInterceptor.m +21 -15
  24. package/ios/Utils/RJEventBuffer.m +57 -69
  25. package/ios/Utils/RJWindowUtils.m +87 -86
  26. package/lib/commonjs/index.js +44 -31
  27. package/lib/commonjs/sdk/autoTracking.js +0 -3
  28. package/lib/commonjs/sdk/constants.js +1 -1
  29. package/lib/commonjs/sdk/networkInterceptor.js +0 -11
  30. package/lib/commonjs/sdk/utils.js +73 -14
  31. package/lib/module/index.js +44 -31
  32. package/lib/module/sdk/autoTracking.js +0 -3
  33. package/lib/module/sdk/constants.js +1 -1
  34. package/lib/module/sdk/networkInterceptor.js +0 -11
  35. package/lib/module/sdk/utils.js +73 -14
  36. package/lib/typescript/sdk/constants.d.ts +1 -1
  37. package/lib/typescript/sdk/utils.d.ts +31 -1
  38. package/package.json +16 -4
  39. package/src/index.ts +42 -20
  40. package/src/sdk/autoTracking.ts +0 -2
  41. package/src/sdk/constants.ts +14 -14
  42. package/src/sdk/networkInterceptor.ts +0 -9
  43. package/src/sdk/utils.ts +76 -14
@@ -249,22 +249,33 @@ RCT_EXPORT_MODULE()
249
249
  didDecideSample = YES;
250
250
  }
251
251
 
252
- BOOL shouldRecord = self.recordingEnabledByConfig && self.sessionSampled;
253
- self.remoteRecordingEnabled = shouldRecord;
252
+ // Decouple video recording from session active state.
253
+ // We want to record video ONLY if:
254
+ // 1. Config says recording is enabled (remote toggles)
255
+ // 2. Sample rate allows it
256
+ //
257
+ // NOTE: Even if shouldRecordVideo is NO, the session remains active
258
+ // (isRecording=YES) and events are still observed and uploaded ("Data-Only
259
+ // Mode").
260
+ BOOL shouldRecordVideo = self.recordingEnabledByConfig && self.sessionSampled;
261
+ self.remoteRecordingEnabled = shouldRecordVideo;
254
262
 
255
263
  if (self.captureEngine) {
256
- self.captureEngine.uploadsEnabled = shouldRecord;
257
- if (!shouldRecord && self.captureEngine.isRecording) {
264
+ self.captureEngine.uploadsEnabled = shouldRecordVideo;
265
+ if (!shouldRecordVideo && self.captureEngine.isRecording) {
266
+ // Stop video capture pipeline to save resources, but keep session alive
258
267
  [self.captureEngine stopSession];
259
268
  }
260
269
  }
261
270
 
262
271
  if (didDecideSample && self.recordingEnabledByConfig &&
263
272
  !self.sessionSampled) {
264
- RJLogWarning(@"Session skipped by sample rate (%ld%%)", (long)clampedRate);
273
+ RJLogInfo(@"Session sampled out for video (%ld%%) - entering Data-Only "
274
+ @"Mode (Events enabled, Video disabled)",
275
+ (long)clampedRate);
265
276
  }
266
277
 
267
- return shouldRecord;
278
+ return shouldRecordVideo;
268
279
  }
269
280
 
270
281
  - (void)ensureFullyInitialized {
@@ -676,10 +687,7 @@ RCT_EXPORT_METHOD(stopSession : (RCTPromiseResolveBlock)
676
687
 
677
688
  NSString *sessionId = self.currentSessionId ?: @"";
678
689
 
679
- // CRITICAL: Compute background time at the moment we end the session.
680
- // When a session ends while the app is in background (e.g. max duration),
681
- // we must include the ongoing background duration, otherwise the backend
682
- // will think the entire wall-clock session duration is playable.
690
+
683
691
  NSTimeInterval totalBgTimeMs = 0;
684
692
  if (self.lifecycleManager) {
685
693
  totalBgTimeMs = self.lifecycleManager.totalBackgroundTimeMs;
@@ -1037,7 +1045,7 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
1037
1045
  if ([gestureType hasPrefix:@"scroll"]) {
1038
1046
  static NSTimeInterval lastScrollLogTime = 0;
1039
1047
  NSTimeInterval now = CACurrentMediaTime();
1040
- if (now - lastScrollLogTime < 0.5) {
1048
+ if (now - lastScrollLogTime < 0.5) {
1041
1049
  return;
1042
1050
  }
1043
1051
  lastScrollLogTime = now;
@@ -1125,7 +1133,7 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
1125
1133
  registerDeviceWithProjectKey:publicKey
1126
1134
  bundleId:bundleId
1127
1135
  platform:@"ios"
1128
- sdkVersion:@"1.0.0"
1136
+ sdkVersion:RJSDKVersion
1129
1137
  apiUrl:apiUrl
1130
1138
  completion:^(BOOL success, NSString *credId,
1131
1139
  NSError *error) {
@@ -1329,12 +1337,10 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
1329
1337
  return;
1330
1338
  }
1331
1339
 
1332
- // Handle security errors (403 = forbidden/mismatch, 404 = not found)
1333
1340
  if ((error.code == 403 || error.code == 404) &&
1334
1341
  [error.domain isEqualToString:@"RJDeviceAuth"]) {
1335
1342
 
1336
1343
  if (!isRetry) {
1337
- // First failure - try re-registration
1338
1344
  RJLogDebug(@"Device auth invalid (%ld), attempting re-registration...",
1339
1345
  (long)error.code);
1340
1346
  NSString *bundleId =
@@ -1345,7 +1351,7 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
1345
1351
  registerDeviceWithProjectKey:publicKey
1346
1352
  bundleId:bundleId
1347
1353
  platform:@"ios"
1348
- sdkVersion:@"1.0.0"
1354
+ sdkVersion:RJSDKVersion
1349
1355
  apiUrl:apiUrl
1350
1356
  completion:^(BOOL retrySuccess, NSString *credId,
1351
1357
  NSError *retryError) {
@@ -2112,7 +2118,7 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
2112
2118
  }
2113
2119
 
2114
2120
  if (self.captureEngine) {
2115
-
2121
+
2116
2122
  RJLogInfo(@"[RJ-TERMINATE] Stopping capture engine synchronously");
2117
2123
  [self.captureEngine stopSessionSync];
2118
2124
  RJLogInfo(@"[RJ-TERMINATE] Capture engine stopped");
@@ -2146,9 +2152,9 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
2146
2152
 
2147
2153
  if (self.uploadManager && events.count > 0) {
2148
2154
  @try {
2149
- [self.uploadManager synchronousUploadWithEvents:events ?: @[]];
2155
+ [self.uploadManager persistTerminationEvents:events ?: @[]];
2150
2156
  } @catch (NSException *e) {
2151
- RJLogWarning(@"Terminate upload failed: %@", e);
2157
+ RJLogWarning(@"Terminate persistence failed: %@", e);
2152
2158
  }
2153
2159
  }
2154
2160
  } @catch (NSException *exception) {
@@ -2212,13 +2218,6 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
2212
2218
  [self handleSessionTimeout:backgroundDuration currentTime:currentTime];
2213
2219
  }
2214
2220
 
2215
-
2216
- /// 1. Capture final background time from lifecycle manager
2217
- /// 2. Stop timers and capture engine for old session
2218
- /// 3. Synchronously end the old session with correct background time
2219
- /// 4. Create new session ID and reset all state
2220
- /// 5. Start capture for new session
2221
- /// 6. Trigger immediate upload to register new session
2222
2221
  - (void)handleSessionTimeout:(NSTimeInterval)backgroundDuration
2223
2222
  currentTime:(NSTimeInterval)currentTime {
2224
2223
  RJLogInfo(
@@ -2232,8 +2231,6 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
2232
2231
  RJLogInfo(@"[RJ-SESSION-TIMEOUT] === ENDING OLD SESSION: %@ ===",
2233
2232
  oldSessionId);
2234
2233
 
2235
- // ========== STEP 1: Capture background time BEFORE any state changes
2236
- // ==========
2237
2234
  NSTimeInterval totalBackgroundMs = 0;
2238
2235
  if (self.lifecycleManager) {
2239
2236
  totalBackgroundMs = self.lifecycleManager.totalBackgroundTimeMs;
@@ -2242,7 +2239,6 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
2242
2239
  totalBackgroundMs);
2243
2240
  }
2244
2241
 
2245
- // ========== STEP 2: Stop all capture/timers for old session ==========
2246
2242
  [self stopBatchUploadTimer];
2247
2243
  [self stopDurationLimitTimer];
2248
2244
 
@@ -2255,14 +2251,10 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
2255
2251
  }
2256
2252
  }
2257
2253
 
2258
- // ========== STEP 3: End old session SYNCHRONOUSLY with correct background
2259
- // time ==========
2260
2254
  if (wasRecording && self.uploadManager && oldSessionId.length > 0 &&
2261
2255
  ![oldSessionId isEqualToString:@"none"]) {
2262
- // Set background time on upload manager
2263
2256
  self.uploadManager.totalBackgroundTimeMs = totalBackgroundMs;
2264
2257
 
2265
- // Get current events for final upload
2266
2258
  __block NSArray<NSDictionary *> *finalEvents = nil;
2267
2259
  [self performStateSync:^{
2268
2260
  finalEvents = [self.sessionEvents copy];
@@ -2272,7 +2264,6 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
2272
2264
  @"bgTime=%.0fms",
2273
2265
  (unsigned long)finalEvents.count, totalBackgroundMs);
2274
2266
 
2275
- // Synchronous upload and session end
2276
2267
  if (finalEvents.count > 0) {
2277
2268
  [self.uploadManager synchronousUploadWithEvents:finalEvents];
2278
2269
  } else {
@@ -2282,7 +2273,6 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
2282
2273
  RJLogInfo(@"[RJ-SESSION-TIMEOUT] Old session %@ ended", oldSessionId);
2283
2274
  }
2284
2275
 
2285
- // ========== STEP 4: Reset ALL state for new session ==========
2286
2276
  RJLogInfo(@"[RJ-SESSION-TIMEOUT] === STARTING NEW SESSION ===");
2287
2277
 
2288
2278
  __block NSString *newSessionId = nil;
@@ -2336,7 +2326,6 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
2336
2326
  self.eventBuffer = [[RJEventBuffer alloc] initWithSessionId:newSessionId
2337
2327
  pendingRootPath:pendingPath];
2338
2328
 
2339
- // ========== STEP 5: Start capture for new session ==========
2340
2329
  [self resetSamplingDecision];
2341
2330
  self.remoteRecordingEnabled = self.recordingEnabledByConfig;
2342
2331
  if (self.captureEngine) {
@@ -2367,8 +2356,6 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
2367
2356
  [self startBatchUploadTimer];
2368
2357
  [self startDurationLimitTimer];
2369
2358
 
2370
- // ========== STEP 6: Log session_start and trigger immediate upload
2371
- // ==========
2372
2359
  NSMutableDictionary *sessionStartEvent = [NSMutableDictionary dictionary];
2373
2360
  sessionStartEvent[@"type"] = RJEventTypeSessionStart;
2374
2361
  sessionStartEvent[@"timestamp"] = @([RJWindowUtils currentTimestampMillis]);
@@ -857,10 +857,8 @@ static const NSInteger RJ_AUTH_MAX_CONSECUTIVE_FAILURES = 10;
857
857
  return;
858
858
  }
859
859
 
860
- // Check cooldown after consecutive failures to prevent flooding
861
860
  NSTimeInterval now = [[NSDate date] timeIntervalSince1970];
862
861
  if (self.consecutiveFailures > 0 && self.lastFailedRegistrationTime > 0) {
863
- // Exponential backoff: 5s, 10s, 20s, 40s... up to 5 minutes
864
862
  NSTimeInterval cooldown = MIN(
865
863
  RJ_AUTH_COOLDOWN_BASE_SECONDS * pow(2, self.consecutiveFailures - 1),
866
864
  RJ_AUTH_COOLDOWN_MAX_SECONDS
@@ -180,6 +180,14 @@ NS_ASSUME_NONNULL_BEGIN
180
180
  */
181
181
  - (BOOL)synchronousUploadWithEvents:(NSArray<NSDictionary *> *)events;
182
182
 
183
+ /**
184
+ * Persists events to disk for termination, skipping network upload.
185
+ * Use this in appWillTerminate to avoid watchdog kills.
186
+ *
187
+ * @param events Array of event dictionaries.
188
+ */
189
+ - (void)persistTerminationEvents:(NSArray<NSDictionary *> *)events;
190
+
183
191
  /**
184
192
  * Uploads a crash report to the dashboard.
185
193
  *
@@ -67,6 +67,7 @@ static NSString *RJRedactedURLForLogFromString(NSString *urlString) {
67
67
 
68
68
  @interface RJUploadManager ()
69
69
 
70
+ @property(nonatomic, assign) BOOL keyboardVisible;
70
71
  @property(nonatomic, strong) dispatch_queue_t uploadQueue;
71
72
 
72
73
  @property(nonatomic, strong, nullable) NSTimer *batchUploadTimer;
@@ -1820,6 +1821,50 @@ static NSString *RJRedactedURLForLogFromString(NSString *urlString) {
1820
1821
  return YES;
1821
1822
  }
1822
1823
 
1824
+ - (void)persistTerminationEvents:(NSArray<NSDictionary *> *)events {
1825
+ if (events.count == 0 && self.sessionId.length == 0)
1826
+ return;
1827
+
1828
+ self.eventBatchNumber += 1;
1829
+ NSInteger currentBatch = self.eventBatchNumber;
1830
+
1831
+ RJLogInfo(@"[RJ-UPLOAD] persistTerminationEvents starting: sessionId=%@, "
1832
+ @"batch=%ld, "
1833
+ @"eventCount=%lu",
1834
+ self.sessionId ?: @"<nil>", (long)currentBatch,
1835
+ (unsigned long)events.count);
1836
+
1837
+ NSDictionary *payload = [self buildEventPayloadWithEvents:events
1838
+ batchNumber:currentBatch
1839
+ isFinal:YES];
1840
+ if (!payload)
1841
+ return;
1842
+
1843
+ NSError *jsonError = nil;
1844
+ NSData *jsonData = [NSJSONSerialization dataWithJSONObject:payload
1845
+ options:0
1846
+ error:&jsonError];
1847
+ if (jsonError || !jsonData)
1848
+ return;
1849
+
1850
+ NSError *gzipError = nil;
1851
+ NSData *compressed = RJGzipData(jsonData, &gzipError);
1852
+ if (gzipError || !compressed)
1853
+ return;
1854
+
1855
+ [self persistPendingUploadWithContentType:@"events"
1856
+ batchNumber:currentBatch
1857
+ keyframe:NO
1858
+ gzipped:compressed
1859
+ eventCount:events.count
1860
+ frameCount:0];
1861
+
1862
+ [self markSessionActiveForRecovery];
1863
+
1864
+ RJLogInfo(@"[RJ-CHECKPOINT] Persisted termination events to disk, skipping "
1865
+ @"sync upload");
1866
+ }
1867
+
1823
1868
  - (BOOL)uploadEventsBatchSync:(NSArray<NSDictionary *> *)events
1824
1869
  isFinal:(BOOL)isFinal {
1825
1870
  self.eventBatchNumber += 1;
@@ -280,28 +280,19 @@ static inline BOOL RJIsValidMaskFrame(CGRect frame) {
280
280
  }
281
281
 
282
282
  if (context) {
283
- // PUSH context so high-level UIKit drawing (NSString drawAtPoint) works
284
283
  UIGraphicsPushContext(context);
285
284
 
286
- // CRITICAL FIX: Flip coordinate system to match UIKit (Top-Left origin)
287
- // CoreGraphics defaults to Bottom-Left origin, so without this flip,
288
- // masking rects are drawn vertically mirrored (wrong place).
289
285
  CGContextTranslateCTM(context, 0, height);
290
286
  CGContextScaleCTM(context, 1.0, -1.0);
291
-
292
- // Apply correct scale transform (Points -> Pixels)
293
287
  CGContextScaleCTM(context, safeScale, safeScale);
294
288
 
295
- // If in background or privacy scan is unavailable, draw full overlay
296
289
  if (self.isInBackground || shouldMaskAll) {
297
- // Create bounds in points (since we scaled the CTM)
298
290
  CGRect boundsPoints =
299
291
  CGRectMake(0, 0, width / safeScale, height / safeScale);
300
292
  [self drawBackgroundOverlayInContext:context
301
293
  bounds:boundsPoints
302
294
  scale:1.0];
303
295
  } else {
304
- // Draw standard masks
305
296
  [self drawMasksWithScanResult:scanResult
306
297
  context:context
307
298
  bounds:CGRectMake(0, 0, width / safeScale,
@@ -332,9 +323,7 @@ static inline BOOL RJIsValidMaskFrame(CGRect frame) {
332
323
  self.lastFrameHadWebView = NO;
333
324
  self.lastFrameHadVideo = NO;
334
325
 
335
- // Handle background state
336
326
  if (self.isInBackground) {
337
- // Only draw background overlay if we have a method for it
338
327
  if ([self respondsToSelector:@selector
339
328
  (drawBackgroundOverlayInContext:bounds:scale:)]) {
340
329
  [self drawBackgroundOverlayInContext:context bounds:bounds scale:scale];
@@ -342,7 +331,6 @@ static inline BOOL RJIsValidMaskFrame(CGRect frame) {
342
331
  return;
343
332
  }
344
333
 
345
- // Early exit if nothing to mask - saves ~1-2ms per frame
346
334
  if (scanResult.textInputFrames.count == 0 &&
347
335
  scanResult.cameraFrames.count == 0 &&
348
336
  scanResult.webViewFrames.count == 0 &&
@@ -363,7 +351,7 @@ static inline BOOL RJIsValidMaskFrame(CGRect frame) {
363
351
  }
364
352
  [self drawBlurRectInContext:context
365
353
  frame:frame
366
- maskType:0]; // 0 = TextInput
354
+ maskType:0];
367
355
  self.lastFrameHadTextInput = YES;
368
356
  }
369
357
 
@@ -396,7 +384,7 @@ static inline BOOL RJIsValidMaskFrame(CGRect frame) {
396
384
  }
397
385
  [self drawBlurRectInContext:context
398
386
  frame:frame
399
- maskType:2]; // 2 = WebView
387
+ maskType:2];
400
388
  self.lastFrameHadWebView = YES;
401
389
  }
402
390
  }
@@ -415,7 +403,7 @@ static inline BOOL RJIsValidMaskFrame(CGRect frame) {
415
403
  }
416
404
  [self drawBlurRectInContext:context
417
405
  frame:frame
418
- maskType:3]; // 3 = Video
406
+ maskType:3];
419
407
  self.lastFrameHadVideo = YES;
420
408
  }
421
409
  }
@@ -427,17 +415,13 @@ static inline BOOL RJIsValidMaskFrame(CGRect frame) {
427
415
  - (void)drawBackgroundOverlayInContext:(CGContextRef)context
428
416
  bounds:(CGRect)bounds
429
417
  scale:(CGFloat)scale {
430
- // Simple black overlay for backgrounding
431
418
  CGContextSetFillColorWithColor(context, [UIColor blackColor].CGColor);
432
419
  CGContextFillRect(context, bounds);
433
-
434
- // Optional: Draw text or logo? Keeping it simple/fast.
435
420
  }
436
421
 
437
422
  - (void)drawBlurRectInContext:(CGContextRef)context
438
423
  frame:(CGRect)frame
439
424
  maskType:(NSInteger)maskType {
440
- // Validate frame to prevent CoreGraphics NaN errors
441
425
  if (isnan(frame.origin.x) || isnan(frame.origin.y) ||
442
426
  isnan(frame.size.width) || isnan(frame.size.height) ||
443
427
  isinf(frame.origin.x) || isinf(frame.origin.y) ||
@@ -446,7 +430,6 @@ static inline BOOL RJIsValidMaskFrame(CGRect frame) {
446
430
  return;
447
431
  }
448
432
 
449
- // Also skip zero or negative sized frames
450
433
  if (frame.size.width <= 0 || frame.size.height <= 0) {
451
434
  return;
452
435
  }
@@ -458,12 +441,11 @@ static inline BOOL RJIsValidMaskFrame(CGRect frame) {
458
441
  cornerRadius:self.blurCornerRadius];
459
442
 
460
443
  UIColor *blurColor;
461
- if (maskType == 1 || maskType == 3) { // Camera / Video
444
+ if (maskType == 1 || maskType == 3) {
462
445
  blurColor = [UIColor colorWithRed:0.12 green:0.12 blue:0.15 alpha:1.0];
463
- } else if (maskType == 2) { // WebView - Use same style as Camera as requested
446
+ } else if (maskType == 2) {
464
447
  blurColor = [UIColor colorWithRed:0.12 green:0.12 blue:0.15 alpha:1.0];
465
448
  } else {
466
- // TextInput and others
467
449
  blurColor = [UIColor blackColor];
468
450
  }
469
451
 
@@ -480,7 +462,6 @@ static inline BOOL RJIsValidMaskFrame(CGRect frame) {
480
462
  if (maskType == 1) {
481
463
  [self drawCameraLabelInContext:context frame:frame];
482
464
  } else if (maskType == 2) {
483
- // Reuse camera label style but with different text
484
465
  [self drawWebViewLabelInContext:context frame:frame];
485
466
  } else if (maskType == 3) {
486
467
  [self drawVideoLabelInContext:context frame:frame];
@@ -492,7 +473,6 @@ static inline BOOL RJIsValidMaskFrame(CGRect frame) {
492
473
  }
493
474
 
494
475
  - (void)drawCameraLabelInContext:(CGContextRef)context frame:(CGRect)frame {
495
- // Skip if frame has invalid values
496
476
  if (isnan(frame.size.width) || isnan(frame.size.height) ||
497
477
  frame.size.width <= 0 || frame.size.height <= 0) {
498
478
  return;
@@ -501,7 +481,6 @@ static inline BOOL RJIsValidMaskFrame(CGRect frame) {
501
481
  CGFloat centerX = CGRectGetMidX(frame);
502
482
  CGFloat centerY = CGRectGetMidY(frame);
503
483
 
504
- // Validate centers aren't NaN
505
484
  if (isnan(centerX) || isnan(centerY)) {
506
485
  return;
507
486
  }
@@ -524,7 +503,6 @@ static inline BOOL RJIsValidMaskFrame(CGRect frame) {
524
503
  }
525
504
 
526
505
  - (void)drawWebViewLabelInContext:(CGContextRef)context frame:(CGRect)frame {
527
- // Skip if frame has invalid values
528
506
  if (isnan(frame.size.width) || isnan(frame.size.height) ||
529
507
  frame.size.width <= 0 || frame.size.height <= 0) {
530
508
  return;
@@ -533,7 +511,6 @@ static inline BOOL RJIsValidMaskFrame(CGRect frame) {
533
511
  CGFloat centerX = CGRectGetMidX(frame);
534
512
  CGFloat centerY = CGRectGetMidY(frame);
535
513
 
536
- // Validate centers aren't NaN
537
514
  if (isnan(centerX) || isnan(centerY)) {
538
515
  return;
539
516
  }
@@ -586,7 +563,6 @@ static inline BOOL RJIsValidMaskFrame(CGRect frame) {
586
563
  }
587
564
 
588
565
  - (void)drawTextInputLabelInContext:(CGContextRef)context frame:(CGRect)frame {
589
- // Skip if frame has invalid values
590
566
  if (isnan(frame.size.width) || isnan(frame.size.height) ||
591
567
  frame.size.width <= 0 || frame.size.height <= 0) {
592
568
  return;
@@ -595,7 +571,6 @@ static inline BOOL RJIsValidMaskFrame(CGRect frame) {
595
571
  CGFloat centerX = CGRectGetMidX(frame);
596
572
  CGFloat centerY = CGRectGetMidY(frame);
597
573
 
598
- // Validate centers aren't NaN
599
574
  if (isnan(centerX) || isnan(centerY)) {
600
575
  return;
601
576
  }
@@ -664,7 +639,6 @@ static inline BOOL RJIsValidMaskFrame(CGRect frame) {
664
639
  if (isTextInput || isCamera || isWebView || isVideo || isManuallyMasked) {
665
640
  CGRect frameInWindow = [view convertRect:view.bounds toView:window];
666
641
 
667
- // Sanitize NaN/Inf values from convertRect:toView:
668
642
  CGFloat x = (isnan(frameInWindow.origin.x) || isinf(frameInWindow.origin.x))
669
643
  ? 0
670
644
  : frameInWindow.origin.x;
package/ios/Rejourney.h CHANGED
@@ -1,17 +1,3 @@
1
- // Licensed under the Apache License, Version 2.0 (the "License");
2
- // you may not use this file except in compliance with the License.
3
- // You may obtain a copy of the License at
4
- //
5
- // http://www.apache.org/licenses/LICENSE-2.0
6
- //
7
- // Unless required by applicable law or agreed to in writing, software
8
- // distributed under the License is distributed on an "AS IS" BASIS,
9
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10
- // See the License for the specific language governing permissions and
11
- // limitations under the License.
12
- //
13
- // Copyright (c) 2026 Rejourney
14
-
15
1
  //
16
2
  //
17
3
  // Rejourney.h
@@ -54,10 +54,10 @@ static void RJ_swizzled_sendEvent(id self, SEL _cmd, UIEvent *event) {
54
54
  static int swizzleSkipCount = 0;
55
55
  if (++swizzleSkipCount % 500 == 1) {
56
56
  RJLogInfo(@"[RJ-TOUCH-SWIZZLE] Touch not handled: interceptor=%@, "
57
- @"isTrackingEnabled=%d (skipCount=%d)",
58
- interceptor ? @"exists" : @"nil",
59
- interceptor ? interceptor.isTrackingEnabled : -1,
60
- swizzleSkipCount);
57
+ @"isTrackingEnabled=%d (skipCount=%d)",
58
+ interceptor ? @"exists" : @"nil",
59
+ interceptor ? interceptor.isTrackingEnabled : -1,
60
+ swizzleSkipCount);
61
61
  }
62
62
  }
63
63
  }
@@ -148,8 +148,8 @@ static const NSTimeInterval kCoalesceInterval = 0.050;
148
148
 
149
149
  - (void)enableGlobalTracking {
150
150
  RJLogInfo(@"[RJ-TOUCH-SETUP] enableGlobalTracking called, current "
151
- @"isTrackingEnabled=%d",
152
- self.isTrackingEnabled);
151
+ @"isTrackingEnabled=%d",
152
+ self.isTrackingEnabled);
153
153
  static dispatch_once_t onceToken;
154
154
  dispatch_once(&onceToken, ^{
155
155
  RJLogInfo(@"[RJ-TOUCH-SETUP] First-time setup - performing swizzle");
@@ -164,13 +164,14 @@ static const NSTimeInterval kCoalesceInterval = 0.050;
164
164
  _swizzlingPerformed = YES;
165
165
  self.isTrackingEnabled = YES;
166
166
  RJLogInfo(@"[RJ-TOUCH-SETUP] Swizzle complete, isTrackingEnabled=%d",
167
- self.isTrackingEnabled);
167
+ self.isTrackingEnabled);
168
168
  } else {
169
169
  RJLogError(@"Failed to enable touch tracking - sendEvent: not found");
170
170
  }
171
171
  });
172
- RJLogInfo(@"[RJ-TOUCH-SETUP] enableGlobalTracking finished, isTrackingEnabled=%d",
173
- self.isTrackingEnabled);
172
+ RJLogInfo(
173
+ @"[RJ-TOUCH-SETUP] enableGlobalTracking finished, isTrackingEnabled=%d",
174
+ self.isTrackingEnabled);
174
175
  }
175
176
 
176
177
  #pragma mark - Touch Event Handling
@@ -181,7 +182,7 @@ static const NSTimeInterval kCoalesceInterval = 0.050;
181
182
  static int skipCount = 0;
182
183
  if (++skipCount % 100 == 1) {
183
184
  RJLogInfo(@"[RJ-TOUCH] Skipping touch: event=%@, isTrackingEnabled=%d",
184
- event ? @"exists" : @"nil", self.isTrackingEnabled);
185
+ event ? @"exists" : @"nil", self.isTrackingEnabled);
185
186
  }
186
187
  return;
187
188
  }
@@ -205,7 +206,7 @@ static const NSTimeInterval kCoalesceInterval = 0.050;
205
206
  static int notRecordingCount = 0;
206
207
  if (++notRecordingCount % 100 == 1) {
207
208
  RJLogInfo(@"[RJ-TOUCH] Not recording, skipping touch (count=%d)",
208
- notRecordingCount);
209
+ notRecordingCount);
209
210
  }
210
211
  return;
211
212
  }
@@ -465,7 +466,11 @@ static const NSTimeInterval kCoalesceInterval = 0.050;
465
466
  @try {
466
467
 
467
468
  NSMutableDictionary<NSNumber *, NSArray *> *touchPathsCopy =
468
- [self.touchPaths mutableCopy];
469
+ [NSMutableDictionary dictionaryWithCapacity:self.touchPaths.count];
470
+ [self.touchPaths enumerateKeysAndObjectsUsingBlock:^(
471
+ NSNumber *key, NSMutableArray *obj, BOOL *stop) {
472
+ touchPathsCopy[key] = [obj copy];
473
+ }];
469
474
  NSTimeInterval duration = MAX(0, timestamp - self.touchStartTime);
470
475
  NSInteger touchCount = self.touchPaths.count;
471
476
  CGPoint startPoint = self.touchStartPoint;
@@ -753,7 +758,8 @@ static const NSTimeInterval kCoalesceInterval = 0.050;
753
758
  respondsToSelector:@selector
754
759
  (touchInterceptorDidRecognizeGesture:
755
760
  touches:duration:targetLabel:)]) {
756
- RJLogInfo(@"[RJ-TOUCH] Calling delegate touchInterceptorDidRecognizeGesture");
761
+ RJLogInfo(
762
+ @"[RJ-TOUCH] Calling delegate touchInterceptorDidRecognizeGesture");
757
763
  [delegate
758
764
  touchInterceptorDidRecognizeGesture:gestureType ?: RJGestureTypeTap
759
765
  touches:touches ?: @[]
@@ -761,8 +767,8 @@ static const NSTimeInterval kCoalesceInterval = 0.050;
761
767
  targetLabel:targetLabel];
762
768
  } else {
763
769
  RJLogInfo(@"[RJ-TOUCH] Delegate missing or doesn't respond to selector: "
764
- @"delegate=%@",
765
- delegate);
770
+ @"delegate=%@",
771
+ delegate);
766
772
  }
767
773
  } @catch (NSException *exception) {
768
774
  RJLogWarning(@"Gesture notification failed: %@", exception);