@attentive-mobile/attentive-react-native-sdk 2.0.0-beta.6 → 2.0.0-beta.8

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.
@@ -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.d(
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,19 @@ 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
+ if (!recordEventSafely("recordProductViewEvent") { AttentiveSdk.recordEvent(productViewEvent) }) return
164
191
 
165
192
  if (debugHelper.isDebuggingEnabled()) {
166
193
  val debugData = mutableMapOf<String, Any>()
167
194
  debugData["items_count"] = itemsList.size.toString()
168
195
  debugData["deeplink"] = deeplink ?: ""
196
+ debugData["all_items"] = itemsDebugData
197
+ itemsDebugData.firstOrNull()?.let { debugData["first_item"] = it }
169
198
  debugHelper.showDebugInfo("Product View Event", debugData)
170
199
  }
171
200
  }
@@ -177,6 +206,9 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
177
206
  cartCoupon: String?
178
207
  ) {
179
208
  Log.i(TAG, "Sending purchase event")
209
+
210
+ val itemsDebugData = if (debugHelper.isDebuggingEnabled()) extractItemsDebugData(items) else emptyList()
211
+
180
212
  val order = Order.Builder().orderId(orderId).build()
181
213
  val itemsList = buildItems(items)
182
214
  val purchaseBuilder = PurchaseEvent.Builder(itemsList, order)
@@ -185,14 +217,16 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
185
217
  }
186
218
  val purchaseEvent = purchaseBuilder.build()
187
219
 
188
- AttentiveSdk.recordEvent(purchaseEvent)
220
+ if (!recordEventSafely("recordPurchaseEvent") { AttentiveSdk.recordEvent(purchaseEvent) }) return
189
221
 
190
222
  if (debugHelper.isDebuggingEnabled()) {
191
223
  val debugData = mutableMapOf<String, Any>()
192
224
  debugData["items_count"] = itemsList.size.toString()
193
225
  debugData["order_id"] = orderId
194
- if (cartId != null) debugData["cart_id"] = cartId
195
- if (cartCoupon != null) debugData["cart_coupon"] = cartCoupon
226
+ if (!cartId.isNullOrEmpty()) debugData["cart_id"] = cartId!!
227
+ if (!cartCoupon.isNullOrEmpty()) debugData["cart_coupon"] = cartCoupon!!
228
+ debugData["all_items"] = itemsDebugData
229
+ itemsDebugData.firstOrNull()?.let { debugData["first_item"] = it }
196
230
  debugHelper.showDebugInfo("Purchase Event", debugData)
197
231
  }
198
232
  }
@@ -200,15 +234,20 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
200
234
  override fun recordAddToCartEvent(items: ReadableArray, deeplink: String?) {
201
235
  Log.i(TAG, "Sending add to cart event")
202
236
 
237
+ // Extract raw debug data before building items so all bridge fields are preserved
238
+ val itemsDebugData = if (debugHelper.isDebuggingEnabled()) extractItemsDebugData(items) else emptyList()
239
+
203
240
  val itemsList = buildItems(items)
204
241
  val addToCartEvent = AddToCartEvent.Builder().items(itemsList).deeplink(deeplink).build()
205
242
 
206
- AttentiveSdk.recordEvent(addToCartEvent)
243
+ if (!recordEventSafely("recordAddToCartEvent") { AttentiveSdk.recordEvent(addToCartEvent) }) return
207
244
 
208
245
  if (debugHelper.isDebuggingEnabled()) {
209
246
  val debugData = mutableMapOf<String, Any>()
210
247
  debugData["items_count"] = itemsList.size.toString()
211
248
  debugData["deeplink"] = deeplink ?: ""
249
+ debugData["all_items"] = itemsDebugData
250
+ itemsDebugData.firstOrNull()?.let { debugData["first_item"] = it }
212
251
  debugHelper.showDebugInfo("Add To Cart Event", debugData)
213
252
  }
214
253
  }
@@ -221,7 +260,7 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
221
260
  val propertiesMap = convertToStringMap(properties.toHashMap())
222
261
  val customEvent = CustomEvent.Builder().type(type).properties(propertiesMap).build()
