@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.
Files changed (27) hide show
  1. package/README.md +146 -10
  2. package/android/build.gradle +4 -1
  3. package/android/src/main/AndroidManifest.xml +2 -0
  4. package/android/src/main/kotlin/com/attentivereactnativesdk/AttentiveNotificationStore.kt +60 -0
  5. package/android/src/main/kotlin/com/attentivereactnativesdk/AttentivePushHelper.kt +101 -0
  6. package/android/src/main/kotlin/com/attentivereactnativesdk/AttentiveReactNativeSdkModule.kt +192 -134
  7. package/android/src/test/kotlin/com/attentivereactnativesdk/AttentiveNotificationStoreTest.kt +103 -0
  8. package/ios/AttentiveReactNativeSdk.mm +49 -2
  9. package/ios/AttentiveReactNativeSdk.xcodeproj/project.xcworkspace/xcuserdata/zheref.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
  10. package/ios/Bridging/ATTNNativeSDK.swift +35 -28
  11. package/lib/commonjs/NativeAttentiveReactNativeSdk.js +1 -1
  12. package/lib/commonjs/NativeAttentiveReactNativeSdk.js.map +1 -1
  13. package/lib/commonjs/eventTypes.js.map +1 -1
  14. package/lib/commonjs/index.js +64 -10
  15. package/lib/commonjs/index.js.map +1 -1
  16. package/lib/module/NativeAttentiveReactNativeSdk.js +2 -2
  17. package/lib/module/NativeAttentiveReactNativeSdk.js.map +1 -1
  18. package/lib/module/eventTypes.js.map +1 -1
  19. package/lib/module/index.js +64 -12
  20. package/lib/module/index.js.map +1 -1
  21. package/lib/typescript/NativeAttentiveReactNativeSdk.d.ts +21 -2
  22. package/lib/typescript/NativeAttentiveReactNativeSdk.d.ts.map +1 -1
  23. package/lib/typescript/index.d.ts +55 -10
  24. package/lib/typescript/index.d.ts.map +1 -1
  25. package/package.json +9 -4
  26. package/src/NativeAttentiveReactNativeSdk.ts +79 -53
  27. package/src/index.tsx +65 -11
@@ -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
@@ -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, rootView)
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
- AttentiveEventTracker.getInstance().recordEvent(productViewEvent)
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 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()
179
187
 
180
- AttentiveEventTracker.getInstance().recordEvent(purchaseEvent)
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
- AttentiveEventTracker.getInstance().recordEvent(addToCartEvent)
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, propertiesMap).build()
222
+ val customEvent = CustomEvent.Builder().type(type).properties(propertiesMap).build()
215
223
 
216
- AttentiveEventTracker.getInstance().recordEvent(customEvent)
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
- // 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
250
255
  // ==========================================================================
251
256
 
252
257
  /**
253
- * Request push notification permission from the user.
258
+ * Request push notification permission and fetch the FCM token via the Attentive SDK.
254
259
  *
255
- * Note: For Android 13+ (API 33+), you need to request POST_NOTIFICATIONS permission
256
- * in your app's AndroidManifest.xml and request it at runtime.
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
- if (debugHelper.isDebuggingEnabled()) {
268
- val debugData = mutableMapOf<String, Any>()
269
- debugData["platform"] = "Android"
270
- debugData["sdk_version"] = "1.0.1"
271
- debugData["note"] = "Permission handling should be done in host app"
272
- debugHelper.showDebugInfo("Push Registration Requested", debugData)
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
- * This method attempts to register the FCM push token with the Attentive SDK.
280
- * 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.
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
- // Note: Attentive Android SDK 1.0.1 may not have direct push token registration
293
- // For SDK version 2.x, use: AttentiveConfig.setDeviceToken() or similar
294
- // For now, we log the token and make it available for custom implementation
295
-
296
- Log.i(TAG, "⚠️ [AttentiveSDK] Push token registration requires manual implementation")
297
- Log.i(TAG, " FCM token available: ${token.take(16)}...")
298
- Log.i(TAG, " Store this token and register it with Attentive backend manually")
299
- 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)")
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["sdk_version"] = "1.0.1"
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 network response tracking.
385
+ * Register the device token with callback for flow consistency with iOS.
324
386
  *
325
- * Note: The Android SDK version 1.0.1 doesn't provide a callback mechanism for
326
- * push token registration. This method logs the token and invokes the callback
327
- * 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.
328
392
  *
329
393
  * @param token The FCM registration token
330
394
  * @param authorizationStatus Push authorization status
331
- * @param callback Callback invoked after registration attempt
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
- // Register using the standard method (which logs the token)
407
+ // Trigger real registration (SDK fetches FCM token and registers with Attentive)
344
408
  registerDeviceToken(token, authorizationStatus)
345
409
 
346
- // Invoke callback immediately with success response
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.0.1",
352
- "note" to "Manual push token registration required"
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 with success response")
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 = com.attentive.androidsdk.events.CustomEvent.Builder(
483
- "push_open",
484
- properties
485
- ).build()
486
-
487
- AttentiveEventTracker.getInstance().recordEvent(customEvent)
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.0.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 = com.attentive.androidsdk.events.CustomEvent.Builder(
549
- "foreground_push",
550
- properties
551
- ).build()
552
-
553
- AttentiveEventTracker.getInstance().recordEvent(customEvent)
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.0.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(BigDecimal(priceValue), Currency.getInstance(currencyCode)).build()
659
-
660
- 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)
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
+ }