@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,119 @@
1
+ //
2
+ // RJViewSerializer.h
3
+ // Rejourney
4
+ //
5
+ // View hierarchy serializer for privacy masking and debugging.
6
+ // Captures view structure as JSON for element identification,
7
+ // tap target resolution, and layout debugging in session replay.
8
+ //
9
+ // Licensed under the Apache License, Version 2.0 (the "License");
10
+ // you may not use this file except in compliance with the License.
11
+ // You may obtain a copy of the License at
12
+ //
13
+ // http://www.apache.org/licenses/LICENSE-2.0
14
+ //
15
+ // Unless required by applicable law or agreed to in writing, software
16
+ // distributed under the License is distributed on an "AS IS" BASIS,
17
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18
+ // See the License for the specific language governing permissions and
19
+ // limitations under the License.
20
+ //
21
+ // Copyright (c) 2026 Rejourney
22
+ //
23
+
24
+ #import <Foundation/Foundation.h>
25
+ #import <UIKit/UIKit.h>
26
+
27
+ @class RJViewHierarchyScanResult;
28
+
29
+ NS_ASSUME_NONNULL_BEGIN
30
+
31
+ /**
32
+ * View hierarchy serializer for session recording.
33
+ *
34
+ * Captures the complete view tree structure as a JSON-compatible dictionary,
35
+ * including:
36
+ * - View types and frames
37
+ * - Accessibility identifiers and labels
38
+ * - Interactive element detection
39
+ * - Privacy masking indicators
40
+ * - Visual properties (colors, alpha)
41
+ *
42
+ * ## Usage
43
+ * ```objc
44
+ * RJViewSerializer *serializer = [[RJViewSerializer alloc] init];
45
+ * NSDictionary *hierarchy = [serializer serializeWindow:keyWindow];
46
+ * NSData *jsonData = [NSJSONSerialization dataWithJSONObject:hierarchy
47
+ * options:0 error:nil];
48
+ * ```
49
+ *
50
+ * ## Privacy
51
+ * Text content is automatically masked (e.g., "password" -> "p•••••d").
52
+ * Sensitive view types (UITextField, UITextView) are flagged with `masked:
53
+ * true`.
54
+ *
55
+ * @note Call from the main thread only.
56
+ */
57
+ @interface RJViewSerializer : NSObject
58
+
59
+ #pragma mark - Configuration
60
+
61
+ /// Whether serialization is enabled. Default: YES.
62
+ @property(nonatomic, assign) BOOL enabled;
63
+
64
+ /// Maximum depth of view tree traversal. Default: 20.
65
+ @property(nonatomic, assign) NSInteger maxDepth;
66
+
67
+ /// Whether to include visual properties (colors, alpha). Default: YES.
68
+ @property(nonatomic, assign) BOOL includeVisualProperties;
69
+
70
+ /// Whether to include text content (masked). Default: YES.
71
+ @property(nonatomic, assign) BOOL includeTextContent;
72
+
73
+ #pragma mark - Serialization
74
+
75
+ /**
76
+ * Serializes the entire window hierarchy to a JSON-compatible dictionary.
77
+ *
78
+ * @param window The window to serialize.
79
+ * @return Dictionary containing:
80
+ * - timestamp: Epoch milliseconds
81
+ * - screen: { width, height, scale }
82
+ * - root: Recursive view tree
83
+ */
84
+ - (NSDictionary *)serializeWindow:(UIWindow *)window;
85
+
86
+ /** * Serialize window using pre-scanned results for better performance.
87
+ * Falls back to regular serialization if scanResult is nil.
88
+ *
89
+ * @param window The window to serialize
90
+ * @param scanResult Pre-scanned view hierarchy result (optional)
91
+ * @return Dictionary with hierarchy structure and metadata
92
+ */
93
+ - (NSDictionary *)serializeWindow:(UIWindow *)window
94
+ withScanResult:
95
+ (nullable RJViewHierarchyScanResult *)scanResult;
96
+
97
+ /** * Serializes a single view and its subviews.
98
+ *
99
+ * @param view The view to serialize.
100
+ * @return Dictionary containing view properties and children.
101
+ */
102
+ - (NSDictionary *)serializeView:(UIView *)view;
103
+
104
+ #pragma mark - Utility
105
+
106
+ /**
107
+ * Finds the view at a specific point in the window.
108
+ * Useful for resolving tap coordinates to view identifiers.
109
+ *
110
+ * @param point The point in window coordinates.
111
+ * @param window The window to search in.
112
+ * @return Dictionary with view info at that point, or nil if none found.
113
+ */
114
+ - (nullable NSDictionary *)viewInfoAtPoint:(CGPoint)point
115
+ inWindow:(UIWindow *)window;
116
+
117
+ @end
118
+
119
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,498 @@
1
+ //
2
+ // RJViewSerializer.m
3
+ // Rejourney
4
+ //
5
+ // View hierarchy serializer implementation.
6
+ //
7
+ // Licensed under the Apache License, Version 2.0 (the "License");
8
+ // you may not use this file except in compliance with the License.
9
+ // You may obtain a copy of the License at
10
+ //
11
+ // http://www.apache.org/licenses/LICENSE-2.0
12
+ //
13
+ // Unless required by applicable law or agreed to in writing, software
14
+ // distributed under the License is distributed on an "AS IS" BASIS,
15
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ // See the License for the specific language governing permissions and
17
+ // limitations under the License.
18
+ //
19
+ // Copyright (c) 2026 Rejourney
20
+ //
21
+
22
+ #import "RJViewSerializer.h"
23
+ #import "../Core/RJLogger.h"
24
+ #import "RJViewHierarchyScanner.h"
25
+
26
+ @implementation RJViewSerializer {
27
+ Class _imageViewClass;
28
+ Class _buttonClass;
29
+ Class _switchClass;
30
+ Class _scrollViewClass;
31
+ Class _textFieldClass;
32
+ Class _textViewClass;
33
+ Class _controlClass;
34
+ Class _sliderClass;
35
+ Class _stepperClass;
36
+ Class _segmentedControlClass;
37
+ Class _datePickerClass;
38
+ Class _pickerViewClass;
39
+ }
40
+
41
+ static NSMutableDictionary *classNameCache = nil;
42
+
43
+ #pragma mark - Initialization
44
+
45
+ + (void)initialize {
46
+ if (self == [RJViewSerializer class]) {
47
+ classNameCache = [NSMutableDictionary new];
48
+ }
49
+ }
50
+
51
+ - (instancetype)init {
52
+ self = [super init];
53
+ if (self) {
54
+ _enabled = YES;
55
+ _maxDepth = 10; // Aggressive optimization
56
+ _includeVisualProperties = YES;
57
+ _includeTextContent = YES;
58
+
59
+ _imageViewClass = [UIImageView class];
60
+ _buttonClass = [UIButton class];
61
+ _switchClass = [UISwitch class];
62
+ _scrollViewClass = [UIScrollView class];
63
+ _textFieldClass = [UITextField class];
64
+ _textViewClass = [UITextView class];
65
+ _controlClass = [UIControl class];
66
+ _sliderClass = [UISlider class];
67
+ _stepperClass = [UIStepper class];
68
+ _segmentedControlClass = [UISegmentedControl class];
69
+ _datePickerClass = [UIDatePicker class];
70
+ _pickerViewClass = [UIPickerView class];
71
+ }
72
+ return self;
73
+ }
74
+
75
+ #pragma mark - Public Methods
76
+
77
+ - (NSDictionary *)serializeWindow:(UIWindow *)window {
78
+ return [self serializeWindow:window withScanResult:nil];
79
+ }
80
+
81
+ - (NSDictionary *)serializeWindow:(UIWindow *)window
82
+ withScanResult:(RJViewHierarchyScanResult *)scanResult {
83
+ if (!self.enabled || !window) {
84
+ return @{};
85
+ }
86
+
87
+ @try {
88
+ NSTimeInterval timestamp = [[NSDate date] timeIntervalSince1970] * 1000;
89
+ CGFloat scale = [UIScreen mainScreen].scale;
90
+
91
+ UIView *rootView = window.rootViewController.view ?: window;
92
+
93
+ // Sanitize window bounds to prevent NaN in output
94
+ CGFloat winWidth = window.bounds.size.width;
95
+ CGFloat winHeight = window.bounds.size.height;
96
+ if (isnan(winWidth) || isinf(winWidth))
97
+ winWidth = 0;
98
+ if (isnan(winHeight) || isinf(winHeight))
99
+ winHeight = 0;
100
+ if (isnan(scale) || isinf(scale))
101
+ scale = 1;
102
+
103
+ NSTimeInterval scanStartTime = CACurrentMediaTime();
104
+ NSDictionary *rootNode = [self serializeViewInternal:rootView
105
+ depth:0
106
+ startTime:scanStartTime];
107
+
108
+ NSMutableDictionary *result = [@{
109
+ @"timestamp" : @(timestamp),
110
+ @"screen" : @{
111
+ @"width" : @(winWidth),
112
+ @"height" : @(winHeight),
113
+ @"scale" : @(scale),
114
+ },
115
+ @"root" : rootNode ?: @{},
116
+ } mutableCopy];
117
+
118
+ if (scanResult && scanResult.layoutSignature) {
119
+ result[@"layoutSignature"] = scanResult.layoutSignature;
120
+ }
121
+
122
+ return result;
123
+ } @catch (NSException *exception) {
124
+ RJLogError(@"View serialization failed: %@", exception);
125
+ return @{
126
+ @"timestamp" : @([[NSDate date] timeIntervalSince1970] * 1000),
127
+ @"error" : exception.reason ?: @"Unknown error",
128
+ };
129
+ }
130
+ }
131
+
132
+ - (NSDictionary *)serializeView:(UIView *)view {
133
+ if (!self.enabled || !view) {
134
+ return @{};
135
+ }
136
+ NSTimeInterval scanStartTime = CACurrentMediaTime();
137
+ return [self serializeViewInternal:view depth:0 startTime:scanStartTime];
138
+ }
139
+
140
+ - (NSDictionary *)viewInfoAtPoint:(CGPoint)point inWindow:(UIWindow *)window {
141
+ if (!window)
142
+ return nil;
143
+
144
+ UIView *hitView = [window hitTest:point withEvent:nil];
145
+ if (!hitView)
146
+ return nil;
147
+
148
+ return [self createViewInfoForView:hitView];
149
+ }
150
+
151
+ #pragma mark - Private Methods
152
+
153
+ // Helper to sanitize CGFloat values for CoreGraphics safety
154
+ static inline CGFloat RJSanitizeCGFloat(CGFloat value) {
155
+ if (isnan(value) || isinf(value)) {
156
+ return 0;
157
+ }
158
+ return value;
159
+ }
160
+
161
+ - (NSDictionary *)serializeViewInternal:(UIView *)view
162
+ depth:(NSInteger)depth
163
+ startTime:(NSTimeInterval)startTime {
164
+ if (!view || depth > self.maxDepth) {
165
+ return @{};
166
+ }
167
+
168
+ // PERFORMANCE BAILOUT: If we've been scanning for > 10ms, stop recursion to
169
+ // avoid main-thread hang. This allows us to return a partial tree instead of
170
+ // freezing the app.
171
+ if (CACurrentMediaTime() - startTime > 0.010) {
172
+ return @{@"type" : NSStringFromClass([view class]), @"bailout" : @YES};
173
+ }
174
+
175
+ NSMutableDictionary *node = [NSMutableDictionary dictionaryWithCapacity:12];
176
+
177
+ static NSString *kType = @"type";
178
+ static NSString *kFrame = @"frame";
179
+ static NSString *kHidden = @"hidden";
180
+ static NSString *kAlpha = @"alpha";
181
+ static NSString *kTestID = @"testID";
182
+ static NSString *kLabel = @"label";
183
+ static NSString *kMasked = @"masked";
184
+ static NSString *kBg = @"bg";
185
+ static NSString *kCornerRadius = @"cornerRadius";
186
+ static NSString *kBorderWidth = @"borderWidth";
187
+ static NSString *kText = @"text";
188
+ static NSString *kTextLength = @"textLength";
189
+ static NSString *kHasImage = @"hasImage";
190
+ static NSString *kImageLabel = @"imageLabel";
191
+ static NSString *kInteractive = @"interactive";
192
+ static NSString *kButtonTitle = @"buttonTitle";
193
+ static NSString *kEnabled = @"enabled";
194
+ static NSString *kSwitchOn = @"switchOn";
195
+ static NSString *kContentOffset = @"contentOffset";
196
+ static NSString *kContentSize = @"contentSize";
197
+ static NSString *kChildren = @"children";
198
+
199
+ Class viewClass = [view class];
200
+ NSString *className = classNameCache[(id<NSCopying>)viewClass];
201
+ if (!className) {
202
+ className = NSStringFromClass(viewClass);
203
+ classNameCache[(id<NSCopying>)viewClass] = className;
204
+ }
205
+ node[kType] = className;
206
+
207
+ CGRect frame = view.frame;
208
+ node[kFrame] = @{
209
+ @"x" : @(RJSanitizeCGFloat(frame.origin.x)),
210
+ @"y" : @(RJSanitizeCGFloat(frame.origin.y)),
211
+ @"w" : @(RJSanitizeCGFloat(frame.size.width)),
212
+ @"h" : @(RJSanitizeCGFloat(frame.size.height)),
213
+ };
214
+
215
+ if (view.hidden) {
216
+ node[kHidden] = @YES;
217
+ }
218
+ if (view.alpha < 1.0) {
219
+ node[kAlpha] = @(view.alpha);
220
+ }
221
+
222
+ if (view.accessibilityIdentifier.length > 0) {
223
+ node[kTestID] = view.accessibilityIdentifier;
224
+ }
225
+ if (view.accessibilityLabel.length > 0) {
226
+ node[kLabel] = view.accessibilityLabel;
227
+ }
228
+
229
+ if ([self isSensitiveView:view]) {
230
+ node[kMasked] = @YES;
231
+ }
232
+
233
+ if (self.includeVisualProperties) {
234
+ if (view.backgroundColor &&
235
+ ![view.backgroundColor isEqual:[UIColor clearColor]]) {
236
+ node[kBg] = [self colorToHex:view.backgroundColor];
237
+ }
238
+
239
+ if (view.layer.cornerRadius > 0) {
240
+ node[kCornerRadius] = @(view.layer.cornerRadius);
241
+ }
242
+
243
+ if (view.layer.borderWidth > 0) {
244
+ node[kBorderWidth] = @(view.layer.borderWidth);
245
+ }
246
+ }
247
+
248
+ if (self.includeTextContent) {
249
+ NSString *text = [self extractTextFromView:view];
250
+ if (text.length > 0) {
251
+ node[kText] = [self maskText:text];
252
+ node[kTextLength] = @(text.length);
253
+ }
254
+ }
255
+
256
+ if ([view isKindOfClass:_imageViewClass]) {
257
+ node[kHasImage] = @YES;
258
+ UIImageView *imageView = (UIImageView *)view;
259
+ if (imageView.accessibilityLabel.length > 0) {
260
+ node[kImageLabel] = imageView.accessibilityLabel;
261
+ }
262
+ }
263
+
264
+ if ([self isInteractiveView:view]) {
265
+ node[kInteractive] = @YES;
266
+
267
+ if ([view isKindOfClass:_buttonClass]) {
268
+ UIButton *button = (UIButton *)view;
269
+ NSString *title = button.currentTitle;
270
+ if (title.length > 0) {
271
+ node[kButtonTitle] = title;
272
+ }
273
+ node[kEnabled] = @(button.enabled);
274
+ }
275
+
276
+ if ([view isKindOfClass:_switchClass]) {
277
+ UISwitch *sw = (UISwitch *)view;
278
+ node[kSwitchOn] = @(sw.isOn);
279
+ }
280
+ }
281
+
282
+ if ([view isKindOfClass:_scrollViewClass]) {
283
+ UIScrollView *scrollView = (UIScrollView *)view;
284
+ node[kContentOffset] = @{
285
+ @"x" : @(RJSanitizeCGFloat(scrollView.contentOffset.x)),
286
+ @"y" : @(RJSanitizeCGFloat(scrollView.contentOffset.y)),
287
+ };
288
+ node[kContentSize] = @{
289
+ @"w" : @(RJSanitizeCGFloat(scrollView.contentSize.width)),
290
+ @"h" : @(RJSanitizeCGFloat(scrollView.contentSize.height)),
291
+ };
292
+ }
293
+
294
+ NSMutableArray *children =
295
+ [NSMutableArray arrayWithCapacity:view.subviews.count];
296
+ CGRect parentBounds = view.bounds;
297
+ for (UIView *child in [view.subviews reverseObjectEnumerator]) {
298
+ if (child.hidden || child.alpha <= 0.01) {
299
+ continue;
300
+ }
301
+
302
+ if (child.frame.size.width <= 0 || child.frame.size.height <= 0) {
303
+ continue;
304
+ }
305
+
306
+ NSDictionary *childNode = [self serializeViewInternal:child
307
+ depth:depth + 1
308
+ startTime:startTime];
309
+ if (childNode.count > 0) {
310
+ [children insertObject:childNode atIndex:0];
311
+ }
312
+
313
+ if (child.opaque && child.alpha >= 1.0 &&
314
+ CGRectEqualToRect(child.frame, parentBounds)) {
315
+ break;
316
+ }
317
+ }
318
+
319
+ if (children.count > 0) {
320
+ node[kChildren] = children;
321
+ }
322
+
323
+ return node;
324
+ }
325
+
326
+ - (NSDictionary *)createViewInfoForView:(UIView *)view {
327
+ if (!view)
328
+ return nil;
329
+
330
+ NSMutableDictionary *info = [NSMutableDictionary dictionaryWithCapacity:5];
331
+
332
+ static NSString *kType = @"type";
333
+ static NSString *kTestID = @"testID";
334
+ static NSString *kLabel = @"label";
335
+ static NSString *kFrame = @"frame";
336
+ static NSString *kInteractive = @"interactive";
337
+
338
+ Class viewClass = [view class];
339
+ NSString *className = classNameCache[(id<NSCopying>)viewClass];
340
+ if (!className) {
341
+ className = NSStringFromClass(viewClass);
342
+ classNameCache[(id<NSCopying>)viewClass] = className;
343
+ }
344
+ info[kType] = className;
345
+
346
+ if (view.accessibilityIdentifier.length > 0) {
347
+ info[kTestID] = view.accessibilityIdentifier;
348
+ }
349
+ if (view.accessibilityLabel.length > 0) {
350
+ info[kLabel] = view.accessibilityLabel;
351
+ }
352
+
353
+ // Sanitize frame values to prevent NaN/Inf
354
+ CGRect frame = view.frame;
355
+ info[kFrame] = @{
356
+ @"x" : @(RJSanitizeCGFloat(frame.origin.x)),
357
+ @"y" : @(RJSanitizeCGFloat(frame.origin.y)),
358
+ @"w" : @(RJSanitizeCGFloat(frame.size.width)),
359
+ @"h" : @(RJSanitizeCGFloat(frame.size.height)),
360
+ };
361
+
362
+ if ([self isInteractiveView:view]) {
363
+ info[kInteractive] = @YES;
364
+ }
365
+
366
+ return info;
367
+ }
368
+
369
+ #pragma mark - Helpers
370
+
371
+ - (BOOL)isSensitiveView:(UIView *)view {
372
+ if ([view isKindOfClass:[UITextField class]]) {
373
+ UITextField *textField = (UITextField *)view;
374
+ if (textField.isSecureTextEntry)
375
+ return YES;
376
+ UIKeyboardType kbType = textField.keyboardType;
377
+ if (kbType == UIKeyboardTypeNumberPad || kbType == UIKeyboardTypePhonePad ||
378
+ kbType == UIKeyboardTypeEmailAddress ||
379
+ kbType == UIKeyboardTypeDecimalPad) {
380
+ return YES;
381
+ }
382
+ UITextContentType contentType = textField.textContentType;
383
+ if (contentType) {
384
+ if ([contentType isEqualToString:UITextContentTypePassword] ||
385
+ [contentType isEqualToString:UITextContentTypeNewPassword] ||
386
+ [contentType isEqualToString:UITextContentTypeOneTimeCode] ||
387
+ [contentType isEqualToString:UITextContentTypeCreditCardNumber]) {
388
+ return YES;
389
+ }
390
+ }
391
+ return NO;
392
+ }
393
+
394
+ if ([view isKindOfClass:[UITextView class]]) {
395
+ UITextView *textView = (UITextView *)view;
396
+ if (textView.isSecureTextEntry)
397
+ return YES;
398
+ UITextContentType contentType = textView.textContentType;
399
+ if (contentType) {
400
+ if ([contentType isEqualToString:UITextContentTypePassword] ||
401
+ [contentType isEqualToString:UITextContentTypeNewPassword] ||
402
+ [contentType isEqualToString:UITextContentTypeOneTimeCode] ||
403
+ [contentType isEqualToString:UITextContentTypeCreditCardNumber]) {
404
+ return YES;
405
+ }
406
+ }
407
+ return NO;
408
+ }
409
+
410
+ Class viewClass = [view class];
411
+ NSString *className = classNameCache[(id<NSCopying>)viewClass];
412
+ if (!className) {
413
+ className = NSStringFromClass(viewClass);
414
+ classNameCache[(id<NSCopying>)viewClass] = className;
415
+ }
416
+ if ([className containsString:@"Password"] ||
417
+ [className containsString:@"Secure"] ||
418
+ [className containsString:@"Credit"] ||
419
+ [className containsString:@"Card"] || [className containsString:@"SSN"] ||
420
+ [className containsString:@"CVV"] || [className containsString:@"CVC"] ||
421
+ [className containsString:@"PIN"]) {
422
+ return YES;
423
+ }
424
+
425
+ if (view.accessibilityTraits & UIAccessibilityTraitKeyboardKey) {
426
+ return YES;
427
+ }
428
+
429
+ return NO;
430
+ }
431
+
432
+ - (BOOL)isInteractiveView:(UIView *)view {
433
+
434
+ return [view isKindOfClass:[UIControl class]] ||
435
+ [view isKindOfClass:[UITextView class]] ||
436
+ (view.gestureRecognizers.count > 0);
437
+ }
438
+
439
+ - (NSString *)extractTextFromView:(UIView *)view {
440
+ @try {
441
+ if ([view respondsToSelector:@selector(text)]) {
442
+ id text = [view performSelector:@selector(text)];
443
+ if ([text isKindOfClass:[NSString class]]) {
444
+ return text;
445
+ }
446
+ }
447
+
448
+ if ([view respondsToSelector:@selector(attributedText)]) {
449
+ id attrText = [view performSelector:@selector(attributedText)];
450
+ if ([attrText isKindOfClass:[NSAttributedString class]]) {
451
+ return [(NSAttributedString *)attrText string];
452
+ }
453
+ }
454
+ } @catch (NSException *exception) {
455
+ return nil;
456
+ }
457
+
458
+ if ([view isKindOfClass:[UIButton class]]) {
459
+ return [(UIButton *)view currentTitle];
460
+ }
461
+
462
+ return nil;
463
+ }
464
+
465
+ - (NSString *)maskText:(NSString *)text {
466
+ if (!text || text.length == 0) {
467
+ return @"";
468
+ }
469
+
470
+ NSInteger maskLength = MIN(text.length, 12);
471
+ NSMutableString *masked = [NSMutableString stringWithCapacity:maskLength];
472
+ for (NSInteger i = 0; i < maskLength; i++) {
473
+ [masked appendString:@"•"];
474
+ }
475
+
476
+ return masked;
477
+ }
478
+
479
+ - (NSString *)colorToHex:(UIColor *)color {
480
+ if (!color)
481
+ return nil;
482
+
483
+ CGFloat r, g, b, a;
484
+ if (![color getRed:&r green:&g blue:&b alpha:&a]) {
485
+
486
+ CGFloat white;
487
+ if ([color getWhite:&white alpha:&a]) {
488
+ r = g = b = white;
489
+ } else {
490
+ return nil;
491
+ }
492
+ }
493
+
494
+ return [NSString stringWithFormat:@"#%02X%02X%02X", (int)(r * 255),
495
+ (int)(g * 255), (int)(b * 255)];
496
+ }
497
+
498
+ @end