@attentive-mobile/attentive-react-native-sdk 2.0.0-beta.4 → 2.0.0-beta.6
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/README.md +146 -10
- package/android/build.gradle +4 -1
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/kotlin/com/attentivereactnativesdk/AttentiveNotificationStore.kt +60 -0
- package/android/src/main/kotlin/com/attentivereactnativesdk/AttentivePushHelper.kt +101 -0
- package/android/src/main/kotlin/com/attentivereactnativesdk/AttentiveReactNativeSdkModule.kt +192 -134
- package/android/src/test/kotlin/com/attentivereactnativesdk/AttentiveNotificationStoreTest.kt +103 -0
- package/ios/AttentiveReactNativeSdk.mm +49 -2
- package/ios/AttentiveReactNativeSdk.xcodeproj/project.xcworkspace/xcuserdata/zheref.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
- package/ios/Bridging/ATTNNativeSDK.swift +35 -28
- package/lib/commonjs/NativeAttentiveReactNativeSdk.js +1 -1
- package/lib/commonjs/NativeAttentiveReactNativeSdk.js.map +1 -1
- package/lib/commonjs/eventTypes.js.map +1 -1
- package/lib/commonjs/index.js +64 -10
- package/lib/commonjs/index.js.map +1 -1
- package/lib/module/NativeAttentiveReactNativeSdk.js +2 -2
- package/lib/module/NativeAttentiveReactNativeSdk.js.map +1 -1
- package/lib/module/eventTypes.js.map +1 -1
- package/lib/module/index.js +64 -12
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/NativeAttentiveReactNativeSdk.d.ts +21 -2
- package/lib/typescript/NativeAttentiveReactNativeSdk.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +55 -10
- package/lib/typescript/index.d.ts.map +1 -1
- package/package.json +9 -4
- package/src/NativeAttentiveReactNativeSdk.ts +79 -53
- package/src/index.tsx +65 -11
package/android/src/main/kotlin/com/attentivereactnativesdk/AttentiveReactNativeSdkModule.kt
CHANGED
|
@@ -1,21 +1,25 @@
|
|
|
1
1
|
package com.attentivereactnativesdk
|
|
2
2
|
|
|
3
3
|
import android.app.Activity
|
|
4
|
+
import android.app.Application
|
|
4
5
|
import android.util.Log
|
|
5
6
|
import android.view.ViewGroup
|
|
6
7
|
import androidx.annotation.NonNull
|
|
7
8
|
import androidx.annotation.Nullable
|
|
8
9
|
import com.attentive.androidsdk.AttentiveConfig
|
|
9
|
-
import com.attentive.androidsdk.
|
|
10
|
+
import com.attentive.androidsdk.AttentiveSdk
|
|
10
11
|
import com.attentive.androidsdk.UserIdentifiers
|
|
11
12
|
import com.attentive.androidsdk.creatives.Creative
|
|
12
13
|
import com.attentive.androidsdk.events.AddToCartEvent
|
|
14
|
+
import com.attentive.androidsdk.events.Cart
|
|
13
15
|
import com.attentive.androidsdk.events.CustomEvent
|
|
14
16
|
import com.attentive.androidsdk.events.Item
|
|
15
17
|
import com.attentive.androidsdk.events.Order
|
|
16
18
|
import com.attentive.androidsdk.events.Price
|
|
17
19
|
import com.attentive.androidsdk.events.ProductViewEvent
|
|
18
20
|
import com.attentive.androidsdk.events.PurchaseEvent
|
|
21
|
+
import com.attentive.androidsdk.push.TokenFetchResult
|
|
22
|
+
import com.facebook.react.bridge.Arguments
|
|
19
23
|
import com.facebook.react.bridge.ReactApplicationContext
|
|
20
24
|
import com.facebook.react.bridge.ReactMethod
|
|
21
25
|
import com.facebook.react.bridge.ReadableArray
|
|
@@ -23,7 +27,9 @@ import com.facebook.react.bridge.ReadableMap
|
|
|
23
27
|
import com.facebook.react.bridge.UiThreadUtil
|
|
24
28
|
import com.facebook.react.bridge.Promise
|
|
25
29
|
import com.facebook.react.bridge.Callback
|
|
30
|
+
import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
26
31
|
import com.attentivereactnativesdk.debug.AttentiveDebugHelper
|
|
32
|
+
import com.attentive.androidsdk.AttentiveLogLevel
|
|
27
33
|
import java.math.BigDecimal
|
|
28
34
|
import java.security.InvalidParameterException
|
|
29
35
|
import java.util.Currency
|
|
@@ -35,6 +41,7 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
35
41
|
companion object {
|
|
36
42
|
const val NAME = "AttentiveReactNativeSdk"
|
|
37
43
|
private const val TAG = NAME
|
|
44
|
+
private const val PUSH_PERMISSION_REQUEST_CODE = 3901
|
|
38
45
|
}
|
|
39
46
|
|
|
40
47
|
private var attentiveConfig: AttentiveConfig? = null
|
|
@@ -49,6 +56,12 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
49
56
|
return NAME
|
|
50
57
|
}
|
|
51
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Initialize the Attentive SDK. Called only from the TypeScript layer (e.g. Bonni App.tsx);
|
|
61
|
+
* this module must not auto-initialize in Application.onCreate or elsewhere.
|
|
62
|
+
* This is operation is a no-op on Android. Initialization should happen directly through
|
|
63
|
+
* native Kotlin for Android builds to enable subscriptions.
|
|
64
|
+
*/
|
|
52
65
|
override fun initialize(
|
|
53
66
|
attentiveDomain: String,
|
|
54
67
|
mode: String,
|
|
@@ -57,14 +70,6 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
57
70
|
) {
|
|
58
71
|
// Initialize debug helper
|
|
59
72
|
debugHelper.initialize(enableDebugger)
|
|
60
|
-
|
|
61
|
-
attentiveConfig = AttentiveConfig.Builder()
|
|
62
|
-
.context(reactApplicationContext)
|
|
63
|
-
.domain(attentiveDomain)
|
|
64
|
-
.mode(AttentiveConfig.Mode.valueOf(mode.uppercase(Locale.ROOT)))
|
|
65
|
-
.skipFatigueOnCreatives(skipFatigueOnCreatives)
|
|
66
|
-
.build()
|
|
67
|
-
AttentiveEventTracker.getInstance().initialize(attentiveConfig)
|
|
68
73
|
}
|
|
69
74
|
|
|
70
75
|
override fun triggerCreative(creativeId: String?) {
|
|
@@ -76,7 +81,7 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
76
81
|
currentActivity.window.decorView.rootView as ViewGroup
|
|
77
82
|
// The following calls edit the view hierarchy so they must run on the UI thread
|
|
78
83
|
UiThreadUtil.runOnUiThread {
|
|
79
|
-
creative = Creative(attentiveConfig,
|
|
84
|
+
creative = Creative(attentiveConfig!!, rootView, currentActivity)
|
|
80
85
|
creative?.trigger(null, creativeId)
|
|
81
86
|
if (debugHelper.isDebuggingEnabled()) {
|
|
82
87
|
val debugData = mutableMapOf<String, Any>()
|
|
@@ -153,9 +158,9 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
153
158
|
Log.i(TAG, "Sending product viewed event")
|
|
154
159
|
|
|
155
160
|
val itemsList = buildItems(items)
|
|
156
|
-
val productViewEvent = ProductViewEvent.Builder(itemsList).deeplink(deeplink).build()
|
|
161
|
+
val productViewEvent = ProductViewEvent.Builder().items(itemsList).deeplink(deeplink).build()
|
|
157
162
|
|
|
158
|
-
|
|
163
|
+
AttentiveSdk.recordEvent(productViewEvent)
|
|
159
164
|
|
|
160
165
|
if (debugHelper.isDebuggingEnabled()) {
|
|
161
166
|
val debugData = mutableMapOf<String, Any>()
|
|
@@ -172,12 +177,15 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
172
177
|
cartCoupon: String?
|
|
173
178
|
) {
|
|
174
179
|
Log.i(TAG, "Sending purchase event")
|
|
175
|
-
val order = Order.Builder(orderId).build()
|
|
176
|
-
|
|
180
|
+
val order = Order.Builder().orderId(orderId).build()
|
|
177
181
|
val itemsList = buildItems(items)
|
|
178
|
-
val
|
|
182
|
+
val purchaseBuilder = PurchaseEvent.Builder(itemsList, order)
|
|
183
|
+
if (!cartId.isNullOrEmpty()) {
|
|
184
|
+
purchaseBuilder.cart(Cart.Builder().cartId(cartId).cartCoupon(cartCoupon).build())
|
|
185
|
+
}
|
|
186
|
+
val purchaseEvent = purchaseBuilder.build()
|
|
179
187
|
|
|
180
|
-
|
|
188
|
+
AttentiveSdk.recordEvent(purchaseEvent)
|
|
181
189
|
|
|
182
190
|
if (debugHelper.isDebuggingEnabled()) {
|
|
183
191
|
val debugData = mutableMapOf<String, Any>()
|
|
@@ -193,9 +201,9 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
193
201
|
Log.i(TAG, "Sending add to cart event")
|
|
194
202
|
|
|
195
203
|
val itemsList = buildItems(items)
|
|
196
|
-
val addToCartEvent = AddToCartEvent.Builder(itemsList).deeplink(deeplink).build()
|
|
204
|
+
val addToCartEvent = AddToCartEvent.Builder().items(itemsList).deeplink(deeplink).build()
|
|
197
205
|
|
|
198
|
-
|
|
206
|
+
AttentiveSdk.recordEvent(addToCartEvent)
|
|
199
207
|
|
|
200
208
|
if (debugHelper.isDebuggingEnabled()) {
|
|
201
209
|
val debugData = mutableMapOf<String, Any>()
|
|
@@ -211,9 +219,9 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
211
219
|
throw IllegalArgumentException("The CustomEvent 'properties' field cannot be null.")
|
|
212
220
|
}
|
|
213
221
|
val propertiesMap = convertToStringMap(properties.toHashMap())
|
|
214
|
-
val customEvent = CustomEvent.Builder(type
|
|
222
|
+
val customEvent = CustomEvent.Builder().type(type).properties(propertiesMap).build()
|
|
215
223
|
|
|
216
|
-
|
|
224
|
+
AttentiveSdk.recordEvent(customEvent)
|
|
217
225
|
|
|
218
226
|
if (debugHelper.isDebuggingEnabled()) {
|
|
219
227
|
val debugData = mutableMapOf<String, Any>()
|
|
@@ -241,45 +249,99 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
241
249
|
// ==========================================================================
|
|
242
250
|
//
|
|
243
251
|
// These methods provide Android push notification support.
|
|
244
|
-
//
|
|
245
|
-
// IMPORTANT NOTE: The Attentive Android SDK version 1.0.1 has limited push notification
|
|
246
|
-
// support compared to version 2.x. These methods provide logging and debugging infrastructure
|
|
247
|
-
// but may require SDK upgrade or custom implementation for full functionality.
|
|
248
252
|
//
|
|
249
|
-
//
|
|
253
|
+
// Push: Uses AttentiveSdk.getPushTokenWithCallback (SDK 2.1.x) to fetch FCM token and register with Attentive.
|
|
254
|
+
// See: https://github.com/attentive-mobile/attentive-android-sdk/blob/main/README.md
|
|
250
255
|
// ==========================================================================
|
|
251
256
|
|
|
252
257
|
/**
|
|
253
|
-
* Request push notification permission
|
|
258
|
+
* Request push notification permission and fetch the FCM token via the Attentive SDK.
|
|
254
259
|
*
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
* For older versions, permissions are granted at install time.
|
|
258
|
-
*
|
|
259
|
-
* This method is currently a logging placeholder for parity with iOS.
|
|
260
|
-
* Actual permission handling should be done in the host app.
|
|
260
|
+
* Uses [AttentiveSdk.getPushTokenWithCallback] so the SDK requests permission (when
|
|
261
|
+
* requestPermission = true) and registers the token with Attentive.
|
|
261
262
|
*/
|
|
262
263
|
override fun registerForPushNotifications() {
|
|
263
264
|
Log.i(TAG, "📱 [AttentiveSDK] registerForPushNotifications called (Android)")
|
|
264
|
-
Log.i(TAG, " Note: Push notification permissions should be requested in your host app")
|
|
265
|
-
Log.i(TAG, " For Android 13+, request POST_NOTIFICATIONS permission at runtime")
|
|
266
265
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
266
|
+
val application = reactApplicationContext.applicationContext as? Application
|
|
267
|
+
if (application == null) {
|
|
268
|
+
Log.w(TAG, " Application context is null; cannot fetch push token.")
|
|
269
|
+
return
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
AttentiveSdk.getPushTokenWithCallback(application, true, createPushTokenCallback())
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
private fun createPushTokenCallback(): AttentiveSdk.PushTokenCallback =
|
|
276
|
+
object : AttentiveSdk.PushTokenCallback {
|
|
277
|
+
override fun onSuccess(result: TokenFetchResult) {
|
|
278
|
+
UiThreadUtil.runOnUiThread {
|
|
279
|
+
val token = result.token
|
|
280
|
+
Log.i(TAG, "🎫 [AttentiveSDK] Push token fetched successfully (preview): ${token.take(16)}...")
|
|
281
|
+
|
|
282
|
+
// Emit the token to JS so the app can store it (e.g. for display in Settings)
|
|
283
|
+
// and mirror the iOS behavior where APNs delivers the token via a "register" event.
|
|
284
|
+
try {
|
|
285
|
+
reactApplicationContext
|
|
286
|
+
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
287
|
+
.emit("AttentiveDeviceToken", token)
|
|
288
|
+
Log.i(TAG, "📡 [AttentiveSDK] AttentiveDeviceToken event emitted to JS")
|
|
289
|
+
} catch (e: Exception) {
|
|
290
|
+
Log.w(TAG, "⚠️ [AttentiveSDK] Could not emit AttentiveDeviceToken event: ${e.message}")
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (debugHelper.isDebuggingEnabled()) {
|
|
294
|
+
val debugData = mutableMapOf<String, Any>()
|
|
295
|
+
debugData["platform"] = "Android"
|
|
296
|
+
debugData["token_preview"] = "${token.take(16)}..."
|
|
297
|
+
debugData["has_token"] = token.isNotEmpty()
|
|
298
|
+
debugData["permission_granted"] = result.permissionGranted
|
|
299
|
+
debugHelper.showDebugInfo("Push Token (AttentiveSdk)", debugData)
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
override fun onFailure(exception: Exception) {
|
|
304
|
+
UiThreadUtil.runOnUiThread {
|
|
305
|
+
Log.e(TAG, "❌ [AttentiveSDK] getPushTokenWithCallback failed: ${exception.message}", exception)
|
|
306
|
+
if (debugHelper.isDebuggingEnabled()) {
|
|
307
|
+
val debugData = mutableMapOf<String, Any>()
|
|
308
|
+
debugData["error"] = exception.message ?: "Unknown error"
|
|
309
|
+
debugHelper.showDebugInfo("Push Token Error", debugData)
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Returns the current push notification authorization status for Android.
|
|
317
|
+
*
|
|
318
|
+
* On API 33+: returns "authorized" if POST_NOTIFICATIONS is granted, else "notDetermined".
|
|
319
|
+
* On API < 33: returns "authorized" (no runtime permission required).
|
|
320
|
+
*/
|
|
321
|
+
override fun getPushAuthorizationStatus(promise: Promise) {
|
|
322
|
+
try {
|
|
323
|
+
val status = AttentivePushHelper.getAuthorizationStatus(
|
|
324
|
+
reactApplicationContext,
|
|
325
|
+
reactApplicationContext.currentActivity
|
|
326
|
+
)
|
|
327
|
+
Log.d(TAG, "getPushAuthorizationStatus: $status")
|
|
328
|
+
promise.resolve(status)
|
|
329
|
+
} catch (e: Exception) {
|
|
330
|
+
Log.e(TAG, "getPushAuthorizationStatus error: ${e.message}", e)
|
|
331
|
+
promise.reject("GET_STATUS_ERROR", e.message ?: "Unknown error", e)
|
|
273
332
|
}
|
|
274
333
|
}
|
|
275
334
|
|
|
276
335
|
/**
|
|
277
336
|
* Register the device token (FCM token) with the Attentive backend.
|
|
278
337
|
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
338
|
+
* The Attentive Android SDK does not expose an API that accepts a token string; it registers
|
|
339
|
+
* by fetching the FCM token from Firebase and sending it to Attentive. When the app passes
|
|
340
|
+
* its own FCM token (e.g. from Firebase Messaging in JS), that token matches the one the SDK
|
|
341
|
+
* will fetch. This method therefore triggers [AttentiveSdk.updatePushPermissionStatus], which
|
|
342
|
+
* fetches the current FCM token and registers it with Attentive, so push targeting works.
|
|
281
343
|
*
|
|
282
|
-
* @param token The FCM registration token from Firebase
|
|
344
|
+
* @param token The FCM registration token from Firebase (used for logging; registration uses SDK fetch)
|
|
283
345
|
* @param authorizationStatus Push authorization status (used for consistency with iOS)
|
|
284
346
|
*/
|
|
285
347
|
override fun registerDeviceToken(token: String, authorizationStatus: String) {
|
|
@@ -289,22 +351,22 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
289
351
|
Log.i(TAG, " Authorization status: $authorizationStatus")
|
|
290
352
|
|
|
291
353
|
try {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
354
|
+
val application = reactApplicationContext.applicationContext
|
|
355
|
+
if (application == null) {
|
|
356
|
+
Log.w(TAG, " Application context is null; cannot register push token with Attentive.")
|
|
357
|
+
return
|
|
358
|
+
}
|
|
359
|
+
// Forward registration to the Attentive SDK. It will fetch the FCM token (same as app-provided
|
|
360
|
+
// token when the app gets it from Firebase) and register it with the Attentive backend.
|
|
361
|
+
AttentiveSdk.updatePushPermissionStatus(application)
|
|
362
|
+
Log.i(TAG, " Attentive SDK updatePushPermissionStatus invoked (token will be fetched and registered)")
|
|
300
363
|
|
|
301
364
|
if (debugHelper.isDebuggingEnabled()) {
|
|
302
365
|
val debugData = mutableMapOf<String, Any>()
|
|
303
366
|
debugData["token_preview"] = "${token.take(16)}..."
|
|
304
367
|
debugData["token_length"] = token.length.toString()
|
|
305
368
|
debugData["authorization_status"] = authorizationStatus
|
|
306
|
-
debugData["
|
|
307
|
-
debugData["implementation_status"] = "manual_required"
|
|
369
|
+
debugData["registration_triggered"] = true
|
|
308
370
|
debugHelper.showDebugInfo("Device Token (Android)", debugData)
|
|
309
371
|
}
|
|
310
372
|
} catch (e: Exception) {
|
|
@@ -320,15 +382,17 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
320
382
|
}
|
|
321
383
|
|
|
322
384
|
/**
|
|
323
|
-
* Register the device token with callback for
|
|
385
|
+
* Register the device token with callback for flow consistency with iOS.
|
|
324
386
|
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
387
|
+
* Triggers the same registration as [registerDeviceToken] via [AttentiveSdk.updatePushPermissionStatus].
|
|
388
|
+
* The Attentive Android SDK does not expose a registration API with a completion callback, so this
|
|
389
|
+
* callback is invoked after registration has been triggered (token is fetched by the SDK and sent
|
|
390
|
+
* to Attentive). Success here means the registration request was triggered, not that the backend
|
|
391
|
+
* responded successfully.
|
|
328
392
|
*
|
|
329
393
|
* @param token The FCM registration token
|
|
330
394
|
* @param authorizationStatus Push authorization status
|
|
331
|
-
* @param callback Callback invoked after registration
|
|
395
|
+
* @param callback Callback invoked after registration has been triggered
|
|
332
396
|
*/
|
|
333
397
|
override fun registerDeviceTokenWithCallback(
|
|
334
398
|
token: String,
|
|
@@ -340,19 +404,18 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
340
404
|
Log.i(TAG, " Authorization status: $authorizationStatus")
|
|
341
405
|
|
|
342
406
|
try {
|
|
343
|
-
//
|
|
407
|
+
// Trigger real registration (SDK fetches FCM token and registers with Attentive)
|
|
344
408
|
registerDeviceToken(token, authorizationStatus)
|
|
345
409
|
|
|
346
|
-
//
|
|
410
|
+
// Callback is invoked after triggering registration; Android SDK does not provide backend result
|
|
347
411
|
val responseData = mapOf(
|
|
348
412
|
"success" to true,
|
|
349
413
|
"token" to "${token.take(16)}...",
|
|
350
414
|
"platform" to "Android",
|
|
351
|
-
"sdk_version" to "1.
|
|
352
|
-
"
|
|
415
|
+
"sdk_version" to "2.1.1",
|
|
416
|
+
"registration_triggered" to true
|
|
353
417
|
)
|
|
354
418
|
|
|
355
|
-
// Invoke callback with: data, url, response, error
|
|
356
419
|
callback.invoke(
|
|
357
420
|
responseData, // data
|
|
358
421
|
null, // url (not available in Android SDK)
|
|
@@ -360,7 +423,7 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
360
423
|
null // error
|
|
361
424
|
)
|
|
362
425
|
|
|
363
|
-
Log.i(TAG, "📥 [AttentiveSDK] Callback invoked
|
|
426
|
+
Log.i(TAG, "📥 [AttentiveSDK] Callback invoked after registration triggered")
|
|
364
427
|
|
|
365
428
|
if (debugHelper.isDebuggingEnabled()) {
|
|
366
429
|
val debugData = mutableMapOf<String, Any>()
|
|
@@ -394,58 +457,14 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
394
457
|
* Handle regular/direct app open (not from a push notification).
|
|
395
458
|
*
|
|
396
459
|
* This tracks app open events using the Attentive SDK's event tracking system.
|
|
460
|
+
* This operation is iOS only. Android version is handled directly by native SDK upon
|
|
461
|
+
* TypeScript initialization.
|
|
397
462
|
*
|
|
398
463
|
* @param authorizationStatus Current push authorization status
|
|
399
464
|
*/
|
|
400
|
-
override fun handleRegularOpen(authorizationStatus: String) {
|
|
465
|
+
override fun handleRegularOpen(authorizationStatus: String) { // Meant to be NOOP
|
|
401
466
|
Log.i(TAG, "🌉 [AttentiveSDK] handleRegularOpen called (Android)")
|
|
402
467
|
Log.i(TAG, " Authorization status: $authorizationStatus")
|
|
403
|
-
Log.i(TAG, " Tracking regular app open event...")
|
|
404
|
-
|
|
405
|
-
try {
|
|
406
|
-
// Attentive Android SDK 1.0.1 doesn't have a built-in handleRegularOpen method
|
|
407
|
-
// We can track this as a custom event or use AttentiveEventTracker
|
|
408
|
-
|
|
409
|
-
// Option 1: Track as custom event
|
|
410
|
-
val properties = mapOf(
|
|
411
|
-
"event_type" to "app_open",
|
|
412
|
-
"authorization_status" to authorizationStatus,
|
|
413
|
-
"platform" to "Android"
|
|
414
|
-
)
|
|
415
|
-
|
|
416
|
-
try {
|
|
417
|
-
val customEvent = com.attentive.androidsdk.events.CustomEvent.Builder(
|
|
418
|
-
"app_open",
|
|
419
|
-
properties
|
|
420
|
-
).build()
|
|
421
|
-
|
|
422
|
-
AttentiveEventTracker.getInstance().recordEvent(customEvent)
|
|
423
|
-
|
|
424
|
-
Log.i(TAG, "✅ [AttentiveSDK] handleRegularOpen completed (tracked as custom event)")
|
|
425
|
-
Log.i(TAG, " Event sent to Attentive backend")
|
|
426
|
-
} catch (e: Exception) {
|
|
427
|
-
Log.w(TAG, "⚠️ [AttentiveSDK] Could not track app open as custom event: ${e.message}")
|
|
428
|
-
Log.i(TAG, " App open tracking requires manual implementation or SDK upgrade")
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
if (debugHelper.isDebuggingEnabled()) {
|
|
432
|
-
val debugData = mutableMapOf<String, Any>()
|
|
433
|
-
debugData["authorization_status"] = authorizationStatus
|
|
434
|
-
debugData["event_type"] = "regular_open"
|
|
435
|
-
debugData["platform"] = "Android"
|
|
436
|
-
debugData["sdk_version"] = "1.0.1"
|
|
437
|
-
debugHelper.showDebugInfo("Regular Open Event", debugData)
|
|
438
|
-
}
|
|
439
|
-
} catch (e: Exception) {
|
|
440
|
-
Log.e(TAG, "❌ [AttentiveSDK] Error in handleRegularOpen: ${e.message}", e)
|
|
441
|
-
|
|
442
|
-
if (debugHelper.isDebuggingEnabled()) {
|
|
443
|
-
val debugData = mutableMapOf<String, Any>()
|
|
444
|
-
debugData["error"] = e.message ?: "Unknown error"
|
|
445
|
-
debugData["error_type"] = e.javaClass.simpleName
|
|
446
|
-
debugHelper.showDebugInfo("Regular Open Error", debugData)
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
468
|
}
|
|
450
469
|
|
|
451
470
|
/**
|
|
@@ -472,20 +491,20 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
472
491
|
properties["event_type"] = "push_open"
|
|
473
492
|
properties["authorization_status"] = authorizationStatus
|
|
474
493
|
properties["platform"] = "Android"
|
|
475
|
-
|
|
494
|
+
|
|
476
495
|
// Add notification payload to properties (converting to strings)
|
|
477
496
|
payload.forEach { (key, value) ->
|
|
478
497
|
properties["notification_$key"] = value?.toString() ?: "null"
|
|
479
498
|
}
|
|
480
499
|
|
|
481
500
|
try {
|
|
482
|
-
val customEvent =
|
|
483
|
-
"push_open"
|
|
484
|
-
properties
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
501
|
+
val customEvent = CustomEvent.Builder()
|
|
502
|
+
.type("push_open")
|
|
503
|
+
.properties(properties)
|
|
504
|
+
.build()
|
|
505
|
+
|
|
506
|
+
AttentiveSdk.recordEvent(customEvent)
|
|
507
|
+
|
|
489
508
|
Log.i(TAG, "✅ [AttentiveSDK] handlePushOpen completed (tracked as custom event)")
|
|
490
509
|
Log.i(TAG, " Push open event sent to Attentive backend")
|
|
491
510
|
} catch (e: Exception) {
|
|
@@ -499,7 +518,7 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
499
518
|
debugData["event_type"] = "push_open"
|
|
500
519
|
debugData["platform"] = "Android"
|
|
501
520
|
debugData["payload_keys"] = payload.keys.joinToString(", ")
|
|
502
|
-
debugData["sdk_version"] = "1.
|
|
521
|
+
debugData["sdk_version"] = "2.1.1"
|
|
503
522
|
debugHelper.showDebugInfo("Push Open Event", debugData)
|
|
504
523
|
}
|
|
505
524
|
} catch (e: Exception) {
|
|
@@ -538,20 +557,20 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
538
557
|
properties["event_type"] = "foreground_push"
|
|
539
558
|
properties["authorization_status"] = authorizationStatus
|
|
540
559
|
properties["platform"] = "Android"
|
|
541
|
-
|
|
560
|
+
|
|
542
561
|
// Add notification payload to properties (converting to strings)
|
|
543
562
|
payload.forEach { (key, value) ->
|
|
544
563
|
properties["notification_$key"] = value?.toString() ?: "null"
|
|
545
564
|
}
|
|
546
565
|
|
|
547
566
|
try {
|
|
548
|
-
val customEvent =
|
|
549
|
-
"foreground_push"
|
|
550
|
-
properties
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
567
|
+
val customEvent = CustomEvent.Builder()
|
|
568
|
+
.type("foreground_push")
|
|
569
|
+
.properties(properties)
|
|
570
|
+
.build()
|
|
571
|
+
|
|
572
|
+
AttentiveSdk.recordEvent(customEvent)
|
|
573
|
+
|
|
555
574
|
Log.i(TAG, "✅ [AttentiveSDK] handleForegroundPush completed (tracked as custom event)")
|
|
556
575
|
Log.i(TAG, " Foreground push event sent to Attentive backend")
|
|
557
576
|
} catch (e: Exception) {
|
|
@@ -565,7 +584,7 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
565
584
|
debugData["event_type"] = "foreground_push"
|
|
566
585
|
debugData["platform"] = "Android"
|
|
567
586
|
debugData["payload_keys"] = payload.keys.joinToString(", ")
|
|
568
|
-
debugData["sdk_version"] = "1.
|
|
587
|
+
debugData["sdk_version"] = "2.1.1"
|
|
569
588
|
debugHelper.showDebugInfo("Foreground Push Event", debugData)
|
|
570
589
|
}
|
|
571
590
|
} catch (e: Exception) {
|
|
@@ -633,6 +652,40 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
633
652
|
handleForegroundPush(userInfo, "authorized")
|
|
634
653
|
}
|
|
635
654
|
|
|
655
|
+
/**
|
|
656
|
+
* Returns the push notification payload that launched the app from a killed state
|
|
657
|
+
* (i.e. the user tapped a notification while the app was not running) and clears it
|
|
658
|
+
* so it is only delivered once.
|
|
659
|
+
*
|
|
660
|
+
* The host app's `MainActivity.onCreate` is responsible for storing the payload in
|
|
661
|
+
* [AttentiveNotificationStore] when a push-tap Intent is detected before the React
|
|
662
|
+
* Native bridge is ready.
|
|
663
|
+
*
|
|
664
|
+
* Returns a [Promise] that resolves to a `ReadableMap` containing the notification data,
|
|
665
|
+
* or `null` if the app was not launched from a push notification tap.
|
|
666
|
+
*
|
|
667
|
+
* @param promise Promise to resolve with the notification payload map or null.
|
|
668
|
+
*/
|
|
669
|
+
override fun getInitialPushNotification(promise: Promise) {
|
|
670
|
+
Log.d(TAG, "getInitialPushNotification called!")
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// ==========================================================================
|
|
674
|
+
// MARK: - NativeEventEmitter support
|
|
675
|
+
// ==========================================================================
|
|
676
|
+
// These stubs are required by React Native's NativeEventEmitter on the old
|
|
677
|
+
// architecture. Without them the bridge logs a warning about missing methods.
|
|
678
|
+
|
|
679
|
+
@ReactMethod
|
|
680
|
+
fun addListener(eventName: String) {
|
|
681
|
+
// No-op: listener bookkeeping is handled by the JS NativeEventEmitter.
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
@ReactMethod
|
|
685
|
+
fun removeListeners(count: Double) {
|
|
686
|
+
// No-op: listener bookkeeping is handled by the JS NativeEventEmitter.
|
|
687
|
+
}
|
|
688
|
+
|
|
636
689
|
private fun convertToStringMap(inputMap: Map<String, Any?>): Map<String, String> {
|
|
637
690
|
val outputMap = mutableMapOf<String, String>()
|
|
638
691
|
for ((key, value) in inputMap) {
|
|
@@ -653,11 +706,16 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
653
706
|
val rawItem = rawItems.getMap(i) ?: continue
|
|
654
707
|
|
|
655
708
|
// Price and currency are now flattened, not nested
|
|
656
|
-
val priceValue = rawItem.getString("price")
|
|
657
|
-
val currencyCode = rawItem.getString("currency")
|
|
658
|
-
val price = Price.Builder(
|
|
659
|
-
|
|
660
|
-
|
|
709
|
+
val priceValue = rawItem.getString("price") ?: continue
|
|
710
|
+
val currencyCode = rawItem.getString("currency") ?: continue
|
|
711
|
+
val price = Price.Builder()
|
|
712
|
+
.price(BigDecimal(priceValue))
|
|
713
|
+
.currency(Currency.getInstance(currencyCode))
|
|
714
|
+
.build()
|
|
715
|
+
|
|
716
|
+
val productId = rawItem.getString("productId") ?: continue
|
|
717
|
+
val productVariantId = rawItem.getString("productVariantId") ?: continue
|
|
718
|
+
val builder = Item.Builder(productId, productVariantId, price)
|
|
661
719
|
|
|
662
720
|
if (rawItem.hasKey("productImage")) {
|
|
663
721
|
builder.productImage(rawItem.getString("productImage"))
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
package com.attentivereactnativesdk
|
|
2
|
+
|
|
3
|
+
import org.junit.After
|
|
4
|
+
import org.junit.Assert.assertEquals
|
|
5
|
+
import org.junit.Assert.assertNull
|
|
6
|
+
import org.junit.Test
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Unit tests for [AttentiveNotificationStore].
|
|
10
|
+
*
|
|
11
|
+
* Verifies the store's core contract:
|
|
12
|
+
* - Starts empty.
|
|
13
|
+
* - Accepts a payload via [AttentiveNotificationStore.setPendingInitialNotification].
|
|
14
|
+
* - Returns the payload exactly once via [AttentiveNotificationStore.getAndClear].
|
|
15
|
+
* - Clears the store after retrieval (subsequent calls return null).
|
|
16
|
+
* - Replaces any previously stored value when set is called again.
|
|
17
|
+
*/
|
|
18
|
+
class AttentiveNotificationStoreTest {
|
|
19
|
+
|
|
20
|
+
@After
|
|
21
|
+
fun tearDown() {
|
|
22
|
+
// Ensure no state leaks between tests.
|
|
23
|
+
AttentiveNotificationStore.getAndClear()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@Test
|
|
27
|
+
fun `getAndClear returns null when no notification has been set`() {
|
|
28
|
+
val result = AttentiveNotificationStore.getAndClear()
|
|
29
|
+
|
|
30
|
+
assertNull("Expected null when nothing was stored", result)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@Test
|
|
34
|
+
fun `getAndClear returns the payload that was previously set`() {
|
|
35
|
+
val payload = mapOf(
|
|
36
|
+
"google.message_id" to "fcm-123",
|
|
37
|
+
"title" to "Flash Sale",
|
|
38
|
+
"body" to "40% off today only",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
AttentiveNotificationStore.setPendingInitialNotification(payload)
|
|
42
|
+
|
|
43
|
+
val result = AttentiveNotificationStore.getAndClear()
|
|
44
|
+
|
|
45
|
+
assertEquals(payload, result)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@Test
|
|
49
|
+
fun `getAndClear returns null on the second call after the first consumed the payload`() {
|
|
50
|
+
val payload = mapOf("messageId" to "msg-1")
|
|
51
|
+
AttentiveNotificationStore.setPendingInitialNotification(payload)
|
|
52
|
+
|
|
53
|
+
// First retrieval — should return the payload.
|
|
54
|
+
val firstResult = AttentiveNotificationStore.getAndClear()
|
|
55
|
+
assertEquals(payload, firstResult)
|
|
56
|
+
|
|
57
|
+
// Second retrieval — store should be cleared.
|
|
58
|
+
val secondResult = AttentiveNotificationStore.getAndClear()
|
|
59
|
+
assertNull("Store should be empty after first getAndClear", secondResult)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@Test
|
|
63
|
+
fun `setPendingInitialNotification replaces any previously stored payload`() {
|
|
64
|
+
val firstPayload = mapOf("title" to "First Notification")
|
|
65
|
+
val secondPayload = mapOf("title" to "Second Notification", "body" to "Replacement")
|
|
66
|
+
|
|
67
|
+
AttentiveNotificationStore.setPendingInitialNotification(firstPayload)
|
|
68
|
+
AttentiveNotificationStore.setPendingInitialNotification(secondPayload)
|
|
69
|
+
|
|
70
|
+
val result = AttentiveNotificationStore.getAndClear()
|
|
71
|
+
|
|
72
|
+
assertEquals("Expected the second (replacement) payload", secondPayload, result)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@Test
|
|
76
|
+
fun `store handles payloads with all standard FCM fields`() {
|
|
77
|
+
val payload = mapOf(
|
|
78
|
+
"google.message_id" to "0:1234%abc",
|
|
79
|
+
"google.sent_time" to "1700000000000",
|
|
80
|
+
"from" to "123456789",
|
|
81
|
+
"collapse_key" to "com.bonni",
|
|
82
|
+
"title" to "New Order",
|
|
83
|
+
"body" to "Your order has shipped",
|
|
84
|
+
"channelId" to "orders",
|
|
85
|
+
"campaignId" to "camp-999",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
AttentiveNotificationStore.setPendingInitialNotification(payload)
|
|
89
|
+
val result = AttentiveNotificationStore.getAndClear()
|
|
90
|
+
|
|
91
|
+
assertEquals(payload, result)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
@Test
|
|
95
|
+
fun `store handles empty payloads without throwing`() {
|
|
96
|
+
val emptyPayload = emptyMap<String, String>()
|
|
97
|
+
|
|
98
|
+
AttentiveNotificationStore.setPendingInitialNotification(emptyPayload)
|
|
99
|
+
val result = AttentiveNotificationStore.getAndClear()
|
|
100
|
+
|
|
101
|
+
assertEquals(emptyPayload, result)
|
|
102
|
+
}
|
|
103
|
+
}
|