@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.
- package/android/build.gradle.kts +135 -0
- package/android/consumer-rules.pro +10 -0
- package/android/proguard-rules.pro +1 -0
- package/android/src/main/AndroidManifest.xml +15 -0
- package/android/src/main/java/com/rejourney/RejourneyModuleImpl.kt +2981 -0
- package/android/src/main/java/com/rejourney/capture/ANRHandler.kt +206 -0
- package/android/src/main/java/com/rejourney/capture/ActivityTracker.kt +98 -0
- package/android/src/main/java/com/rejourney/capture/CaptureEngine.kt +1553 -0
- package/android/src/main/java/com/rejourney/capture/CaptureHeuristics.kt +375 -0
- package/android/src/main/java/com/rejourney/capture/CrashHandler.kt +153 -0
- package/android/src/main/java/com/rejourney/capture/MotionEvent.kt +215 -0
- package/android/src/main/java/com/rejourney/capture/SegmentUploader.kt +512 -0
- package/android/src/main/java/com/rejourney/capture/VideoEncoder.kt +773 -0
- package/android/src/main/java/com/rejourney/capture/ViewHierarchyScanner.kt +633 -0
- package/android/src/main/java/com/rejourney/capture/ViewSerializer.kt +286 -0
- package/android/src/main/java/com/rejourney/core/Constants.kt +117 -0
- package/android/src/main/java/com/rejourney/core/Logger.kt +93 -0
- package/android/src/main/java/com/rejourney/core/Types.kt +124 -0
- package/android/src/main/java/com/rejourney/lifecycle/SessionLifecycleService.kt +162 -0
- package/android/src/main/java/com/rejourney/network/DeviceAuthManager.kt +747 -0
- package/android/src/main/java/com/rejourney/network/HttpClientProvider.kt +16 -0
- package/android/src/main/java/com/rejourney/network/NetworkMonitor.kt +272 -0
- package/android/src/main/java/com/rejourney/network/UploadManager.kt +1363 -0
- package/android/src/main/java/com/rejourney/network/UploadWorker.kt +492 -0
- package/android/src/main/java/com/rejourney/privacy/PrivacyMask.kt +645 -0
- package/android/src/main/java/com/rejourney/touch/GestureClassifier.kt +233 -0
- package/android/src/main/java/com/rejourney/touch/KeyboardTracker.kt +158 -0
- package/android/src/main/java/com/rejourney/touch/TextInputTracker.kt +181 -0
- package/android/src/main/java/com/rejourney/touch/TouchInterceptor.kt +591 -0
- package/android/src/main/java/com/rejourney/utils/EventBuffer.kt +284 -0
- package/android/src/main/java/com/rejourney/utils/OEMDetector.kt +154 -0
- package/android/src/main/java/com/rejourney/utils/PerfTiming.kt +235 -0
- package/android/src/main/java/com/rejourney/utils/Telemetry.kt +297 -0
- package/android/src/main/java/com/rejourney/utils/WindowUtils.kt +84 -0
- package/android/src/newarch/java/com/rejourney/RejourneyModule.kt +187 -0
- package/android/src/newarch/java/com/rejourney/RejourneyPackage.kt +40 -0
- package/android/src/oldarch/java/com/rejourney/RejourneyModule.kt +218 -0
- package/android/src/oldarch/java/com/rejourney/RejourneyPackage.kt +23 -0
- package/ios/Capture/RJANRHandler.h +42 -0
- package/ios/Capture/RJANRHandler.m +328 -0
- package/ios/Capture/RJCaptureEngine.h +275 -0
- package/ios/Capture/RJCaptureEngine.m +2062 -0
- package/ios/Capture/RJCaptureHeuristics.h +80 -0
- package/ios/Capture/RJCaptureHeuristics.m +903 -0
- package/ios/Capture/RJCrashHandler.h +46 -0
- package/ios/Capture/RJCrashHandler.m +313 -0
- package/ios/Capture/RJMotionEvent.h +183 -0
- package/ios/Capture/RJMotionEvent.m +183 -0
- package/ios/Capture/RJPerformanceManager.h +100 -0
- package/ios/Capture/RJPerformanceManager.m +373 -0
- package/ios/Capture/RJPixelBufferDownscaler.h +42 -0
- package/ios/Capture/RJPixelBufferDownscaler.m +85 -0
- package/ios/Capture/RJSegmentUploader.h +146 -0
- package/ios/Capture/RJSegmentUploader.m +778 -0
- package/ios/Capture/RJVideoEncoder.h +247 -0
- package/ios/Capture/RJVideoEncoder.m +1036 -0
- package/ios/Capture/RJViewControllerTracker.h +73 -0
- package/ios/Capture/RJViewControllerTracker.m +508 -0
- package/ios/Capture/RJViewHierarchyScanner.h +215 -0
- package/ios/Capture/RJViewHierarchyScanner.m +1464 -0
- package/ios/Capture/RJViewSerializer.h +119 -0
- package/ios/Capture/RJViewSerializer.m +498 -0
- package/ios/Core/RJConstants.h +124 -0
- package/ios/Core/RJConstants.m +88 -0
- package/ios/Core/RJLifecycleManager.h +85 -0
- package/ios/Core/RJLifecycleManager.m +308 -0
- package/ios/Core/RJLogger.h +61 -0
- package/ios/Core/RJLogger.m +211 -0
- package/ios/Core/RJTypes.h +176 -0
- package/ios/Core/RJTypes.m +66 -0
- package/ios/Core/Rejourney.h +64 -0
- package/ios/Core/Rejourney.mm +2495 -0
- package/ios/Network/RJDeviceAuthManager.h +94 -0
- package/ios/Network/RJDeviceAuthManager.m +967 -0
- package/ios/Network/RJNetworkMonitor.h +68 -0
- package/ios/Network/RJNetworkMonitor.m +267 -0
- package/ios/Network/RJRetryManager.h +73 -0
- package/ios/Network/RJRetryManager.m +325 -0
- package/ios/Network/RJUploadManager.h +267 -0
- package/ios/Network/RJUploadManager.m +2296 -0
- package/ios/Privacy/RJPrivacyMask.h +163 -0
- package/ios/Privacy/RJPrivacyMask.m +922 -0
- package/ios/Rejourney.h +63 -0
- package/ios/Touch/RJGestureClassifier.h +130 -0
- package/ios/Touch/RJGestureClassifier.m +333 -0
- package/ios/Touch/RJTouchInterceptor.h +169 -0
- package/ios/Touch/RJTouchInterceptor.m +772 -0
- package/ios/Utils/RJEventBuffer.h +112 -0
- package/ios/Utils/RJEventBuffer.m +358 -0
- package/ios/Utils/RJGzipUtils.h +33 -0
- package/ios/Utils/RJGzipUtils.m +89 -0
- package/ios/Utils/RJKeychainManager.h +48 -0
- package/ios/Utils/RJKeychainManager.m +111 -0
- package/ios/Utils/RJPerfTiming.h +209 -0
- package/ios/Utils/RJPerfTiming.m +264 -0
- package/ios/Utils/RJTelemetry.h +92 -0
- package/ios/Utils/RJTelemetry.m +320 -0
- package/ios/Utils/RJWindowUtils.h +66 -0
- package/ios/Utils/RJWindowUtils.m +133 -0
- package/lib/commonjs/NativeRejourney.js +40 -0
- package/lib/commonjs/components/Mask.js +79 -0
- package/lib/commonjs/index.js +1381 -0
- package/lib/commonjs/sdk/autoTracking.js +1259 -0
- package/lib/commonjs/sdk/constants.js +151 -0
- package/lib/commonjs/sdk/errorTracking.js +199 -0
- package/lib/commonjs/sdk/index.js +50 -0
- package/lib/commonjs/sdk/metricsTracking.js +204 -0
- package/lib/commonjs/sdk/navigation.js +151 -0
- package/lib/commonjs/sdk/networkInterceptor.js +412 -0
- package/lib/commonjs/sdk/utils.js +363 -0
- package/lib/commonjs/types/expo-router.d.js +2 -0
- package/lib/commonjs/types/index.js +2 -0
- package/lib/module/NativeRejourney.js +38 -0
- package/lib/module/components/Mask.js +72 -0
- package/lib/module/index.js +1284 -0
- package/lib/module/sdk/autoTracking.js +1233 -0
- package/lib/module/sdk/constants.js +145 -0
- package/lib/module/sdk/errorTracking.js +189 -0
- package/lib/module/sdk/index.js +12 -0
- package/lib/module/sdk/metricsTracking.js +187 -0
- package/lib/module/sdk/navigation.js +143 -0
- package/lib/module/sdk/networkInterceptor.js +401 -0
- package/lib/module/sdk/utils.js +342 -0
- package/lib/module/types/expo-router.d.js +2 -0
- package/lib/module/types/index.js +2 -0
- package/lib/typescript/NativeRejourney.d.ts +147 -0
- package/lib/typescript/components/Mask.d.ts +39 -0
- package/lib/typescript/index.d.ts +117 -0
- package/lib/typescript/sdk/autoTracking.d.ts +204 -0
- package/lib/typescript/sdk/constants.d.ts +120 -0
- package/lib/typescript/sdk/errorTracking.d.ts +32 -0
- package/lib/typescript/sdk/index.d.ts +9 -0
- package/lib/typescript/sdk/metricsTracking.d.ts +58 -0
- package/lib/typescript/sdk/navigation.d.ts +33 -0
- package/lib/typescript/sdk/networkInterceptor.d.ts +47 -0
- package/lib/typescript/sdk/utils.d.ts +148 -0
- package/lib/typescript/types/index.d.ts +624 -0
- package/package.json +102 -0
- package/rejourney.podspec +21 -0
- package/src/NativeRejourney.ts +165 -0
- package/src/components/Mask.tsx +80 -0
- package/src/index.ts +1459 -0
- package/src/sdk/autoTracking.ts +1373 -0
- package/src/sdk/constants.ts +134 -0
- package/src/sdk/errorTracking.ts +231 -0
- package/src/sdk/index.ts +11 -0
- package/src/sdk/metricsTracking.ts +232 -0
- package/src/sdk/navigation.ts +157 -0
- package/src/sdk/networkInterceptor.ts +440 -0
- package/src/sdk/utils.ts +369 -0
- package/src/types/expo-router.d.ts +7 -0
- 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
|