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