@scalebun/react-native 1.2.2 → 1.2.3

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.
@@ -611,6 +611,19 @@ class ReplaySdkModule(reactContext: ReactApplicationContext) : ReplaySdkSpec(rea
611
611
  @ReactMethod
612
612
  override fun setPrivacyOptions(options: ReadableMap, promise: Promise) {
613
613
  try {
614
+ // Was a no-op stub: the promise resolved but nothing propagated, so
615
+ // `setPrivacy({ disableScreenshots })` did nothing at runtime on Android and only a
616
+ // restart (which re-reads the init config) honoured it. Now each supplied field is
617
+ // merged onto the running config via SessionManager.updatePrivacy, which routes through
618
+ // updateConfig → the capture manager, so the next frame respects it. Absent keys are
619
+ // left null → unchanged (a partial setPrivacy call must not reset the others).
620
+ sessionManager?.updatePrivacy(
621
+ disableScreenshots = if (options.hasKey("disableScreenshots")) options.getBoolean("disableScreenshots") else null,
622
+ maskTextInputs = if (options.hasKey("maskTextInputs")) options.getBoolean("maskTextInputs") else null,
623
+ maskImages = if (options.hasKey("maskImages")) options.getBoolean("maskImages") else null,
624
+ ignoredScreens = if (options.hasKey("ignoredScreens")) readStringList(options.getArray("ignoredScreens")) else null,
625
+ maskedElementIds = if (options.hasKey("maskedElementIds")) readStringList(options.getArray("maskedElementIds")) else null,
626
+ )
614
627
  promise.resolve(null)
615
628
  } catch (e: Exception) {
616
629
  promise.reject("PRIVACY_ERROR", e.message, e)
@@ -620,6 +633,7 @@ class ReplaySdkModule(reactContext: ReactApplicationContext) : ReplaySdkSpec(rea
620
633
  @ReactMethod
621
634
  override fun maskElement(elementId: String, promise: Promise) {
622
635
  try {
636
+ sessionManager?.addMaskedElement(elementId)
623
637
  promise.resolve(null)
624
638
  } catch (e: Exception) {
625
639
  promise.reject("PRIVACY_ERROR", e.message, e)
@@ -629,12 +643,23 @@ class ReplaySdkModule(reactContext: ReactApplicationContext) : ReplaySdkSpec(rea
629
643
  @ReactMethod
630
644
  override fun unmaskElement(elementId: String, promise: Promise) {
631
645
  try {
646
+ sessionManager?.removeMaskedElement(elementId)
632
647
  promise.resolve(null)
633
648
  } catch (e: Exception) {
634
649
  promise.reject("PRIVACY_ERROR", e.message, e)
635
650
  }
636
651
  }
637
652
 
653
+ /** ReadableArray → List<String>, nulls and non-strings dropped. */
654
+ private fun readStringList(arr: ReadableArray?): List<String> {
655
+ if (arr == null) return emptyList()
656
+ val out = ArrayList<String>(arr.size())
657
+ for (i in 0 until arr.size()) {
658
+ arr.getString(i)?.let { out.add(it) }
659
+ }
660
+ return out
661
+ }
662
+
638
663
  // ─── User ───────────────────────────────────────────────────────────
639
664
 
640
665
  @ReactMethod
@@ -226,6 +226,47 @@ class SessionManager(
226
226
  ReplayLogger.verbose = newConfig.verboseLogging
227
227
  }
228
228
 
229
+ /**
230
+ * Runtime privacy update — the missing half of `ScaleBun.replay.setPrivacy(...)`.
231
+ *
232
+ * `captureFrame` gates every capture on `config.disableScreenshots` / `config.ignoredScreens`
233
+ * and passes `config` (with `maskedElementIds`) into `scheduleCapture`, all reading THIS
234
+ * object's `config`. So merging the new privacy fields onto the current config and routing
235
+ * through `updateConfig` makes a runtime toggle take effect on the very next frame — no
236
+ * restart. MERGE, not replace: only the fields the caller supplied change; a null argument
237
+ * leaves that field as it was (the native `setPrivacyOptions` was a no-op stub before this,
238
+ * so the toggle silently did nothing on Android).
239
+ */
240
+ fun updatePrivacy(
241
+ disableScreenshots: Boolean? = null,
242
+ maskTextInputs: Boolean? = null,
243
+ maskImages: Boolean? = null,
244
+ ignoredScreens: List<String>? = null,
245
+ maskedElementIds: List<String>? = null,
246
+ ) {
247
+ updateConfig(
248
+ config.copy(
249
+ disableScreenshots = disableScreenshots ?: config.disableScreenshots,
250
+ maskTextInputs = maskTextInputs ?: config.maskTextInputs,
251
+ maskImages = maskImages ?: config.maskImages,
252
+ ignoredScreens = ignoredScreens ?: config.ignoredScreens,
253
+ maskedElementIds = maskedElementIds ?: config.maskedElementIds,
254
+ )
255
+ )
256
+ }
257
+
258
+ /** Add one element id to the runtime mask set (idempotent). Backs `replay.maskElement(id)`. */
259
+ fun addMaskedElement(elementId: String) {
260
+ if (config.maskedElementIds.contains(elementId)) return
261
+ updateConfig(config.copy(maskedElementIds = config.maskedElementIds + elementId))
262
+ }
263
+
264
+ /** Remove one element id from the runtime mask set. Backs `replay.unmaskElement(id)`. */
265
+ fun removeMaskedElement(elementId: String) {
266
+ if (!config.maskedElementIds.contains(elementId)) return
267
+ updateConfig(config.copy(maskedElementIds = config.maskedElementIds - elementId))
268
+ }
269
+
229
270
  // ─── Query ──────────────────────────────────────────────────────────
230
271
 
231
272
  fun getSessionInfo(): Map<String, Any?>? = session?.toMap()
@@ -483,18 +483,31 @@ class ReplaySdk: RCTEventEmitter {
483
483
  @objc func setPrivacyOptions(_ options: NSDictionary,
484
484
  resolve: @escaping RCTPromiseResolveBlock,
485
485
  reject: @escaping RCTPromiseRejectBlock) {
486
+ // Was a no-op stub — the runtime toggle silently did nothing (only a restart, which
487
+ // re-reads the init config, honoured it). Each supplied key merges onto the running
488
+ // config via SessionManager.updatePrivacy → the capture manager; absent keys stay nil →
489
+ // unchanged, so a partial setPrivacy call never resets the other fields.
490
+ sessionManager?.updatePrivacy(
491
+ disableScreenshots: options["disableScreenshots"] as? Bool,
492
+ maskTextInputs: options["maskTextInputs"] as? Bool,
493
+ maskImages: options["maskImages"] as? Bool,
494
+ ignoredScreens: options["ignoredScreens"] as? [String],
495
+ maskedElementIds: options["maskedElementIds"] as? [String]
496
+ )
486
497
  resolve(NSNull())
487
498
  }
488
499
 
489
500
  @objc func maskElement(_ elementId: String,
490
501
  resolve: @escaping RCTPromiseResolveBlock,
491
502
  reject: @escaping RCTPromiseRejectBlock) {
503
+ sessionManager?.addMaskedElement(elementId)
492
504
  resolve(NSNull())
493
505
  }
494
506
 
495
507
  @objc func unmaskElement(_ elementId: String,
496
508
  resolve: @escaping RCTPromiseResolveBlock,
497
509
  reject: @escaping RCTPromiseRejectBlock) {
510
+ sessionManager?.removeMaskedElement(elementId)
498
511
  resolve(NSNull())
499
512
  }
500
513
 
@@ -21,6 +21,49 @@ class SessionManager {
21
21
  var onLimitReached: ((String, Int, Int, String) -> Void)? // limitType, current, max, sessionId
22
22
  var onScreenChanged: ((String) -> Void)? // screenName — forwarded to JS for backend upload
23
23
 
24
+ /// Runtime config update — propagates to the capture manager, which applies masking +
25
+ /// screenshot settings on the next frame. `config` is `private(set)`, so this is the one
26
+ /// sanctioned way to change it after init.
27
+ func updateConfig(_ newConfig: ReplayConfig) {
28
+ config = newConfig
29
+ captureManager.updateConfig(newConfig)
30
+ }
31
+
32
+ /// Runtime privacy update — the missing half of `ScaleBun.replay.setPrivacy(...)`.
33
+ ///
34
+ /// `captureFrame` gates on `config.disableScreenshots` and the capture manager applies
35
+ /// `PrivacyMaskProcessor` from `config`, all reading THIS object's config. Merging the
36
+ /// supplied fields and routing through `updateConfig` makes a runtime toggle take effect on
37
+ /// the next frame with no restart. Merge, not replace: a nil argument leaves that field.
38
+ /// (`setPrivacyOptions` was a no-op stub before this, so the toggle did nothing at runtime.)
39
+ func updatePrivacy(disableScreenshots: Bool? = nil,
40
+ maskTextInputs: Bool? = nil,
41
+ maskImages: Bool? = nil,
42
+ ignoredScreens: [String]? = nil,
43
+ maskedElementIds: [String]? = nil) {
44
+ var next = config
45
+ if let v = disableScreenshots { next.disableScreenshots = v }
46
+ if let v = maskTextInputs { next.maskTextInputs = v }
47
+ if let v = maskImages { next.maskImages = v }
48
+ if let v = ignoredScreens { next.ignoredScreens = v }
49
+ if let v = maskedElementIds { next.maskedElementIds = v }
50
+ updateConfig(next)
51
+ }
52
+
53
+ func addMaskedElement(_ elementId: String) {
54
+ guard !config.maskedElementIds.contains(elementId) else { return }
55
+ var next = config
56
+ next.maskedElementIds = config.maskedElementIds + [elementId]
57
+ updateConfig(next)
58
+ }
59
+
60
+ func removeMaskedElement(_ elementId: String) {
61
+ guard config.maskedElementIds.contains(elementId) else { return }
62
+ var next = config
63
+ next.maskedElementIds = config.maskedElementIds.filter { $0 != elementId }
64
+ updateConfig(next)
65
+ }
66
+
24
67
  init(config: ReplayConfig) {
25
68
  self.config = config
26
69
  self.captureManager = ScreenshotCaptureManager(config: config)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scalebun/react-native",
3
- "version": "1.2.2",
3
+ "version": "1.2.3",
4
4
  "description": "Production-grade React Native SDK for ScaleBun",
5
5
  "main": "lib/commonjs/index",
6
6
  "module": "lib/module/index",