223
262
 
224
- AttentiveSdk.recordEvent(customEvent)
263
+ if (!recordEventSafely("recordCustomEvent") { AttentiveSdk.recordEvent(customEvent) }) return
225
264
 
226
265
  if (debugHelper.isDebuggingEnabled()) {
227
266
  val debugData = mutableMapOf<String, Any>()
@@ -457,14 +496,66 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
457
496
  * Handle regular/direct app open (not from a push notification).
458
497
  *
459
498
  * 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
499
  *
463
500
  * @param authorizationStatus Current push authorization status
464
501
  */
465
502
  override fun handleRegularOpen(authorizationStatus: String) { // Meant to be NOOP
466
503
  Log.i(TAG, "🌉 [AttentiveSDK] handleRegularOpen called (Android)")
467
504
  Log.i(TAG, " Authorization status: $authorizationStatus")
505
+ // Log.i(TAG, " Tracking regular app open event...")
506
+
507
+ // try {
508
+ // // Attentive Android SDK 1.0.1 doesn't have a built-in handleRegularOpen method
509
+ // // Track app open as custom event
510
+
511
+ // // Option 1: Track as custom event
512
+
513
+ // Log.i(TAG, " Tracking regular open as custom event 'app_open' with properties")
514
+
515
+ // val properties = mapOf(
516
+ // "event_type" to "app_open",
517
+ // "authorization_status" to authorizationStatus,
518
+ // "platform" to "Android"
519
+ // )
520
+
521
+ // try {
522
+ // Log.i(TAG, " Attempting to track custom event for regular app open")
523
+
524
+
525
+ // val customEvent = CustomEvent.Builder()
526
+ // .type("app_open")
527
+ // .properties(properties)
528
+ // .build()
529
+
530
+ // Log.i(TAG, " Custom event built successfully, recording event...")
531
+
532
+ // AttentiveSdk.recordEvent(customEvent)
533
+
534
+ // Log.i(TAG, "✅ [AttentiveSDK] handleRegularOpen completed (tracked as custom event)")
535
+ // Log.i(TAG, " Event sent to Attentive backend")
536
+ // } catch (e: Exception) {
537
+ // Log.w(TAG, "⚠️ [AttentiveSDK] Could not track app open as custom event: ${e.message}")
538
+ // Log.i(TAG, " App open tracking requires manual implementation or SDK upgrade")
539
+ // }
540
+
541
+ // if (debugHelper.isDebuggingEnabled()) {
542
+ // val debugData = mutableMapOf<String, Any>()
543
+ // debugData["authorization_status"] = authorizationStatus
544
+ // debugData["event_type"] = "regular_open"
545
+ // debugData["platform"] = "Android"
546
+ // debugData["sdk_version"] = "2.1.1"
547
+ // debugHelper.showDebugInfo("Regular Open Event", debugData)
548
+ // }
549
+ // } catch (e: Exception) {
550
+ // Log.e(TAG, "❌ [AttentiveSDK] Error in handleRegularOpen: ${e.message}", e)
551
+
552
+ // if (debugHelper.isDebuggingEnabled()) {
553
+ // val debugData = mutableMapOf<String, Any>()
554
+ // debugData["error"] = e.message ?: "Unknown error"
555
+ // debugData["error_type"] = e.javaClass.simpleName
556
+ // debugHelper.showDebugInfo("Regular Open Error", debugData)
557
+ // }
558
+ // }
468
559
  }
469
560
 
470
561
  /**
@@ -668,6 +759,23 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
668
759
  */
669
760
  override fun getInitialPushNotification(promise: Promise) {
670
761
  Log.d(TAG, "getInitialPushNotification called!")
762
+
763
+ // try {
764
+ // val payload = AttentiveNotificationStore.getAndClear()
765
+ // if (payload == null) {
766
+ // Log.d(TAG, "getInitialPushNotification: no pending initial notification")
767
+ // promise.resolve(null)
768
+ // return
769
+ // }
770
+ //
771
+ // Log.i(TAG, "getInitialPushNotification: returning stored notification with keys=${payload.keys}")
772
+ // val result = Arguments.createMap()
773
+ // payload.forEach { (key, value) -> result.putString(key, value) }
774
+ // promise.resolve(result)
775
+ // } catch (e: Exception) {
776
+ // Log.e(TAG, "getInitialPushNotification: error — ${e.message}", e)
777
+ // promise.reject("INITIAL_PUSH_ERROR", "Failed to retrieve initial push notification: ${e.message}", e)
778
+ // }
671
779
  }
672
780
 
673
781
  // ==========================================================================
@@ -686,6 +794,68 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
686
794
  // No-op: listener bookkeeping is handled by the JS NativeEventEmitter.
687
795
  }
