@attentive-mobile/attentive-react-native-sdk 2.0.0-beta.5 → 2.0.0-beta.7

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 (29) hide show
  1. package/README.md +117 -11
  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 +370 -140
  6. package/android/src/test/kotlin/com/attentivereactnativesdk/AttentiveNotificationStoreTest.kt +103 -0
  7. package/attentive-react-native-sdk.podspec +1 -1
  8. package/ios/AttentiveReactNativeSdk.mm +17 -2
  9. package/ios/AttentiveReactNativeSdk.xcodeproj/project.xcworkspace/xcuserdata/zheref.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
  10. package/ios/Bridging/ATTNNativeSDK.swift +116 -46
  11. package/ios/Bridging/AttentiveSDKManager.swift +196 -27
  12. package/ios/Podfile +1 -1
  13. package/lib/commonjs/NativeAttentiveReactNativeSdk.js +1 -1
  14. package/lib/commonjs/NativeAttentiveReactNativeSdk.js.map +1 -1
  15. package/lib/commonjs/eventTypes.js.map +1 -1
  16. package/lib/commonjs/index.js +50 -17
  17. package/lib/commonjs/index.js.map +1 -1
  18. package/lib/module/NativeAttentiveReactNativeSdk.js +2 -2
  19. package/lib/module/NativeAttentiveReactNativeSdk.js.map +1 -1
  20. package/lib/module/eventTypes.js.map +1 -1
  21. package/lib/module/index.js +50 -18
  22. package/lib/module/index.js.map +1 -1
  23. package/lib/typescript/NativeAttentiveReactNativeSdk.d.ts +12 -1
  24. package/lib/typescript/NativeAttentiveReactNativeSdk.d.ts.map +1 -1
  25. package/lib/typescript/index.d.ts +46 -18
  26. package/lib/typescript/index.d.ts.map +1 -1
  27. package/package.json +3 -2
  28. package/src/NativeAttentiveReactNativeSdk.ts +69 -52
  29. package/src/index.tsx +53 -17
@@ -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,22 +56,45 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
50
56
  return NAME
51
57
  }
52
58
 
59
+ /**
60
+ * TypeScript-facing initialize() — intentionally a no-op on Android.
61
+ *
62
+ * On Android, AttentiveSdk.initialize() MUST be called from your Application.onCreate()
63
+ * (native code) so that lifecycle observers (AppLaunchTracker, etc.) are registered
64
+ * before the React Native bridge is ready. Calling this from TypeScript on Android has
65
+ * no effect and will not initialize the SDK.
66
+ *
67
+ * Required native setup in your Application class:
68
+ * ```kotlin
69
+ * override fun onCreate() {
70
+ * super.onCreate()
71
+ * val config = AttentiveConfig.Builder()
72
+ * .applicationContext(this)
73
+ * .domain("YOUR_ATTENTIVE_DOMAIN")
74
+ * .mode(AttentiveConfig.Mode.PRODUCTION)
75
+ * .build()
76
+ * AttentiveSdk.initialize(config)
77
+ * }
78
+ * ```
79
+ *
80
+ * See the README.md "Android Native Initialization" section for the full guide.
81
+ * All other SDK operations (identify, recordEvent, push) are handled from TypeScript as normal.
82
+ */
53
83
  override fun initialize(
54
84
  attentiveDomain: String,
55
85
  mode: String,
56
86
  skipFatigueOnCreatives: Boolean,
57
87
  enableDebugger: Boolean
58
88
  ) {
59
- // Initialize debug helper
60
89
  debugHelper.initialize(enableDebugger)
61
90
 
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)
91
+ Log.w(
92
+ TAG,
93
+ "[AttentiveSDK] initialize() called from TypeScript is a NO-OP on Android. " +
94
+ "You must call AttentiveSdk.initialize(config) from your Application.onCreate() " +
95
+ "so that lifecycle observers are registered before the React Native bridge is ready. " +
96
+ "See README.md § 'Android Native Initialization' for the required setup."
97
+ )
69
98
  }
70
99
 
