@rejourneyco/react-native 1.0.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.
Files changed (152) hide show
  1. package/android/build.gradle.kts +135 -0
  2. package/android/consumer-rules.pro +10 -0
  3. package/android/proguard-rules.pro +1 -0
  4. package/android/src/main/AndroidManifest.xml +15 -0
  5. package/android/src/main/java/com/rejourney/RejourneyModuleImpl.kt +2981 -0
  6. package/android/src/main/java/com/rejourney/capture/ANRHandler.kt +206 -0
  7. package/android/src/main/java/com/rejourney/capture/ActivityTracker.kt +98 -0
  8. package/android/src/main/java/com/rejourney/capture/CaptureEngine.kt +1553 -0
  9. package/android/src/main/java/com/rejourney/capture/CaptureHeuristics.kt +375 -0
  10. package/android/src/main/java/com/rejourney/capture/CrashHandler.kt +153 -0
  11. package/android/src/main/java/com/rejourney/capture/MotionEvent.kt +215 -0
  12. package/android/src/main/java/com/rejourney/capture/SegmentUploader.kt +512 -0
  13. package/android/src/main/java/com/rejourney/capture/VideoEncoder.kt +773 -0
  14. package/android/src/main/java/com/rejourney/capture/ViewHierarchyScanner.kt +633 -0
  15. package/android/src/main/java/com/rejourney/capture/ViewSerializer.kt +286 -0
  16. package/android/src/main/java/com/rejourney/core/Constants.kt +117 -0
  17. package/android/src/main/java/com/rejourney/core/Logger.kt +93 -0
  18. package/android/src/main/java/com/rejourney/core/Types.kt +124 -0
  19. package/android/src/main/java/com/rejourney/lifecycle/SessionLifecycleService.kt +162 -0
  20. package/android/src/main/java/com/rejourney/network/DeviceAuthManager.kt +747 -0
  21. package/android/src/main/java/com/rejourney/network/HttpClientProvider.kt +16 -0
  22. package/android/src/main/java/com/rejourney/network/NetworkMonitor.kt +272 -0
  23. package/android/src/main/java/com/rejourney/network/UploadManager.kt +1363 -0
  24. package/android/src/main/java/com/rejourney/network/UploadWorker.kt +492 -0
  25. package/android/src/main/java/com/rejourney/privacy/PrivacyMask.kt +645 -0
  26. package/android/src/main/java/com/rejourney/touch/GestureClassifier.kt +233 -0
  27. package/android/src/main/java/com/rejourney/touch/KeyboardTracker.kt +158 -0
  28. package/android/src/main/java/com/rejourney/touch/TextInputTracker.kt +181 -0
  29. package/android/src/main/java/com/rejourney/touch/TouchInterceptor.kt +591 -0
  30. package/android/src/main/java/com/rejourney/utils/EventBuffer.kt +284 -0
  31. package/android/src/main/java/com/rejourney/utils/OEMDetector.kt +154 -0
  32. package/android/src/main/java/com/rejourney/utils/PerfTiming.kt +235 -0
  33. package/android/src/main/java/com/rejourney/utils/Telemetry.kt +297 -0
  34. package/android/src/main/java/com/rejourney/utils/WindowUtils.kt +84 -0
  35. package/android/src/newarch/java/com/rejourney/RejourneyModule.kt +187 -0
  36. package/android/src/newarch/java/com/rejourney/RejourneyPackage.kt +40 -0
  37. package/android/src/oldarch/java/com/rejourney/RejourneyModule.kt +218 -0
  38. package/android/src/oldarch/java/com/rejourney/RejourneyPackage.kt +23 -0
  39. package/ios/Capture/RJANRHandler.h +42 -0
  40. package/ios/Capture/RJANRHandler.m +328 -0
  41. package/ios/Capture/RJCaptureEngine.h +275 -0
  42. package/ios/Capture/RJCaptureEngine.m +2062 -0
  43. package/ios/Capture/RJCaptureHeuristics.h +80 -0
  44. package/ios/Capture/RJCaptureHeuristics.m +903 -0
  45. package/ios/Capture/RJCrashHandler.h +46 -0
  46. package/ios/Capture/RJCrashHandler.m +313 -0
  47. package/ios/Capture/RJMotionEvent.h +183 -0
  48. package/ios/Capture/RJMotionEvent.m +183 -0
  49. package/ios/Capture/RJPerformanceManager.h +100 -0
  50. package/ios/Capture/RJPerformanceManager.m +373 -0
  51. package/ios/Capture/RJPixelBufferDownscaler.h +42 -0
  52. package/ios/Capture/RJPixelBufferDownscaler.m +85 -0
  53. package/ios/Capture/RJSegmentUploader.h +146 -0
  54. package/ios/Capture/RJSegmentUploader.m +778 -0
  55. package/ios/Capture/RJVideoEncoder.h +247 -0
  56. package/ios/Capture/RJVideoEncoder.m +1036 -0
  57. package/ios/Capture/RJViewControllerTracker.h +73 -0
  58. package/ios/Capture/RJViewControllerTracker.m +508 -0
  59. package/ios/Capture/RJViewHierarchyScanner.h +215 -0
  60. package/ios/Capture/RJViewHierarchyScanner.m +1464 -0
  61. package/ios/Capture/RJViewSerializer.h +119 -0
  62. package/ios/Capture/RJViewSerializer.m +498 -0
  63. package/ios/Core/RJConstants.h +124 -0
  64. package/ios/Core/RJConstants.m +88 -0
  65. package/ios/Core/RJLifecycleManager.h +85 -0
  66. package/ios/Core/RJLifecycleManager.m +308 -0
  67. package/ios/Core/RJLogger.h +61 -0
  68. package/ios/Core/RJLogger.m +211 -0
  69. package/ios/Core/RJTypes.h +176 -0
  70. package/ios/Core/RJTypes.m +66 -0
  71. package/ios/Core/Rejourney.h +64 -0
  72. package/ios/Core/Rejourney.mm +2495 -0
  73. package/ios/Network/RJDeviceAuthManager.h +94 -0
  74. package/ios/Network/RJDeviceAuthManager.m +967 -0
  75. package/ios/Network/RJNetworkMonitor.h +68 -0
  76. package/ios/Network/RJNetworkMonitor.m +267 -0
  77. package/ios/Network/RJRetryManager.h +73 -0
  78. package/ios/Network/RJRetryManager.m +325 -0
  79. package/ios/Network/RJUploadManager.h +267 -0
  80. package/ios/Network/RJUploadManager.m +2296 -0
  81. package/ios/Privacy/RJPrivacyMask.h +163 -0
  82. package/ios/Privacy/RJPrivacyMask.m +922 -0
  83. package/ios/Rejourney.h +63 -0
  84. package/ios/Touch/RJGestureClassifier.h +130 -0
  85. package/ios/Touch/RJGestureClassifier.m +333 -0
  86. package/ios/Touch/RJTouchInterceptor.h +169 -0
  87. package/ios/Touch/RJTouchInterceptor.m +772 -0
  88. package/ios/Utils/RJEventBuffer.h +112 -0
  89. package/ios/Utils/RJEventBuffer.m +358 -0
  90. package/ios/Utils/RJGzipUtils.h +33 -0
  91. package/ios/Utils/RJGzipUtils.m +89 -0
  92. package/ios/Utils/RJKeychainManager.h +48 -0
  93. package/ios/Utils/RJKeychainManager.m +111 -0
  94. package/ios/Utils/RJPerfTiming.h +209 -0
  95. package/ios/Utils/RJPerfTiming.m +264 -0
  96. package/ios/Utils/RJTelemetry.h +92 -0
  97. package/ios/Utils/RJTelemetry.m +320 -0
  98. package/ios/Utils/RJWindowUtils.h +66 -0
  99. package/ios/Utils/RJWindowUtils.m +133 -0
  100. package/lib/commonjs/NativeRejourney.js +40 -0
  101. package/lib/commonjs/components/Mask.js +79 -0
  102. package/lib/commonjs/index.js +1381 -0
  103. package/lib/commonjs/sdk/autoTracking.js +1259 -0
  104. package/lib/commonjs/sdk/constants.js +151 -0
  105. package/lib/commonjs/sdk/errorTracking.js +199 -0
  106. package/lib/commonjs/sdk/index.js +50 -0
  107. package/lib/commonjs/sdk/metricsTracking.js +204 -0
  108. package/lib/commonjs/sdk/navigation.js +151 -0
  109. package/lib/commonjs/sdk/networkInterceptor.js +412 -0
  110. package/lib/commonjs/sdk/utils.js +363 -0
  111. package/lib/commonjs/types/expo-router.d.js +2 -0
  112. package/lib/commonjs/types/index.js +2 -0
  113. package/lib/module/NativeRejourney.js +38 -0
  114. package/lib/module/components/Mask.js +72 -0
  115. package/lib/module/index.js +1284 -0
  116. package/lib/module/sdk/autoTracking.js +1233 -0
  117. package/lib/module/sdk/constants.js +145 -0
  118. package/lib/module/sdk/errorTracking.js +189 -0
  119. package/lib/module/sdk/index.js +12 -0
  120. package/lib/module/sdk/metricsTracking.js +187 -0
  121. package/lib/module/sdk/navigation.js +143 -0
  122. package/lib/module/sdk/networkInterceptor.js +401 -0
  123. package/lib/module/sdk/utils.js +342 -0
  124. package/lib/module/types/expo-router.d.js +2 -0
  125. package/lib/module/types/index.js +2 -0
  126. package/lib/typescript/NativeRejourney.d.ts +147 -0
  127. package/lib/typescript/components/Mask.d.ts +39 -0
  128. package/lib/typescript/index.d.ts +117 -0
  129. package/lib/typescript/sdk/autoTracking.d.ts +204 -0
  130. package/lib/typescript/sdk/constants.d.ts +120 -0
  131. package/lib/typescript/sdk/errorTracking.d.ts +32 -0
  132. package/lib/typescript/sdk/index.d.ts +9 -0
  133. package/lib/typescript/sdk/metricsTracking.d.ts +58 -0
  134. package/lib/typescript/sdk/navigation.d.ts +33 -0
  135. package/lib/typescript/sdk/networkInterceptor.d.ts +47 -0
  136. package/lib/typescript/sdk/utils.d.ts +148 -0
  137. package/lib/typescript/types/index.d.ts +624 -0
  138. package/package.json +102 -0
  139. package/rejourney.podspec +21 -0
  140. package/src/NativeRejourney.ts +165 -0
  141. package/src/components/Mask.tsx +80 -0
  142. package/src/index.ts +1459 -0
  143. package/src/sdk/autoTracking.ts +1373 -0
  144. package/src/sdk/constants.ts +134 -0
  145. package/src/sdk/errorTracking.ts +231 -0
  146. package/src/sdk/index.ts +11 -0
  147. package/src/sdk/metricsTracking.ts +232 -0
  148. package/src/sdk/navigation.ts +157 -0
  149. package/src/sdk/networkInterceptor.ts +440 -0
  150. package/src/sdk/utils.ts +369 -0
  151. package/src/types/expo-router.d.ts +7 -0
  152. package/src/types/index.ts +739 -0
