react-native-userleap 4.0.0 → 4.1.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.
package/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { Platform, NativeModules } from "react-native";
1
+ import { Platform, NativeModules, TurboModuleRegistry } from "react-native";
2
2
 
3
3
  import { version } from "./package.json";
4
4
 
@@ -15,6 +15,100 @@ const SprigUserInterfaceMode = {
15
15
  dark: 2,
16
16
  };
17
17
 
18
+ const SprigEventName = {
19
+ sdkReady: "sdkReady",
20
+ visitorIdUpdated: "visitorIdUpdated",
21
+ surveyHeight: "surveyHeight",
22
+ surveyStateReturned: "surveyStateReturned",
23
+ surveyWillPresent: "surveyWillPresent",
24
+ surveyPresented: "surveyPresented",
25
+ surveyAppeared: "surveyAppeared",
26
+ questionAnswered: "questionAnswered",
27
+ surveyCloseRequested: "surveyCloseRequested",
28
+ surveyWillClose: "surveyWillClose",
29
+ surveyClosed: "surveyClosed",
30
+ surveyCompleted: "surveyCompleted",
31
+ replayCapture: "replayCapture",
32
+ replayCaptureStarted: "replayCaptureStarted",
33
+ replayCaptureStopped: "replayCaptureStopped",
34
+ replayCaptureCompleted: "replayCaptureCompleted",
35
+ replayRenderingCompleted: "replayRenderingCompleted",
36
+ replayUploadCompleted: "replayUploadCompleted",
37
+ replayEventsUploadCompleted: "replayEventsUploadCompleted",
38
+ loggingEvent: "loggingEvent",
39
+ };
40
+
41
+ // Resolved lazily; needed for the onSprigEvent channel (a JSI EventEmitter the legacy NativeModules interop doesn't expose).
42
+ let _module = null;
43
+ const getModule = () => {
44
+ if (_module) return _module;
45
+ _module =
46
+ (TurboModuleRegistry && TurboModuleRegistry.get
47
+ ? TurboModuleRegistry.get("UserLeapBindings")
48
+ : null) ||
49
+ NativeModules.UserLeapBindings ||
50
+ null;
51
+ return _module;
52
+ };
53
+
54
+ // All native events arrive on one onSprigEvent channel as a {name,json} JSON-string envelope
55
+ // (payloads are heterogeneous); we subscribe once and fan out to per-event listeners.
56
+ const _listenersByEvent = new Map();
57
+ let _channelSub = null;
58
+
59
+ const ensureChannelSubscribed = () => {
60
+ if (_channelSub) return;
61
+ const mod = getModule();
62
+ if (!mod || typeof mod.onSprigEvent !== "function") return;
63
+ _channelSub = mod.onSprigEvent((envelope) => {
64
+ if (!envelope || !envelope.name) return;
65
+ const set = _listenersByEvent.get(envelope.name);
66
+ if (!set || set.size === 0) return;
67
+
68
+ let payload;
69
+ try {
70
+ payload = envelope.json ? JSON.parse(envelope.json) : {};
71
+ } catch (e) {
72
+ payload = {};
73
+ }
74
+ if (payload && typeof payload === "object") {
75
+ payload.type = envelope.name;
76
+ }
77
+
78
+ // Copy so a listener (un)subscribing during dispatch can't mutate the set we're walking.
79
+ for (const cb of Array.from(set)) {
80
+ try {
81
+ cb(payload);
82
+ } catch (err) {
83
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
84
+ console.warn(
85
+ '[Sprig] listener for "' + envelope.name + '" threw:',
86
+ err
87
+ );
88
+ }
89
+ }
90
+ }
91
+ });
92
+ };
93
+
94
+ const NOOP_SUBSCRIPTION = { remove: () => {} };
95
+
96
+ let _supportedEventNamesCache = null;
97
+ const getSupportedEventNames = () => {
98
+ if (_supportedEventNamesCache !== null) return _supportedEventNamesCache;
99
+ if (!isValidPlatform()) {
100
+ _supportedEventNamesCache = [];
101
+ return _supportedEventNamesCache;
102
+ }
103
+ try {
104
+ const result = NativeModules.UserLeapBindings.getSupportedEventNames();
105
+ _supportedEventNamesCache = Array.isArray(result) ? result : [];
106
+ } catch (err) {
107
+ _supportedEventNamesCache = [];
108
+ }
109
+ return _supportedEventNamesCache;
110
+ };
111
+
18
112
 
