@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.
Files changed (26) hide show
  1. package/README.md +3 -2
  2. package/android/build.gradle +4 -1
  3. package/android/src/main/kotlin/com/attentivereactnativesdk/AttentiveNotificationStore.kt +60 -0
  4. package/android/src/main/kotlin/com/attentivereactnativesdk/AttentivePushHelper.kt +15 -7
  5. package/android/src/main/kotlin/com/attentivereactnativesdk/AttentiveReactNativeSdkModule.kt +168 -136
  6. package/android/src/test/kotlin/com/attentivereactnativesdk/AttentiveNotificationStoreTest.kt +103 -0
  7. package/ios/AttentiveReactNativeSdk.mm +17 -2
  8. package/ios/AttentiveReactNativeSdk.xcodeproj/project.xcworkspace/xcuserdata/zheref.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
  9. package/ios/Bridging/ATTNNativeSDK.swift +35 -28
  10. package/lib/commonjs/NativeAttentiveReactNativeSdk.js +1 -1
  11. package/lib/commonjs/NativeAttentiveReactNativeSdk.js.map +1 -1
  12. package/lib/commonjs/eventTypes.js.map +1 -1
  13. package/lib/commonjs/index.js +36 -1
  14. package/lib/commonjs/index.js.map +1 -1
  15. package/lib/module/NativeAttentiveReactNativeSdk.js +2 -2
  16. package/lib/module/NativeAttentiveReactNativeSdk.js.map +1 -1
  17. package/lib/module/eventTypes.js.map +1 -1
  18. package/lib/module/index.js +36 -2
  19. package/lib/module/index.js.map +1 -1
  20. package/lib/typescript/NativeAttentiveReactNativeSdk.d.ts +12 -1
  21. package/lib/typescript/NativeAttentiveReactNativeSdk.d.ts.map +1 -1
  22. package/lib/typescript/index.d.ts +32 -2
  23. package/lib/typescript/index.d.ts.map +1 -1
  24. package/package.json +3 -2
  25. package/src/NativeAttentiveReactNativeSdk.ts +69 -52
  26. package/src/index.tsx +36 -1
@@ -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.AttentiveEventTracker
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, rootView)
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
- AttentiveEventTracker.getInstance().recordEvent(productViewEvent)
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 purchaseEvent = PurchaseEvent.Builder(itemsList, order).build()
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
- AttentiveEventTracker.getInstance().recordEvent(purchaseEvent)
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
- AttentiveEventTracker.getInstance().recordEvent(addToCartEvent)
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, propertiesMap).build()
222
+ val customEvent = CustomEvent.Builder().type(type).properties(propertiesMap).build()
216
223
 
217
- AttentiveEventTracker.getInstance().recordEvent(customEvent)
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
- // IMPORTANT NOTE: The Attentive Android SDK version 1.0.1 has limited push notification
247
- // support compared to version 2.x. These methods provide logging and debugging infrastructure
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 from the user.
258
+ * Request push notification permission and fetch the FCM token via the Attentive SDK.
255
259
  *
256
- * On Android 13+ (API 33+), requests [android.permission.POST_NOTIFICATIONS] via the
257
- * system dialog. On older versions, no-op (notifications allowed by default).
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
- UiThreadUtil.runOnUiThread {
264
- val activity = reactApplicationContext.currentActivity
265
- val requested = AttentivePushHelper.requestPermissionIfNeeded(activity, PUSH_PERMISSION_REQUEST_CODE)
266
- if (!requested && activity == null) {
267
- Log.w(TAG, " Current activity is null; permission request deferred. Call again when app is in foreground.")
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
- if (debugHelper.isDebuggingEnabled()) {
270
- val debugData = mutableMapOf<String, Any>()
271
- debugData["platform"] = "Android"
272
- debugData["request_started"] = requested
273
- debugHelper.showDebugInfo("Push Registration Requested", debugData)
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(reactApplicationContext)
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
- * This method attempts to register the FCM push token with the Attentive SDK.
299
- * Note: The exact API for push token registration may vary by SDK version.
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
- // Note: Attentive Android SDK 1.0.1 may not have direct push token registration
312
- // For SDK version 2.x, use: AttentiveConfig.setDeviceToken() or similar
313
- // For now, we log the token and make it available for custom implementation
314
-
315
- Log.i(TAG, "⚠️ [AttentiveSDK] Push token registration requires manual implementation")
316
- Log.i(TAG, " FCM token available: ${token.take(16)}...")
317
- Log.i(TAG, " Store this token and register it with Attentive backend manually")
318
- Log.i(TAG, " Or upgrade to Attentive Android SDK 2.x for built-in support")
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["sdk_version"] = "1.0.1"
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 network response tracking.
385
+ * Register the device token with callback for flow consistency with iOS.
343
386
  *
344
- * Note: The Android SDK version 1.0.1 doesn't provide a callback mechanism for
345
- * push token registration. This method logs the token and invokes the callback
346
- * immediately for consistency with the iOS API.
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 attempt
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
- // Register using the standard method (which logs the token)
407
+ // Trigger real registration (SDK fetches FCM token and registers with Attentive)
363
408
  registerDeviceToken(token, authorizationStatus)
364
409
 
365
- // Invoke callback immediately with success response
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.0.1",
371
- "note" to "Manual push token registration required"
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 with success response")
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 = com.attentive.androidsdk.events.CustomEvent.Builder(
509
- "push_open",
510
- properties
511
- ).build()
501
+ val customEvent = CustomEvent.Builder()
502
+ .type("push_open")
503
+ .properties(properties)
504
+ .build()
512
505
 
513
- AttentiveEventTracker.getInstance().recordEvent(customEvent)
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.0.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 = com.attentive.androidsdk.events.CustomEvent.Builder(
575
- "foreground_push",
576
- properties
577
- ).build()
567
+ val customEvent = CustomEvent.Builder()
568
+ .type("foreground_push")
569
+ .properties(properties)
570
+ .build()
578
571
 
579
- AttentiveEventTracker.getInstance().recordEvent(customEvent)
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.0.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(BigDecimal(priceValue), Currency.getInstance(currencyCode)).build()
685
-
686
- val builder = Item.Builder(rawItem.getString("productId"), rawItem.getString("productVariantId"), price)
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