@rejourneyco/react-native 1.0.0 → 1.0.2

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 (49) hide show
  1. package/README.md +29 -0
  2. package/android/src/main/java/com/rejourney/RejourneyModuleImpl.kt +47 -30
  3. package/android/src/main/java/com/rejourney/capture/CaptureEngine.kt +25 -1
  4. package/android/src/main/java/com/rejourney/capture/CaptureHeuristics.kt +70 -32
  5. package/android/src/main/java/com/rejourney/core/Constants.kt +4 -4
  6. package/android/src/newarch/java/com/rejourney/RejourneyModule.kt +14 -0
  7. package/android/src/oldarch/java/com/rejourney/RejourneyModule.kt +9 -0
  8. package/ios/Capture/RJCaptureEngine.m +72 -34
  9. package/ios/Capture/RJCaptureHeuristics.h +7 -5
  10. package/ios/Capture/RJCaptureHeuristics.m +138 -112
  11. package/ios/Capture/RJVideoEncoder.m +0 -26
  12. package/ios/Core/Rejourney.mm +64 -102
  13. package/ios/Utils/RJPerfTiming.m +0 -5
  14. package/ios/Utils/RJWindowUtils.m +0 -1
  15. package/lib/commonjs/components/Mask.js +1 -6
  16. package/lib/commonjs/index.js +12 -101
  17. package/lib/commonjs/sdk/autoTracking.js +55 -353
  18. package/lib/commonjs/sdk/constants.js +2 -13
  19. package/lib/commonjs/sdk/errorTracking.js +1 -29
  20. package/lib/commonjs/sdk/metricsTracking.js +3 -24
  21. package/lib/commonjs/sdk/navigation.js +3 -42
  22. package/lib/commonjs/sdk/networkInterceptor.js +7 -49
  23. package/lib/commonjs/sdk/utils.js +0 -5
  24. package/lib/module/components/Mask.js +1 -6
  25. package/lib/module/index.js +11 -105
  26. package/lib/module/sdk/autoTracking.js +55 -354
  27. package/lib/module/sdk/constants.js +2 -13
  28. package/lib/module/sdk/errorTracking.js +1 -29
  29. package/lib/module/sdk/index.js +0 -2
  30. package/lib/module/sdk/metricsTracking.js +3 -24
  31. package/lib/module/sdk/navigation.js +3 -42
  32. package/lib/module/sdk/networkInterceptor.js +7 -49
  33. package/lib/module/sdk/utils.js +0 -5
  34. package/lib/typescript/NativeRejourney.d.ts +2 -0
  35. package/lib/typescript/sdk/autoTracking.d.ts +5 -6
  36. package/lib/typescript/types/index.d.ts +0 -1
  37. package/package.json +11 -3
  38. package/src/NativeRejourney.ts +4 -0
  39. package/src/components/Mask.tsx +0 -3
  40. package/src/index.ts +11 -88
  41. package/src/sdk/autoTracking.ts +72 -331
  42. package/src/sdk/constants.ts +13 -13
  43. package/src/sdk/errorTracking.ts +1 -17
  44. package/src/sdk/index.ts +0 -2
  45. package/src/sdk/metricsTracking.ts +5 -33
  46. package/src/sdk/navigation.ts +8 -29
  47. package/src/sdk/networkInterceptor.ts +9 -33
  48. package/src/sdk/utils.ts +0 -5
  49. package/src/types/index.ts +0 -29
package/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # @rejourneyco/react-native
2
+
3
+ Lightweight session replay and observability SDK for React Native. Pixel-perfect video capture with real-time incident detection.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @rejourneyco/react-native
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```typescript
14
+ import { initRejourney, startRejourney } from '@rejourneyco/react-native';
15
+
16
+ // Initialize with your public key
17
+ initRejourney('pk_live_xxxxxxxxxxxx');
18
+
19
+ // Start recording after obtaining user consent
20
+ startRejourney();
21
+ ```
22
+
23
+ ## Documentation
24
+
25
+ Full integration guides and API reference: https://rejourney.co/docs/reactnative/overview
26
+
27
+ ## License
28
+
29
+ Licensed under Apache 2.0
@@ -57,7 +57,6 @@ import java.io.File
57
57
  import java.util.*
