@attentive-mobile/attentive-react-native-sdk 2.0.0-beta.6 → 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.
package/README.md CHANGED
@@ -68,12 +68,59 @@ See [DEBUGGING.md](./DEBUGGING.md) for detailed information about debugging feat
68
68
 
69
69
  ### Initialize the SDK
70
70
 
71
+ > **Platform difference:** iOS and Android have different initialization requirements.
72
+
73
+ #### iOS — Initialize from TypeScript
74
+
75
+ On iOS, call `initialize` from TypeScript as early as possible (e.g. the root `App` component's `useEffect`):
76
+
71
77
  ```typescript
72
- // 'initialize' should be called as soon as possible after the app starts (see the example app for an example of initializing the SDK in the App element)
73
- // Note: 'initialize' should only be called once per app session - if you call it multiple times it will throw an exception
78
+ // Called once per app session, before any other SDK operations.
74
79
  Attentive.initialize(config);
75
80
  ```
76
81
 
82
+ #### Android — Initialize from Native Code
83
+
84
+ On Android, `AttentiveSdk.initialize()` **must** be called from your `Application.onCreate()` in native Kotlin/Java code. This is required so that lifecycle observers (e.g. `AppLaunchTracker`) are registered before the React Native bridge is ready. Calling `initialize()` from TypeScript on Android is a **no-op** — the SDK will not be started and all subsequent event calls will fail.
85
+
86
+ Add the following to your `MainApplication.kt` (or `MainApplication.java`):
87
+
88
+ ```kotlin
89
+ import android.app.Application
90
+ import com.attentive.androidsdk.AttentiveConfig
91
+ import com.attentive.androidsdk.AttentiveSdk
92
+ import com.attentive.androidsdk.AttentiveLogLevel
93
+ import com.facebook.react.bridge.UiThreadUtil
94
+
95
+ class MainApplication : Application(), ReactApplication {
96
+
97
+ override fun onCreate() {
98
+ super.onCreate()
99
+ // ... your existing setup ...
100
+ initAttentiveSDK()
101
+ }
102
+
103
+ private fun initAttentiveSDK() {
104
+ val config = AttentiveConfig.Builder()
105
+ .applicationContext(this)
106
+ .domain("YOUR_ATTENTIVE_DOMAIN")
107
+ .mode(AttentiveConfig.Mode.PRODUCTION) // or Mode.DEBUG for testing
108
+ .skipFatigueOnCreatives(false)
109
+ .logLevel(AttentiveLogLevel.VERBOSE)
110
+ .build()
111
+
112
+ // AttentiveSdk.initialize registers lifecycle observers and must run on the main thread.
113
+ UiThreadUtil.runOnUiThread {
114
+ AttentiveSdk.initialize(config)
115
+ }
116
+ }
117
+ }
118
+ ```
119
+
120
+ After the native initialization, all other SDK operations (`identify`, `recordAddToCartEvent`, `recordPurchaseEvent`, etc.) are called from TypeScript as normal on both platforms. The TypeScript `initialize()` call is still required on iOS but is safely ignored on Android.
121
+
122
+ > **Tip:** If you see `[AttentiveSDK] recordAddToCartEvent failed — SDK may not be initialized` in your Android logcat, it means `AttentiveSdk.initialize()` was not called from native code before the event was recorded. Check your `Application.onCreate()` setup.
123
+
77
124
  ### Destroy the creative
78
125
 
79
126
  ```typescript
@@ -173,6 +220,25 @@ Attentive.identify({phone: '+15556667777'};)
173
220
 
174
221
  The SDK supports push notification integration on both iOS (APNs) and Android (runtime permission + optional FCM). The following sections cover iOS-specific flows and a full **App events on Android** implementation that mirrors the behavior of the [Bonni](https://github.com/attentive-mobile/attentive-react-native-sdk/tree/main/Bonni) example app.
175
222
 
223
+ > **iOS — required setup:** Your AppDelegate **must** forward notification
224
+ > responses to the SDK for push tracking to work. Add this single line to your
225
+ > `userNotificationCenter(_:didReceive:withCompletionHandler:)`:
226
+ >
227
+ > ```swift
228
+ > AttentiveSDKManager.shared.handleNotificationResponse(response)
229
+ > ```
230
+ >
231
+ > Without this, push open and foreground push events **will not be tracked** on
232
+ > iOS. See [iOS AppDelegate Integration](#ios-appdelegate-integration) for full
233
+ > details.
234
+ >
235
+ > **Migrating from an earlier version?** If you previously called
236
+ > `AttentiveSDKManager.shared.handleForegroundPush(response:authorizationStatus:)`
237
+ > or `AttentiveSDKManager.shared.handlePushOpen(response:authorizationStatus:)`
238
+ > directly from your AppDelegate, **replace** that code with the single
239
+ > `handleNotificationResponse` call above. Using both will result in
240
+ > double-tracked events. The old methods are now deprecated.
241
+
176
242
  ---
177
243
 
178
244
  ### App Events on Android
@@ -205,7 +271,7 @@ This section describes how to implement Attentive app events on Android so they
205
271
  </manifest>
206
272
  ```
207
273
 
208
- 2. **Initialize and identify first** – In your app entry (e.g. root component `useEffect`), call `initialize(config)` and `identify(identifiers)` before any push or app-event logic.
274
+ 2. **Initialize and identify first** – The SDK must be initialized natively from `Application.onCreate()` on Android (see [Android Native Initialization](#android--initialize-from-native-code) above). Then, in your app entry (e.g. root component `useEffect`), call `identify(identifiers)` before any push or app-event logic.
209
275
 
210
276
  #### 1. On app launch (Android)
211
277
 
@@ -228,7 +294,13 @@ import {
228
294
  } from 'attentive-react-native-sdk';
229
295
 
230
296
  // Inside your root component (e.g. App.tsx useEffect):
231
- initialize(config);
297
+
298
+ // iOS only: initialize from TypeScript.
299
+ // Android: initialization must be done natively from Application.onCreate() — see README.
300
+ if (Platform.OS === 'ios') {
301
+ initialize(config);
302
+ }
303
+
232
304
  identify({ email: 'user@example.com', clientUserId: 'id-123' });
233
305
 
234
306
  if (Platform.OS === 'android') {
@@ -304,7 +376,7 @@ Get `authorizationStatus` via `getPushAuthorizationStatus()` when handling the e
304
376
 
305
377
  The [Bonni](https://github.com/attentive-mobile/attentive-react-native-sdk/tree/main/Bonni) example app ([App.tsx](https://github.com/attentive-mobile/attentive-react-native-sdk/blob/main/Bonni/App.tsx)) implements the full flow:
306
378
 
307
- 1. **Launch:** `initialize` → `identify` → (Android) `getPushAuthorizationStatus()` → `handleRegularOpen(authStatus)` → `registerForPushNotifications()`.
379
+ 1. **Launch:** Native `AttentiveSdk.initialize(config)` (from `Application.onCreate()`) TypeScript `identify` → `getPushAuthorizationStatus()` → `handleRegularOpen(authStatus)` → `registerForPushNotifications()`.
308
380
  2. **Foreground:** `AppState.addEventListener('change', …)` → when `active` and Android → `getPushAuthorizationStatus()` → `handleRegularOpen(authStatus)`.
309
381
  3. **Optional:** When FCM token is available → `registerDeviceTokenWithCallback(token, authStatus, callback)` → in callback call `handleRegularOpen(authStatus)`.
310
382
  4. **Optional:** When user opens a notification or receives one in foreground → `handlePushOpen` / `handleForegroundPush` with payload and status from `getPushAuthorizationStatus()`.
@@ -373,7 +445,40 @@ For proper push notification integration, your iOS AppDelegate needs to:
373
445
 
374
446
  1. Request notification permissions via the SDK
375
447
  2. Implement `application:didRegisterForRemoteNotificationsWithDeviceToken:` to register the token
376
- 3. Implement `UNUserNotificationCenterDelegate` methods to handle notification events
448
+ 3. **Forward notification responses to the SDK for push-open tracking**
449
+
450
+ ##### Push Open Tracking (Required)
451
+
452
+ Add **one line** to your AppDelegate's `didReceive` handler so the SDK can track
453
+ push opens and foreground push events. Without this, `handlePushOpen()` and
454
+ `handleForegroundPush()` called from JavaScript will not be able to track events
455
+ on iOS (the native SDK requires a `UNNotificationResponse` which cannot cross the
456
+ React Native bridge).
457
+
458
+ ```swift
459
+ // In AppDelegate.swift — UNUserNotificationCenterDelegate
460
+ func userNotificationCenter(
461
+ _ center: UNUserNotificationCenter,
462
+ didReceive response: UNNotificationResponse,
463
+ withCompletionHandler completionHandler: @escaping () -> Void
464
+ ) {
465
+ // Attentive push tracking (handles app-state + auth status automatically)
466
+ AttentiveSDKManager.shared.handleNotificationResponse(response)
467
+
468
+ // Forward to your push library (e.g. RNCPushNotificationIOS) for JS events
469
+ RNCPushNotificationIOS.didReceive(response)
470
+ completionHandler()
471
+ }
472
+ ```
473
+
474
+ `handleNotificationResponse` automatically:
475
+ - Detects whether the app is in the foreground or background
476
+ - Fetches the current authorization status
477
+ - Calls the correct native SDK method (`handlePushOpen` or `handleForegroundPush`)
478
+ - Caches the response so the JS-side `handlePushOpen()` / `handleForegroundPush()` calls
479
+ are fulfilled without double-tracking
480
+ - **Cold-launch safe:** If the user taps a push while the app is killed, the
481
+ response is cached and automatically tracked once the SDK initializes
377
482
 
378
483
  ##### Callback-Based Registration (Recommended)
379
484
 
@@ -390,13 +495,13 @@ func application(
390
495
  ) {
391
496
  UNUserNotificationCenter.current().getNotificationSettings { settings in
392
497
  let authStatus = settings.authorizationStatus
393
-
498
+
394
499
  // Get SDK instance with proper type
395
500
  guard let attentiveSdk = AttentiveSDKManager.shared.sdk as? ATTNNativeSDK else {
396
501
  print("[Attentive] SDK not initialized")
397
502
  return
398
503
  }
399
-
504
+
400
505
  // Register device token with callback
401
506
  attentiveSdk.registerDeviceToken(
402
507
  deviceToken,
@@ -407,7 +512,7 @@ func application(
407
512
  if let error = error {
408
513
  print("[Attentive] Registration failed: \(error.localizedDescription)")
409
514
  }
410
-
515
+
411
516
  // Trigger regular open event after registration
412
517
  attentiveSdk.handleRegularOpen(authorizationStatus: authStatus)
413
518
  }
@@ -85,7 +85,7 @@ dependencies {
85
85
  // Use `api` so that the Attentive Android SDK types (AttentiveSdk, CustomEvent, etc.)
86
86
  // are visible to app-level code (e.g. Bonni's AttentiveFirebaseMessagingService) that
87
87
  // depends on this library and needs to call the SDK directly from native components.
88
- api 'com.attentive:attentive-android-sdk:2.1.3-beta.1'
88
+ api 'com.attentive:attentive-android-sdk:2.1.3'
89
89
  implementation "org.jetbrains.kotlin:kotlin-stdlib:1.9.10"
90
90
  implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.9.10"))
91
91
 
@@ -57,10 +57,28 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
57
57
  }
58
58
 
59
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.
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.
64
82
  */
65
83
  override fun initialize(
66
84
  attentiveDomain: String,
@@ -68,8 +86,15 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
68
86
  skipFatigueOnCreatives: Boolean,
69
87
  enableDebugger: Boolean
70
88
  ) {
71
- // Initialize debug helper
72
89
  debugHelper.initialize(enableDebugger)
90
+
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
+ )
73
98
  }
74
99
 
75
100
  override fun triggerCreative(creativeId: String?) {
@@ -157,15 +182,29 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
157
182
  override fun recordProductViewEvent(items: ReadableArray, deeplink: String?) {
158
183
  Log.i(TAG, "Sending product viewed event")
159
184
 
185
+ val itemsDebugData = if (debugHelper.isDebuggingEnabled()) extractItemsDebugData(items) else emptyList()
186
+
160
187
  val itemsList = buildItems(items)
161
188
  val productViewEvent = ProductViewEvent.Builder().items(itemsList).deeplink(deeplink).build()
162
189
 
163
- AttentiveSdk.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
+ }
164
201
 
165
202
  if (debugHelper.isDebuggingEnabled()) {
166
203
  val debugData = mutableMapOf<String, Any>()
167
204
  debugData["items_count"] = itemsList.size.toString()
168
205
  debugData["deeplink"] = deeplink ?: ""
206
+ debugData["all_items"] = itemsDebugData
207
+ itemsDebugData.firstOrNull()?.let { debugData["first_item"] = it }
169
208
  debugHelper.showDebugInfo("Product View Event", debugData)
170
209
  }
171
210
  }
@@ -177,6 +216,9 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
177
216
  cartCoupon: String?
178
217
  ) {
179
218
  Log.i(TAG, "Sending purchase event")
219
+
220
+ val itemsDebugData = if (debugHelper.isDebuggingEnabled()) extractItemsDebugData(items) else emptyList()
221
+
180
222
  val order = Order.Builder().orderId(orderId).build()
181
223
  val itemsList = buildItems(items)
182
224
  val purchaseBuilder = PurchaseEvent.Builder(itemsList, order)
@@ -185,14 +227,26 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
185
227
  }
186
228
  val purchaseEvent = purchaseBuilder.build()
187
229
 
188
- AttentiveSdk.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
+ }
189
241
 
190
242
  if (debugHelper.isDebuggingEnabled()) {
191
243
  val debugData = mutableMapOf<String, Any>()
192
244
  debugData["items_count"] = itemsList.size.toString()
193
245
  debugData["order_id"] = orderId
194
- if (cartId != null) debugData["cart_id"] = cartId
195
- 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 }
196
250
  debugHelper.showDebugInfo("Purchase Event", debugData)
197
251
  }
198
252
  }
@@ -200,15 +254,30 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
200
254
  override fun recordAddToCartEvent(items: ReadableArray, deeplink: String?) {
201
255
  Log.i(TAG, "Sending add to cart event")
202
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
+
203
260
  val itemsList = buildItems(items)
204
261
  val addToCartEvent = AddToCartEvent.Builder().items(itemsList).deeplink(deeplink).build()
205
262
 
206
- AttentiveSdk.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
+ }
207
274
 
208
275
  if (debugHelper.isDebuggingEnabled()) {
209
276
  val debugData = mutableMapOf<String, Any>()
210
277
  debugData["items_count"] = itemsList.size.toString()
211
278
  debugData["deeplink"] = deeplink ?: ""
279
+ debugData["all_items"] = itemsDebugData
280
+ itemsDebugData.firstOrNull()?.let { debugData["first_item"] = it }
212
281
  debugHelper.showDebugInfo("Add To Cart Event", debugData)
213
282
  }
214
283
  }
@@ -221,7 +290,17 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
221
290
  val propertiesMap = convertToStringMap(properties.toHashMap())
222
291
  val customEvent = CustomEvent.Builder().type(type).properties(propertiesMap).build()
223
292
 
224
- AttentiveSdk.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
+ }
225
304
 
226
305
  if (debugHelper.isDebuggingEnabled()) {
227
306
  val debugData = mutableMapOf<String, Any>()
@@ -457,14 +536,66 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
457
536
  * Handle regular/direct app open (not from a push notification).
458
537
  *
459
538
  * 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.
462
539
  *
463
540
  * @param authorizationStatus Current push authorization status
464
541
  */
465
542
  override fun handleRegularOpen(authorizationStatus: String) { // Meant to be NOOP
466
543
  Log.i(TAG, "🌉 [AttentiveSDK] handleRegularOpen called (Android)")
467
544
  Log.i(TAG, " Authorization status: $authorizationStatus")
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
+ // }
468
599
  }
469
600
 
470
601
  /**
@@ -668,6 +799,23 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
668
799
  */
669
800
  override fun getInitialPushNotification(promise: Promise) {
670
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
+ // }
671
819
  }
672
820
 
673
821
  // ==========================================================================
@@ -686,6 +834,38 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
686
834
  // No-op: listener bookkeeping is handled by the JS NativeEventEmitter.
687
835
  }
688
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
+
689
869
  private fun convertToStringMap(inputMap: Map<String, Any?>): Map<String, String> {
690
870
  val outputMap = mutableMapOf<String, String>()
691
871
  for ((key, value) in inputMap) {
@@ -702,19 +882,37 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
702
882
  private fun buildItems(rawItems: ReadableArray): List<Item> {
703
883
  Log.i(TAG, "buildItems method called with rawItems: $rawItems")
704
884
  val items = mutableListOf<Item>()
885
+
705
886
  for (i in 0 until rawItems.size()) {
706
887
  val rawItem = rawItems.getMap(i) ?: continue
707
888
 
708
- // Price and currency are now flattened, not nested
889
+ // Required scalar fields
709
890
  val priceValue = rawItem.getString("price") ?: continue
710
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
+
711
911
  val price = Price.Builder()
712
- .price(BigDecimal(priceValue))
713
- .currency(Currency.getInstance(currencyCode))
912
+ .price(priceDecimal)
913
+ .currency(currency)
714
914
  .build()
715
915
 
716
- val productId = rawItem.getString("productId") ?: continue
717
- val productVariantId = rawItem.getString("productVariantId") ?: continue
718
916
  val builder = Item.Builder(productId, productVariantId, price)
719
917
 
720
918
  if (rawItem.hasKey("productImage")) {
@@ -725,16 +923,16 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
725
923
  builder.name(rawItem.getString("name"))
726
924
  }
727
925
 
926
+ // JS numbers are doubles on the bridge; use getDouble().toInt() to avoid ClassCastException
728
927
  if (rawItem.hasKey("quantity")) {
729
- builder.quantity(rawItem.getInt("quantity"))
928
+ builder.quantity(rawItem.getDouble("quantity").toInt())
730
929
  }
731
930
 
732
931
  if (rawItem.hasKey("category")) {
733
932
  builder.category(rawItem.getString("category"))
734
933
  }
735
934
 
736
- val item = builder.build()
737
- items.add(item)
935
+ items.add(builder.build())
738
936
  }
739
937
 
740
938
  return items
@@ -16,7 +16,7 @@ Pod::Spec.new do |s|
16
16
 
17
17
  s.source_files = "ios/**/*.{h,m,mm,swift}"
18
18
 
19
- s.dependency 'attentive-ios-sdk', '2.0.8'
19
+ s.dependency 'attentive-ios-sdk', '2.0.13'
20
20
  s.swift_versions = ['5']
21
21
  s.dependency "React-Core"
22
22