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.
@@ -1,81 +1,90 @@
1
1
  // android/build.gradle
2
- // based on:
3
2
  //
4
- // * https://github.com/facebook/react-native/blob/0.60-stable/template/android/build.gradle
5
- // original location:
6
- // - https://github.com/facebook/react-native/blob/0.58-stable/local-cli/templates/HelloWorld/android/build.gradle
3
+ // Modernized for React Native 0.85:
7
4
  //
8
- // * https://github.com/facebook/react-native/blob/0.60-stable/template/android/app/build.gradle
9
- // original location:
10
- // - https://github.com/facebook/react-native/blob/0.58-stable/local-cli/templates/HelloWorld/android/app/build.gradle
11
- def DEFAULT_COMPILE_SDK_VERSION = 28
12
- def DEFAULT_BUILD_TOOLS_VERSION = '30.0.2'
13
- def DEFAULT_MIN_SDK_VERSION = 16
14
- def DEFAULT_TARGET_SDK_VERSION = 28
5
+ // - Applies the `com.facebook.react` plugin so RN's codegen runs against
6
+ // our TurboModule spec in `../src/` and generates the Java abstract base
7
+ // class our Kotlin module inherits from plus the C++ JNI glue the New
8
+ // Architecture autolinking links against. Codegen is configured by the
9
+ // `react { ... }` block below. `jsRootDir` MUST point at the isolated
10
+ // `src/` dir (not the package root) so codegen scans only our spec and
11
+ // not the sibling `example/` app's node_modules.
12
+ // - Applies the Kotlin plugin so `UserLeapModule.kt` compiles.
13
+ // - Sets `namespace` (required by Android Gradle Plugin 8+).
14
+ // - Targets Java 17 / Kotlin (matches RN 0.85's host-app build).
15
+ //
16
+ // Versions for compileSdk / buildTools / NDK / Java are pulled from the
17
+ // host application's rootProject.ext via safeExtGet, so consumers can
18
+ // override them centrally without editing this file.
19
+
15
20
  def safeExtGet(prop, fallback) {
16
21
  rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
17
22
  }
23
+
18
24
  buildscript {
19
- // The Android Gradle plugin is only required when opening the android folder stand-alone.
20
- // This avoids unnecessary downloads and potential conflicts when the library is included as a
21
- // module dependency in an application project.
22
- // ref: https://docs.gradle.org/current/userguide/tutorial_using_tasks.html#sec:build_script_external_dependencies
25
+ // Standalone-only block: dependencies here only apply when the
26
+ // wrapper is opened directly in Android Studio. Consuming apps
27
+ // provide their own AGP / Kotlin classpath via their root build.
23
28
  if (project == rootProject) {
24
29
  repositories {
25
30
  google()
26
- jcenter()
31
+ mavenCentral()
27
32
  }
28
33
  dependencies {
29
- classpath 'com.android.tools.build:gradle:7.0.4'
34
+ classpath "com.android.tools.build:gradle:8.7.2"
35
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.20"
30
36
  }
31
37
  }
32
38
  }
39
+
33
40
  apply plugin: 'com.android.library'
41
+ apply plugin: 'org.jetbrains.kotlin.android'
42
+ apply plugin: 'com.facebook.react'
43
+
34
44
  android {
35
- compileSdkVersion safeExtGet('compileSdkVersion', DEFAULT_COMPILE_SDK_VERSION)
36
- buildToolsVersion safeExtGet('buildToolsVersion', DEFAULT_BUILD_TOOLS_VERSION)
45
+ namespace "com.userleap.reactnative"
46
+ compileSdkVersion safeExtGet('compileSdkVersion', 36)
47
+ buildToolsVersion safeExtGet('buildToolsVersion', '35.0.0')
48
+
37
49
  compileOptions {
38
- sourceCompatibility JavaVersion.VERSION_1_8
39
- targetCompatibility JavaVersion.VERSION_1_8
50
+ sourceCompatibility JavaVersion.VERSION_17
51
+ targetCompatibility JavaVersion.VERSION_17
52
+ }
53
+
54
+ kotlinOptions {
55
+ jvmTarget = '17'
40
56
  }
57
+
41
58
  defaultConfig {
42
- minSdkVersion safeExtGet('minSdkVersion', DEFAULT_MIN_SDK_VERSION)
43
- targetSdkVersion safeExtGet('targetSdkVersion', DEFAULT_TARGET_SDK_VERSION)
44
- versionCode 1
45
- versionName "1.0"
59
+ minSdkVersion safeExtGet('minSdkVersion', 24)
60
+ targetSdkVersion safeExtGet('targetSdkVersion', 36)
46
61
  }
62
+
47
63
  lintOptions {
48
64
  abortOnError false
49
65
  }
50
66
  }