58
58
  import java.util.concurrent.CopyOnWriteArrayList
59
59
 
60
- // Enum for session end reasons
61
60
  enum class EndReason {
62
61
  SESSION_TIMEOUT,
63
62
  MANUAL_STOP,
@@ -107,7 +106,6 @@ class RejourneyModuleImpl(
107
106
  }
108
107
  }
109
108
 
110
- // Core components
111
109
  private var captureEngine: CaptureEngine? = null
112
110
  private var uploadManager: UploadManager? = null
113
111
  private var touchInterceptor: TouchInterceptor? = null
@@ -116,7 +114,6 @@ class RejourneyModuleImpl(
116
114
  private var keyboardTracker: KeyboardTracker? = null
117
115
  private var textInputTracker: TextInputTracker? = null
118
116
 
119
- // Session state
120
117
  private var currentSessionId: String? = null
121
118
  private var userId: String? = null
122
119
  @Volatile private var isRecording: Boolean = false
@@ -134,59 +131,43 @@ class RejourneyModuleImpl(
134
131
  private var maxRecordingMinutes: Int = 10
135
132
  @Volatile private var sessionEndSent: Boolean = false
136
133
 
137
- // Keyboard state
138
134
  private var keyPressCount: Int = 0
139
135
  private var isKeyboardVisible: Boolean = false
140
136
  private var lastKeyboardHeight: Int = 0
141
137
 
142
- // Session state saved on background - used to restore on foreground if within timeout
143
138
  private var savedApiUrl: String = ""
144
139
  private var savedPublicKey: String = ""
145
140
  private var savedDeviceHash: String = ""
146
141
 
147
- // Events buffer
148
142
  private val sessionEvents = CopyOnWriteArrayList<Map<String, Any?>>()
149
143
 
150
- // Throttle immediate upload kicks (ms)
151
144
  @Volatile private var lastImmediateUploadKickMs: Long = 0
152
145
 
153
- // Write-first event buffer for crash-safe persistence
154
146
  private var eventBuffer: EventBuffer? = null
155
147
 
156
- // Coroutine scope for async operations
157
148
  private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
158
149
 
159
- // Dedicated scope for background flush - survives independently of main scope
160
- // This prevents cancellation when app goes to background
161
150
  private val backgroundScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
162
151
 
163
- // Timer jobs
164
152
  private var batchUploadJob: Job? = null
165
153
  private var durationLimitJob: Job? = null
166
154
 
167
- // Main thread handler for posting delayed tasks
168
155
  private val mainHandler = android.os.Handler(android.os.Looper.getMainLooper())
169
156
 
170
- // Debounced background detection (prevents transient pauses from ending sessions)
171
157
  private var scheduledBackgroundRunnable: Runnable? = null
172
158
  private var backgroundScheduled: Boolean = false
173
159
 
174
- // Safety flag
175
160
  @Volatile private var isShuttingDown = false
176
161
 
177
- // Auth resilience - retry mechanism
178
162
  private var authRetryCount = 0
179
163
  private var authPermanentlyFailed = false
180
164
  private var authRetryJob: Job? = null
181
165
 
182
166
  init {
183
167
  // DO NOT initialize anything here that could throw exceptions
184
- // React Native needs the module constructor to complete cleanly
185
- // All initialization will happen lazily on first method call
186
168
  Logger.debug("RejourneyModuleImpl constructor completed")
187
169
  }
188
170
 
189
- // Lazy initialization flag
190
171
  @Volatile
191
172
  private var isInitialized = false
192
173
  private val initLock = Any()
@@ -237,21 +218,19 @@ class RejourneyModuleImpl(
237
218
  Logger.error("Failed to schedule recovery upload (non-critical)", e)
238
219
  }
239
220
 
240
- // Check if app was killed in previous session (Android 11+)
221
+ // Che ck if app was killed in previous session (Android 11+)
241
222
  try {
242
223
  checkPreviousAppKill()
243
224
  } catch (e: Exception) {
244
225
  Logger.error("Failed to check previous app kill (non-critical)", e)
245
226
  }
246
227
 
247
- // Check for unclosed sessions from previous launch
248
228
  try {
249
229
  checkForUnclosedSessions()
250
230
  } catch (e: Exception) {
251
231
  Logger.error("Failed to check for unclosed sessions (non-critical)", e)
252
232
  }
253
233
 
254
- // Log OEM information for debugging
255
234
  val oem = OEMDetector.getOEM()
256
235
  Logger.debug("Device OEM: $oem")
257
236
  Logger.debug("OEM Recommendations: ${OEMDetector.getRecommendations()}")
@@ -291,7 +270,6 @@ class RejourneyModuleImpl(
291
270
  Logger.error("Failed to set up task removed listener (non-critical)", e)
292
271
  }
293
272
 
294
- // Use lifecycle log - only shown in debug builds
295
273
  Logger.logInitSuccess(Constants.SDK_VERSION)
296
274
 
297
275
  isInitialized = true
@@ -313,8 +291,6 @@ class RejourneyModuleImpl(
313
291
 
314
292
  Logger.debug("[Rejourney] addEventWithPersistence: type=$eventType, sessionId=$sessionId, inMemoryCount=${sessionEvents.size + 1}")
315
293
 
316
- // CRITICAL: Write to disk immediately for crash safety
317
- // This ensures events are never lost even if app is force-killed
318
294
  val bufferSuccess = eventBuffer?.appendEvent(event) ?: false
319
295
  if (!bufferSuccess) {
320
296
  Logger.warning("[Rejourney] addEventWithPersistence: Failed to append event to buffer: type=$eventType")
@@ -322,7 +298,6 @@ class RejourneyModuleImpl(
322
298
  Logger.debug("[Rejourney] addEventWithPersistence: Event appended to buffer: type=$eventType")
323
299
  }
324
300
 
325
- // Also add to in-memory buffer for batched upload
326
301
  sessionEvents.add(event)
327
302
  Logger.debug("[Rejourney] addEventWithPersistence: Event added to in-memory list: type=$eventType, totalInMemory=${sessionEvents.size}")
328
303
  }
@@ -332,14 +307,12 @@ class RejourneyModuleImpl(
332
307
  * This is more reliable than Activity lifecycle callbacks.
333
308
  */
334
309
  private fun registerProcessLifecycleObserver() {
335
- // Must run on main thread
336
310
  Handler(Looper.getMainLooper()).post {
337
311
  try {
338
312
  ProcessLifecycleOwner.get().lifecycle.addObserver(this)
339
313
  Logger.debug("ProcessLifecycleOwner observer registered")
340
314
  } catch (e: Exception) {
341
315
  Logger.error("Failed to register ProcessLifecycleOwner observer (non-critical)", e)
342
- // This is non-critical - we can still use Activity lifecycle callbacks as fallback
343
316
  }
344
317
  }
345
318
  }
@@ -472,7 +445,40 @@ class RejourneyModuleImpl(
472
445
  }
473
446
  }
474
447
 
475
- // ==================== React Native Methods ====================
448
+ fun getDeviceInfo(promise: Promise) {
449
+ try {
450
+ val map = Arguments.createMap()
451
+ map.putString("model", android.os.Build.MODEL)
452
+ map.putString("brand", android.os.Build.MANUFACTURER)
453
+ map.putString("systemName", "Android")
454
+ map.putString("systemVersion", android.os.Build.VERSION.RELEASE)
455
+ map.putString("bundleId", reactContext.packageName)
456
+
457
+ try {
458
+ val pInfo = reactContext.packageManager.getPackageInfo(reactContext.packageName, 0)
459
+ map.putString("appVersion", pInfo.versionName)
460
+ if (android.os.Build.VERSION.SDK_INT >= 28) {
461
+ map.putString("buildNumber", pInfo.longVersionCode.toString())
462
+ } else {
463
+ @Suppress("DEPRECATION")
464
+ map.putString("buildNumber", pInfo.versionCode.toString())
465
+ }
466
+ } catch (e: Exception) {
467
+ map.putString("appVersion", "unknown")
468
+ }
469
+
470
+ map.putBoolean("isTablet", isTablet())
471
+ promise.resolve(map)
472
+ } catch (e: Exception) {
473
+ promise.reject("DEVICE_INFO_ERROR", e)
474
+ }
475
+ }
476
+
477
+ private fun isTablet(): Boolean {
478
+ val configuration = reactContext.resources.configuration
479
+ return (configuration.screenLayout and android.content.res.Configuration.SCREENLAYOUT_SIZE_MASK) >=
480
+ android.content.res.Configuration.SCREENLAYOUT_SIZE_LARGE
481
+ }
476
482
 
477
483
  fun startSession(userId: String, apiUrl: String, publicKey: String, promise: Promise) {
478
484
  ensureInitialized() // Lazy init on first call
@@ -1005,7 +1011,14 @@ class RejourneyModuleImpl(
1005
1011
  try {
1006
1012
  val safeUserId = userId.ifEmpty { "anonymous" }
1007
1013
 
1008
- // Update userId
1014
+ // KEY CHANGE: Persist directly to SharedPreferences (Native Storage)
1015
+ // This replaces the need for async-storage on the JS side
1016
+ reactContext.getSharedPreferences("rejourney", 0)
1017
+ .edit()
1018
+ .putString("rj_user_identity", safeUserId)
1019
+ .apply()
1020
+
1021
+ // Update in-memory state
1009
1022
  this.userId = safeUserId
1010
1023
 
1011
1024
  // Update upload manager
@@ -1030,6 +1043,10 @@ class RejourneyModuleImpl(
1030
1043
  }
1031
1044
  }
1032
1045
 
1046
+ fun getUserIdentity(promise: Promise) {
1047
+ promise.resolve(userId)
1048
+ }
1049
+
1033
1050
  // ==================== Helper Methods ====================
1034
1051
 
1035
1052
  private fun createResultMap(success: Boolean, sessionId: String, error: String? = null): WritableMap {
@@ -178,6 +178,7 @@ class CaptureEngine(private val context: Context) : VideoEncoderDelegate {
178
178
  private val isShuttingDown = AtomicBoolean(false)
179
179
  private var _isRecording: Boolean = false
180
180
  private val captureInProgress = AtomicBoolean(false)
181
+ private val isWarmingUp = AtomicBoolean(false)
181
182
  private var sessionId: String? = null
182
183
  private var currentScreenName: String? = null
183
184
  private var viewScanner: ViewHierarchyScanner? = null
@@ -407,6 +408,23 @@ class CaptureEngine(private val context: Context) : VideoEncoderDelegate {
407
408
 
408
409
  Logger.info("[CaptureEngine] Resuming video capture")
409
410
 
411
+ // DEFENSIVE FIX: Warmup period
412
+ // When returning from background, the view hierarchy and layout may not be stable immediately.
413
+ // This is primarily an iOS issue but we apply it here for consistency and safety.
414
+ isWarmingUp.set(true)
415
+ Logger.debug("[CaptureEngine] Warmup started (200ms)")
416
+
417
+ mainHandler.postDelayed({
418
+ if (isShuttingDown.get()) return@postDelayed
419
+ isWarmingUp.set(false)
420
+ Logger.debug("[CaptureEngine] Warmup complete")
421
+
422
+ // Trigger immediate capture check
423
+ if (_isRecording) {
424
+ requestCapture(CaptureImportance.MEDIUM, "warmup_complete", forceCapture = false)
425
+ }
426
+ }, 200)
427
+
410
428
  captureHeuristics.reset()
411
429
  resetCachedFrames()
412
430
  pendingCapture = null
@@ -594,6 +612,11 @@ class CaptureEngine(private val context: Context) : VideoEncoderDelegate {
594
612
  }
595
613
  private fun requestCapture(importance: CaptureImportance, reason: String, forceCapture: Boolean) {
596
614
  if (!_isRecording || isShuttingDown.get()) return
615
+
616
+ if (isWarmingUp.get()) {
617
+ return
618
+ }
619
+
597
620
  if (!forceCapture && !shouldCapture(importance)) {
598
621
  Logger.debug("[CaptureEngine] Capture throttled: $reason (importance: $importance)")
599
622
  return
@@ -687,7 +710,8 @@ class CaptureEngine(private val context: Context) : VideoEncoderDelegate {
687
710
  val decision = captureHeuristics.decisionForSignature(
688
711
  pending.layoutSignature,
689
712
  now,
690
- hasLastFrame = lastCapturedBitmap != null
713
+ hasLastFrame = lastCapturedBitmap != null,
714
+ importance = pending.importance
691
715
  )
692
716
 
693
717
  if (decision.action == CaptureAction.Defer) {
@@ -1,5 +1,7 @@
1
1
  package com.rejourney.capture
2
2
 
3
+ import com.rejourney.core.CaptureImportance
4
+
3
5
 
4
6
  enum class CaptureAction {
5
7
  RenderNow,
@@ -216,53 +218,89 @@ class CaptureHeuristics {
216
218
  }
217
219
  }
218
220
 
219
- fun decisionForSignature(signature: String?, nowMs: Long, hasLastFrame: Boolean): CaptureDecision {
221
+ fun decisionForSignature(signature: String?, nowMs: Long, hasLastFrame: Boolean, importance: CaptureImportance): CaptureDecision {
220
222
  var earliestSafeTime = nowMs
221
223
  var blockerReason = CaptureReason.RenderNow
222
224
 
223
- considerBlockerSince(lastTouchTime, QUIET_TOUCH_MS, nowMs, earliestSafeTime)?.let {
224
- earliestSafeTime = it
225
- blockerReason = CaptureReason.DeferTouch
225
+ // Check importance to potentially bypass heuristics
226
+ val isUrgent = importance == CaptureImportance.HIGH || importance == CaptureImportance.CRITICAL
227
+
228
+ // Touch - Usually want smooth input, but CRITICAL updates (like navigation) take precedence
229
+ if (!isUrgent) {
230
+ considerBlockerSince(lastTouchTime, QUIET_TOUCH_MS, nowMs, earliestSafeTime)?.let {
231
+ earliestSafeTime = it
232
+ blockerReason = CaptureReason.DeferTouch
233
+ }
226
234
  }
227
- considerBlockerSince(lastScrollTime, QUIET_SCROLL_MS, nowMs, earliestSafeTime)?.let {
228
- earliestSafeTime = it
229
- blockerReason = CaptureReason.DeferScroll
235
+
236
+ // Scroll - High jank risk
237
+ // Even urgent captures should respect scroll to avoid visible hitching, unless CRITICAL
238
+ if (importance != CaptureImportance.CRITICAL) {
239
+ considerBlockerSince(lastScrollTime, QUIET_SCROLL_MS, nowMs, earliestSafeTime)?.let {
240
+ earliestSafeTime = it
241
+ blockerReason = CaptureReason.DeferScroll
242
+ }
230
243
  }
231
- considerBlockerSince(lastBounceTime, QUIET_BOUNCE_MS, nowMs, earliestSafeTime)?.let {
232
- earliestSafeTime = it
233
- blockerReason = CaptureReason.DeferBounce
244
+
245
+ // Bounce/Rubber-banding
246
+ if (!isUrgent) {
247
+ considerBlockerSince(lastBounceTime, QUIET_BOUNCE_MS, nowMs, earliestSafeTime)?.let {
248
+ earliestSafeTime = it
249
+ blockerReason = CaptureReason.DeferBounce
250
+ }
234
251
  }
235
- considerBlockerSince(lastRefreshTime, QUIET_REFRESH_MS, nowMs, earliestSafeTime)?.let {
236
- earliestSafeTime = it
237
- blockerReason = CaptureReason.DeferRefresh
252
+
253
+ // Refresh
254
+ if (!isUrgent) {
255
+ considerBlockerSince(lastRefreshTime, QUIET_REFRESH_MS, nowMs, earliestSafeTime)?.let {
256
+ earliestSafeTime = it
257
+ blockerReason = CaptureReason.DeferRefresh
258
+ }
238
259
  }
239
- considerBlockerSince(lastTransitionTime, QUIET_TRANSITION_MS, nowMs, earliestSafeTime)?.let {
240
- earliestSafeTime = it
241
- blockerReason = CaptureReason.DeferTransition
260
+
261
+ // Transition - KEY FIX: Urgent captures (NAVIGATION) must bypass this!
262
+ if (!isUrgent) {
263
+ considerBlockerSince(lastTransitionTime, QUIET_TRANSITION_MS, nowMs, earliestSafeTime)?.let {
264
+ earliestSafeTime = it
265
+ blockerReason = CaptureReason.DeferTransition
266
+ }
242
267
  }
243
268
 
244
269
  if (keyboardAnimating) {
245
270
  lastKeyboardTime = nowMs
246
271
  }
247
- considerBlockerSince(lastKeyboardTime, QUIET_KEYBOARD_MS, nowMs, earliestSafeTime)?.let {
248
- earliestSafeTime = it
249
- blockerReason = CaptureReason.DeferKeyboard
272
+ // Keyboard animations can be jerky
273
+ if (!isUrgent) {
274
+ considerBlockerSince(lastKeyboardTime, QUIET_KEYBOARD_MS, nowMs, earliestSafeTime)?.let {
275
+ earliestSafeTime = it
276
+ blockerReason = CaptureReason.DeferKeyboard
277
+ }
250
278
  }
251
279
 
252
- considerBlockerSince(lastMapTime, QUIET_MAP_MS, nowMs, earliestSafeTime)?.let {
253
- earliestSafeTime = it
254
- blockerReason = CaptureReason.DeferMap
255
- }
280
+ // Map - Always defer map motion as it's very expensive and glitchy
281
+ // Maps are special; even CRITICAL captures might want to wait for map settle if possible,
282
+ // but we'll allow CRITICAL to force it if absolutely needed.
283
+ if (importance != CaptureImportance.CRITICAL) {
284
+ considerBlockerSince(lastMapTime, QUIET_MAP_MS, nowMs, earliestSafeTime)?.let {
285
+ earliestSafeTime = it
286
+ blockerReason = CaptureReason.DeferMap
287
+ }
256
288
 
257
- if (mapSettleUntilMs > nowMs && mapSettleUntilMs > earliestSafeTime) {
258
- earliestSafeTime = mapSettleUntilMs
259
- blockerReason = CaptureReason.DeferMap
289
+ if (mapSettleUntilMs > nowMs && mapSettleUntilMs > earliestSafeTime) {
290
+ earliestSafeTime = mapSettleUntilMs
291
+ blockerReason = CaptureReason.DeferMap
292
+ }
260
293
  }
261
294
 
262
295
  if (animationBlocking) {
263
- considerBlockerSince(lastAnimationTime, QUIET_ANIMATION_MS, nowMs, earliestSafeTime)?.let {
264
- earliestSafeTime = it
265
- blockerReason = CaptureReason.DeferBigAnimation
296
+ // Big animations (Lottie etc).
297
+ // If urgent, we might want to capture the final state of an animation or screen change
298
+ // regardless of the animation loop.
299
+ if (!isUrgent) {
300
+ considerBlockerSince(lastAnimationTime, QUIET_ANIMATION_MS, nowMs, earliestSafeTime)?.let {
301
+ earliestSafeTime = it
302
+ blockerReason = CaptureReason.DeferBigAnimation
303
+ }
266
304
  }
267
305
  }
268
306
 
@@ -278,11 +316,11 @@ class CaptureHeuristics {
278
316
  val staleOnly = stale && hasLastFrame && !signatureChanged && !keyframeDue
279
317
  val suppressStaleRender = staleOnly && (hasVideoSurface || hasWebSurface || hasCameraSurface)
280
318
 
281
- if (suppressStaleRender) {
319
+ if (suppressStaleRender && !isUrgent) {
282
320
  return CaptureDecision(CaptureAction.ReuseLast, CaptureReason.ReuseSignatureUnchanged)
283
321
  }
284
322
 
285
- if (!hasLastFrame || signatureChanged || stale || keyframeDue) {
323
+ if (!hasLastFrame || signatureChanged || stale || keyframeDue || isUrgent) {
286
324
  return CaptureDecision(CaptureAction.RenderNow, CaptureReason.RenderNow)
287
325
  }
288
326
 
@@ -351,7 +389,7 @@ class CaptureHeuristics {
351
389
  private const val QUIET_BOUNCE_MS = 200L
352
390
  private const val QUIET_REFRESH_MS = 220L
353
391
  private const val QUIET_MAP_MS = 550L
354
- private const val QUIET_TRANSITION_MS = 200L
392
+ private const val QUIET_TRANSITION_MS = 100L
355
393
  private const val QUIET_KEYBOARD_MS = 250L
356
394
  private const val QUIET_ANIMATION_MS = 250L
357
395
 
@@ -11,11 +11,11 @@ object Constants {
11
11
  // Video Capture Configuration (H.264 Segment Mode)
12
12
  // These match iOS RJCaptureEngine defaults for performance
13
13
 
14
- /** Default capture scale factor (0.35 = 35% of original size) - matches iOS */
15
- const val DEFAULT_CAPTURE_SCALE = 0.35f
14
+ /** Default capture scale factor (0.25 = 25% of original size) - matches iOS */
15
+ const val DEFAULT_CAPTURE_SCALE = 0.25f
16
16
 
17
- /** Target video bitrate in bits per second (1.5 Mbps) - matches iOS */
18
- const val DEFAULT_VIDEO_BITRATE = 1_500_000
17
+ /** Target video bitrate in bits per second (1.0 Mbps) - matches iOS */
18
+ const val DEFAULT_VIDEO_BITRATE = 1_000_000
19
19
 
20
20
  /** Maximum video dimension in pixels (longest edge) - matches iOS */
21
21
  const val MAX_VIDEO_DIMENSION = 1920
@@ -132,6 +132,13 @@ class RejourneyModule(reactContext: ReactApplicationContext) :
132
132
  instance.getSDKMetrics(promise)
133
133
  }
134
134
 
135
+ @ReactMethod
136
+ @DoNotStrip
137
+ override fun getDeviceInfo(promise: Promise) {
138
+ val instance = getImplOrReject(promise) ?: return
139
+ instance.getDeviceInfo(promise)
140
+ }
141
+
135
142
  @ReactMethod
136
143
  @DoNotStrip
137
144
  override fun debugCrash() {
@@ -171,6 +178,13 @@ class RejourneyModule(reactContext: ReactApplicationContext) :
171
178
  instance.setUserIdentity(userId, promise)
172
179
  }
173
180
 
181
+ @ReactMethod
182
+ @DoNotStrip
183
+ override fun getUserIdentity(promise: Promise) {
184
+ val instance = getImplOrReject(promise) ?: return
185
+ instance.getUserIdentity(promise)
186
+ }
187
+
174
188
  @ReactMethod
175
189
  @DoNotStrip
176
190
  override fun setDebugMode(enabled: Boolean, promise: Promise) {
@@ -128,6 +128,15 @@ class RejourneyModule(reactContext: ReactApplicationContext) :
128
128
  }
129
129
  }
130
130
 
131
+ @ReactMethod
132
+ fun getDeviceInfo(promise: Promise) {
133
+ try {
134
+ impl.getDeviceInfo(promise)
135
+ } catch (e: Exception) {
136
+ promise.resolve(Arguments.createMap())
137
+ }
138
+ }
139
+
131
140
  @ReactMethod
132
141
  fun debugCrash() {
133
142
  try {