688
796
 
797
+ /**
798
+ * Extracts a human-readable list of item maps from the raw bridge array for debug display.
799
+ *
800
+ * This intentionally reads all fields from the original [ReadableArray] rather than from
801
+ * the built [Item] objects so that every field sent from TypeScript (including optional ones)
802
+ * is visible in the debugger — even fields that may be skipped during SDK item construction.
803
+ *
804
+ * @param rawItems The raw item array as received from the React Native bridge.
805
+ * @return A list of maps, one per item, containing all present fields.
806
+ */
807
+ /**
808
+ * Executes [block] (which calls [AttentiveSdk.recordEvent]) and catches any [Exception].
809
+ *
810
+ * When an exception is caught the log always includes the exception's simple class name so
811
+ * callers can distinguish an uninitialized-SDK error (typically [IllegalStateException]) from
812
+ * a programming mistake such as [NullPointerException] or [IllegalArgumentException] in event
813
+ * construction — both of which would have been silently misattributed to initialization
814
+ * failure under the previous broad catch-and-blame pattern.
815
+ *
816
+ * @param callerName Method name to include in the log for quick triage (e.g. "recordPurchaseEvent").
817
+ * @param block Lambda that performs the [AttentiveSdk.recordEvent] call.
818
+ * @return `true` if [block] completed without throwing, `false` if an exception was caught.
819
+ */
820
+ private fun recordEventSafely(callerName: String, block: () -> Unit): Boolean {
821
+ return try {
822
+ block()
823
+ true
824
+ } catch (e: Exception) {
825
+ // Include the exception class so the developer can tell apart an uninitialized SDK
826
+ // (IllegalStateException) from a malformed-event bug (NullPointerException, etc.)
827
+ Log.e(
828
+ TAG,
829
+ "[AttentiveSDK] $callerName failed with ${e.javaClass.simpleName}: ${e.message}. " +
830
+ "If the SDK was not initialized via AttentiveSdk.initialize() in Application.onCreate(), " +
831
+ "that is the most likely cause. Otherwise, inspect the exception type above."
832
+ )
833
+ false
834
+ }
835
+ }
836
+
837
+ private fun extractItemsDebugData(rawItems: ReadableArray): List<Map<String, Any>> {
838
+ val result = mutableListOf<Map<String, Any>>()
839
+ for (i in 0 until rawItems.size()) {
840
+ val rawItem = rawItems.getMap(i) ?: continue
841
+ val itemData = mutableMapOf<String, Any>()
842
+
843
+ rawItem.getString("productId")?.let { itemData["productId"] = it }
844
+ rawItem.getString("productVariantId")?.let { itemData["productVariantId"] = it }
845
+ rawItem.getString("price")?.let { itemData["price"] = it }
846
+ rawItem.getString("currency")?.let { itemData["currency"] = it }
847
+ rawItem.getString("name")?.let { itemData["name"] = it }
848
+ rawItem.getString("productImage")?.let { itemData["productImage"] = it }
849
+ rawItem.getString("category")?.let { itemData["category"] = it }
850
+ if (rawItem.hasKey("quantity")) {
851
+ itemData["quantity"] = rawItem.getDouble("quantity").toInt()
852
+ }
853
+
854
+ result.add(itemData)
855
+ }
856
+ return result
857
+ }
858
+
689
859
  private fun convertToStringMap(inputMap: Map<String, Any?>): Map<String, String> {
690
860
  val outputMap = mutableMapOf<String, String>()
691
861
  for ((key, value) in inputMap) {
@@ -702,19 +872,37 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
702
872
  private fun buildItems(rawItems: ReadableArray): List<Item> {
703
873
  Log.i(TAG, "buildItems method called with rawItems: $rawItems")
704
874
  val items = mutableListOf<Item>()
875
+
705
876
  for (i in 0 until rawItems.size()) {
706
877
  val rawItem = rawItems.getMap(i) ?: continue
707
878
 
708
- // Price and currency are now flattened, not nested
879
+ // Required scalar fields
709
880
  val priceValue = rawItem.getString("price") ?: continue
710
881
  val currencyCode = rawItem.getString("currency") ?: continue
882
+ val productId = rawItem.getString("productId") ?: continue
883
+ val productVariantId = rawItem.getString("productVariantId") ?: continue
884
+
885
+ // Parse price amount — skip item on malformed value rather than crash
886
+ val priceDecimal = try {
887
+ BigDecimal(priceValue)
888
+ } catch (e: NumberFormatException) {
889
+ Log.w(TAG, "buildItems: invalid price value '$priceValue' at index $i — skipping item")
890
+ continue
891
+ }
892
+
893
+ // Parse currency — skip item on unrecognised ISO 4217 code rather than crash
894
+ val currency = try {
895
+ Currency.getInstance(currencyCode)
896
+ } catch (e: IllegalArgumentException) {
897
+ Log.w(TAG, "buildItems: invalid currency code '$currencyCode' at index $i — skipping item")
898
+ continue
899
+ }
900
+
711
901
  val price = Price.Builder()
712
- .price(BigDecimal(priceValue))
713
- .currency(Currency.getInstance(currencyCode))
902
+ .price(priceDecimal)
903
+ .currency(currency)
714
904
  .build()
715
905
 
716
- val productId = rawItem.getString("productId") ?: continue
717
- val productVariantId = rawItem.getString("productVariantId") ?: continue
718
906
  val builder = Item.Builder(productId, productVariantId, price)
719
907
 
720
908
  if (rawItem.hasKey("productImage")) {
@@ -725,16 +913,16 @@ class AttentiveReactNativeSdkModule(reactContext: ReactApplicationContext) :
725
913
  builder.name(rawItem.getString("name"))
726
914
  }
727
915
 
916
+ // JS numbers are doubles on the bridge; use getDouble().toInt() to avoid ClassCastException
728
917
  if (rawItem.hasKey("quantity")) {
729
- builder.quantity(rawItem.getInt("quantity"))
918
+ builder.quantity(rawItem.getDouble("quantity").toInt())
730
919
  }
731
920
 
732
921
  if (rawItem.hasKey("category")) {
733
922
  builder.category(rawItem.getString("category"))
734
923
  }
735
924
 
736
- val item = builder.build()
737
- items.add(item)
925
+ items.add(builder.build())
738
926
  }
739
927
 
740
928
  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
 
@@ -280,7 +280,7 @@ struct DebugEvent {
280
280
 
281
281
  /**
282
282
  * Handle when a push notification is opened by the user.
283
- * Note: SDK 2.0.8 changed the API to require UNNotificationResponse instead of userInfo dictionary.
283
+ * Note: The SDK requires UNNotificationResponse instead of a plain userInfo dictionary.
284
284
  * This method is kept for backward compatibility but has limited functionality.
285
285
  * For full functionality, handle push notifications natively in AppDelegate.
286
286
  *
@@ -290,11 +290,11 @@ struct DebugEvent {
290
290
  */
291
291
  @objc(handlePushOpened:applicationState:authorizationStatus:)
292
292
  public func handlePushOpened(_ userInfo: [String: Any], applicationState: String, authorizationStatus: String) {
293
- // Note: SDK 2.0.8 changed the API to require UNNotificationResponse
293
+ // Note: The native SDK requires UNNotificationResponse for push tracking.
294
294
  // Since React Native doesn't provide direct access to UNNotificationResponse,
295
- // apps should handle push notifications natively in AppDelegate for full functionality
296
- print("[AttentiveSDK] Warning: Push notification handling from React Native is limited in SDK 2.0.8")
297
- print("[AttentiveSDK] The native SDK now requires UNNotificationResponse for push tracking")
295
+ // apps should handle push notifications natively in AppDelegate for full functionality.
296
+ print("[AttentiveSDK] Warning: Push notification handling from React Native is limited")
297
+ print("[AttentiveSDK] The native SDK requires UNNotificationResponse for push tracking")
298
298
  print("[AttentiveSDK] Please implement push handling in AppDelegate for full functionality")
299
299
 
300
300
  if debuggingEnabled {
@@ -302,7 +302,7 @@ struct DebugEvent {
302
302
  "applicationState": applicationState,
303
303
  "authorizationStatus": authorizationStatus,
304
304
  "userInfo": userInfo,
305
- "warning": "SDK 2.0.8 requires native UNNotificationResponse handling in AppDelegate"
305
+ "warning": "Native SDK requires UNNotificationResponse handling in AppDelegate"
306
306
  ])
307
307
  }
308
308
  }
@@ -313,15 +313,15 @@ struct DebugEvent {
313
313
  */
314
314
  @objc(handleForegroundNotification:)
315
315
  public func handleForegroundNotification(_ userInfo: [String: Any]) {
316
- // Note: SDK 2.0.8 changed the API to require UNNotificationResponse
317
- // Since React Native doesn't provide this, we'll log a warning
318
- print("[AttentiveSDK] Warning: Foreground notification handling from React Native is limited in SDK 2.0.8")
316
+ // Note: The native SDK requires UNNotificationResponse for push tracking.
317
+ // Since React Native doesn't provide this, we'll log a warning.
318
+ print("[AttentiveSDK] Warning: Foreground notification handling from React Native is limited")
319
319
  print("[AttentiveSDK] Please handle foreground notifications natively in AppDelegate for full functionality")
320
320
 
321
321
  if debuggingEnabled {
322
322
  showDebugInfo(event: "Foreground Notification", data: [
323
323
  "userInfo": userInfo,
324
- "warning": "SDK 2.0.8 requires native UNNotificationResponse handling"
324
+ "warning": "Native SDK requires UNNotificationResponse handling"
325
325
  ])
326
326
  }
327
327
  }
@@ -383,63 +383,126 @@ struct DebugEvent {
383
383
  /**
384
384
  * React Native bridge entry point for foreground push tracking.
385
385
  *
386
- * **Important — iOS limitation**: The Attentive iOS SDK requires a `UNNotificationResponse`
387
- * object for `handleForegroundPush`, which is an opaque system type that cannot be
388
- * serialised across the React Native bridge. Calling this method from JS therefore
389
- * **cannot invoke the native SDK** and is a no-op.
386
+ * The Attentive iOS SDK requires a `UNNotificationResponse` which cannot be
387
+ * serialised across the React Native bridge. To work around this, the method
388
+ * checks `AttentiveSDKManager.shared` for a cached response (set by the
389
+ * recommended `handleNotificationResponse(_:)` call in AppDelegate).
390
390
  *
391
- * The correct solution for iOS is to call
392
- * `AttentiveSDKManager.shared.handleForegroundPush(response:authorizationStatus:)`
393
- * directly from the host app's `UNUserNotificationCenterDelegate` implementation
394
- * (i.e. `AppDelegate.userNotificationCenter(_:didReceive:withCompletionHandler:)`).
395
- * Bonni's `AppDelegate.swift` already does this, so foreground push events are tracked
396
- * correctly via the native path — this RN bridge variant is intentionally unused on iOS.
391
+ * - If a cached response exists **and was already tracked**, this is a no-op
392
+ * (prevents double-tracking).
393
+ * - If a cached response exists **but was not yet tracked**, it is used to
394
+ * call the real native SDK method.
395
+ * - If no cached response is available, a warning is logged instructing the
396
+ * developer to add `AttentiveSDKManager.shared.handleNotificationResponse(response)`
397
+ * to their AppDelegate.
397
398
  *
398
- * @param userInfo The notification payload dictionary (unused on iOS).
399
- * @param authorizationStatus Current push authorization status string (unused on iOS).
399
+ * @param userInfo The notification payload dictionary.
400
+ * @param authorizationStatus Current push authorization status string.
400
401
  */
401
402
  @objc(handleForegroundPushFromRN:authorizationStatus:)
402
403
  public func handleForegroundPushFromRN(_ userInfo: [String: Any], authorizationStatus: String) {
403
- // iOS: no-op tracking is performed natively via AttentiveSDKManager in AppDelegate.
404
- // The UNNotificationResponse required by the iOS SDK cannot cross the RN bridge.
405
- print("[AttentiveSDK] handleForegroundPushFromRN: iOS push tracking is handled natively via AttentiveSDKManager in AppDelegate. This RN bridge call is a no-op on iOS.")
404
+ if let pending = AttentiveSDKManager.shared.consumePendingResponse() {
405
+ if pending.alreadyTracked {
406
+ // Already tracked by handleNotificationResponse nothing to do.
407
+ if debuggingEnabled {
408
+ showDebugInfo(event: "Foreground Push (already tracked natively)", data: [
409
+ "authorizationStatus": authorizationStatus,
410
+ "note": "Push was already tracked via AttentiveSDKManager.handleNotificationResponse."
411
+ ])
412
+ }
413
+ return
414
+ }
406
415
 
407
- if debuggingEnabled {
408
- showDebugInfo(event: "Foreground Push (RN bridge — iOS no-op)", data: [
409
- "authorizationStatus": authorizationStatus,
410
- "userInfo": userInfo,
411
- "note": "iOS: tracked via AppDelegate → AttentiveSDKManager. UNNotificationResponse not available from RN bridge."
412
- ])
416
+ // Cached response exists but wasn't tracked yet — track it now.
417
+ let authStatus = mapAuthorizationStatus(authorizationStatus)
418
+ sdk.handleForegroundPush(response: pending.response, authorizationStatus: authStatus)
419
+
420
+ if debuggingEnabled {
421
+ let cachedUserInfo = pending.response.notification.request.content.userInfo
422
+ showDebugInfo(event: "Foreground Push (tracked via cached response)", data: [
423
+ "authorizationStatus": authorizationStatus,
424
+ "userInfo": cachedUserInfo as? [String: Any] ?? [:],
425
+ "actionIdentifier": pending.response.actionIdentifier
426
+ ])
427
+ }
428
+ } else {
429
+ // No cached response. Either:
430
+ // (a) The client's AppDelegate is missing handleNotificationResponse, or
431
+ // (b) A prior bridge call (handlePushOpen / handleForegroundPush) already consumed it.
432
+ print("[AttentiveSDK] handleForegroundPushFromRN: No cached UNNotificationResponse available.")
433
+ print("[AttentiveSDK] If push tracking is not working, add this line to your AppDelegate's didReceive handler:")
434
+ print("[AttentiveSDK] AttentiveSDKManager.shared.handleNotificationResponse(response)")
435
+
436
+ if debuggingEnabled {
437
+ showDebugInfo(event: "Foreground Push (no cached response)", data: [
438
+ "authorizationStatus": authorizationStatus,
439
+ "userInfo": userInfo,
440
+ "note": "No cached response — either already consumed by a prior call or AppDelegate setup missing."
441
+ ])
442
+ }
413
443
  }
414
444
  }
415
445
 
416
446
  /**
417
447
  * React Native bridge entry point for push-open tracking (background/inactive tap).
418
448
  *
419
- * **Important — iOS limitation**: The Attentive iOS SDK requires a `UNNotificationResponse`
420
- * object for `handlePushOpen`, which cannot be serialised across the React Native bridge.
421
- * Calling this method from JS is therefore a **no-op on iOS**.
449
+ * The Attentive iOS SDK requires a `UNNotificationResponse` which cannot be
450
+ * serialised across the React Native bridge. To work around this, the method
451
+ * checks `AttentiveSDKManager.shared` for a cached response (set by the
452
+ * recommended `handleNotificationResponse(_:)` call in AppDelegate).
422
453
  *
423
- * The correct solution is to call
424
- * `AttentiveSDKManager.shared.handlePushOpen(response:authorizationStatus:)` directly
425
- * from `AppDelegate.userNotificationCenter(_:didReceive:withCompletionHandler:)`.
426
- * Bonni's `AppDelegate.swift` already does this via the native path.
454
+ * - If a cached response exists **and was already tracked**, this is a no-op
455
+ * (prevents double-tracking).
456
+ * - If a cached response exists **but was not yet tracked**, it is used to
457
+ * call the real native SDK method.
458
+ * - If no cached response is available, a warning is logged instructing the
459
+ * developer to add `AttentiveSDKManager.shared.handleNotificationResponse(response)`
460
+ * to their AppDelegate.
427
461
  *
428
- * @param userInfo The notification payload dictionary (unused on iOS).
429
- * @param authorizationStatus Current push authorization status string (unused on iOS).
462
+ * @param userInfo The notification payload dictionary.
463
+ * @param authorizationStatus Current push authorization status string.
430
464
  */
431
465
  @objc(handlePushOpenFromRN:authorizationStatus:)
432
466
  public func handlePushOpenFromRN(_ userInfo: [String: Any], authorizationStatus: String) {
433
- // iOS: no-op tracking is performed natively via AttentiveSDKManager in AppDelegate.
434
- // The UNNotificationResponse required by the iOS SDK cannot cross the RN bridge.
435
- print("[AttentiveSDK] handlePushOpenFromRN: iOS push tracking is handled natively via AttentiveSDKManager in AppDelegate. This RN bridge call is a no-op on iOS.")
467
+ if let pending = AttentiveSDKManager.shared.consumePendingResponse() {
468
+ if pending.alreadyTracked {
469
+ // Already tracked by handleNotificationResponse nothing to do.
470
+ if debuggingEnabled {
471
+ showDebugInfo(event: "Push Open (already tracked natively)", data: [
472
+ "authorizationStatus": authorizationStatus,
473
+ "note": "Push was already tracked via AttentiveSDKManager.handleNotificationResponse."
474
+ ])
475
+ }
476
+ return
477
+ }
436
478
 
437
- if debuggingEnabled {
438
- showDebugInfo(event: "Push Open (RN bridge — iOS no-op)", data: [
439
- "authorizationStatus": authorizationStatus,
440
- "userInfo": userInfo,
441
- "note": "iOS: tracked via AppDelegate → AttentiveSDKManager. UNNotificationResponse not available from RN bridge."
442
- ])
479
+ // Cached response exists but wasn't tracked yet — track it now.
480
+ let authStatus = mapAuthorizationStatus(authorizationStatus)
481
+ sdk.handlePushOpen(response: pending.response, authorizationStatus: authStatus)
482
+
483
+ if debuggingEnabled {
484
+ let cachedUserInfo = pending.response.notification.request.content.userInfo
485
+ showDebugInfo(event: "Push Open (tracked via cached response)", data: [
486
+ "authorizationStatus": authorizationStatus,
487
+ "userInfo": cachedUserInfo as? [String: Any] ?? [:],
488
+ "actionIdentifier": pending.response.actionIdentifier
489
+ ])
490
+ }
491
+ } else {
492
+ // No cached response. Either:
493
+ // (a) The client's AppDelegate is missing handleNotificationResponse, or
494
+ // (b) A prior bridge call (handlePushOpen / handleForegroundPush) already consumed it.
495
+ print("[AttentiveSDK] handlePushOpenFromRN: No cached UNNotificationResponse available.")
496
+ print("[AttentiveSDK] If push tracking is not working, add this line to your AppDelegate's didReceive handler:")
497
+ print("[AttentiveSDK] AttentiveSDKManager.shared.handleNotificationResponse(response)")
498
+
499
+ if debuggingEnabled {
500
+ showDebugInfo(event: "Push Open (no cached response)", data: [
501
+ "authorizationStatus": authorizationStatus,
502
+ "userInfo": userInfo,
503
+ "note": "No cached response — either already consumed by a prior call or AppDelegate setup missing."
504
+ ])
505
+ }
443
506
  }
444
507
  }
445
508