67
+
68
+ react {
69
+ jsRootDir = file("../src/")
70
+ libraryName = "RNUserLeapBindingsSpec"
71
+ codegenJavaPackageName = "com.userleap.reactnative"
72
+ }
73
+
51
74
  repositories {
52
- // ref: https://www.baeldung.com/maven-local-repository
53
- maven {
54
- // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
55
- url "$rootDir/../node_modules/react-native/android"
56
- }
57
- maven {
58
- // Android JSC is installed from npm
59
- url "$rootDir/../node_modules/jsc-android/dist"
60
- }
61
- mavenCentral {
62
- // We don't want to fetch react-native from Maven Central as there are
63
- // older versions over there.
64
- content {
65
- excludeGroup "com.facebook.react"
66
- }
67
- }
75
+ mavenCentral()
68
76
  google()
69
77
  maven { url 'https://www.jitpack.io' }
70
78
  }
79
+
71
80
  dependencies {
72
- implementation 'com.facebook.react:react-native:+' // From node_modules
81
+ implementation 'com.facebook.react:react-native:+'
73
82
  implementation "androidx.webkit:webkit:1.8.0"
74
83
 
75
84
  // Use conditional logic for local vs Maven
76
85
  if (findProject(':userleap-android-sdk') != null) {
77
86
  implementation project(':userleap-android-sdk')
78
87
  } else {
79
- implementation ("com.userleap:userleap-android-sdk:2.26.2") // update this version on android updates
88
+ implementation ("com.userleap:userleap-android-sdk:2.27.0") // update this version on android updates
80
89
  }
81
- }
90
+ }
@@ -0,0 +1,336 @@
1
+ package com.userleap.reactnative
2
+
3
+ import android.app.Activity
4
+ import android.util.Log
5
+ import androidx.fragment.app.FragmentActivity
6
+ import com.facebook.react.bridge.Arguments
7
+ import com.facebook.react.bridge.Callback
8
+ import com.facebook.react.bridge.ReactApplicationContext
9
+ import com.facebook.react.bridge.ReadableArray
10
+ import com.facebook.react.bridge.ReadableMap
11
+ import com.facebook.react.bridge.WritableMap
12
+ import com.userleap.EventListener
13
+ import com.userleap.EventName
14
+ import com.userleap.EventPayload
15
+ import com.userleap.SprigEvent
16
+ import com.userleap.SprigSurveyResult
17
+ import com.userleap.SurveyState
18
+ import com.userleap.UserLeap
19
+ import com.userleap.SprigUserInterfaceMode
20
+ import org.json.JSONObject
21
+
22
+ class UserLeapModule(reactContext: ReactApplicationContext) :
23
+ NativeUserLeapBindingsSpec(reactContext) {
24
+
25
+ private val reactContext: ReactApplicationContext = reactContext
26
+
27
+ // listenerLock guards activeListeners and `alive` so init-time registration and invalidate() cannot race.
28
+ private val listenerLock = Any()
29
+ private val activeListeners: MutableMap<EventName, EventListener> = HashMap()
30
+ @Volatile private var alive = true
31
+
32
+ init {
33
+ synchronized(listenerLock) {
34
+ registerAllNativeListeners()
35
+ }
36
+ }
37
+
38
+ // Tear down SDK subscriptions here so a late callback can't hit a dead module ("emit after invalidation").
39
+ override fun invalidate() {
40
+ synchronized(listenerLock) {
41
+ alive = false
42
+ unregisterAllNativeListeners()
43
+ }
44
+ super.invalidate()
45
+ }
46
+
47
+ private fun toCamelCase(upperSnakeCase: String): String {
48
+ val parts = upperSnakeCase.lowercase().split("_")
49
+ val sb = StringBuilder(parts[0])
50
+ for (i in 1 until parts.size) {
51
+ sb.append(parts[i].replaceFirstChar { it.uppercase() })
52
+ }
53
+ return sb.toString()
54
+ }
55
+
56
+ private fun surveyResultToWritableMap(result: SprigSurveyResult): WritableMap {
57
+ val map = Arguments.createMap()
58
+ map.putString("surveyState", result.surveyState.name)
59
+ map.putInt("surveyId", result.surveyId)
60
+ return map
61
+ }
62
+
63
+ private fun wrapResultCallback(callback: Callback?): ((SprigSurveyResult) -> Unit)? {
64
+ if (callback == null) return null
65
+ return { result -> callback.invoke(surveyResultToWritableMap(result)) }
66
+ }
67
+
68
+ private fun stringifyMap(map: ReadableMap?): Map<String, String> {
69
+ if (map == null) return emptyMap()
70
+ return map.toHashMap()
71
+ .entries
72
+ .filter { it.key != null && it.value != null }
73
+ .associate { it.key!! to it.value.toString() }
74
+ }
75
+
76
+ private fun getFragmentActivity(): FragmentActivity? {
77
+ val activity: Activity? = currentActivity
78
+ return activity as? FragmentActivity
79
+ }
80
+
81
+ override fun visitorIdentifier(): Double {
82
+ return UserLeap.visitorIdentifier?.toDouble() ?: 0.0
83
+ }
84
+
85
+ override fun visitorIdentifierString(): String {
86
+ return UserLeap.visitorIdentifierString ?: ""
87
+ }
88
+
89
+ override fun getSdkVersion(): String {
90
+ return UserLeap.sdkVersion
91
+ }
92
+
93
+ override fun configure(environmentId: String, configuration: ReadableMap) {
94
+ UserLeap.configure(
95
+ reactContext,
96
+ environmentId,
97
+ stringifyMap(configuration),
98
+ getFragmentActivity()
99
+ )
100
+ }
101
+
102
+ override fun setPreviewKey(previewKey: String) {
103
+ UserLeap.setPreviewKey(previewKey)
104
+ }
105
+
106
+ override fun setUserIdentifier(identifier: String) {
107
+ UserLeap.setUserIdentifier(identifier)
108
+ }
109
+
110
+ override fun setEmailAddress(emailAddress: String) {
111
+ UserLeap.setEmailAddress(emailAddress)
112
+ }
113
+
114
+ override fun logout() {
115
+ UserLeap.logout()
116
+ }
117
+
118
+ override fun setVisitorAttribute(key: String, value: String) {
119
+ UserLeap.setVisitorAttribute(key, value)
120
+ }
121
+
122
+ override fun setVisitorAttributes(attributes: ReadableMap) {
123
+ UserLeap.setVisitorAttributes(stringifyMap(attributes))
124
+ }
125
+
126
+ override fun removeVisitorAttributes(keys: ReadableArray) {
127
+ val list = keys.toArrayList().mapNotNull { it?.toString() }
128
+ UserLeap.removeVisitorAttributes(list)
129
+ }
130
+
131
+ override fun setVisitorAttributesAndIdentify(
132
+ attributes: ReadableMap,
133
+ userId: String?,
134
+ partnerAnonymousId: String?
135
+ ) {
136
+ UserLeap.setVisitorAttributes(
137
+ stringifyMap(attributes),
138
+ userId,
139
+ partnerAnonymousId
140
+ )
141
+ }
142
+
143
+ override fun presentSurvey() {
144
+ UserLeap.presentSurvey(null)
145
+ }
146
+
147
+ override fun dismissActiveSurvey() {
148
+ UserLeap.dismissActiveSurvey()
149
+ }
150
+
151
+ override fun pauseDisplayingSurveys() {
152
+ UserLeap.pauseDisplayingSurveys()
153
+ }
154
+
155
+ override fun unpauseDisplayingSurveys() {
156
+ UserLeap.unpauseDisplayingSurveys()
157
+ }
158
+
159
+ override fun trackAndPresent(eventName: String) {
160
+ // The SDK requires a FragmentActivity; bail out gracefully if the current activity isn't one.
161
+ val activity = getFragmentActivity()
162
+ if (activity == null) {
163
+ Log.w(TAG, "trackAndPresent('$eventName') skipped: current activity is not a FragmentActivity")
164
+ return
165
+ }
166
+ UserLeap.trackAndPresent(eventName, activity)
167
+ }
168
+
169
+ override fun trackIdentifyAndPresent(
170
+ eventName: String,
171
+ userId: String?,
172
+ partnerAnonymousId: String?
173
+ ) {
174
+ val activity = getFragmentActivity()
175
+ if (activity == null) {
176
+ Log.w(TAG, "trackIdentifyAndPresent('$eventName') skipped: current activity is not a FragmentActivity")
177
+ return
178
+ }
179
+ UserLeap.trackAndPresent(eventName, userId, partnerAnonymousId, activity)
180
+ }
181
+
182
+ override fun overrideUserInterfaceMode(mode: Double) {
183
+ val modeInt = mode.toInt()
184
+ SprigUserInterfaceMode.values().firstOrNull { it.value == modeInt }?.let {
185
+ UserLeap.overrideUserInterfaceMode(it)
186
+ }
187
+ }
188
+
189
+ override fun trackEvent(eventName: String, surveyResultCallback: Callback) {
190
+ trackEventAndIdentify(eventName, null, null, surveyResultCallback)
191
+ }
192
+
193
+ override fun trackEventWithProperties(
194
+ eventName: String,
195
+ userId: String?,
196
+ partnerAnonymousId: String?,
197
+ properties: ReadableMap,
198
+ surveyResultCallback: Callback
199
+ ) {
200
+ val payload = EventPayload(
201
+ eventName,
202
+ userId,
203
+ partnerAnonymousId,
204
+ stringifyMap(properties),
205
+ wrapResultCallback(surveyResultCallback),
206
+ null, // shouldShowSurveyCallback
207
+ null // deprecated callback
208
+ )
209
+ UserLeap.track(payload)
210
+ }
211
+
212
+ override fun trackEventAndIdentify(
213
+ eventName: String,
214
+ userId: String?,
215
+ partnerAnonymousId: String?,
216
+ surveyResultCallback: Callback
217
+ ) {
218
+ val payload = EventPayload(
219
+ eventName,
220
+ userId,
221
+ partnerAnonymousId,
222
+ null, // properties
223
+ wrapResultCallback(surveyResultCallback),
224
+ null, // shouldShowSurveyCallback
225
+ null // deprecated callback
226
+ )
227
+ UserLeap.track(payload)
228
+ }
229
+
230
+ // Callback is `Callback?`: the RN bridge can pass null at runtime, so guard before invoking it.
231
+ override fun track(eventName: String, surveyStateCallback: Callback?) {
232
+ trackAndIdentify(eventName, null, null, surveyStateCallback)
233
+ }
234
+
235
+ override fun trackWithProperties(
236
+ eventName: String,
237
+ userId: String?,
238
+ partnerAnonymousId: String?,
239
+ properties: ReadableMap,
240
+ surveyStateCallback: Callback?
241
+ ) {
242
+ UserLeap.track(
243
+ eventName,
244
+ userId,
245
+ partnerAnonymousId,
246
+ stringifyMap(properties)
247
+ ) { state: SurveyState -> surveyStateCallback?.invoke(state.name) }
248
+ }
249
+
250
+ override fun trackAndIdentify(
251
+ eventName: String,
252
+ userId: String?,
253
+ partnerAnonymousId: String?,
254
+ surveyStateCallback: Callback?
255
+ ) {
256
+ UserLeap.track(eventName, userId, partnerAnonymousId) {
257
+ state: SurveyState -> surveyStateCallback?.invoke(state.name)
258
+ }
259
+ }
260
+
261
+ override fun getSupportedEventNames(): com.facebook.react.bridge.WritableArray {
262
+ val array = Arguments.createArray()
263
+ for (eventName in EventName.values()) {
264
+ if (eventName == EventName.REPLAY_EVENTS_UPLOADED_COMPLETED) continue
265
+ array.pushString(toCamelCase(eventName.name))
266
+ }
267
+ return array
268
+ }
269
+
270
+ // Caller must hold listenerLock.
271
+ private fun registerAllNativeListeners() {
272
+ if (!alive) return
273
+ for (eventName in EventName.values()) {
274
+ // Skip the deprecated alias - it would double-fire alongside REPLAY_EVENTS_UPLOAD_COMPLETED.
275
+ if (eventName == EventName.REPLAY_EVENTS_UPLOADED_COMPLETED) continue
276
+ if (activeListeners.containsKey(eventName)) continue
277
+
278
+ val listener = EventListener { event -> emitEventToJS(event) }
279
+ activeListeners[eventName] = listener
280
+ try {
281
+ UserLeap.addEventListener(eventName, listener)
282
+ } catch (t: Throwable) {
283
+ Log.w(TAG, "Failed to register native listener for $eventName", t)
284
+ activeListeners.remove(eventName)
285
+ }
286
+ }
287
+ }
288
+
289
+ // Caller must hold listenerLock.
290
+ private fun unregisterAllNativeListeners() {
291
+ for ((eventName, listener) in activeListeners) {
292
+ try {
293
+ UserLeap.removeEventListener(eventName, listener)
294
+ } catch (t: Throwable) {
295
+ Log.w(TAG, "Failed to remove native listener for $eventName", t)
296
+ }
297
+ }
298
+ activeListeners.clear()
299
+ }
300
+
301
+ private fun emitEventToJS(event: SprigEvent) {
302
+ if (!alive) return
303
+
304
+ val jsName = toCamelCase(event.name.name)
305
+ val json = buildEventJson(event)
306
+ val envelope = Arguments.createMap().apply {
307
+ putString("name", jsName)
308
+ putString("json", json)
309
+ }
310
+ try {
311
+ emitOnSprigEvent(envelope)
312
+ } catch (t: Throwable) {
313
+ Log.w(TAG, "Failed to emit '$jsName' to JS", t)
314
+ }
315
+ }
316
+
317
+ private fun buildEventJson(event: SprigEvent): String {
318
+ return try {
319
+ if (event.name == EventName.LOGGING_EVENT) {
320
+ JSONObject()
321
+ .put("message", event.logMessage ?: "")
322
+ .put("level", event.logLevel.toString().lowercase())
323
+ .toString()
324
+ } else {
325
+ event.data?.toString() ?: "{}"
326
+ }
327
+ } catch (t: Throwable) {
328
+ Log.w(TAG, "Failed to build JSON payload for ${event.name}", t)
329
+ "{}"
330
+ }
331
+ }
332
+
333
+ companion object {
334
+ private const val TAG = "UserLeapModule"
335
+ }
336
+ }
package/index.d.ts CHANGED
@@ -11,6 +11,84 @@ declare namespace UserLeap {
11
11
  dark: number;
12
12
  };
