@attentive-mobile/attentive-react-native-sdk 2.0.0-beta.5 → 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 +3 -2
- package/android/build.gradle +4 -1
- package/android/src/main/kotlin/com/attentivereactnativesdk/AttentiveNotificationStore.kt +60 -0
- package/android/src/main/kotlin/com/attentivereactnativesdk/AttentivePushHelper.kt +15 -7
- package/android/src/main/kotlin/com/attentivereactnativesdk/AttentiveReactNativeSdkModule.kt +168 -136
- package/android/src/test/kotlin/com/attentivereactnativesdk/AttentiveNotificationStoreTest.kt +103 -0
- package/ios/AttentiveReactNativeSdk.mm +17 -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 +36 -1
- 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 +36 -2
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/NativeAttentiveReactNativeSdk.d.ts +12 -1
- package/lib/typescript/NativeAttentiveReactNativeSdk.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +32 -2
- package/lib/typescript/index.d.ts.map +1 -1
- package/package.json +3 -2
- package/src/NativeAttentiveReactNativeSdk.ts +69 -52
- package/src/index.tsx +36 -1
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
|
|
@@ -50,6 +56,12 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
50
56
|
return NAME
|
|
51
57
|
}
|
|
52
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
|
+
*/
|
|
53
65
|
override fun initialize(
|
|
54
66
|
attentiveDomain: String,
|
|
55
67
|
mode: String,
|
|
@@ -58,14 +70,6 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
58
70
|
) {
|
|
59
71
|
// Initialize debug helper
|
|
60
72
|
debugHelper.initialize(enableDebugger)
|
|
61
|
-
|
|
62
|
-
attentiveConfig = AttentiveConfig.Builder()
|
|
63
|
-
.context(reactApplicationContext)
|
|
64
|
-
.domain(attentiveDomain)
|
|
65
|
-
.mode(AttentiveConfig.Mode.valueOf(mode.uppercase(Locale.ROOT)))
|
|
66
|
-
.skipFatigueOnCreatives(skipFatigueOnCreatives)
|
|
67
|
-
.build()
|
|
68
|
-
AttentiveEventTracker.getInstance().initialize(attentiveConfig)
|
|
69
73
|
}
|
|
70
74
|
|
|
71
75
|
override fun triggerCreative(creativeId: String?) {
|
|
@@ -77,7 +81,7 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
77
81
|
currentActivity.window.decorView.rootView as ViewGroup
|
|
78
82
|
// The following calls edit the view hierarchy so they must run on the UI thread
|
|
79
83
|
UiThreadUtil.runOnUiThread {
|
|
80
|
-
creative = Creative(attentiveConfig,
|
|
84
|
+
creative = Creative(attentiveConfig!!, rootView, currentActivity)
|
|
81
85
|
creative?.trigger(null, creativeId)
|
|
82
86
|
if (debugHelper.isDebuggingEnabled()) {
|
|
83
87
|
val debugData = mutableMapOf<String, Any>()
|
|
@@ -154,9 +158,9 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
154
158
|
Log.i(TAG, "Sending product viewed event")
|
|
155
159
|
|
|
156
160
|
val itemsList = buildItems(items)
|
|
157
|
-
val productViewEvent = ProductViewEvent.Builder(itemsList).deeplink(deeplink).build()
|
|
161
|
+
val productViewEvent = ProductViewEvent.Builder().items(itemsList).deeplink(deeplink).build()
|
|
158
162
|
|
|
159
|
-
|
|
163
|
+
AttentiveSdk.recordEvent(productViewEvent)
|
|
160
164
|
|
|
161
165
|
if (debugHelper.isDebuggingEnabled()) {
|
|
162
166
|
val debugData = mutableMapOf<String, Any>()
|
|
@@ -173,12 +177,15 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
173
177
|
cartCoupon: String?
|
|
174
178
|
) {
|
|
175
179
|
Log.i(TAG, "Sending purchase event")
|
|
176
|
-
val order = Order.Builder(orderId).build()
|
|
177
|
-
|
|
180
|
+
val order = Order.Builder().orderId(orderId).build()
|
|
178
181
|
val itemsList = buildItems(items)
|
|
179
|
-
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()
|
|
180
187
|
|
|
181
|
-
|
|
188
|
+
AttentiveSdk.recordEvent(purchaseEvent)
|
|
182
189
|
|
|
183
190
|
if (debugHelper.isDebuggingEnabled()) {
|
|
184
191
|
val debugData = mutableMapOf<String, Any>()
|
|
@@ -194,9 +201,9 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
194
201
|
Log.i(TAG, "Sending add to cart event")
|
|
195
202
|
|
|
196
203
|
val itemsList = buildItems(items)
|
|
197
|
-
val addToCartEvent = AddToCartEvent.Builder(itemsList).deeplink(deeplink).build()
|
|
204
|
+
val addToCartEvent = AddToCartEvent.Builder().items(itemsList).deeplink(deeplink).build()
|
|
198
205
|
|
|
199
|
-
|
|
206
|
+
AttentiveSdk.recordEvent(addToCartEvent)
|
|
200
207
|
|
|
201
208
|
if (debugHelper.isDebuggingEnabled()) {
|
|
202
209
|
val debugData = mutableMapOf<String, Any>()
|
|
@@ -212,9 +219,9 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
212
219
|
throw IllegalArgumentException("The CustomEvent 'properties' field cannot be null.")
|
|
213
220
|
}
|
|
214
221
|
val propertiesMap = convertToStringMap(properties.toHashMap())
|
|
215
|
-
val customEvent = CustomEvent.Builder(type
|
|
222
|
+
val customEvent = CustomEvent.Builder().type(type).properties(propertiesMap).build()
|
|
216
223
|
|
|
217
|
-
|
|
224
|
+
AttentiveSdk.recordEvent(customEvent)
|
|
218
225
|
|
|
219
226
|
if (debugHelper.isDebuggingEnabled()) {
|
|
220
227
|
val debugData = mutableMapOf<String, Any>()
|
|
@@ -243,37 +250,67 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
243
250
|
//
|
|
244
251
|
// These methods provide Android push notification support.
|
|
245
252
|
//
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
// but may require SDK upgrade or custom implementation for full functionality.
|
|
249
|
-
//
|
|
250
|
-
// The iOS implementation uses APNs; Android uses Firebase Cloud Messaging (FCM).
|
|
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
|
|
251
255
|
// ==========================================================================
|
|
252
256
|
|
|
253
257
|
/**
|
|
254
|
-
* Request push notification permission
|
|
258
|
+
* Request push notification permission and fetch the FCM token via the Attentive SDK.
|
|
255
259
|
*
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
* Uses [AttentivePushHelper] for the actual request.
|
|
260
|
+
* Uses [AttentiveSdk.getPushTokenWithCallback] so the SDK requests permission (when
|
|
261
|
+
* requestPermission = true) and registers the token with Attentive.
|
|
259
262
|
*/
|
|
260
263
|
override fun registerForPushNotifications() {
|
|
261
264
|
Log.i(TAG, "📱 [AttentiveSDK] registerForPushNotifications called (Android)")
|
|
262
265
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
+
}
|
|
268
302
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
+
}
|
|
274
312
|
}
|
|
275
313
|
}
|
|
276
|
-
}
|
|
277
314
|
|
|
278
315
|
/**
|
|
279
316
|
* Returns the current push notification authorization status for Android.
|
|
@@ -283,7 +320,10 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
283
320
|
*/
|
|
284
321
|
override fun getPushAuthorizationStatus(promise: Promise) {
|
|
285
322
|
try {
|
|
286
|
-
val status = AttentivePushHelper.getAuthorizationStatus(
|
|
323
|
+
val status = AttentivePushHelper.getAuthorizationStatus(
|
|
324
|
+
reactApplicationContext,
|
|
325
|
+
reactApplicationContext.currentActivity
|
|
326
|
+
)
|
|
287
327
|
Log.d(TAG, "getPushAuthorizationStatus: $status")
|
|
288
328
|
promise.resolve(status)
|
|
289
329
|
} catch (e: Exception) {
|
|
@@ -295,10 +335,13 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
295
335
|
/**
|
|
296
336
|
* Register the device token (FCM token) with the Attentive backend.
|
|
297
337
|
*
|
|
298
|
-
*
|
|
299
|
-
*
|
|
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.
|
|
300
343
|
*
|
|
301
|
-
* @param token The FCM registration token from Firebase
|
|
344
|
+
* @param token The FCM registration token from Firebase (used for logging; registration uses SDK fetch)
|
|
302
345
|
* @param authorizationStatus Push authorization status (used for consistency with iOS)
|
|
303
346
|
*/
|
|
304
347
|
override fun registerDeviceToken(token: String, authorizationStatus: String) {
|
|
@@ -308,22 +351,22 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
308
351
|
Log.i(TAG, " Authorization status: $authorizationStatus")
|
|
309
352
|
|
|
310
353
|
try {
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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)")
|
|
319
363
|
|
|
320
364
|
if (debugHelper.isDebuggingEnabled()) {
|
|
321
365
|
val debugData = mutableMapOf<String, Any>()
|
|
322
366
|
debugData["token_preview"] = "${token.take(16)}..."
|
|
323
367
|
debugData["token_length"] = token.length.toString()
|
|
324
368
|
debugData["authorization_status"] = authorizationStatus
|
|
325
|
-
debugData["
|
|
326
|
-
debugData["implementation_status"] = "manual_required"
|
|
369
|
+
debugData["registration_triggered"] = true
|
|
327
370
|
debugHelper.showDebugInfo("Device Token (Android)", debugData)
|
|
328
371
|
}
|
|
329
372
|
} catch (e: Exception) {
|
|
@@ -339,15 +382,17 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
339
382
|
}
|
|
340
383
|
|
|
341
384
|
/**
|
|
342
|
-
* Register the device token with callback for
|
|
385
|
+
* Register the device token with callback for flow consistency with iOS.
|
|
343
386
|
*
|
|
344
|
-
*
|
|
345
|
-
*
|
|
346
|
-
*
|
|
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.
|
|
347
392
|
*
|
|
348
393
|
* @param token The FCM registration token
|
|
349
394
|
* @param authorizationStatus Push authorization status
|
|
350
|
-
* @param callback Callback invoked after registration
|
|
395
|
+
* @param callback Callback invoked after registration has been triggered
|
|
351
396
|
*/
|
|
352
397
|
override fun registerDeviceTokenWithCallback(
|
|
353
398
|
token: String,
|
|
@@ -359,19 +404,18 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
359
404
|
Log.i(TAG, " Authorization status: $authorizationStatus")
|
|
360
405
|
|
|
361
406
|
try {
|
|
362
|
-
//
|
|
407
|
+
// Trigger real registration (SDK fetches FCM token and registers with Attentive)
|
|
363
408
|
registerDeviceToken(token, authorizationStatus)
|
|
364
409
|
|
|
365
|
-
//
|
|
410
|
+
// Callback is invoked after triggering registration; Android SDK does not provide backend result
|
|
366
411
|
val responseData = mapOf(
|
|
367
412
|
"success" to true,
|
|
368
413
|
"token" to "${token.take(16)}...",
|
|
369
414
|
"platform" to "Android",
|
|
370
|
-
"sdk_version" to "1.
|
|
371
|
-
"
|
|
415
|
+
"sdk_version" to "2.1.1",
|
|
416
|
+
"registration_triggered" to true
|
|
372
417
|
)
|
|
373
418
|
|
|
374
|
-
// Invoke callback with: data, url, response, error
|
|
375
419
|
callback.invoke(
|
|
376
420
|
responseData, // data
|
|
377
421
|
null, // url (not available in Android SDK)
|
|
@@ -379,7 +423,7 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
379
423
|
null // error
|
|
380
424
|
)
|
|
381
425
|
|
|
382
|
-
Log.i(TAG, "📥 [AttentiveSDK] Callback invoked
|
|
426
|
+
Log.i(TAG, "📥 [AttentiveSDK] Callback invoked after registration triggered")
|
|
383
427
|
|
|
384
428
|
if (debugHelper.isDebuggingEnabled()) {
|
|
385
429
|
val debugData = mutableMapOf<String, Any>()
|
|
@@ -413,65 +457,14 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
413
457
|
* Handle regular/direct app open (not from a push notification).
|
|
414
458
|
*
|
|
415
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.
|
|
416
462
|
*
|
|
417
463
|
* @param authorizationStatus Current push authorization status
|
|
418
464
|
*/
|
|
419
|
-
override fun handleRegularOpen(authorizationStatus: String) {
|
|
465
|
+
override fun handleRegularOpen(authorizationStatus: String) { // Meant to be NOOP
|
|
420
466
|
Log.i(TAG, "🌉 [AttentiveSDK] handleRegularOpen called (Android)")
|
|
421
467
|
Log.i(TAG, " Authorization status: $authorizationStatus")
|
|
422
|
-
Log.i(TAG, " Tracking regular app open event...")
|
|
423
|
-
|
|
424
|
-
try {
|
|
425
|
-
// Attentive Android SDK 1.0.1 doesn't have a built-in handleRegularOpen method
|
|
426
|
-
// We can track this as a custom event or use AttentiveEventTracker
|
|
427
|
-
|
|
428
|
-
// Option 1: Track as custom event
|
|
429
|
-
|
|
430
|
-
Log.i(TAG, " Tracking regular open as custom event 'app_open' with properties")
|
|
431
|
-
|
|
432
|
-
val properties = mapOf(
|
|
433
|
-
"event_type" to "app_open",
|
|
434
|
-
"authorization_status" to authorizationStatus,
|
|
435
|
-
"platform" to "Android"
|
|
436
|
-
)
|
|
437
|
-
|
|
438
|
-
try {
|
|
439
|
-
Log.i(TAG, " Attempting to track custom event for regular app open")
|
|
440
|
-
|
|
441
|
-
val customEvent = com.attentive.androidsdk.events.CustomEvent.Builder(
|
|
442
|
-
"app_open",
|
|
443
|
-
properties
|
|
444
|
-
).build()
|
|
445
|
-
|
|
446
|
-
Log.i(TAG, " Custom event built successfully, recording event...")
|
|
447
|
-
|
|
448
|
-
AttentiveEventTracker.getInstance().recordEvent(customEvent)
|
|
449
|
-
|
|
450
|
-
Log.i(TAG, "✅ [AttentiveSDK] handleRegularOpen completed (tracked as custom event)")
|
|
451
|
-
Log.i(TAG, " Event sent to Attentive backend")
|
|
452
|
-
} catch (e: Exception) {
|
|
453
|
-
Log.w(TAG, "⚠️ [AttentiveSDK] Could not track app open as custom event: ${e.message}")
|
|
454
|
-
Log.i(TAG, " App open tracking requires manual implementation or SDK upgrade")
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
if (debugHelper.isDebuggingEnabled()) {
|
|
458
|
-
val debugData = mutableMapOf<String, Any>()
|
|
459
|
-
debugData["authorization_status"] = authorizationStatus
|
|
460
|
-
debugData["event_type"] = "regular_open"
|
|
461
|
-
debugData["platform"] = "Android"
|
|
462
|
-
debugData["sdk_version"] = "1.0.1"
|
|
463
|
-
debugHelper.showDebugInfo("Regular Open Event", debugData)
|
|
464
|
-
}
|
|
465
|
-
} catch (e: Exception) {
|
|
466
|
-
Log.e(TAG, "❌ [AttentiveSDK] Error in handleRegularOpen: ${e.message}", e)
|
|
467
|
-
|
|
468
|
-
if (debugHelper.isDebuggingEnabled()) {
|
|
469
|
-
val debugData = mutableMapOf<String, Any>()
|
|
470
|
-
debugData["error"] = e.message ?: "Unknown error"
|
|
471
|
-
debugData["error_type"] = e.javaClass.simpleName
|
|
472
|
-
debugHelper.showDebugInfo("Regular Open Error", debugData)
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
468
|
}
|
|
476
469
|
|
|
477
470
|
/**
|
|
@@ -505,12 +498,12 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
505
498
|
}
|
|
506
499
|
|
|
507
500
|
try {
|
|
508
|
-
val customEvent =
|
|
509
|
-
"push_open"
|
|
510
|
-
properties
|
|
511
|
-
|
|
501
|
+
val customEvent = CustomEvent.Builder()
|
|
502
|
+
.type("push_open")
|
|
503
|
+
.properties(properties)
|
|
504
|
+
.build()
|
|
512
505
|
|
|
513
|
-
|
|
506
|
+
AttentiveSdk.recordEvent(customEvent)
|
|
514
507
|
|
|
515
508
|
Log.i(TAG, "✅ [AttentiveSDK] handlePushOpen completed (tracked as custom event)")
|
|
516
509
|
Log.i(TAG, " Push open event sent to Attentive backend")
|
|
@@ -525,7 +518,7 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
525
518
|
debugData["event_type"] = "push_open"
|
|
526
519
|
debugData["platform"] = "Android"
|
|
527
520
|
debugData["payload_keys"] = payload.keys.joinToString(", ")
|
|
528
|
-
debugData["sdk_version"] = "1.
|
|
521
|
+
debugData["sdk_version"] = "2.1.1"
|
|
529
522
|
debugHelper.showDebugInfo("Push Open Event", debugData)
|
|
530
523
|
}
|
|
531
524
|
} catch (e: Exception) {
|
|
@@ -571,12 +564,12 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
571
564
|
}
|
|
572
565
|
|
|
573
566
|
try {
|
|
574
|
-
val customEvent =
|
|
575
|
-
"foreground_push"
|
|
576
|
-
properties
|
|
577
|
-
|
|
567
|
+
val customEvent = CustomEvent.Builder()
|
|
568
|
+
.type("foreground_push")
|
|
569
|
+
.properties(properties)
|
|
570
|
+
.build()
|
|
578
571
|
|
|
579
|
-
|
|
572
|
+
AttentiveSdk.recordEvent(customEvent)
|
|
580
573
|
|
|
581
574
|
Log.i(TAG, "✅ [AttentiveSDK] handleForegroundPush completed (tracked as custom event)")
|
|
582
575
|
Log.i(TAG, " Foreground push event sent to Attentive backend")
|
|
@@ -591,7 +584,7 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
591
584
|
debugData["event_type"] = "foreground_push"
|
|
592
585
|
debugData["platform"] = "Android"
|
|
593
586
|
debugData["payload_keys"] = payload.keys.joinToString(", ")
|
|
594
|
-
debugData["sdk_version"] = "1.
|
|
587
|
+
debugData["sdk_version"] = "2.1.1"
|
|
595
588
|
debugHelper.showDebugInfo("Foreground Push Event", debugData)
|
|
596
589
|
}
|
|
597
590
|
} catch (e: Exception) {
|
|
@@ -659,6 +652,40 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
659
652
|
handleForegroundPush(userInfo, "authorized")
|
|
660
653
|
}
|
|
661
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
|
+
|
|
662
689
|
private fun convertToStringMap(inputMap: Map<String, Any?>): Map<String, String> {
|
|
663
690
|
val outputMap = mutableMapOf<String, String>()
|
|
664
691
|
for ((key, value) in inputMap) {
|
|
@@ -679,11 +706,16 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
|
|
|
679
706
|
val rawItem = rawItems.getMap(i) ?: continue
|
|
680
707
|
|
|
681
708
|
// Price and currency are now flattened, not nested
|
|
682
|
-
val priceValue = rawItem.getString("price")
|
|
683
|
-
val currencyCode = rawItem.getString("currency")
|
|
684
|
-
val price = Price.Builder(
|
|
685
|
-
|
|
686
|
-
|
|
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)
|
|
687
719
|
|
|
688
720
|
if (rawItem.hasKey("productImage")) {
|
|
689
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
|
+
}
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
RCT_EXPORT_MODULE()
|
|
23
23
|
|
|
24
24
|
#ifdef RCT_NEW_ARCH_ENABLED
|
|
25
|
-
// New Architecture implementation with flattened parameters
|
|
25
|
+
// New Architecture implementation with flattened parameters.
|
|
26
|
+
// Initialize is invoked only from TypeScript (e.g. Bonni App); native must not auto-initialize.
|
|
26
27
|
- (void)initialize:(NSString *)attentiveDomain
|
|
27
28
|
mode:(NSString *)mode
|
|
28
29
|
skipFatigueOnCreatives:(BOOL)skipFatigueOnCreatives
|
|
@@ -189,7 +190,8 @@ customIdentifiers:(NSDictionary *)customIdentifiers {
|
|
|
189
190
|
}
|
|
190
191
|
|
|
191
192
|
#else
|
|
192
|
-
// Old Architecture implementation with dictionary parameters
|
|
193
|
+
// Old Architecture implementation with dictionary parameters.
|
|
194
|
+
// Initialize is invoked only from TypeScript (e.g. Bonni App); native must not auto-initialize.
|
|
193
195
|
- (void)initialize:(NSDictionary*)configuration {
|
|
194
196
|
_sdk = [[ATTNNativeSDK alloc] initWithDomain:configuration[@"attentiveDomain"]
|
|
195
197
|
mode:configuration[@"mode"]
|
|
@@ -311,6 +313,19 @@ customIdentifiers:(NSDictionary *)customIdentifiers {
|
|
|
311
313
|
authorizationStatus:(NSString *)authorizationStatus {
|
|
312
314
|
[_sdk handlePushOpenFromRN:userInfo authorizationStatus:authorizationStatus];
|
|
313
315
|
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* iOS stub for getInitialPushNotification.
|
|
319
|
+
*
|
|
320
|
+
* On iOS, the killed-state push-open event is tracked natively in
|
|
321
|
+
* AppDelegate.userNotificationCenter(_:didReceive:withCompletionHandler:) via
|
|
322
|
+
* AttentiveSDKManager.shared, so there is no pending payload to return here.
|
|
323
|
+
* Resolves with nil so callers on both platforms can share the same code path.
|
|
324
|
+
*/
|
|
325
|
+
- (void)getInitialPushNotification:(RCTPromiseResolveBlock)resolve
|
|
326
|
+
reject:(RCTPromiseRejectBlock)reject {
|
|
327
|
+
resolve(nil);
|
|
328
|
+
}
|
|
314
329
|
#endif
|
|
315
330
|
|
|
316
331
|
|