71
100
  override fun triggerCreative(creativeId: String?) {
@@ -77,7 +106,7 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
77
106
  currentActivity.window.decorView.rootView as ViewGroup
78
107
  // The following calls edit the view hierarchy so they must run on the UI thread
79
108
  UiThreadUtil.runOnUiThread {
80
- creative = Creative(attentiveConfig, rootView)
109
+ creative = Creative(attentiveConfig!!, rootView, currentActivity)
81
110
  creative?.trigger(null, creativeId)
82
111
  if (debugHelper.isDebuggingEnabled()) {
83
112
  val debugData = mutableMapOf<String, Any>()
@@ -153,15 +182,29 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
153
182
  override fun recordProductViewEvent(items: ReadableArray, deeplink: String?) {
154
183
  Log.i(TAG, "Sending product viewed event")
155
184
 
185
+ val itemsDebugData = if (debugHelper.isDebuggingEnabled()) extractItemsDebugData(items) else emptyList()
186
+
156
187
  val itemsList = buildItems(items)
157
- val productViewEvent = ProductViewEvent.Builder(itemsList).deeplink(deeplink).build()
188
+ val productViewEvent = ProductViewEvent.Builder().items(itemsList).deeplink(deeplink).build()
158
189
 
159
- AttentiveEventTracker.getInstance().recordEvent(productViewEvent)
190
+ try {
191
+ AttentiveSdk.recordEvent(productViewEvent)
192
+ } catch (e: Exception) {
193
+ Log.e(
194
+ TAG,
195
+ "[AttentiveSDK] recordProductViewEvent failed — SDK may not be initialized. " +
196
+ "On Android, call AttentiveSdk.initialize(config) from Application.onCreate() " +
197
+ "before recording events. Error: ${e.message}"
198
+ )
199
+ return
200
+ }
160
201
 
161
202
  if (debugHelper.isDebuggingEnabled()) {
162
203
  val debugData = mutableMapOf<String, Any>()
163
204
  debugData["items_count"] = itemsList.size.toString()
164
205
  debugData["deeplink"] = deeplink ?: ""
206
+ debugData["all_items"] = itemsDebugData
207
+ itemsDebugData.firstOrNull()?.let { debugData["first_item"] = it }
165
208
  debugHelper.showDebugInfo("Product View Event", debugData)
166
209
  }
167
210
  }
@@ -173,19 +216,37 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
173
216
  cartCoupon: String?
174
217
  ) {
175
218
  Log.i(TAG, "Sending purchase event")
176
- val order = Order.Builder(orderId).build()
177
219
 
220
+ val itemsDebugData = if (debugHelper.isDebuggingEnabled()) extractItemsDebugData(items) else emptyList()
221
+
222
+ val order = Order.Builder().orderId(orderId).build()
178
223
  val itemsList = buildItems(items)
179
- val purchaseEvent = PurchaseEvent.Builder(itemsList, order).build()
224
+ val purchaseBuilder = PurchaseEvent.Builder(itemsList, order)
225
+ if (!cartId.isNullOrEmpty()) {
226
+ purchaseBuilder.cart(Cart.Builder().cartId(cartId).cartCoupon(cartCoupon).build())
227
+ }
228
+ val purchaseEvent = purchaseBuilder.build()
180
229
 
181
- AttentiveEventTracker.getInstance().recordEvent(purchaseEvent)
230
+ try {
231
+ AttentiveSdk.recordEvent(purchaseEvent)
232
+ } catch (e: Exception) {
233
+ Log.e(
234
+ TAG,
235
+ "[AttentiveSDK] recordPurchaseEvent failed — SDK may not be initialized. " +
236
+ "On Android, call AttentiveSdk.initialize(config) from Application.onCreate() " +
237
+ "before recording events. Error: ${e.message}"
238
+ )
239
+ return
240
+ }
182
241
 
183
242
  if (debugHelper.isDebuggingEnabled()) {
184
243
  val debugData = mutableMapOf<String, Any>()
185
244
  debugData["items_count"] = itemsList.size.toString()
186
245
  debugData["order_id"] = orderId
187
- if (cartId != null) debugData["cart_id"] = cartId
188
- if (cartCoupon != null) debugData["cart_coupon"] = cartCoupon
246
+ if (!cartId.isNullOrEmpty()) debugData["cart_id"] = cartId!!
247
+ if (!cartCoupon.isNullOrEmpty()) debugData["cart_coupon"] = cartCoupon!!
248
+ debugData["all_items"] = itemsDebugData
249
+ itemsDebugData.firstOrNull()?.let { debugData["first_item"] = it }
189
250
  debugHelper.showDebugInfo("Purchase Event", debugData)
190
251
  }
191
252
  }
@@ -193,15 +254,30 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
193
254
  override fun recordAddToCartEvent(items: ReadableArray, deeplink: String?) {
194
255
  Log.i(TAG, "Sending add to cart event")
195
256
 
257
+ // Extract raw debug data before building items so all bridge fields are preserved
258
+ val itemsDebugData = if (debugHelper.isDebuggingEnabled()) extractItemsDebugData(items) else emptyList()
259
+
196
260
  val itemsList = buildItems(items)
197
- val addToCartEvent = AddToCartEvent.Builder(itemsList).deeplink(deeplink).build()
261
+ val addToCartEvent = AddToCartEvent.Builder().items(itemsList).deeplink(deeplink).build()
198
262
 
199
- AttentiveEventTracker.getInstance().recordEvent(addToCartEvent)
263
+ try {
264
+ AttentiveSdk.recordEvent(addToCartEvent)
265
+ } catch (e: Exception) {
266
+ Log.e(
267
+ TAG,
268
+ "[AttentiveSDK] recordAddToCartEvent failed — SDK may not be initialized. " +
269
+ "On Android, call AttentiveSdk.initialize(config) from Application.onCreate() " +
270
+ "before recording events. Error: ${e.message}"
271
+ )
272
+ return
273
+ }
200
274
 
201
275
  if (debugHelper.isDebuggingEnabled()) {
202
276
  val debugData = mutableMapOf<String, Any>()
203
277
  debugData["items_count"] = itemsList.size.toString()
204
278
  debugData["deeplink"] = deeplink ?: ""
279
+ debugData["all_items"] = itemsDebugData
280
+ itemsDebugData.firstOrNull()?.let { debugData["first_item"] = it }
205
281
  debugHelper.showDebugInfo("Add To Cart Event", debugData)
206
282
  }
207
283
  }
@@ -212,9 +288,19 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
212
288
  throw IllegalArgumentException("The CustomEvent 'properties' field cannot be null.")
213
289
  }
214
290
  val propertiesMap = convertToStringMap(properties.toHashMap())
215
- val customEvent = CustomEvent.Builder(type, propertiesMap).build()
291
+ val customEvent = CustomEvent.Builder().type(type).properties(propertiesMap).build()
216
292
 
217
- AttentiveEventTracker.getInstance().recordEvent(customEvent)
293
+ try {
294
+ AttentiveSdk.recordEvent(customEvent)
295
+ } catch (e: Exception) {
296
+ Log.e(
297
+ TAG,
298
+ "[AttentiveSDK] recordCustomEvent failed — SDK may not be initialized. " +
299
+ "On Android, call AttentiveSdk.initialize(config) from Application.onCreate() " +
300
+ "before recording events. Error: ${e.message}"
301
+ )
302
+ return
303
+ }
218
304
 
219
305
  if (debugHelper.isDebuggingEnabled()) {
220
306
  val debugData = mutableMapOf<String, Any>()
@@ -243,37 +329,67 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
243
329
  //
244
330
  // These methods provide Android push notification support.
245
331
  //
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).
332
+ // Push: Uses AttentiveSdk.getPushTokenWithCallback (SDK 2.1.x) to fetch FCM token and register with Attentive.
333
+ // See: https://github.com/attentive-mobile/attentive-android-sdk/blob/main/README.md
251
334
  // ==========================================================================
252
335
 
253
336
  /**
254
- * Request push notification permission from the user.
337
+ * Request push notification permission and fetch the FCM token via the Attentive SDK.
255
338
  *
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.
339
+ * Uses [AttentiveSdk.getPushTokenWithCallback] so the SDK requests permission (when
340
+ * requestPermission = true) and registers the token with Attentive.
259
341
  */
260
342
  override fun registerForPushNotifications() {
261
343
  Log.i(TAG, "📱 [AttentiveSDK] registerForPushNotifications called (Android)")
262
344
 
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.")
345
+ val application = reactApplicationContext.applicationContext as? Application
346
+ if (application == null) {
347
+ Log.w(TAG, " Application context is null; cannot fetch push token.")
348
+ return
349
+ }
350
+
351
+ AttentiveSdk.getPushTokenWithCallback(application, true, createPushTokenCallback())
352
+ }
353
+
354
+ private fun createPushTokenCallback(): AttentiveSdk.PushTokenCallback =
355
+ object : AttentiveSdk.PushTokenCallback {
356
+ override fun onSuccess(result: TokenFetchResult) {
357
+ UiThreadUtil.runOnUiThread {
358
+ val token = result.token
359
+ Log.i(TAG, "🎫 [AttentiveSDK] Push token fetched successfully (preview): ${token.take(16)}...")
360
+
361
+ // Emit the token to JS so the app can store it (e.g. for display in Settings)
362
+ // and mirror the iOS behavior where APNs delivers the token via a "register" event.
363
+ try {
364
+ reactApplicationContext
365
+ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
366
+ .emit("AttentiveDeviceToken", token)
367
+ Log.i(TAG, "📡 [AttentiveSDK] AttentiveDeviceToken event emitted to JS")
368
+ } catch (e: Exception) {
369
+ Log.w(TAG, "⚠️ [AttentiveSDK] Could not emit AttentiveDeviceToken event: ${e.message}")
370
+ }
371
+
372
+ if (debugHelper.isDebuggingEnabled()) {
373
+ val debugData = mutableMapOf<String, Any>()
374
+ debugData["platform"] = "Android"
375
+ debugData["token_preview"] = "${token.take(16)}..."
376
+ debugData["has_token"] = token.isNotEmpty()
377
+ debugData["permission_granted"] = result.permissionGranted
378
+ debugHelper.showDebugInfo("Push Token (AttentiveSdk)", debugData)
379
+ }
380
+ }
268
381
  }
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)
382
+ override fun onFailure(exception: Exception) {
383
+ UiThreadUtil.runOnUiThread {
384
+ Log.e(TAG, "❌ [AttentiveSDK] getPushTokenWithCallback failed: ${exception.message}", exception)
385
+ if (debugHelper.isDebuggingEnabled()) {
386
+ val debugData = mutableMapOf<String, Any>()
387
+ debugData["error"] = exception.message ?: "Unknown error"
388
+ debugHelper.showDebugInfo("Push Token Error", debugData)
389
+ }
390
+ }
274
391
  }
275
392
  }
