react-native-userleap 4.1.0 → 4.2.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.
@@ -85,6 +85,6 @@ dependencies {
85
85
  if (findProject(':userleap-android-sdk') != null) {
86
86
  implementation project(':userleap-android-sdk')
87
87
  } else {
88
- implementation ("com.userleap:userleap-android-sdk:2.27.0") // update this version on android updates
88
+ implementation ("com.userleap:userleap-android-sdk:2.28.0") // update this version on android updates
89
89
  }
90
90
  }
@@ -17,6 +17,7 @@ import com.userleap.SprigSurveyResult
17
17
  import com.userleap.SurveyState
18
18
  import com.userleap.UserLeap
19
19
  import com.userleap.SprigUserInterfaceMode
20
+ import com.userleap.ReplaySessionRecordingFormat
20
21
  import org.json.JSONObject
21
22
 
22
23
  class UserLeapModule(reactContext: ReactApplicationContext) :
@@ -186,6 +187,15 @@ class UserLeapModule(reactContext: ReactApplicationContext) :
186
187
  }
187
188
  }
188
189
 
190
+ // Android-only: iOS's SDK has no equivalent. `format` is the ReplaySessionRecordingFormat
191
+ // ordinal value (0 = SCREENSHOT, 1 = WIREFRAME) passed as a Double across the RN bridge.
192
+ override fun setReplaySessionRecordingFormat(format: Double) {
193
+ val formatInt = format.toInt()
194
+ ReplaySessionRecordingFormat.values().firstOrNull { it.value == formatInt }?.let {
195
+ UserLeap.setReplaySessionRecordingFormat(it)
196
+ }
197
+ }
198
+
189
199
  override fun trackEvent(eventName: String, surveyResultCallback: Callback) {
190
200
  trackEventAndIdentify(eventName, null, null, surveyResultCallback)
191
201
  }
package/index.d.ts CHANGED
@@ -11,6 +11,14 @@ declare namespace UserLeap {
11
11
  dark: number;
12
12
  };
13
13
 
14
+ /** Android only — replay session recording formats for {@link setAndroidReplaySessionRecordingFormat}. */
15
+ const ReplaySessionRecordingFormat: {
16
+ /** Full-fidelity screenshots (the native default). */
17
+ screenshot: number;
18
+ /** Lightweight wireframe of the view hierarchy. */
19
+ wireframe: number;
20
+ };
21
+
14
22
  /** Lifecycle event names for {@link addEventListener}. */