13
13
 
14
+ /** Lifecycle event names for {@link addEventListener}. */
15
+ const SprigEventName: {
16
+ sdkReady: "sdkReady";
17
+ visitorIdUpdated: "visitorIdUpdated";
18
+ surveyHeight: "surveyHeight";
19
+ surveyStateReturned: "surveyStateReturned";
20
+ surveyWillPresent: "surveyWillPresent";
21
+ surveyPresented: "surveyPresented";
22
+ surveyAppeared: "surveyAppeared";
23
+ questionAnswered: "questionAnswered";
24
+ surveyCloseRequested: "surveyCloseRequested";
25
+ surveyWillClose: "surveyWillClose";
26
+ surveyClosed: "surveyClosed";
27
+ surveyCompleted: "surveyCompleted";
28
+ replayCapture: "replayCapture";
29
+ replayCaptureStarted: "replayCaptureStarted";
30
+ replayCaptureStopped: "replayCaptureStopped";
31
+ replayCaptureCompleted: "replayCaptureCompleted";
32
+ replayRenderingCompleted: "replayRenderingCompleted";
33
+ replayUploadCompleted: "replayUploadCompleted";
34
+ replayEventsUploadCompleted: "replayEventsUploadCompleted";
35
+ loggingEvent: "loggingEvent";
36
+ };
37
+
38
+ type SprigEventNameValue =
39
+ | "sdkReady"
40
+ | "visitorIdUpdated"
41
+ | "surveyHeight"
42
+ | "surveyStateReturned"
43
+ | "surveyWillPresent"
44
+ | "surveyPresented"
45
+ | "surveyAppeared"
46
+ | "questionAnswered"
47
+ | "surveyCloseRequested"
48
+ | "surveyWillClose"
49
+ | "surveyClosed"
50
+ | "surveyCompleted"
51
+ | "replayCapture"
52
+ | "replayCaptureStarted"
53
+ | "replayCaptureStopped"
54
+ | "replayCaptureCompleted"
55
+ | "replayRenderingCompleted"
56
+ | "replayUploadCompleted"
57
+ | "replayEventsUploadCompleted"
58
+ | "loggingEvent";
59
+
60
+ interface SprigEventSubscription {
61
+ remove(): void;
62
+ }
63
+
64
+ /**
65
+ * Severity tag on an emitted `loggingEvent` (read-only — host apps receive it,
66
+ * they don't set it). Android emits the real level; iOS has no log-level
67
+ * concept and always reports "info".
68
+ */
69
+ type SprigLogLevel = "info" | "debug" | "warning" | "error" | "critical";
70
+
71
+ /** Canonical payload for `loggingEvent`. */
72
+ interface SprigLoggingEventData {
73
+ type: "loggingEvent";
74
+ message: string;
75
+ level: SprigLogLevel;
76
+ }
77
+
78
+ interface SprigEventData {
79
+ /** Echoes the event name (e.g. "surveyPresented"). */
80
+ type?: string;
81
+ /** Survey identifier when the event is survey-scoped. */
82
+ surveyId?: number;
83
+ /** Survey state when the event carries one (e.g. surveyStateReturned). */
84
+ surveyState?: string;
85
+ /** Log message text on `loggingEvent`. */
86
+ message?: string;
87
+ /** Log level on `loggingEvent`. */
88
+ level?: SprigLogLevel;
89
+ [key: string]: any;
90
+ }
91
+
14
92
  function overrideUserInterfaceMode(mode: SprigUserInterfaceMode): void;
15
93
  function visitorIdentifier(): number;
16
94
  function visitorIdentifierString(): string;
@@ -60,5 +138,17 @@ declare namespace UserLeap {
60
138
  function unpauseDisplayingSurveys(): void;
61
139
  function trackAndPresent(event: string): void;
62
140
  function trackIdentifyAndPresent(event: string, userId: string | undefined, partnerAnonymousId: string | undefined): void;
141
+
142
+ /** Subscribe to a native SDK lifecycle event; `.remove()` unsubscribes only this listener. */
143
+ function addEventListener(
144
+ eventName: SprigEventNameValue,
145
+ callback: (data: SprigEventData) => void
146
+ ): SprigEventSubscription;
147
+
148
+ /** Remove every listener for the given event name, or all listeners if no name is given. */
149
+ function removeAllEventListeners(eventName?: SprigEventNameValue): void;
150
+
151
+ /** Lifecycle event names this platform's SDK emits (cached; `[]` when unavailable). */
152
+ function getSupportedEventNames(): SprigEventNameValue[];
63
153
  }
64
154
  export default UserLeap;