@@ -0,0 +1,922 @@
1
+ //
2
+ // RJPrivacyMask.m
3
+ // Rejourney
4
+ //
5
+ // Privacy masking implementation using Core Graphics drawing.
6
+ // Draws blur overlays directly into the captured image without
7
+ // adding any views to the window hierarchy.
8
+ //
9
+ // Licensed under the Apache License, Version 2.0 (the "License").
10
+ // Copyright (c) 2026 Rejourney
11
+ //
12
+
13
+ #import "RJPrivacyMask.h"
14
+ #import "../Capture/RJViewHierarchyScanner.h"
15
+ #import "../Core/RJLogger.h"
16
+ #import <AVFoundation/AVFoundation.h>
17
+ #import <UIKit/UIKit.h>
18
+ #import <WebKit/WebKit.h>
19
+
20
+ static inline BOOL RJIsValidMaskFrame(CGRect frame) {
21
+ return isfinite(frame.origin.x) && isfinite(frame.origin.y) &&
22
+ isfinite(frame.size.width) && isfinite(frame.size.height) &&
23
+ frame.size.width > 0 && frame.size.height > 0;
24
+ }
25
+
26
+ #pragma mark - Implementation
27
+
28
+ @interface RJPrivacyMask ()
29
+
30
+ @property(nonatomic, assign, readwrite) BOOL isInBackground;
31
+
32
+ @property(nonatomic, strong) NSMutableSet<NSValue *> *scannedViews;
33
+
34
+ @property(nonatomic, assign) BOOL lastFrameHadCamera;
35
+
36
+ @property(nonatomic, assign) BOOL lastFrameHadTextInput;
37
+ @property(nonatomic, assign) BOOL lastFrameHadWebView;
38
+
39
+ @property(nonatomic, assign) BOOL lastFrameHadVideo;
40
+
41
+ @property(nonatomic, strong) NSMutableArray<NSValue *> *textInputFrames;
42
+ @property(nonatomic, strong) NSMutableArray<NSValue *> *cameraFrames;
43
+ @property(nonatomic, strong) NSMutableArray<NSValue *> *webViewFrames;
44
+ @property(nonatomic, strong) NSMutableArray<NSValue *> *videoFrames;
45
+
46
+ @property(nonatomic, strong, readwrite)
47
+ NSMutableSet<NSString *> *maskedNativeIDs;
48
+
49
+ @property(nonatomic, assign) CGColorSpaceRef commonColorSpace;
50
+
51
+ // Forward declarations to fix 'method not found' warnings/erros
52
+ - (void)findSensitiveViewsInWindow:(UIWindow *)window;
53
+ - (void)scanView:(UIView *)view inWindow:(UIWindow *)window;
54
+ - (void)drawBackgroundOverlayInContext:(CGContextRef)context
55
+ bounds:(CGRect)bounds
56
+ scale:(CGFloat)scale;
57
+ - (void)drawBlurRectInContext:(CGContextRef)context
58
+ frame:(CGRect)frame
59
+ maskType:(NSInteger)maskType;
60
+ - (void)drawMasksWithScanResult:(RJViewHierarchyScanResult *)scanResult
61
+ context:(CGContextRef)context
62
+ bounds:(CGRect)bounds
63
+ scale:(CGFloat)scale;
64
+ - (BOOL)shouldMaskAllForScanResult:(RJViewHierarchyScanResult *)scanResult;
65
+
66
+ @end
67
+
68
+ @implementation RJPrivacyMask
69
+
70
+ #pragma mark - Initialization
71
+
72
+ - (instancetype)init {
73
+ self = [super init];
74
+ if (self) {
75
+ _maskCameraViews = YES;
76
+ _maskWebViews = YES;
77
+ _maskVideoLayers = YES;
78
+ _blurCornerRadius = 8.0;
79
+ _maskPadding = 4.0;
80
+ _isInBackground = NO;
81
+ _scannedViews = [NSMutableSet new];
82
+ _textInputFrames = [NSMutableArray new];
83
+ _cameraFrames = [NSMutableArray new];
84
+ _webViewFrames = [NSMutableArray new];
85
+ _videoFrames = [NSMutableArray new];
86
+ _lastFrameHadCamera = NO;
87
+ _lastFrameHadTextInput = NO;
88
+ _lastFrameHadWebView = NO;
89
+ _lastFrameHadVideo = NO;
90
+ _maskedNativeIDs = [NSMutableSet new];
91
+
92
+ // Optimization #10: Cache color space once
93
+ _commonColorSpace = CGColorSpaceCreateDeviceRGB();
94
+
95
+ [[NSNotificationCenter defaultCenter]
96
+ addObserver:self
97
+ selector:@selector(appDidEnterBackground:)
98
+ name:UIApplicationDidEnterBackgroundNotification
99
+ object:nil];
100
+ [[NSNotificationCenter defaultCenter]
101
+ addObserver:self
102
+ selector:@selector(appWillEnterForeground:)
103
+ name:UIApplicationWillEnterForegroundNotification
104
+ object:nil];
105
+ [[NSNotificationCenter defaultCenter]
106
+ addObserver:self
107
+ selector:@selector(appDidBecomeActive:)
108
+ name:UIApplicationDidBecomeActiveNotification
109
+ object:nil];
110
+ }
111
+ return self;
112
+ }
113
+
114
+ #pragma mark - Notification Handlers
115
+
116
+ - (void)appDidEnterBackground:(NSNotification *)notification {
117
+ self.isInBackground = YES;
118
+ }
119
+
120
+ - (void)appWillEnterForeground:(NSNotification *)notification {
121
+ // Optional: keep masked until active
122
+ }
123
+
124
+ - (void)appDidBecomeActive:(NSNotification *)notification {
125
+ self.isInBackground = NO;
126
+ }
127
+
128
+ - (void)dealloc {
129
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
130
+ if (_commonColorSpace) {
131
+ CGColorSpaceRelease(_commonColorSpace);
132
+ _commonColorSpace = NULL;
133
+ }
134
+ }
135
+
136
+ - (BOOL)shouldMaskAllForScanResult:(RJViewHierarchyScanResult *)scanResult {
137
+ if (!scanResult) {
138
+ return YES;
139
+ }
140
+ if (scanResult.didBailOutEarly && scanResult.totalViewsScanned == 0) {
141
+ return YES;
142
+ }
143
+ return NO;
144
+ }
145
+
146
+ - (void)drawMasksForWindow:(UIWindow *)window
147
+ bounds:(CGRect)bounds
148
+ scale:(CGFloat)scale {
149
+ if (!window) {
150
+ return;
151
+ }
152
+
153
+ CGContextRef context = UIGraphicsGetCurrentContext();
154
+ if (!context) {
155
+ return;
156
+ }
157
+
158
+ CGFloat safeScale =
159
+ (isfinite(scale) && scale > 0.0) ? scale : 1.0;
160
+
161
+ [self findSensitiveViewsInWindow:window];
162
+
163
+ RJViewHierarchyScanResult *scanResult =
164
+ [[RJViewHierarchyScanResult alloc] init];
165
+ scanResult.textInputFrames = [self.textInputFrames copy];
166
+ scanResult.cameraFrames = [self.cameraFrames copy];
167
+ scanResult.webViewFrames = [self.webViewFrames copy];
168
+ scanResult.videoFrames = [self.videoFrames copy];
169
+
170
+ if ([self shouldMaskAllForScanResult:scanResult] || self.isInBackground) {
171
+ [self drawBackgroundOverlayInContext:context bounds:bounds scale:safeScale];
172
+ return;
173
+ }
174
+
175
+ [self drawMasksWithScanResult:scanResult
176
+ context:context
177
+ bounds:bounds
178
+ scale:safeScale];
179
+ }
180
+
181
+ - (void)drawMasksWithScanResult:(RJViewHierarchyScanResult *)scanResult
182
+ bounds:(CGRect)bounds
183
+ scale:(CGFloat)scale {
184
+ CGContextRef context = UIGraphicsGetCurrentContext();
185
+ if (!context) {
186
+ return;
187
+ }
188
+
189
+ CGFloat safeScale =
190
+ (isfinite(scale) && scale > 0.0) ? scale : 1.0;
191
+
192
+ if ([self shouldMaskAllForScanResult:scanResult] || self.isInBackground) {
193
+ [self drawBackgroundOverlayInContext:context bounds:bounds scale:safeScale];
194
+ return;
195
+ }
196
+
197
+ [self drawMasksWithScanResult:scanResult
198
+ context:context
199
+ bounds:bounds
200
+ scale:safeScale];
201
+ }
202
+
203
+ - (UIImage *)applyMasksToImage:(UIImage *)image
204
+ scanResult:(RJViewHierarchyScanResult *)scanResult
205
+ isInBackground:(BOOL)isInBackground {
206
+ if (!image) {
207
+ return image;
208
+ }
209
+
210
+ BOOL shouldMaskAll = isInBackground ||
211
+ [self shouldMaskAllForScanResult:scanResult];
212
+ BOOL hasFrames = (scanResult.textInputFrames.count > 0 ||
213
+ scanResult.cameraFrames.count > 0 ||
214
+ scanResult.webViewFrames.count > 0 ||
215
+ scanResult.videoFrames.count > 0);
216
+ if (!shouldMaskAll && !hasFrames) {
217
+ return image;
218
+ }
219
+
220
+ CGSize size = image.size;
221
+ CGFloat imageScale = image.scale;
222
+ CGRect bounds = CGRectMake(0, 0, size.width, size.height);
223
+
224
+ UIGraphicsBeginImageContextWithOptions(size, YES, imageScale);
225
+ [image drawAtPoint:CGPointZero];
226
+ CGContextRef context = UIGraphicsGetCurrentContext();
227
+
228
+ BOOL previousBackground = self.isInBackground;
229
+ self.isInBackground = isInBackground;
230
+
231
+ if (context) {
232
+ if (shouldMaskAll) {
233
+ [self drawBackgroundOverlayInContext:context bounds:bounds scale:1.0];
234
+ } else {
235
+ [self drawMasksWithScanResult:scanResult
236
+ context:context
237
+ bounds:bounds
238
+ scale:1.0];
239
+ }
240
+ }
241
+
242
+ UIImage *maskedImage = UIGraphicsGetImageFromCurrentImageContext();
243
+ UIGraphicsEndImageContext();
244
+
245
+ self.isInBackground = previousBackground;
246
+ return maskedImage ?: image;
247
+ }
248
+
249
+ - (void)applyToPixelBuffer:(CVPixelBufferRef)pixelBuffer
250
+ withScanResult:(RJViewHierarchyScanResult *)scanResult
251
+ scale:(CGFloat)scale {
252
+ if (!pixelBuffer)
253
+ return;
254
+
255
+ CGFloat safeScale =
256
+ (isfinite(scale) && scale > 0.0) ? scale : 1.0;
257
+ BOOL shouldMaskAll = [self shouldMaskAllForScanResult:scanResult];
258
+
259
+ CVPixelBufferLockBaseAddress(pixelBuffer, 0);
260
+
261
+ void *baseAddress = CVPixelBufferGetBaseAddress(pixelBuffer);
262
+ size_t width = CVPixelBufferGetWidth(pixelBuffer);
263
+ size_t height = CVPixelBufferGetHeight(pixelBuffer);
264
+ size_t bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer);
265
+
266
+ // Optimization #10: Use cached color space
267
+ CGColorSpaceRef colorSpace = self.commonColorSpace;
268
+ if (!colorSpace) {
269
+ // Fallback just in case
270
+ colorSpace = CGColorSpaceCreateDeviceRGB();
271
+ }
272
+
273
+ CGContextRef context = CGBitmapContextCreate(
274
+ baseAddress, width, height, 8, bytesPerRow, colorSpace,
275
+ kCGImageAlphaNoneSkipFirst | kCGBitmapByteOrder32Little);
276
+
277
+ // If we created a fallback, release it. If cached, don't.
278
+ if (colorSpace != self.commonColorSpace) {
279
+ CGColorSpaceRelease(colorSpace);
280
+ }
281
+
282
+ if (context) {
283
+ // PUSH context so high-level UIKit drawing (NSString drawAtPoint) works
284
+ UIGraphicsPushContext(context);
285
+
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
+ CGContextTranslateCTM(context, 0, height);
290
+ CGContextScaleCTM(context, 1.0, -1.0);
291
+
292
+ // Apply correct scale transform (Points -> Pixels)
293
+ CGContextScaleCTM(context, safeScale, safeScale);
294
+
295
+ // If in background or privacy scan is unavailable, draw full overlay
296
+ if (self.isInBackground || shouldMaskAll) {
297
+ // Create bounds in points (since we scaled the CTM)
298
+ CGRect boundsPoints =
299
+ CGRectMake(0, 0, width / safeScale, height / safeScale);
300
+ [self drawBackgroundOverlayInContext:context
301
+ bounds:boundsPoints
302
+ scale:1.0];
303
+ } else {
304
+ // Draw standard masks
305
+ [self drawMasksWithScanResult:scanResult
306
+ context:context
307
+ bounds:CGRectMake(0, 0, width / safeScale,
308
+ height / safeScale)
309
+ scale:1.0];
310
+ }
311
+
312
+ UIGraphicsPopContext();
313
+ CGContextRelease(context);
314
+ } else {
315
+ // Log sparingly or once to avoid spam
316
+ }
317
+
318
+ CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);
319
+ }
320
+
321
+ - (void)drawMasksWithScanResult:(RJViewHierarchyScanResult *)scanResult
322
+ context:(CGContextRef)context
323
+ bounds:(CGRect)bounds
324
+ scale:(CGFloat)scale {
325
+ if (!scanResult || !context) {
326
+ return;
327
+ }
328
+
329
+ @try {
330
+ self.lastFrameHadCamera = NO;
331
+ self.lastFrameHadTextInput = NO;
332
+ self.lastFrameHadWebView = NO;
333
+ self.lastFrameHadVideo = NO;
334
+
335
+ // Handle background state
336
+ if (self.isInBackground) {
337
+ // Only draw background overlay if we have a method for it
338
+ if ([self respondsToSelector:@selector
339
+ (drawBackgroundOverlayInContext:bounds:scale:)]) {
340
+ [self drawBackgroundOverlayInContext:context bounds:bounds scale:scale];
341
+ }
342
+ return;
343
+ }
344
+
345
+ // Early exit if nothing to mask - saves ~1-2ms per frame
346
+ if (scanResult.textInputFrames.count == 0 &&
347
+ scanResult.cameraFrames.count == 0 &&
348
+ scanResult.webViewFrames.count == 0 &&
349
+ scanResult.videoFrames.count == 0) {
350
+ return;
351
+ }
352
+
353
+ for (NSValue *frameValue in scanResult.textInputFrames) {
354
+ CGRect frame = [frameValue CGRectValue];
355
+
356
+ if (!RJIsValidMaskFrame(frame)) {
357
+ continue;
358
+ }
359
+
360
+ frame = CGRectInset(frame, -self.maskPadding, -self.maskPadding);
361
+ if (!RJIsValidMaskFrame(frame)) {
362
+ continue;
363
+ }
364
+ [self drawBlurRectInContext:context
365
+ frame:frame
366
+ maskType:0]; // 0 = TextInput
367
+ self.lastFrameHadTextInput = YES;
368
+ }
369
+
370
+ for (NSValue *frameValue in scanResult.cameraFrames) {
371
+ CGRect frame = [frameValue CGRectValue];
372
+
373
+ if (!RJIsValidMaskFrame(frame)) {
374
+ continue;
375
+ }
376
+
377
+ frame = CGRectInset(frame, -self.maskPadding, -self.maskPadding);
378
+ if (!RJIsValidMaskFrame(frame)) {
379
+ continue;
380
+ }
381
+ [self drawBlurRectInContext:context frame:frame maskType:1]; // 1 = Camera
382
+ self.lastFrameHadCamera = YES;
383
+ }
384
+
385
+ if (self.maskWebViews && scanResult.webViewFrames.count > 0) {
386
+ for (NSValue *frameValue in scanResult.webViewFrames) {
387
+ CGRect frame = [frameValue CGRectValue];
388
+
389
+ if (!RJIsValidMaskFrame(frame)) {
390
+ continue;
391
+ }
392
+
393
+ frame = CGRectInset(frame, -self.maskPadding, -self.maskPadding);
394
+ if (!RJIsValidMaskFrame(frame)) {
395
+ continue;
396
+ }
397
+ [self drawBlurRectInContext:context
398
+ frame:frame
399
+ maskType:2]; // 2 = WebView
400
+ self.lastFrameHadWebView = YES;
401
+ }
402
+ }
403
+
404
+ if (self.maskVideoLayers && scanResult.videoFrames.count > 0) {
405
+ for (NSValue *frameValue in scanResult.videoFrames) {
406
+ CGRect frame = [frameValue CGRectValue];
407
+
408
+ if (!RJIsValidMaskFrame(frame)) {
409
+ continue;
410
+ }
411
+
412
+ frame = CGRectInset(frame, -self.maskPadding, -self.maskPadding);
413
+ if (!RJIsValidMaskFrame(frame)) {
414
+ continue;
415
+ }
416
+ [self drawBlurRectInContext:context
417
+ frame:frame
418
+ maskType:3]; // 3 = Video
419
+ self.lastFrameHadVideo = YES;
420
+ }
421
+ }
422
+ } @catch (NSException *exception) {
423
+ RJLogWarning(@"Privacy mask drawing failed: %@", exception);
424
+ }
425
+ }
426
+
427
+ - (void)drawBackgroundOverlayInContext:(CGContextRef)context
428
+ bounds:(CGRect)bounds
429
+ scale:(CGFloat)scale {
430
+ // Simple black overlay for backgrounding
431
+ CGContextSetFillColorWithColor(context, [UIColor blackColor].CGColor);
432
+ CGContextFillRect(context, bounds);
433
+
434
+ // Optional: Draw text or logo? Keeping it simple/fast.
435
+ }
436
+
437
+ - (void)drawBlurRectInContext:(CGContextRef)context
438
+ frame:(CGRect)frame
439
+ maskType:(NSInteger)maskType {
440
+ // Validate frame to prevent CoreGraphics NaN errors
441
+ if (isnan(frame.origin.x) || isnan(frame.origin.y) ||
442
+ isnan(frame.size.width) || isnan(frame.size.height) ||
443
+ isinf(frame.origin.x) || isinf(frame.origin.y) ||
444
+ isinf(frame.size.width) || isinf(frame.size.height)) {
445
+ RJLogWarning(@"PrivacyMask: Skipping invalid frame with NaN/Inf values");
446
+ return;
447
+ }
448
+
449
+ // Also skip zero or negative sized frames
450
+ if (frame.size.width <= 0 || frame.size.height <= 0) {
451
+ return;
452
+ }
453
+
454
+ CGContextSaveGState(context);
455
+
456
+ UIBezierPath *path =
457
+ [UIBezierPath bezierPathWithRoundedRect:frame
458
+ cornerRadius:self.blurCornerRadius];
459
+
460
+ UIColor *blurColor;
461
+ if (maskType == 1 || maskType == 3) { // Camera / Video
462
+ 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
464
+ blurColor = [UIColor colorWithRed:0.12 green:0.12 blue:0.15 alpha:1.0];
465
+ } else {
466
+ // TextInput and others
467
+ blurColor = [UIColor blackColor];
468
+ }
469
+
470
+ CGContextSetFillColorWithColor(context, blurColor.CGColor);
471
+ CGContextAddPath(context, path.CGPath);
472
+ CGContextFillPath(context);
473
+
474
+ CGContextSetStrokeColorWithColor(
475
+ context, [UIColor colorWithWhite:0.5 alpha:0.3].CGColor);
476
+ CGContextSetLineWidth(context, 0.5);
477
+ CGContextAddPath(context, path.CGPath);
478
+ CGContextStrokePath(context);
479
+
480
+ if (maskType == 1) {
481
+ [self drawCameraLabelInContext:context frame:frame];
482
+ } else if (maskType == 2) {
483
+ // Reuse camera label style but with different text
484
+ [self drawWebViewLabelInContext:context frame:frame];
485
+ } else if (maskType == 3) {
486
+ [self drawVideoLabelInContext:context frame:frame];
487
+ } else {
488
+ [self drawTextInputLabelInContext:context frame:frame];
489
+ }
490
+
491
+ CGContextRestoreGState(context);
492
+ }
493
+
494
+ - (void)drawCameraLabelInContext:(CGContextRef)context frame:(CGRect)frame {
495
+ // Skip if frame has invalid values
496
+ if (isnan(frame.size.width) || isnan(frame.size.height) ||
497
+ frame.size.width <= 0 || frame.size.height <= 0) {
498
+ return;
499
+ }
500
+
501
+ CGFloat centerX = CGRectGetMidX(frame);
502
+ CGFloat centerY = CGRectGetMidY(frame);
503
+
504
+ // Validate centers aren't NaN
505
+ if (isnan(centerX) || isnan(centerY)) {
506
+ return;
507
+ }
508
+
509
+ NSString *label = @"📷 Camera Hidden";
510
+ UIFont *font = [UIFont systemFontOfSize:14 weight:UIFontWeightMedium];
511
+ NSDictionary *attrs = @{
512
+ NSFontAttributeName : font,
513
+ NSForegroundColorAttributeName : [UIColor colorWithWhite:0.7 alpha:1.0]
514
+ };
515
+
516
+ CGSize textSize = [label sizeWithAttributes:attrs];
517
+
518
+ if (textSize.width < frame.size.width - 20 &&
519
+ textSize.height < frame.size.height - 10) {
520
+ CGPoint textPoint = CGPointMake(centerX - textSize.width / 2,
521
+ centerY - textSize.height / 2);
522
+ [label drawAtPoint:textPoint withAttributes:attrs];
523
+ }
524
+ }
525
+
526
+ - (void)drawWebViewLabelInContext:(CGContextRef)context frame:(CGRect)frame {
527
+ // Skip if frame has invalid values
528
+ if (isnan(frame.size.width) || isnan(frame.size.height) ||
529
+ frame.size.width <= 0 || frame.size.height <= 0) {
530
+ return;
531
+ }
532
+
533
+ CGFloat centerX = CGRectGetMidX(frame);
534
+ CGFloat centerY = CGRectGetMidY(frame);
535
+
536
+ // Validate centers aren't NaN
537
+ if (isnan(centerX) || isnan(centerY)) {
538
+ return;
539
+ }
540
+
541
+ NSString *label = @"🌐 Web View Hidden";
542
+ UIFont *font = [UIFont systemFontOfSize:14 weight:UIFontWeightMedium];
543
+ NSDictionary *attrs = @{
544
+ NSFontAttributeName : font,
545
+ NSForegroundColorAttributeName : [UIColor colorWithWhite:0.7 alpha:1.0]
546
+ };
547
+
548
+ CGSize textSize = [label sizeWithAttributes:attrs];
549
+
550
+ if (textSize.width < frame.size.width - 20 &&
551
+ textSize.height < frame.size.height - 10) {
552
+ CGPoint textPoint = CGPointMake(centerX - textSize.width / 2,
553
+ centerY - textSize.height / 2);
554
+ [label drawAtPoint:textPoint withAttributes:attrs];
555
+ }
556
+ }
557
+
558
+ - (void)drawVideoLabelInContext:(CGContextRef)context frame:(CGRect)frame {
559
+ if (isnan(frame.size.width) || isnan(frame.size.height) ||
560
+ frame.size.width <= 0 || frame.size.height <= 0) {
561
+ return;
562
+ }
563
+
564
+ CGFloat centerX = CGRectGetMidX(frame);
565
+ CGFloat centerY = CGRectGetMidY(frame);
566
+
567
+ if (isnan(centerX) || isnan(centerY)) {
568
+ return;
569
+ }
570
+
571
+ NSString *label = @"🎥 Video Hidden";
572
+ UIFont *font = [UIFont systemFontOfSize:14 weight:UIFontWeightMedium];
573
+ NSDictionary *attrs = @{
574
+ NSFontAttributeName : font,
575
+ NSForegroundColorAttributeName : [UIColor colorWithWhite:0.7 alpha:1.0]
576
+ };
577
+
578
+ CGSize textSize = [label sizeWithAttributes:attrs];
579
+
580
+ if (textSize.width < frame.size.width - 20 &&
581
+ textSize.height < frame.size.height - 10) {
582
+ CGPoint textPoint = CGPointMake(centerX - textSize.width / 2,
583
+ centerY - textSize.height / 2);
584
+ [label drawAtPoint:textPoint withAttributes:attrs];
585
+ }
586
+ }
587
+
588
+ - (void)drawTextInputLabelInContext:(CGContextRef)context frame:(CGRect)frame {
589
+ // Skip if frame has invalid values
590
+ if (isnan(frame.size.width) || isnan(frame.size.height) ||
591
+ frame.size.width <= 0 || frame.size.height <= 0) {
592
+ return;
593
+ }
594
+
595
+ CGFloat centerX = CGRectGetMidX(frame);
596
+ CGFloat centerY = CGRectGetMidY(frame);
597
+
598
+ // Validate centers aren't NaN
599
+ if (isnan(centerX) || isnan(centerY)) {
600
+ return;
601
+ }
602
+
603
+ NSString *label = @"********";
604
+ UIFont *font = [UIFont systemFontOfSize:14 weight:UIFontWeightMedium];
605
+ NSDictionary *attrs = @{
606
+ NSFontAttributeName : font,
607
+ NSForegroundColorAttributeName : [UIColor whiteColor]
608
+ };
609
+
610
+ CGSize textSize = [label sizeWithAttributes:attrs];
611
+
612
+ if (textSize.width < frame.size.width - 10 &&
613
+ textSize.height < frame.size.height - 4) {
614
+ CGPoint textPoint = CGPointMake(centerX - textSize.width / 2,
615
+ centerY - textSize.height / 2);
616
+ [label drawAtPoint:textPoint withAttributes:attrs];
617
+ }
618
+ }
619
+
620
+ #pragma mark - Sensitive View Detection
621
+
622
+ - (void)findSensitiveViewsInWindow:(UIWindow *)window {
623
+ if (!window) {
624
+ return;
625
+ }
626
+
627
+ [self.scannedViews removeAllObjects];
628
+ [self.textInputFrames removeAllObjects];
629
+ [self.cameraFrames removeAllObjects];
630
+ [self.webViewFrames removeAllObjects];
631
+ [self.videoFrames removeAllObjects];
632
+
633
+ [self scanView:window inWindow:window];
634
+ }
635
+
636
+ - (NSArray<NSValue *> *)findSensitiveFramesInWindow:(UIWindow *)window {
637
+
638
+ [self findSensitiveViewsInWindow:window];
639
+
640
+ NSMutableArray<NSValue *> *allFrames = [NSMutableArray new];
641
+ [allFrames addObjectsFromArray:self.textInputFrames];
642
+ [allFrames addObjectsFromArray:self.cameraFrames];
643
+ [allFrames addObjectsFromArray:self.webViewFrames];
644
+ [allFrames addObjectsFromArray:self.videoFrames];
645
+
646
+ return allFrames;
647
+ }
648
+
649
+ - (void)scanView:(UIView *)view inWindow:(UIWindow *)window {
650
+ if (!view || view.isHidden || view.alpha < 0.01)
651
+ return;
652
+
653
+ NSValue *viewPtr = [NSValue valueWithNonretainedObject:view];
654
+ if ([self.scannedViews containsObject:viewPtr])
655
+ return;
656
+ [self.scannedViews addObject:viewPtr];
657
+
658
+ BOOL isTextInput = self.maskTextInputs && [self isActualTextInput:view];
659
+ BOOL isCamera = self.maskCameraViews && [self isCameraPreview:view];
660
+ BOOL isWebView = self.maskWebViews && [self isWebViewSurface:view];
661
+ BOOL isVideo = self.maskVideoLayers && [self isVideoLayerView:view];
662
+ BOOL isManuallyMasked = [self isManuallyMaskedView:view];
663
+
664
+ if (isTextInput || isCamera || isWebView || isVideo || isManuallyMasked) {
665
+ CGRect frameInWindow = [view convertRect:view.bounds toView:window];
666
+
667
+ // Sanitize NaN/Inf values from convertRect:toView:
668
+ CGFloat x = (isnan(frameInWindow.origin.x) || isinf(frameInWindow.origin.x))
669
+ ? 0
670
+ : frameInWindow.origin.x;
671
+ CGFloat y = (isnan(frameInWindow.origin.y) || isinf(frameInWindow.origin.y))
672
+ ? 0
673
+ : frameInWindow.origin.y;
674
+ CGFloat w =
675
+ (isnan(frameInWindow.size.width) || isinf(frameInWindow.size.width))
676
+ ? 0
677
+ : frameInWindow.size.width;
678
+ CGFloat h =
679
+ (isnan(frameInWindow.size.height) || isinf(frameInWindow.size.height))
680
+ ? 0
681
+ : frameInWindow.size.height;
682
+ CGRect sanitizedFrame = CGRectMake(x, y, w, h);
683
+
684
+ if (!CGRectIsEmpty(sanitizedFrame) && sanitizedFrame.size.width > 10 &&
685
+ sanitizedFrame.size.height > 10 &&
686
+ CGRectIntersectsRect(sanitizedFrame, window.bounds)) {
687
+
688
+ if (isCamera) {
689
+ [self.cameraFrames addObject:[NSValue valueWithCGRect:sanitizedFrame]];
690
+ RJLogDebug(@"PrivacyMask: Found camera %@ at %@",
691
+ NSStringFromClass([view class]),
692
+ NSStringFromCGRect(sanitizedFrame));
693
+ } else if (isWebView) {
694
+ [self.webViewFrames addObject:[NSValue valueWithCGRect:sanitizedFrame]];
695
+ RJLogDebug(@"PrivacyMask: Found web view %@ at %@",
696
+ NSStringFromClass([view class]),
697
+ NSStringFromCGRect(sanitizedFrame));
698
+ } else if (isVideo) {
699
+ [self.videoFrames addObject:[NSValue valueWithCGRect:sanitizedFrame]];
700
+ RJLogDebug(@"PrivacyMask: Found video view %@ at %@",
701
+ NSStringFromClass([view class]),
702
+ NSStringFromCGRect(sanitizedFrame));
703
+ } else {
704
+
705
+ [self.textInputFrames
706
+ addObject:[NSValue valueWithCGRect:sanitizedFrame]];
707
+ RJLogDebug(@"PrivacyMask: Found %@ %@ at %@",
708
+ isManuallyMasked ? @"masked view" : @"text input",
709
+ NSStringFromClass([view class]),
710
+ NSStringFromCGRect(sanitizedFrame));
711
+ }
712
+ }
713
+ }
714
+
715
+ for (UIView *subview in view.subviews) {
716
+ [self scanView:subview inWindow:window];
717
+ }
718
+ }
719
+
720
+ #pragma mark - Text Input Detection (STRICT)
721
+
722
+ - (BOOL)isActualTextInput:(UIView *)view {
723
+ if (!view)
724
+ return NO;
725
+
726
+ if ([view isKindOfClass:[UITextField class]]) {
727
+ return YES;
728
+ }
729
+
730
+ if ([view isKindOfClass:[UITextView class]]) {
731
+ UITextView *tv = (UITextView *)view;
732
+ return tv.isEditable;
733
+ }
734
+
735
+ if ([view isKindOfClass:[UISearchBar class]]) {
736
+ return YES;
737
+ }
738
+
739
+ NSString *className = NSStringFromClass([view class]);
740
+
741
+ static NSSet<NSString *> *rnInputClasses = nil;
742
+ static dispatch_once_t onceToken;
743
+ dispatch_once(&onceToken, ^{
744
+ rnInputClasses = [NSSet setWithArray:@[
745
+ @"RCTUITextField",
746
+ @"RCTBaseTextInputView",
747
+ @"RCTSinglelineTextInputView",
748
+ @"RCTMultilineTextInputView",
749
+ ]];
750
+ });
751
+
752
+ if ([rnInputClasses containsObject:className]) {
753
+ return YES;
754
+ }
755
+
756
+ Class currentClass = [view class];
757
+ while (currentClass && currentClass != [UIView class]) {
758
+ NSString *name = NSStringFromClass(currentClass);
759
+ if ([name isEqualToString:@"RCTBaseTextInputView"]) {
760
+ return YES;
761
+ }
762
+ currentClass = [currentClass superclass];
763
+ }
764
+
765
+ if ([view isFirstResponder] &&
766
+ [view conformsToProtocol:@protocol(UITextInput)]) {
767
+
768
+ NSString *className = NSStringFromClass([view class]);
769
+ if (![className containsString:@"Keyboard"] &&
770
+ ![className containsString:@"InputView"]) {
771
+ return YES;
772
+ }
773
+ }
774
+
775
+ return NO;
776
+ }
777
+
778
+ #pragma mark - Manual Masking Detection
779
+
780
+ - (BOOL)isManuallyMaskedView:(UIView *)view {
781
+ if (!view)
782
+ return NO;
783
+
784
+ if (view.tag == 98765) {
785
+ return YES;
786
+ }
787
+
788
+ if ([view.accessibilityHint isEqualToString:@"rejourney_occlude"]) {
789
+ return YES;
790
+ }
791
+
792
+ NSString *nativeID = view.accessibilityIdentifier;
793
+ if (nativeID.length > 0) {
794
+ if ([self.maskedNativeIDs containsObject:nativeID]) {
795
+ RJLogDebug(@"PrivacyMask: Found masked nativeID in view: %@", nativeID);
796
+ return YES;
797
+ }
798
+ }
799
+
800
+ for (UIView *subview in view.subviews) {
801
+ NSString *childNativeID = subview.accessibilityIdentifier;
802
+ if (childNativeID.length > 0 &&
803
+ [self.maskedNativeIDs containsObject:childNativeID]) {
804
+ RJLogDebug(@"PrivacyMask: Found masked nativeID in child: %@",
805
+ childNativeID);
806
+ return YES;
807
+ }
808
+ }
809
+
810
+ return NO;
811
+ }
812
+
813
+ #pragma mark - Manual nativeID Masking
814
+
815
+ - (void)addMaskedNativeID:(NSString *)nativeID {
816
+ if (nativeID.length > 0) {
817
+ [self.maskedNativeIDs addObject:nativeID];
818
+ RJLogDebug(@"PrivacyMask: Added masked nativeID: %@", nativeID);
819
+ }
820
+ }
821
+
822
+ - (void)removeMaskedNativeID:(NSString *)nativeID {
823
+ if (nativeID.length > 0) {
824
+ [self.maskedNativeIDs removeObject:nativeID];
825
+ RJLogDebug(@"PrivacyMask: Removed masked nativeID: %@", nativeID);
826
+ }
827
+ }
828
+
829
+ #pragma mark - Camera Detection
830
+
831
+ - (BOOL)isCameraPreview:(UIView *)view {
832
+ if (!view)
833
+ return NO;
834
+
835
+ if ([view.layer isKindOfClass:[AVCaptureVideoPreviewLayer class]]) {
836
+ return YES;
837
+ }
838
+
839
+ for (CALayer *sublayer in view.layer.sublayers) {
840
+ if ([sublayer isKindOfClass:[AVCaptureVideoPreviewLayer class]]) {
841
+ return YES;
842
+ }
843
+ }
844
+
845
+ NSString *className = NSStringFromClass([view class]);
846
+
847
+ static NSSet<NSString *> *cameraClasses = nil;
848
+ static dispatch_once_t onceToken;
849
+ dispatch_once(&onceToken, ^{
850
+ cameraClasses = [NSSet setWithArray:@[
851
+ @"AVCaptureVideoPreviewView",
852
+ @"CameraView",
853
+ @"CKCameraView",
854
+ @"RNCameraView",
855
+ @"VisionCameraView",
856
+ @"RNVisionCameraView",
857
+ @"ExpoCameraView",
858
+ ]];
859
+ });
860
+
861
+ if ([cameraClasses containsObject:className]) {
862
+ return YES;
863
+ }
864
+
865
+ return NO;
866
+ }
867
+
868
+ - (BOOL)isWebViewSurface:(UIView *)view {
869
+ if (!view)
870
+ return NO;
871
+
872
+ Class wkClass = NSClassFromString(@"WKWebView");
873
+ if (wkClass && [view isKindOfClass:wkClass]) {
874
+ return YES;
875
+ }
876
+
877
+ NSString *className = NSStringFromClass([view class]);
878
+ static NSSet<NSString *> *webViewClasses = nil;
879
+ static dispatch_once_t onceToken;
880
+ dispatch_once(&onceToken, ^{
881
+ webViewClasses = [NSSet setWithArray:@[
882
+ @"WKWebView",
883
+ @"UIWebView",
884
+ @"RCTWebView",
885
+ @"RNCWebView",
886
+ @"RNCWKWebView",
887
+ @"RCTWKWebView",
888
+ @"RNWebView",
889
+ ]];
890
+ });
891
+
892
+ return [webViewClasses containsObject:className] ||
893
+ [className containsString:@"WebView"];
894
+ }
895
+
896
+ - (BOOL)isVideoLayerView:(UIView *)view {
897
+ if (!view)
898
+ return NO;
899
+
900
+ if ([view.layer isKindOfClass:[AVPlayerLayer class]]) {
901
+ return YES;
902
+ }
903
+
904
+ for (CALayer *sublayer in view.layer.sublayers) {
905
+ if ([sublayer isKindOfClass:[AVPlayerLayer class]]) {
906
+ return YES;
907
+ }
908
+ }
909
+
910
+ NSString *className = NSStringFromClass([view class]);
911
+ return ([className containsString:@"Video"] &&
912
+ [className containsString:@"View"]);
913
+ }
914
+
915
+ #pragma mark - Cleanup
916
+
917
+ - (void)forceCleanup {
918
+
919
+ [self.scannedViews removeAllObjects];
920
+ }
921
+
922
+ @end