15
23
  const SprigEventName: {
16
24
  sdkReady: "sdkReady";
@@ -90,6 +98,11 @@ declare namespace UserLeap {
90
98
  }
91
99
 
92
100
  function overrideUserInterfaceMode(mode: SprigUserInterfaceMode): void;
101
+ /**
102
+ * Android only — selects the replay session recording format. No-op on iOS,
103
+ * whose native SDK has no equivalent API.
104
+ */
105
+ function setAndroidReplaySessionRecordingFormat(format: number): void;
93
106
  function visitorIdentifier(): number;
94
107
  function visitorIdentifierString(): string;
95
108
  function sdkVersion(): string;
package/index.js CHANGED
@@ -15,6 +15,12 @@ const SprigUserInterfaceMode = {
15
15
  dark: 2,
16
16
  };
17
17
 
18
+ // Android only: iOS's native SDK has no equivalent replay-format API.
19
+ const ReplaySessionRecordingFormat = {
20
+ screenshot: 0,
21
+ wireframe: 1,
22
+ };
23
+
18
24
  const SprigEventName = {
19
25
  sdkReady: "sdkReady",
20
26
  visitorIdUpdated: "visitorIdUpdated",
@@ -361,6 +367,13 @@ const overrideUserInterfaceMode = (mode) => {
361
367
  }
362
368
  }
363
369
 
370
+ // Android only: iOS's native SDK has no equivalent, so this is a no-op on iOS.
371
+ const setAndroidReplaySessionRecordingFormat = (format) => {
372
+ if (Platform.OS === "android" && isValidPlatform()) {
373
+ NativeModules.UserLeapBindings.setReplaySessionRecordingFormat(format);
374
+ }
375
+ };
376
+
364
377
  /**
365
378
  * Subscribe to a native SDK lifecycle event. Returns a subscription whose
366
379
  * `.remove()` unsubscribes only this listener.
@@ -445,6 +458,7 @@ const UserLeap = {
445
458
  pauseDisplayingSurveys,
446
459
  unpauseDisplayingSurveys,
447
460
  overrideUserInterfaceMode,
461
+ setAndroidReplaySessionRecordingFormat,
448
462
  addEventListener,
449
463
  removeAllEventListeners,
450
464
  getSupportedEventNames,
@@ -452,6 +466,7 @@ const UserLeap = {
452
466
 
453
467
  UserLeap.SurveyState = SurveyState;
454
468
  UserLeap.SprigUserInterfaceMode = SprigUserInterfaceMode;
469
+ UserLeap.ReplaySessionRecordingFormat = ReplaySessionRecordingFormat;
455
470
  UserLeap.SprigEventName = SprigEventName;
456
471
 
457
472
  export default UserLeap;
@@ -1,7 +1,8 @@
1
1
  #import <UserLeapKit/UserLeapKit.h>
2
2
  #import <UserLeapKit/UserLeapKit-Swift.h>
3
3
 
4
+ #import <React/RCTInvalidating.h>
4
5
  #import <RNUserLeapBindingsSpec/RNUserLeapBindingsSpec.h>
5
6
 
6
- @interface UserLeapBindings : NativeUserLeapBindingsSpecBase <NativeUserLeapBindingsSpec, _SGRNExtractor>
7
+ @interface UserLeapBindings : NativeUserLeapBindingsSpecBase <NativeUserLeapBindingsSpec, _SGRNExtractor, RCTInvalidating>
7
8
  @end
@@ -1,5 +1,7 @@
1
1
  #import "UserLeapBindings.h"
2
2
  #import <React/RCTUtils.h>
3
+ #import <ReactCommon/CallInvoker.h>
4
+ #import <os/lock.h>
3
5
  // Legacy Paper includes gated on RCT_REMOVE_LEGACY_ARCH: these classes are removed on RN 0.85+.
4
6
  #ifndef RCT_REMOVE_LEGACY_ARCH
5
7
  #import <React/RCTTextView.h>
@@ -16,7 +18,9 @@
16
18
  #import <RNUserLeapBindingsSpec/RNUserLeapBindingsSpec.h>
17
19
 
18
20
  @implementation UserLeapBindings {
21
+ os_unfair_lock _lock; // guards _alive and _jsInvoker (shared_ptr copy isn't atomic); never held across an emit
19
22
  BOOL _alive;
23
+ std::shared_ptr<facebook::react::CallInvoker> _jsInvoker;
20
24
  }
21
25
 
22
26
  + (BOOL)requiresMainQueueSetup
@@ -27,16 +31,25 @@
27
31
  - (instancetype)init
28
32
  {
29
33
  if (self = [super init]) {
34
+ _lock = OS_UNFAIR_LOCK_INIT;
30
35
  _alive = YES;
31
- // emitOnSprigEvent is a safe no-op when JS has no listener, so eager subscription is fine.
32
36
  [self subscribeToSdkEvents];
33
37
  }
34
38
  return self;
35
39
  }
36
40
 
37
- - (void)dealloc
41
+ // Deterministic teardown hook: stop delivering before the runtime is destroyed.
42
+ - (void)invalidate
38
43
  {
44
+ os_unfair_lock_lock(&_lock);
39
45
  _alive = NO;
46
+ os_unfair_lock_unlock(&_lock);
47
+ [self unsubscribeFromSdkEvents];
48
+ }
49
+
50
+ - (void)dealloc
51
+ {
52
+ // Backstop; -invalidate is the real teardown path.
40
53
  [self unsubscribeFromSdkEvents];
41
54
  }
42
55
 
@@ -177,6 +190,14 @@ RCT_EXPORT_MODULE()
177
190
  [[UserLeap shared] overrideUserInterfaceModeWithMode:(enum SprigUserInterfaceMode)(NSInteger)mode];
178
191
  }
179
192
 
193
+ // Android-only API. iOS's UserLeapKit SDK has no replay recording-format selector,
194
+ // so this is an intentional no-op that satisfies the shared codegen spec. The JS
195
+ // layer guards this call to Android, so it is never invoked on iOS.
196
+ - (void)setReplaySessionRecordingFormat:(double)format
197
+ {
198
+ // No-op: not supported on iOS.
199
+ }
200
+
180
201
  - (void)trackEvent:(NSString *)eventName
181
202
  surveyResultCallback:(RCTResponseSenderBlock)surveyResultCallback
182
203
  {
@@ -433,11 +454,26 @@ surveyStateCallback:(RCTResponseSenderBlock)surveyStateCallback
433
454
 
434
455
  [sdk registerEventListenerFor:eventType listener:^(NSDictionary<NSString *,id> * _Nonnull data) {
435
456
  __strong __typeof(weakSelf) strongSelf = weakSelf;
436
- // Weak capture + _alive guard: a callback firing during teardown must not touch a dead module.
437
- if (!strongSelf || !strongSelf->_alive) return;
457
+ if (!strongSelf) return;
458
+
459
+ // Snapshot liveness + invoker under the lock; bail if torn down.
460
+ std::shared_ptr<facebook::react::CallInvoker> invoker;
461
+ os_unfair_lock_lock(&strongSelf->_lock);
462
+ if (strongSelf->_alive) { invoker = strongSelf->_jsInvoker; }
463
+ os_unfair_lock_unlock(&strongSelf->_lock);
464
+ if (!invoker) return;
465
+
438
466
  NSDictionary *payload = [strongSelf normalizePayloadForEvent:name data:data];
439
467
  NSString *json = [strongSelf jsonStringFromDictionary:payload];
440
- [strongSelf emitOnSprigEvent:@{@"name": name, @"json": json}];
468
+
469
+ // Emit on the JS thread so it can't race runtime teardown; dropped if already invalidated.
470
+ invoker->invokeAsync([strongSelf, name, json]() {
471
+ os_unfair_lock_lock(&strongSelf->_lock);
472
+ BOOL alive = strongSelf->_alive;
473
+ os_unfair_lock_unlock(&strongSelf->_lock);
474
+ if (!alive) return;
475
+ [strongSelf emitOnSprigEvent:@{@"name": name, @"json": json}];
476
+ });
441
477
  }];
442
478
  }
443
479
  }
@@ -453,6 +489,10 @@ surveyStateCallback:(RCTResponseSenderBlock)surveyStateCallback
453
489
 
454
490
  - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const facebook::react::ObjCTurboModule::InitParams &)params
455
491
  {
492
+ // Capture the JS CallInvoker so event callbacks can marshal emits onto the JS thread.
493
+ os_unfair_lock_lock(&_lock);
494
+ _jsInvoker = params.jsInvoker;
495
+ os_unfair_lock_unlock(&_lock);
456
496
  return std::make_shared<facebook::react::NativeUserLeapBindingsSpecJSI>(params);
457
497
  }
458
498
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-userleap",
3
- "version": "4.1.0",
3
+ "version": "4.2.0",
4
4
  "description": "React Native module for UserLeap",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -18,7 +18,7 @@ Pod::Spec.new do |s|
18
18
  s.source_files = "ios/**/*.{h,c,m,mm,swift}"
19
19
  s.requires_arc = true
20
20
 
21
- s.dependency "UserLeapKit", "4.33.0"
21
+ s.dependency "UserLeapKit", "4.34.0"
22
22
 
23
23
  # New Architecture: wires up the React-Codegen dependency and header search
24
24
  # paths so the generated RNUserLeapBindingsSpec (Obj-C++) resolves.
@@ -50,6 +50,13 @@ export interface Spec extends TurboModule {
50
50
  ): void;
51
51
  overrideUserInterfaceMode(mode: number): void;
52
52
 
53
+ /**
54
+ * Android only. Selects the replay session recording format
55
+ * (0 = SCREENSHOT, 1 = WIREFRAME). No-op on iOS, whose native SDK has no
56
+ * equivalent API; the JS layer guards this call to Android.
57
+ */
58
+ setReplaySessionRecordingFormat(format: number): void;
59
+
53
60
  trackEvent(
54
61
  eventName: string,
55
62
  surveyResultCallback: (result: SurveyResult) => void,