@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,73 @@
1
+ //
2
+ // RJViewControllerTracker.h
3
+ // Rejourney
4
+ //
5
+ // Automatic ViewController lifecycle tracking via method swizzling.
6
+ // Detects navigation changes, tab switches, and significant UI transitions.
7
+ //
8
+ // Copyright (c) 2026 Rejourney
9
+ //
10
+
11
+ #import <UIKit/UIKit.h>
12
+
13
+ NS_ASSUME_NONNULL_BEGIN
14
+
15
+ @protocol RJViewControllerTrackerDelegate <NSObject>
16
+ @optional
17
+ /// Called when a new screen appears
18
+ - (void)viewControllerDidAppear:(UIViewController *)viewController
19
+ screenName:(NSString *)screenName;
20
+ /// Called when view controller will disappear
21
+ - (void)viewControllerWillDisappear:(UIViewController *)viewController
22
+ screenName:(NSString *)screenName;
23
+ /// Called when a tab bar selection changes
24
+ - (void)tabBarDidSelectIndex:(NSInteger)index
25
+ fromIndex:(NSInteger)previousIndex;
26
+ @end
27
+
28
+ @interface RJViewControllerTracker : NSObject
29
+
30
+ /// Shared instance
31
+ + (instancetype)sharedInstance;
32
+
33
+ /// The delegate that receives navigation events
34
+ @property(nonatomic, weak, nullable) id<RJViewControllerTrackerDelegate>
35
+ delegate;
36
+
37
+ /// Whether tracking is currently enabled
38
+ @property(nonatomic, assign, readonly) BOOL isEnabled;
39
+
40
+ /// Enable automatic ViewController tracking (swizzles
41
+ /// viewDidAppear/viewWillDisappear)
42
+ - (void)enableTracking;
43
+
44
+ /// Disable tracking and restore original implementations
45
+ - (void)disableTracking;
46
+
47
+ /// Get a human-readable name for a view controller
48
+ + (NSString *)screenNameForViewController:(UIViewController *)viewController;
49
+
50
+ /// Set the authoritative screen name from JavaScript
51
+ /// This takes priority over native auto-detection
52
+ + (void)setAuthoritativeScreenName:(NSString *)screenName;
53
+
54
+ /// Get the current authoritative screen name (if any)
55
+ + (nullable NSString *)authoritativeScreenName;
56
+
57
+ /// Clear the authoritative screen name
58
+ + (void)clearAuthoritativeScreenName;
59
+
60
+ @end
61
+
62
+ #pragma mark - UIViewController Tracking Category
63
+
64
+ /// Category on UIViewController for tracking purposes
65
+ @interface UIViewController (RJTracking)
66
+
67
+ /// Returns whether this view controller should be skipped for tracking
68
+ /// (internal VCs, containers, system VCs, etc.)
69
+ - (BOOL)rj_shouldSkipTracking;
70
+
71
+ @end
72
+
73
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,508 @@
1
+ //
2
+ // RJViewControllerTracker.m
3
+ // Rejourney
4
+ //
5
+ // Automatic ViewController lifecycle tracking via method swizzling.
6
+ // Detects navigation changes, tab switches, and significant UI transitions.
7
+ //
8
+ // Copyright (c) 2026 Rejourney
9
+ //
10
+
11
+ #import "RJViewControllerTracker.h"
12
+ #import "../Core/RJLogger.h"
13
+ #import <objc/runtime.h>
14
+
15
+ static IMP _originalViewDidAppear = NULL;
16
+ static IMP _originalViewWillDisappear = NULL;
17
+ static IMP _originalTabBarDidSelect = NULL;
18
+
19
+ static NSString *_lastScreenName = nil;
20
+ static NSInteger _lastTabIndex = -1;
21
+
22
+ static NSString *_authoritativeScreenName = nil;
23
+ static NSTimeInterval _authoritativeScreenNameTime = 0;
24
+
25
+ static const NSTimeInterval kAuthoritativeNameTimeout = 0.5;
26
+
27
+ #pragma mark - Swizzled Method Implementations
28
+
29
+ static void RJ_swizzled_viewDidAppear(UIViewController *self, SEL _cmd,
30
+ BOOL animated) {
31
+
32
+ if (_originalViewDidAppear) {
33
+ ((void (*)(id, SEL, BOOL))_originalViewDidAppear)(self, _cmd, animated);
34
+ }
35
+
36
+
37
+ if ([self rj_shouldSkipTracking]) {
38
+ return;
39
+ }
40
+
41
+
42
+ NSString *screenName =
43
+ [RJViewControllerTracker screenNameForViewController:self];
44
+
45
+
46
+
47
+ if (!screenName || screenName.length == 0) {
48
+ return;
49
+ }
50
+
51
+
52
+ static dispatch_queue_t _screenNameQueue;
53
+ static dispatch_once_t onceToken;
54
+ dispatch_once(&onceToken, ^{
55
+ _screenNameQueue = dispatch_queue_create("com.rejourney.screenname",
56
+ DISPATCH_QUEUE_SERIAL);
57
+ });
58
+
59
+ __block BOOL shouldNotify = NO;
60
+ dispatch_sync(_screenNameQueue, ^{
61
+ if (![screenName isEqualToString:_lastScreenName]) {
62
+ _lastScreenName = [screenName copy];
63
+ shouldNotify = YES;
64
+ }
65
+ });
66
+
67
+ if (shouldNotify) {
68
+ RJViewControllerTracker *tracker = [RJViewControllerTracker sharedInstance];
69
+ if (tracker.isEnabled && tracker.delegate) {
70
+
71
+ __weak UIViewController *weakVC = self;
72
+ dispatch_async(dispatch_get_main_queue(), ^{
73
+ UIViewController *strongVC = weakVC;
74
+ if (strongVC && [tracker.delegate respondsToSelector:@selector
75
+ (viewControllerDidAppear:
76
+ screenName:)]) {
77
+ [tracker.delegate viewControllerDidAppear:strongVC
78
+ screenName:screenName];
79
+ }
80
+ });
81
+ }
82
+ }
83
+ }
84
+
85
+ static void RJ_swizzled_viewWillDisappear(UIViewController *self, SEL _cmd,
86
+ BOOL animated) {
87
+
88
+ if (_originalViewWillDisappear) {
89
+ ((void (*)(id, SEL, BOOL))_originalViewWillDisappear)(self, _cmd, animated);
90
+ }
91
+
92
+
93
+ if ([self rj_shouldSkipTracking]) {
94
+ return;
95
+ }
96
+
97
+ NSString *screenName =
98
+ [RJViewControllerTracker screenNameForViewController:self];
99
+
100
+
101
+ if (!screenName || screenName.length == 0) {
102
+ return;
103
+ }
104
+
105
+ RJViewControllerTracker *tracker = [RJViewControllerTracker sharedInstance];
106
+ if (tracker.isEnabled && tracker.delegate) {
107
+
108
+ __weak UIViewController *weakVC = self;
109
+ dispatch_async(dispatch_get_main_queue(), ^{
110
+ UIViewController *strongVC = weakVC;
111
+ if (strongVC && [tracker.delegate respondsToSelector:@selector
112
+ (viewControllerWillDisappear:
113
+ screenName:)]) {
114
+ [tracker.delegate viewControllerWillDisappear:strongVC
115
+ screenName:screenName];
116
+ }
117
+ });
118
+ }
119
+ }
120
+
121
+ #pragma mark - UIViewController Category for Skip Logic
122
+
123
+ @implementation UIViewController (RJTracking)
124
+
125
+ - (BOOL)rj_shouldSkipTracking {
126
+
127
+
128
+ static NSSet *_skipPrefixes;
129
+ static NSSet *_skipContains;
130
+ static dispatch_once_t onceToken;
131
+ dispatch_once(&onceToken, ^{
132
+
133
+
134
+ _skipPrefixes = [[NSSet alloc] initWithArray:@[ @"_", @"UI" ]];
135
+ _skipContains = [[NSSet alloc] initWithArray:@[
136
+ @"Navigation", @"Container", @"Keyboard", @"StatusBar", @"InputAccessory",
137
+ @"FabricModal", @"ModalHost", @"Fabric", @"HostingView", @"SafeArea",
138
+ @"ScrollViewContent", @"RCTRootContentView", @"ReactNative"
139
+ ]];
140
+ });
141
+
142
+ NSString *className = NSStringFromClass([self class]);
143
+
144
+
145
+ for (NSString *prefix in _skipPrefixes) {
146
+ if ([className hasPrefix:prefix]) {
147
+ return YES;
148
+ }
149
+ }
150
+
151
+
152
+ for (NSString *substring in _skipContains) {
153
+ if ([className containsString:substring]) {
154
+ return YES;
155
+ }
156
+ }
157
+
158
+
159
+ if (!self.isViewLoaded || !self.view.window) {
160
+ return YES;
161
+ }
162
+
163
+
164
+ if (self.parentViewController &&
165
+ ![self.parentViewController
166
+ isKindOfClass:[UINavigationController class]] &&
167
+ ![self.parentViewController isKindOfClass:[UITabBarController class]]) {
168
+ return YES;
169
+ }
170
+
171
+ return NO;
172
+ }
173
+
174
+ @end
175
+
176
+ #pragma mark - RJViewControllerTracker Implementation
177
+
178
+ @interface RJViewControllerTracker ()
179
+ @property(nonatomic, assign, readwrite) BOOL isEnabled;
180
+ @property(nonatomic, assign) BOOL hasSwizzled;
181
+ @end
182
+
183
+ @implementation RJViewControllerTracker
184
+
185
+ + (instancetype)sharedInstance {
186
+ static RJViewControllerTracker *instance = nil;
187
+ static dispatch_once_t onceToken;
188
+ dispatch_once(&onceToken, ^{
189
+ instance = [[RJViewControllerTracker alloc] init];
190
+ });
191
+ return instance;
192
+ }
193
+
194
+ - (instancetype)init {
195
+ self = [super init];
196
+ if (self) {
197
+ _isEnabled = NO;
198
+ _hasSwizzled = NO;
199
+ }
200
+ return self;
201
+ }
202
+
203
+ - (void)enableTracking {
204
+ if (self.isEnabled)
205
+ return;
206
+
207
+ @synchronized(self) {
208
+ if (self.hasSwizzled) {
209
+ self.isEnabled = YES;
210
+ return;
211
+ }
212
+
213
+ @try {
214
+
215
+ Method originalDidAppear = class_getInstanceMethod(
216
+ [UIViewController class], @selector(viewDidAppear:));
217
+ if (originalDidAppear) {
218
+ _originalViewDidAppear = method_getImplementation(originalDidAppear);
219
+ method_setImplementation(originalDidAppear,
220
+ (IMP)RJ_swizzled_viewDidAppear);
221
+ }
222
+
223
+
224
+ Method originalWillDisappear = class_getInstanceMethod(
225
+ [UIViewController class], @selector(viewWillDisappear:));
226
+ if (originalWillDisappear) {
227
+ _originalViewWillDisappear =
228
+ method_getImplementation(originalWillDisappear);
229
+ method_setImplementation(originalWillDisappear,
230
+ (IMP)RJ_swizzled_viewWillDisappear);
231
+ }
232
+
233
+
234
+ [[NSNotificationCenter defaultCenter]
235
+ addObserver:self
236
+ selector:@selector(handleTabBarSelectionChange:)
237
+ name:@"UITabBarControllerDidSelectViewControllerNotification"
238
+ object:nil];
239
+
240
+
241
+ [[NSNotificationCenter defaultCenter]
242
+ addObserver:self
243
+ selector:@selector(handleWindowChange:)
244
+ name:UIWindowDidBecomeKeyNotification
245
+ object:nil];
246
+
247
+ self.hasSwizzled = YES;
248
+ self.isEnabled = YES;
249
+
250
+ RJLogDebug(@"ViewController tracking enabled");
251
+ } @catch (NSException *exception) {
252
+ RJLogError(@"Failed to enable VC tracking: %@", exception);
253
+ }
254
+ }
255
+ }
256
+
257
+ - (void)disableTracking {
258
+ self.isEnabled = NO;
259
+ RJLogDebug(@"ViewController tracking disabled");
260
+ }
261
+
262
+ - (void)handleTabBarSelectionChange:(NSNotification *)notification {
263
+ if (!self.isEnabled || !self.delegate)
264
+ return;
265
+
266
+ UITabBarController *tabBar = notification.object;
267
+ if ([tabBar isKindOfClass:[UITabBarController class]]) {
268
+ NSInteger newIndex = tabBar.selectedIndex;
269
+ NSInteger previousIndex = _lastTabIndex;
270
+
271
+ if (newIndex != previousIndex) {
272
+ _lastTabIndex = newIndex;
273
+
274
+ if ([self.delegate respondsToSelector:@selector(tabBarDidSelectIndex:
275
+ fromIndex:)]) {
276
+ [self.delegate tabBarDidSelectIndex:newIndex fromIndex:previousIndex];
277
+ }
278
+ }
279
+ }
280
+ }
281
+
282
+ - (void)handleWindowChange:(NSNotification *)notification {
283
+ if (!self.isEnabled || !self.delegate)
284
+ return;
285
+
286
+
287
+ UIWindow *window = notification.object;
288
+ if ([window isKindOfClass:[UIWindow class]] && window.rootViewController) {
289
+
290
+ UIViewController *topVC =
291
+ [self topViewControllerFromViewController:window.rootViewController];
292
+ if (topVC) {
293
+ NSString *screenName =
294
+ [RJViewControllerTracker screenNameForViewController:topVC];
295
+ if (![screenName isEqualToString:_lastScreenName]) {
296
+ _lastScreenName = [screenName copy];
297
+
298
+ if ([self.delegate respondsToSelector:@selector
299
+ (viewControllerDidAppear:screenName:)]) {
300
+ [self.delegate viewControllerDidAppear:topVC screenName:screenName];
301
+ }
302
+ }
303
+ }
304
+ }
305
+ }
306
+
307
+ - (UIViewController *)topViewControllerFromViewController:
308
+ (UIViewController *)viewController {
309
+ if ([viewController isKindOfClass:[UINavigationController class]]) {
310
+ UINavigationController *nav = (UINavigationController *)viewController;
311
+ return [self topViewControllerFromViewController:nav.visibleViewController];
312
+ }
313
+ if ([viewController isKindOfClass:[UITabBarController class]]) {
314
+ UITabBarController *tab = (UITabBarController *)viewController;
315
+ return
316
+ [self topViewControllerFromViewController:tab.selectedViewController];
317
+ }
318
+ if (viewController.presentedViewController) {
319
+ return
320
+ [self topViewControllerFromViewController:viewController
321
+ .presentedViewController];
322
+ }
323
+ return viewController;
324
+ }
325
+
326
+ + (void)setAuthoritativeScreenName:(NSString *)screenName {
327
+ @synchronized(self) {
328
+ _authoritativeScreenName = [screenName copy];
329
+ _authoritativeScreenNameTime = [[NSDate date] timeIntervalSince1970];
330
+ }
331
+ }
332
+
333
+ + (nullable NSString *)authoritativeScreenName {
334
+ @synchronized(self) {
335
+ if (!_authoritativeScreenName)
336
+ return nil;
337
+
338
+
339
+ NSTimeInterval now = [[NSDate date] timeIntervalSince1970];
340
+ if (now - _authoritativeScreenNameTime > kAuthoritativeNameTimeout) {
341
+ _authoritativeScreenName = nil;
342
+ return nil;
343
+ }
344
+ return _authoritativeScreenName;
345
+ }
346
+ }
347
+
348
+ + (void)clearAuthoritativeScreenName {
349
+ @synchronized(self) {
350
+ _authoritativeScreenName = nil;
351
+ _authoritativeScreenNameTime = 0;
352
+ }
353
+ }
354
+
355
+ + (NSString *)screenNameForViewController:(UIViewController *)viewController {
356
+ if (!viewController)
357
+ return @"Unknown";
358
+
359
+
360
+ NSString *authoritativeName = [self authoritativeScreenName];
361
+ if (authoritativeName.length > 0) {
362
+ return authoritativeName;
363
+ }
364
+
365
+
366
+
367
+
368
+ if (viewController.title.length > 0) {
369
+ return [self normalizeScreenName:viewController.title];
370
+ }
371
+
372
+
373
+ if (viewController.navigationItem.title.length > 0) {
374
+ return [self normalizeScreenName:viewController.navigationItem.title];
375
+ }
376
+
377
+
378
+ if (viewController.tabBarItem.title.length > 0) {
379
+ return [self normalizeScreenName:viewController.tabBarItem.title];
380
+ }
381
+
382
+
383
+
384
+
385
+ if (viewController.view.accessibilityIdentifier.length > 0) {
386
+ NSString *identifier = viewController.view.accessibilityIdentifier;
387
+
388
+ if (![identifier hasPrefix:@"RCT"] && ![identifier hasPrefix:@"RNS"] &&
389
+ ![identifier containsString:@"ContentView"]) {
390
+ return [self normalizeScreenName:identifier];
391
+ }
392
+ }
393
+
394
+
395
+
396
+ if (viewController.view.accessibilityLabel.length > 0 &&
397
+ viewController.view.accessibilityLabel.length < 40) {
398
+ return [self normalizeScreenName:viewController.view.accessibilityLabel];
399
+ }
400
+
401
+
402
+
403
+
404
+
405
+ if (viewController.navigationController) {
406
+ UINavigationItem *navItem =
407
+ viewController.navigationController.navigationBar.topItem;
408
+ if (navItem.title.length > 0) {
409
+ return [self normalizeScreenName:navItem.title];
410
+ }
411
+ }
412
+
413
+
414
+ return [self normalizeClassName:NSStringFromClass([viewController class])];
415
+ }
416
+
417
+ + (NSString *)normalizeScreenName:(NSString *)name {
418
+ if (!name || name.length == 0)
419
+ return @"Unknown";
420
+
421
+ NSString *result = name;
422
+
423
+
424
+ NSArray *suffixes = @[ @"Screen", @"Page", @"View", @"Controller", @"VC" ];
425
+ for (NSString *suffix in suffixes) {
426
+ if ([result hasSuffix:suffix] && result.length > suffix.length) {
427
+ result = [result substringToIndex:result.length - suffix.length];
428
+ break;
429
+ }
430
+ }
431
+
432
+
433
+ result = [result
434
+ stringByTrimmingCharactersInSet:[NSCharacterSet
435
+ whitespaceAndNewlineCharacterSet]];
436
+
437
+ return result.length > 0 ? result : @"Unknown";
438
+ }
439
+
440
+ + (NSString *)normalizeClassName:(NSString *)className {
441
+ if (!className || className.length == 0)
442
+ return nil;
443
+
444
+ NSString *result = className;
445
+
446
+
447
+ static NSSet *_noiseNames;
448
+ static dispatch_once_t onceToken;
449
+ dispatch_once(&onceToken, ^{
450
+ _noiseNames = [[NSSet alloc] initWithArray:@[
451
+ @"Screen", @"View", @"Controller", @"Host", @"Modal", @"Wrapper",
452
+ @"Content", @"Root", @"Main", @"Base", @"Default", @"Generic",
453
+ @"Fabric Modal Host", @"Fabric", @"ModalHost"
454
+ ]];
455
+ });
456
+
457
+
458
+ NSArray *prefixesToRemove = @[ @"RNS", @"RCT", @"RN", @"UI", @"Fabric" ];
459
+ for (NSString *prefix in prefixesToRemove) {
460
+ if ([result hasPrefix:prefix] && result.length > prefix.length + 2) {
461
+ result = [result substringFromIndex:prefix.length];
462
+ break;
463
+ }
464
+ }
465
+
466
+
467
+ NSArray *suffixes = @[
468
+ @"ViewController", @"Controller", @"VC", @"Screen", @"View",
469
+ @"StackHeaderConfig", @"ContentView", @"ScreenView", @"ScreenContainer",
470
+ @"Host", @"Wrapper", @"HostingController", @"ModalHost"
471
+ ];
472
+ for (NSString *suffix in suffixes) {
473
+ if ([result hasSuffix:suffix] && result.length > suffix.length) {
474
+ result = [result substringToIndex:result.length - suffix.length];
475
+ break;
476
+ }
477
+ }
478
+
479
+
480
+ if (result.length < 2) {
481
+ return nil;
482
+ }
483
+
484
+
485
+ NSMutableString *spaced = [NSMutableString string];
486
+ for (NSUInteger i = 0; i < result.length; i++) {
487
+ unichar c = [result characterAtIndex:i];
488
+ if (i > 0 &&
489
+ [[NSCharacterSet uppercaseLetterCharacterSet] characterIsMember:c]) {
490
+ [spaced appendString:@" "];
491
+ }
492
+ [spaced appendFormat:@"%c", c];
493
+ }
494
+
495
+
496
+ NSString *finalName = spaced.length > 0 ? spaced : nil;
497
+ if (finalName && [_noiseNames containsObject:finalName]) {
498
+ return nil;
499
+ }
500
+
501
+ return finalName;
502
+ }
503
+
504
+ - (void)dealloc {
505
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
506
+ }
507
+
508
+ @end