276
- }
277
393
 
278
394
  /**
279
395
  * Returns the current push notification authorization status for Android.
@@ -283,7 +399,10 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
283
399
  */
284
400
  override fun getPushAuthorizationStatus(promise: Promise) {
285
401
  try {
286
- val status = AttentivePushHelper.getAuthorizationStatus(reactApplicationContext)
402
+ val status = AttentivePushHelper.getAuthorizationStatus(
403
+ reactApplicationContext,
404
+ reactApplicationContext.currentActivity
405
+ )
287
406
  Log.d(TAG, "getPushAuthorizationStatus: $status")
288
407
  promise.resolve(status)
289
408
  } catch (e: Exception) {
@@ -295,10 +414,13 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
295
414
  /**
296
415
  * Register the device token (FCM token) with the Attentive backend.
297
416
  *
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.
417
+ * The Attentive Android SDK does not expose an API that accepts a token string; it registers
418
+ * by fetching the FCM token from Firebase and sending it to Attentive. When the app passes
419
+ * its own FCM token (e.g. from Firebase Messaging in JS), that token matches the one the SDK
420
+ * will fetch. This method therefore triggers [AttentiveSdk.updatePushPermissionStatus], which
421
+ * fetches the current FCM token and registers it with Attentive, so push targeting works.
300
422
  *
301
- * @param token The FCM registration token from Firebase
423
+ * @param token The FCM registration token from Firebase (used for logging; registration uses SDK fetch)
302
424
  * @param authorizationStatus Push authorization status (used for consistency with iOS)
303
425
  */
304
426
  override fun registerDeviceToken(token: String, authorizationStatus: String) {
@@ -308,22 +430,22 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
308
430
  Log.i(TAG, " Authorization status: $authorizationStatus")
309
431
 
310
432
  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")
433
+ val application = reactApplicationContext.applicationContext
434
+ if (application == null) {
435
+ Log.w(TAG, " Application context is null; cannot register push token with Attentive.")
436
+ return
437
+ }
438
+ // Forward registration to the Attentive SDK. It will fetch the FCM token (same as app-provided
439
+ // token when the app gets it from Firebase) and register it with the Attentive backend.
440
+ AttentiveSdk.updatePushPermissionStatus(application)
441
+ Log.i(TAG, " Attentive SDK updatePushPermissionStatus invoked (token will be fetched and registered)")
319
442
 
320
443
  if (debugHelper.isDebuggingEnabled()) {
321
444
  val debugData = mutableMapOf<String, Any>()
322
445
  debugData["token_preview"] = "${token.take(16)}..."
323
446
  debugData["token_length"] = token.length.toString()
324
447
  debugData["authorization_status"] = authorizationStatus
325
- debugData["sdk_version"] = "1.0.1"
326
- debugData["implementation_status"] = "manual_required"
448
+ debugData["registration_triggered"] = true
327
449
  debugHelper.showDebugInfo("Device Token (Android)", debugData)
328
450
  }
329
451
  } catch (e: Exception) {
@@ -339,15 +461,17 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
339
461
  }
340
462
 
341
463
  /**
342
- * Register the device token with callback for network response tracking.
464
+ * Register the device token with callback for flow consistency with iOS.
343
465
  *
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.
466
+ * Triggers the same registration as [registerDeviceToken] via [AttentiveSdk.updatePushPermissionStatus].
467
+ * The Attentive Android SDK does not expose a registration API with a completion callback, so this
468
+ * callback is invoked after registration has been triggered (token is fetched by the SDK and sent
469
+ * to Attentive). Success here means the registration request was triggered, not that the backend
470
+ * responded successfully.
347
471
  *
348
472
  * @param token The FCM registration token
349
473
  * @param authorizationStatus Push authorization status
350
- * @param callback Callback invoked after registration attempt
474
+ * @param callback Callback invoked after registration has been triggered
351
475
  */
352
476
  override fun registerDeviceTokenWithCallback(
353
477
  token: String,
@@ -359,19 +483,18 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
359
483
  Log.i(TAG, " Authorization status: $authorizationStatus")
360
484
 
361
485
  try {
362
- // Register using the standard method (which logs the token)
486
+ // Trigger real registration (SDK fetches FCM token and registers with Attentive)
363
487
  registerDeviceToken(token, authorizationStatus)
364
488
 
365
- // Invoke callback immediately with success response
489
+ // Callback is invoked after triggering registration; Android SDK does not provide backend result
366
490
  val responseData = mapOf(
367
491
  "success" to true,
368
492
  "token" to "${token.take(16)}...",
369
493
  "platform" to "Android",
370
- "sdk_version" to "1.0.1",
371
- "note" to "Manual push token registration required"
494
+ "sdk_version" to "2.1.1",
495
+ "registration_triggered" to true
372
496
  )
373
497
 
374
- // Invoke callback with: data, url, response, error
375
498
  callback.invoke(
376
499
  responseData, // data
377
500
  null, // url (not available in Android SDK)
@@ -379,7 +502,7 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
379
502
  null // error
380
503
  )
381
504
 
382
- Log.i(TAG, "📥 [AttentiveSDK] Callback invoked with success response")
505
+ Log.i(TAG, "📥 [AttentiveSDK] Callback invoked after registration triggered")
383
506
 
384
507
  if (debugHelper.isDebuggingEnabled()) {
385
508
  val debugData = mutableMapOf<String, Any>()
@@ -416,62 +539,63 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
416
539
  *
417
540
  * @param authorizationStatus Current push authorization status
418
541
  */
419
- override fun handleRegularOpen(authorizationStatus: String) {
542
+ override fun handleRegularOpen(authorizationStatus: String) { // Meant to be NOOP
420
543
  Log.i(TAG, "🌉 [AttentiveSDK] handleRegularOpen called (Android)")
421
544
  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
- }
545
+ // Log.i(TAG, " Tracking regular app open event...")
546
+
547
+ // try {
548
+ // // Attentive Android SDK 1.0.1 doesn't have a built-in handleRegularOpen method
549
+ // // Track app open as custom event
550
+
551
+ // // Option 1: Track as custom event
552
+
553
+ // Log.i(TAG, " Tracking regular open as custom event 'app_open' with properties")
554
+
555
+ // val properties = mapOf(
556
+ // "event_type" to "app_open",
557
+ // "authorization_status" to authorizationStatus,
558
+ // "platform" to "Android"
559
+ // )
560
+
561
+ // try {
562
+ // Log.i(TAG, " Attempting to track custom event for regular app open")
563
+
564
+
565
+ // val customEvent = CustomEvent.Builder()
566
+ // .type("app_open")
567
+ // .properties(properties)
568
+ // .build()
569
+
570
+ // Log.i(TAG, " Custom event built successfully, recording event...")
571
+
572
+ // AttentiveSdk.recordEvent(customEvent)
573
+
574
+ // Log.i(TAG, " [AttentiveSDK] handleRegularOpen completed (tracked as custom event)")
575
+ // Log.i(TAG, " Event sent to Attentive backend")
576
+ // } catch (e: Exception) {
577
+ // Log.w(TAG, "⚠️ [AttentiveSDK] Could not track app open as custom event: ${e.message}")
578
+ // Log.i(TAG, " App open tracking requires manual implementation or SDK upgrade")
579
+ // }
580
+
581
+ // if (debugHelper.isDebuggingEnabled()) {
582
+ // val debugData = mutableMapOf<String, Any>()
583
+ // debugData["authorization_status"] = authorizationStatus
584
+ // debugData["event_type"] = "regular_open"
585
+ // debugData["platform"] = "Android"
586
+ // debugData["sdk_version"] = "2.1.1"
587
+ // debugHelper.showDebugInfo("Regular Open Event", debugData)
588
+ // }
589
+ // } catch (e: Exception) {
590
+ // Log.e(TAG, "❌ [AttentiveSDK] Error in handleRegularOpen: ${e.message}", e)
591
+
592
+ // if (debugHelper.isDebuggingEnabled()) {
593
+ // val debugData = mutableMapOf<String, Any>()
594
+ // debugData["error"] = e.message ?: "Unknown error"
595
+ // debugData["error_type"] = e.javaClass.simpleName
596
+ // debugHelper.showDebugInfo("Regular Open Error", debugData)
597
+ // }
598
+ // }
475
599
  }
476
600
 
477
601
  /**
@@ -505,12 +629,12 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
505
629
  }
506
630
 
507
631
  try {
508
- val customEvent = com.attentive.androidsdk.events.CustomEvent.Builder(
509
- "push_open",
510
- properties
511
- ).build()
632
+ val customEvent = CustomEvent.Builder()
633
+ .type("push_open")
634
+ .properties(properties)
635
+ .build()
512
636
 
513
- AttentiveEventTracker.getInstance().recordEvent(customEvent)
637
+ AttentiveSdk.recordEvent(customEvent)
514
638
 
515
639
  Log.i(TAG, "✅ [AttentiveSDK] handlePushOpen completed (tracked as custom event)")
516
640
  Log.i(TAG, " Push open event sent to Attentive backend")
@@ -525,7 +649,7 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
525
649
  debugData["event_type"] = "push_open"
526
650
  debugData["platform"] = "Android"
527
651
  debugData["payload_keys"] = payload.keys.joinToString(", ")
528
- debugData["sdk_version"] = "1.0.1"
652
+ debugData["sdk_version"] = "2.1.1"
529
653
  debugHelper.showDebugInfo("Push Open Event", debugData)
530
654
  }
531
655
  } catch (e: Exception) {
@@ -571,12 +695,12 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
571
695
  }
572
696
 
573
697
  try {
574
- val customEvent = com.attentive.androidsdk.events.CustomEvent.Builder(
575
- "foreground_push",
576
- properties
577
- ).build()
698
+ val customEvent = CustomEvent.Builder()
699
+ .type("foreground_push")
700
+ .properties(properties)
701
+ .build()
578
702
 
579
- AttentiveEventTracker.getInstance().recordEvent(customEvent)
703
+ AttentiveSdk.recordEvent(customEvent)
580
704
 
581
705
  Log.i(TAG, "✅ [AttentiveSDK] handleForegroundPush completed (tracked as custom event)")
582
706
  Log.i(TAG, " Foreground push event sent to Attentive backend")
@@ -591,7 +715,7 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
591
715
  debugData["event_type"] = "foreground_push"
592
716
  debugData["platform"] = "Android"
593
717
  debugData["payload_keys"] = payload.keys.joinToString(", ")
594
- debugData["sdk_version"] = "1.0.1"
718
+ debugData["sdk_version"] = "2.1.1"
595
719
  debugHelper.showDebugInfo("Foreground Push Event", debugData)
596
720
  }
597
721
  } catch (e: Exception) {
@@ -659,6 +783,89 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
659
783
  handleForegroundPush(userInfo, "authorized")
660
784
  }
661
785
 
786
+ /**
787
+ * Returns the push notification payload that launched the app from a killed state
788
+ * (i.e. the user tapped a notification while the app was not running) and clears it
789
+ * so it is only delivered once.
790
+ *
791
+ * The host app's `MainActivity.onCreate` is responsible for storing the payload in
792
+ * [AttentiveNotificationStore] when a push-tap Intent is detected before the React
793
+ * Native bridge is ready.
794
+ *
795
+ * Returns a [Promise] that resolves to a `ReadableMap` containing the notification data,
796
+ * or `null` if the app was not launched from a push notification tap.
797
+ *
798
+ * @param promise Promise to resolve with the notification payload map or null.
799
+ */
800
+ override fun getInitialPushNotification(promise: Promise) {
801
+ Log.d(TAG, "getInitialPushNotification called!")
802
+
803
+ // try {
804
+ // val payload = AttentiveNotificationStore.getAndClear()
805
+ // if (payload == null) {
806
+ // Log.d(TAG, "getInitialPushNotification: no pending initial notification")
807
+ // promise.resolve(null)
808
+ // return
809
+ // }
810
+ //
811
+ // Log.i(TAG, "getInitialPushNotification: returning stored notification with keys=${payload.keys}")
812
+ // val result = Arguments.createMap()
813
+ // payload.forEach { (key, value) -> result.putString(key, value) }
814
+ // promise.resolve(result)
815
+ // } catch (e: Exception) {
816
+ // Log.e(TAG, "getInitialPushNotification: error — ${e.message}", e)
817
+ // promise.reject("INITIAL_PUSH_ERROR", "Failed to retrieve initial push notification: ${e.message}", e)
818
+ // }
819
+ }
820
+
821
+ // ==========================================================================
822
+ // MARK: - NativeEventEmitter support
823
+ // ==========================================================================
824
+ // These stubs are required by React Native's NativeEventEmitter on the old
825
+ // architecture. Without them the bridge logs a warning about missing methods.
826
+
827
+ @ReactMethod
828
+ fun addListener(eventName: String) {
829
+ // No-op: listener bookkeeping is handled by the JS NativeEventEmitter.
830
+ }
831
+
832
+ @ReactMethod
833
+ fun removeListeners(count: Double) {
834
+ // No-op: listener bookkeeping is handled by the JS NativeEventEmitter.
835
+ }
836
+
837
+ /**
838
+ * Extracts a human-readable list of item maps from the raw bridge array for debug display.
839
+ *
840
+ * This intentionally reads all fields from the original [ReadableArray] rather than from
841
+ * the built [Item] objects so that every field sent from TypeScript (including optional ones)
842
+ * is visible in the debugger — even fields that may be skipped during SDK item construction.
843
+ *
844
+ * @param rawItems The raw item array as received from the React Native bridge.
845
+ * @return A list of maps, one per item, containing all present fields.
846
+ */
847
+ private fun extractItemsDebugData(rawItems: ReadableArray): List<Map<String, Any>> {
848
+ val result = mutableListOf<Map<String, Any>>()
849
+ for (i in 0 until rawItems.size()) {
850
+ val rawItem = rawItems.getMap(i) ?: continue
851
+ val itemData = mutableMapOf<String, Any>()
852
+
853
+ rawItem.getString("productId")?.let { itemData["productId"] = it }
854
+ rawItem.getString("productVariantId")?.let { itemData["productVariantId"] = it }
855
+ rawItem.getString("price")?.let { itemData["price"] = it }
856
+ rawItem.getString("currency")?.let { itemData["currency"] = it }
857
+ rawItem.getString("name")?.let { itemData["name"] = it }
858
+ rawItem.getString("productImage")?.let { itemData["productImage"] = it }
859
+ rawItem.getString("category")?.let { itemData["category"] = it }
860
+ if (rawItem.hasKey("quantity")) {
861
+ itemData["quantity"] = rawItem.getDouble("quantity").toInt()
862
+ }
863
+
864
+ result.add(itemData)
865
+ }
866
+ return result
867
+ }
868
+
662
869
  private fun convertToStringMap(inputMap: Map<String, Any?>): Map<String, String> {
663
870
  val outputMap = mutableMapOf<String, String>()
664
871
  for ((key, value) in inputMap) {
@@ -675,15 +882,38 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
675
882
  private fun buildItems(rawItems: ReadableArray): List<Item> {
676
883
  Log.i(TAG, "buildItems method called with rawItems: $rawItems")
677
884
  val items = mutableListOf<Item>()
885
+
678
886
  for (i in 0 until rawItems.size()) {
679
887
  val rawItem = rawItems.getMap(i) ?: continue
680
888
 
681
- // 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()
889
+ // Required scalar fields
890
+ val priceValue = rawItem.getString("price") ?: continue
891
+ val currencyCode = rawItem.getString("currency") ?: continue
892
+ val productId = rawItem.getString("productId") ?: continue
893
+ val productVariantId = rawItem.getString("productVariantId") ?: continue
894
+
895
+ // Parse price amount — skip item on malformed value rather than crash
896
+ val priceDecimal = try {
897
+ BigDecimal(priceValue)
898
+ } catch (e: NumberFormatException) {
899
+ Log.w(TAG, "buildItems: invalid price value '$priceValue' at index $i — skipping item")
900
+ continue
901
+ }
902
+
903
+ // Parse currency — skip item on unrecognised ISO 4217 code rather than crash
904
+ val currency = try {
905
+ Currency.getInstance(currencyCode)
906
+ } catch (e: IllegalArgumentException) {
907
+ Log.w(TAG, "buildItems: invalid currency code '$currencyCode' at index $i — skipping item")
908
+ continue
909
+ }
910
+
911
+ val price = Price.Builder()
912
+ .price(priceDecimal)
913
+ .currency(currency)
914
+ .build()
685
915
 
686
- val builder = Item.Builder(rawItem.getString("productId"), rawItem.getString("productVariantId"), price)
916
+ val builder = Item.Builder(productId, productVariantId, price)
687
917
 
688
918
  if (rawItem.hasKey("productImage")) {
689
919
  builder.productImage(rawItem.getString("productImage"))
@@ -693,16 +923,16 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
693
923
  builder.name(rawItem.getString("name"))
694
924
  }
695
925
 
926
+ // JS numbers are doubles on the bridge; use getDouble().toInt() to avoid ClassCastException
696
927
  if (rawItem.hasKey("quantity")) {
697
- builder.quantity(rawItem.getInt("quantity"))
928
+ builder.quantity(rawItem.getDouble("quantity").toInt())
698
929
  }
699
930
 
700
931
  if (rawItem.hasKey("category")) {
701
932
  builder.category(rawItem.getString("category"))
702
933
  }
703
934
 
704
- val item = builder.build()
705
- items.add(item)
935
+ items.add(builder.build())
706
936
  }
707
937
 
708
938
  return items