19
113
  const stringifyAttributes = (attributes) => {
20
114
  if (!(attributes instanceof Object && attributes.constructor === Object))
@@ -42,11 +136,7 @@ const visitorIdentifier = () => {
42
136
  };
43
137
 
44
138
  const sdkVersion = () => {
45
- if (Platform.OS === "ios") {
46
- if (String(Platform.Version) >= "15.0") {
47
- return NativeModules.UserLeapBindings.sdkVersion();
48
- }
49
- } else {
139
+ if (isValidPlatform()) {
50
140
  return NativeModules.UserLeapBindings.getSdkVersion();
51
141
  }
52
142
  return "0.0.0";
@@ -271,6 +361,63 @@ const overrideUserInterfaceMode = (mode) => {
271
361
  }
272
362
  }
273
363
 
364
+ /**
365
+ * Subscribe to a native SDK lifecycle event. Returns a subscription whose
366
+ * `.remove()` unsubscribes only this listener.
367
+ * @param {string} eventName
368
+ * @param {(data: object) => void} callback
369
+ * @returns {{ remove: () => void }} subscription
370
+ */
371
+ const addEventListener = (eventName, callback) => {
372
+ if (!isValidPlatform()) return NOOP_SUBSCRIPTION;
373
+
374
+ const supported = getSupportedEventNames();
375
+ if (supported.length > 0 && !supported.includes(eventName)) {
376
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
377
+ console.warn(
378
+ '[Sprig] "' + eventName +
379
+ '" is not emitted on this platform (' +
380
+ Platform.OS + '); subscription is a no-op.'
381
+ );
382
+ }
383
+ return NOOP_SUBSCRIPTION;
384
+ }
385
+
386
+ ensureChannelSubscribed();
387
+
388
+ let set = _listenersByEvent.get(eventName);
389
+ if (!set) {
390
+ set = new Set();
391
+ _listenersByEvent.set(eventName, set);
392
+ }
393
+ set.add(callback);
394
+
395
+ let removed = false;
396
+ return {
397
+ remove: () => {
398
+ if (removed) return;
399
+ removed = true;
400
+ const s = _listenersByEvent.get(eventName);
401
+ if (s) {
402
+ s.delete(callback);
403
+ if (s.size === 0) _listenersByEvent.delete(eventName);
404
+ }
405
+ },
406
+ };
407
+ };
408
+
409
+ /**
410
+ * Remove every listener for the given event name, or all listeners if no name is given.
411
+ * @param {string} [eventName]
412
+ */
413
+ const removeAllEventListeners = (eventName) => {
414
+ if (eventName) {
415
+ _listenersByEvent.delete(eventName);
416
+ return;
417
+ }
418
+ _listenersByEvent.clear();
419
+ };
420
+
274
421
 
275
422
  const UserLeap = {
276
423
  visitorIdentifier,
@@ -298,9 +445,13 @@ const UserLeap = {
298
445
  pauseDisplayingSurveys,
299
446
  unpauseDisplayingSurveys,
300
447
  overrideUserInterfaceMode,
448
+ addEventListener,
449
+ removeAllEventListeners,
450
+ getSupportedEventNames,
301
451
  };
302
452
 
303
453
  UserLeap.SurveyState = SurveyState;
304
454
  UserLeap.SprigUserInterfaceMode = SprigUserInterfaceMode;
455
+ UserLeap.SprigEventName = SprigEventName;
305
456
 
306
457
  export default UserLeap;
@@ -1,6 +1,7 @@
1
- #import <React/RCTBridgeModule.h>
2
- @import UserLeapKit;
1
+ #import <UserLeapKit/UserLeapKit.h>
2
+ #import <UserLeapKit/UserLeapKit-Swift.h>
3
3
 
4
- @interface UserLeapBindings : NSObject <RCTBridgeModule, _SGRNExtractor>
5
- @property (nonatomic, weak) RCTBridge *bridge;
4
+ #import <RNUserLeapBindingsSpec/RNUserLeapBindingsSpec.h>
5
+
6
+ @interface UserLeapBindings : NativeUserLeapBindingsSpecBase <NativeUserLeapBindingsSpec, _SGRNExtractor>
6
7
  @end
@@ -0,0 +1,459 @@
1
+ #import "UserLeapBindings.h"
2
+ #import <React/RCTUtils.h>
3
+ // Legacy Paper includes gated on RCT_REMOVE_LEGACY_ARCH: these classes are removed on RN 0.85+.
4
+ #ifndef RCT_REMOVE_LEGACY_ARCH
5
+ #import <React/RCTTextView.h>
6
+ #import <React/RCTUIManager.h>
7
+ #import <React/RCTTextShadowView.h>
8
+ #import <React/RCTShadowView.h>
9
+ #import <React/RCTRawTextShadowView.h>
10
+ #import <React/RCTVirtualTextShadowView.h>
11
+ #import <React/RCTUIManagerUtils.h>
12
+ #endif
13
+ #import <React/RCTView.h>
14
+
15
+ // The codegen spec header is Obj-C++ only, which is why this file is `.mm`.
16
+ #import <RNUserLeapBindingsSpec/RNUserLeapBindingsSpec.h>
17
+
18
+ @implementation UserLeapBindings {
19
+ BOOL _alive;
20
+ }
21
+
22
+ + (BOOL)requiresMainQueueSetup
23
+ {
24
+ return YES;
25
+ }
26
+
27
+ - (instancetype)init
28
+ {
29
+ if (self = [super init]) {
30
+ _alive = YES;
31
+ // emitOnSprigEvent is a safe no-op when JS has no listener, so eager subscription is fine.
32
+ [self subscribeToSdkEvents];
33
+ }
34
+ return self;
35
+ }
36
+
37
+ - (void)dealloc
38
+ {
39
+ _alive = NO;
40
+ [self unsubscribeFromSdkEvents];
41
+ }
42
+
43
+ - (dispatch_queue_t)methodQueue
44
+ {
45
+ return dispatch_get_main_queue();
46
+ }
47
+
48
+ - (NSDictionary *)parseSurveyResult:(SprigSurveyResult *)result {
49
+ SurveyState state = [result surveyState];
50
+ NSString *surveyStateString = [self getSurveyState:state];
51
+ NSInteger surveyId = [result surveyId];
52
+ return @{@"surveyState": surveyStateString, @"surveyId": @(surveyId)};
53
+ }
54
+
55
+ - (NSString *)getSurveyState:(SurveyState)surveyState
56
+ {
57
+ NSString *surveyStateBinding = @"NO_SURVEY";
58
+ switch(surveyState) {
59
+ case SurveyStateDisabled:
60
+ surveyStateBinding = @"DISABLED";
61
+ break;
62
+ case SurveyStateReady:
63
+ surveyStateBinding = @"READY";
64
+ break;
65
+ case SurveyStateNoSurvey:
66
+ surveyStateBinding = @"NO_SURVEY";
67
+ break;
68
+ case SurveyStatePreviousSurveyReady:
69
+ surveyStateBinding = @"PREVIOUS_SURVEY_READY";
70
+ break;
71
+ }
72
+ return surveyStateBinding;
73
+ }
74
+
75
+ RCT_EXPORT_MODULE()
76
+
77
+ - (NSNumber *)visitorIdentifier
78
+ {
79
+ // No @() boxing: @() expects a primitive C type, and the SDK already returns NSNumber *.
80
+ return [[UserLeap shared] visitorIdentifier];
81
+ }
82
+
83
+ - (NSString *)visitorIdentifierString
84
+ {
85
+ return [[UserLeap shared] visitorIdentifierString];
86
+ }
87
+
88
+ - (NSString *)getSdkVersion
89
+ {
90
+ // Renamed from `sdkVersion` to `getSdkVersion` so iOS and Android share one spec method name.
91
+ return [[UserLeap shared] sdkVersion];
92
+ }
93
+
94
+ - (void)configure:(NSString *)environmentId configuration:(NSDictionary *)configuration
95
+ {
96
+ [[UserLeap shared] configureWithEnvironment:environmentId configuration:configuration];
97
+ [[UserLeap shared] _passWithRnExtractor:self];
98
+ }
99
+
100
+ - (void)setPreviewKey:(NSString *)previewKey
101
+ {
102
+ [[UserLeap shared] setPreviewKey:previewKey];
103
+ }
104
+
105
+ - (void)setUserIdentifier:(NSString *)identifier
106
+ {
107
+ [[UserLeap shared] setUserIdentifier:identifier];
108
+ }
109
+
110
+ - (void)setEmailAddress:(NSString *)emailAddress
111
+ {
112
+ [[UserLeap shared] setEmailAddress:emailAddress];
113
+ }
114
+
115
+ - (void)logout
116
+ {
117
+ [[UserLeap shared] logout];
118
+ }
119
+
120
+ - (void)setVisitorAttribute:(NSString *)key value:(NSString *)value
121
+ {
122
+ [[UserLeap shared] setVisitorAttributeWithKey:key value:value];
123
+ }
124
+
125
+ - (void)setVisitorAttributes:(NSDictionary *)attributes
126
+ {
127
+ [[UserLeap shared] setVisitorAttributes:attributes];
128
+ }
129
+
130
+ - (void)removeVisitorAttributes:(NSArray *)keys
131
+ {
132
+ [[UserLeap shared] removeVisitorAttributes:keys];
133
+ }
134
+
135
+ - (void)setVisitorAttributesAndIdentify:(NSDictionary *)attributes
136
+ userId:(NSString *)userId
137
+ partnerAnonymousId:(NSString *)partnerAnonymousId
138
+ {
139
+ [[UserLeap shared] setVisitorAttributes:attributes userId:userId partnerAnonymousId:partnerAnonymousId];
140
+ }
141
+
142
+ - (void)presentSurvey
143
+ {
144
+ [[UserLeap shared] presentSurveyFrom:RCTPresentedViewController()];
145
+ }
146
+
147
+ - (void)dismissActiveSurvey
148
+ {
149
+ [[UserLeap shared] dismissActiveSurvey];
150
+ }
151
+
152
+ - (void)pauseDisplayingSurveys
153
+ {
154
+ [[UserLeap shared] pauseDisplayingSurveys];
155
+ }
156
+
157
+ - (void)unpauseDisplayingSurveys
158
+ {
159
+ [[UserLeap shared] unpauseDisplayingSurveys];
160
+ }
161
+
162
+ - (void)trackAndPresent:(NSString *)eventName
163
+ {
164
+ [[UserLeap shared] trackAndPresentWithEventName:eventName from:RCTPresentedViewController()];
165
+ }
166
+
167
+ - (void)trackIdentifyAndPresent:(NSString *)eventName
168
+ userId:(NSString *)userId
169
+ partnerAnonymousId:(NSString *)partnerAnonymousId
170
+ {
171
+ [[UserLeap shared] trackAndPresentWithEventName:eventName userId:userId partnerAnonymousId:partnerAnonymousId from:RCTPresentedViewController()];
172
+ }
173
+
174
+ - (void)overrideUserInterfaceMode:(double)mode
175
+ {
176
+ // Explicit enum cast required: Obj-C++ won't implicitly convert numeric → enum.
177
+ [[UserLeap shared] overrideUserInterfaceModeWithMode:(enum SprigUserInterfaceMode)(NSInteger)mode];
178
+ }
179
+
180
+ - (void)trackEvent:(NSString *)eventName
181
+ surveyResultCallback:(RCTResponseSenderBlock)surveyResultCallback
182
+ {
183
+ EventPayload *payload = [[EventPayload alloc] initWithEventName:eventName
184
+ userId:nil
185
+ partnerAnonymousId:nil
186
+ properties:nil
187
+ handler:nil
188
+ resultHandler:^(SprigSurveyResult * result) {
189
+ if (surveyResultCallback != nil) {
190
+ surveyResultCallback(@[[self parseSurveyResult:result]]);
191
+ }
192
+ }];
193
+ [[UserLeap shared] trackWithPayload:payload];
194
+ }
195
+
196
+ - (void)trackEventWithProperties:(NSString *)eventName
197
+ userId:(NSString *)userId
198
+ partnerAnonymousId:(NSString *)partnerAnonymousId
199
+ properties:(NSDictionary *)properties
200
+ surveyResultCallback:(RCTResponseSenderBlock)surveyResultCallback
201
+ {
202
+ EventPayload *payload = [[EventPayload alloc] initWithEventName:eventName
203
+ userId:userId
204
+ partnerAnonymousId:partnerAnonymousId
205
+ properties:properties
206
+ handler:nil
207
+ resultHandler:^(SprigSurveyResult * result) {
208
+ if (surveyResultCallback != nil) {
209
+ surveyResultCallback(@[[self parseSurveyResult:result]]);
210
+ }
211
+ }];
212
+ [[UserLeap shared] trackWithPayload:payload];
213
+ }
214
+
215
+ - (void)trackEventAndIdentify:(NSString *)eventName
216
+ userId:(NSString *)userId
217
+ partnerAnonymousId:(NSString *)partnerAnonymousId
218
+ surveyResultCallback:(RCTResponseSenderBlock)surveyResultCallback
219
+ {
220
+ EventPayload *payload = [[EventPayload alloc] initWithEventName:eventName
221
+ userId:userId
222
+ partnerAnonymousId:partnerAnonymousId
223
+ properties:nil
224
+ handler:nil
225
+ resultHandler:^(SprigSurveyResult * result) {
226
+ if (surveyResultCallback != nil) {
227
+ surveyResultCallback(@[[self parseSurveyResult:result]]);
228
+ }
229
+ }];
230
+ [[UserLeap shared] trackWithPayload:payload];
231
+ }
232
+
233
+ - (void)track:(NSString *)eventName
234
+ surveyStateCallback:(RCTResponseSenderBlock)surveyStateCallback
235
+ {
236
+ if (surveyStateCallback != nil) {
237
+ [[UserLeap shared] trackWithEventName:eventName handler:^(enum SurveyState surveyState) {
238
+ surveyStateCallback(@[[self getSurveyState:surveyState]]);
239
+ }];
240
+ } else {
241
+ [[UserLeap shared] trackWithEventName:eventName handler:nil];
242
+ }
243
+ }
244
+
245
+ - (void)trackWithProperties:(NSString *)eventName
246
+ userId:(NSString *)userId
247
+ partnerAnonymousId:(NSString *)partnerAnonymousId
248
+ properties:(NSDictionary *)properties
249
+ surveyStateCallback:(RCTResponseSenderBlock)surveyStateCallback
250
+ {
251
+ [[UserLeap shared] trackWithEventName:eventName
252
+ userId:userId
253
+ partnerAnonymousId:partnerAnonymousId
254
+ properties:properties
255
+ handler:^(enum SurveyState surveyState) {
256
+ if (surveyStateCallback != nil) {
257
+ surveyStateCallback(@[[self getSurveyState:surveyState]]);
258
+ }
259
+ }];
260
+ }
261
+
262
+ - (void)trackAndIdentify:(NSString *)eventName
263
+ userId:(NSString *)userId
264
+ partnerAnonymousId:(NSString *)partnerAnonymousId
265
+ surveyStateCallback:(RCTResponseSenderBlock)surveyStateCallback
266
+ {
267
+ if (surveyStateCallback != nil) {
268
+ [[UserLeap shared] trackWithEventName:eventName
269
+ userId:userId
270
+ partnerAnonymousId:partnerAnonymousId
271
+ handler:^(enum SurveyState surveyState) {
272
+ surveyStateCallback(@[[self getSurveyState:surveyState]]);
273
+ }];
274
+ } else {
275
+ [[UserLeap shared] trackWithEventName:eventName
276
+ userId:userId
277
+ partnerAnonymousId:partnerAnonymousId
278
+ handler:nil];
279
+ }
280
+ }
281
+
282
+ - (_SGRNTextProperties *)textPropertiesFromView:(UIView * _Nonnull)passedView {
283
+
284
+ #ifndef RCT_REMOVE_LEGACY_ARCH
285
+ if ([passedView isKindOfClass:[RCTTextView class]]) {
286
+ RCTUIManager* uiManager = [self.bridge moduleForClass:[RCTUIManager class]];
287
+ RCTTextView *textView = (RCTTextView *)passedView;
288
+ __block _SGRNTextProperties *textProperties = [[_SGRNTextProperties alloc] init];
289
+ dispatch_sync(RCTGetUIManagerQueue(), ^{
290
+ RCTShadowView *shadowView = [uiManager shadowViewForReactTag:textView.reactTag];
291
+ if ([shadowView isKindOfClass:[RCTTextShadowView class]]) {
292
+ RCTTextShadowView *textShadowView = (RCTTextShadowView *)shadowView;
293
+ NSString *text = [self extractText: textShadowView.reactSubviews];
294
+ if (text.length == 0) return;
295
+ textProperties.text = text;
296
+ textProperties.color = textShadowView.textAttributes.foregroundColor;
297
+ textProperties.alignment = textShadowView.textAttributes.alignment;
298
+ textProperties.font = textShadowView.textAttributes.effectiveFont;
299
+ }
300
+ });
301
+ if (textProperties.text.length == 0) return nil;
302
+ return textProperties;
303
+ }
304
+ #endif
305
+
306
+ Class ParagraphComponentView = NSClassFromString(@"RCTParagraphComponentView");
307
+ if (ParagraphComponentView && [passedView isKindOfClass:ParagraphComponentView]) {
308
+ NSString *text = [self extractTextFromParagraphComponentView:passedView];
309
+ if (text.length == 0) return nil;
310
+ _SGRNTextProperties *textProperties = [[_SGRNTextProperties alloc] init];
311
+ textProperties.text = text;
312
+ if ([passedView respondsToSelector:@selector(attributedText)]) {
313
+ NSAttributedString *attrText = [passedView performSelector:@selector(attributedText)];
314
+ if (attrText.length > 0) {
315
+ NSDictionary *attrs = [attrText attributesAtIndex:0 effectiveRange:nil];
316
+ if (!textProperties.color && attrs[NSForegroundColorAttributeName]) {
317
+ textProperties.color = attrs[NSForegroundColorAttributeName];
318
+ }
319
+ if (!textProperties.font && attrs[NSFontAttributeName]) {
320
+ textProperties.font = attrs[NSFontAttributeName];
321
+ }
322
+ NSParagraphStyle *style = attrs[NSParagraphStyleAttributeName];
323
+ if (style) {
324
+ textProperties.alignment = style.alignment;
325
+ }
326
+ }
327
+ }
328
+ return textProperties;
329
+ }
330
+ return nil;
331
+ }
332
+
333
+ - (_SGRNViewProperties * _Nullable)propertiesFromView:(UIView * _Nonnull)passedView {
334
+ if ([passedView isKindOfClass:[RCTView class]]) {
335
+ RCTView *rView = (RCTView *) passedView;
336
+ if (rView.borderWidth > 0) {
337
+ _SGRNViewProperties *props = [[_SGRNViewProperties alloc] init];
338
+ props.borderColor = rView.borderColor;
339
+ props.borderWidth = rView.borderWidth;
340
+ return props;
341
+ }
342
+ }
343
+ return nil;
344
+ }
345
+
346
+ #ifndef RCT_REMOVE_LEGACY_ARCH
347
+ - (NSString * _Nullable)extractText:(NSArray<RCTShadowView *> * _Nonnull) subviews {
348
+ NSMutableString *text = [[NSMutableString alloc] init];
349
+ for (RCTShadowView *subview in subviews) {
350
+ if ([subview isKindOfClass:[RCTRawTextShadowView class]]) {
351
+ [text appendString:((RCTRawTextShadowView *)subview).text];
352
+ }
353
+ if ([subview isKindOfClass:[RCTVirtualTextShadowView class]]) {
354
+ // Append, don't early-return: returning here would drop text already collected.
355
+ NSString *nested = [self extractText: ((RCTVirtualTextShadowView *)subview).reactSubviews];
356
+ if (nested) [text appendString:nested];
357
+ }
358
+ }
359
+ return text;
360
+ }
361
+ #endif
362
+
363
+ - (NSString *)extractTextFromParagraphComponentView:(UIView *)view {
364
+ if ([view respondsToSelector:@selector(attributedText)]) {
365
+ NSAttributedString *attrText = [view performSelector:@selector(attributedText)];
366
+ return attrText.string;
367
+ }
368
+ if ([view respondsToSelector:@selector(text)]) {
369
+ NSString *text = [view performSelector:@selector(text)];
370
+ return text;
371
+ }
372
+ NSMutableString *result = [NSMutableString string];
373
+ for (UIView *subview in view.subviews) {
374
+ NSString *subText = [self extractTextFromParagraphComponentView:subview];
375
+ if (subText) [result appendString:subText];
376
+ }
377
+ return result;
378
+ }
379
+
380
+ - (NSArray<NSString *> *)getSupportedEventNames
381
+ {
382
+ NSMutableArray<NSString *> *events = [NSMutableArray array];
383
+ for (NSString *name in [LifecycleEventUtil allNameStrings]) {
384
+ if ([name isEqualToString:@"unknown"]) continue;
385
+ [events addObject:name];
386
+ }
387
+ return events;
388
+ }
389
+
390
+ - (NSDictionary *)normalizePayloadForEvent:(NSString *)eventName
391
+ data:(NSDictionary<NSString *, id> *)data
392
+ {
393
+ if (![eventName isEqualToString:@"loggingEvent"]) {
394
+ return data ?: @{};
395
+ }
396
+
397
+ NSString *raw = data[@"message"];
398
+ NSString *message = @"";
399
+ if ([raw isKindOfClass:[NSString class]]) {
400
+ NSString *prefix = @"Sprig: ";
401
+ message = [raw hasPrefix:prefix] ? [raw substringFromIndex:prefix.length] : raw;
402
+ }
403
+
404
+ return @{
405
+ @"type": @"loggingEvent",
406
+ @"message": message,
407
+ @"level": @"info",
408
+ };
409
+ }
410
+
411
+ - (NSString *)jsonStringFromDictionary:(NSDictionary *)dict
412
+ {
413
+ if (![NSJSONSerialization isValidJSONObject:dict]) {
414
+ return @"{}";
415
+ }
416
+ NSError *error = nil;
417
+ NSData *data = [NSJSONSerialization dataWithJSONObject:dict options:0 error:&error];
418
+ if (!data || error) {
419
+ return @"{}";
420
+ }
421
+ return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] ?: @"{}";
422
+ }
423
+
424
+ - (void)subscribeToSdkEvents
425
+ {
426
+ __weak __typeof(self) weakSelf = self;
427
+ UserLeap *sdk = [UserLeap shared];
428
+
429
+ for (NSNumber *raw in [LifecycleEventUtil all]) {
430
+ LifecycleEvent eventType = (LifecycleEvent)raw.integerValue;
431
+ NSString *name = [LifecycleEventUtil stringName:eventType];
432
+ if ([name isEqualToString:@"unknown"]) continue;
433
+
434
+ [sdk registerEventListenerFor:eventType listener:^(NSDictionary<NSString *,id> * _Nonnull data) {
435
+ __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;
438
+ NSDictionary *payload = [strongSelf normalizePayloadForEvent:name data:data];
439
+ NSString *json = [strongSelf jsonStringFromDictionary:payload];
440
+ [strongSelf emitOnSprigEvent:@{@"name": name, @"json": json}];
441
+ }];
442
+ }
443
+ }
444
+
445
+ - (void)unsubscribeFromSdkEvents
446
+ {
447
+ UserLeap *sdk = [UserLeap shared];
448
+ for (NSNumber *raw in [LifecycleEventUtil all]) {
449
+ LifecycleEvent eventType = (LifecycleEvent)raw.integerValue;
450
+ [sdk unregisterAllEventListenersFor:eventType];
451
+ }
452
+ }
453
+
454
+ - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const facebook::react::ObjCTurboModule::InitParams &)params
455
+ {
456
+ return std::make_shared<facebook::react::NativeUserLeapBindingsSpecJSI>(params);
457
+ }
458
+
459
+ @end
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-userleap",
3
- "version": "4.0.0",
3
+ "version": "4.1.0",
4
4
  "description": "React Native module for UserLeap",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -12,7 +12,8 @@
12
12
  "index.js",
13
13
  "index.d.ts",
14
14
  "ios",
15
- "react-native-userleap.podspec"
15
+ "react-native-userleap.podspec",
16
+ "src"
16
17
  ],
17
18
  "scripts": {
18
19
  "test": "echo \"Error: no test specified\" && exit 1"
@@ -40,5 +41,13 @@
40
41
  },
41
42
  "overrides": {
42
43
  "js-yaml": "3.14.2"
44
+ },
45
+ "codegenConfig": {
46
+ "name": "RNUserLeapBindingsSpec",
47
+ "type": "modules",
48
+ "jsSrcsDir": "./src",
49
+ "android": {
50
+ "javaPackageName": "com.userleap.reactnative"
51
+ }
43
52
  }
44
53
  }