@attentive-mobile/attentive-react-native-sdk 1.0.5 → 2.0.0-beta.2

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 (37) hide show
  1. package/README.md +126 -0
  2. package/android/build.gradle +1 -0
  3. package/android/src/main/kotlin/com/attentivereactnativesdk/AttentiveReactNativeSdkModule.kt +384 -0
  4. package/android/src/main/kotlin/com/attentivereactnativesdk/AttentiveReactNativeSdkPackage.kt +36 -0
  5. package/android/src/main/kotlin/com/attentivereactnativesdk/debug/AttentiveDebugHelper.kt +22 -6
  6. package/attentive-react-native-sdk.podspec +3 -3
  7. package/ios/AttentiveReactNativeSdk.h +1 -1
  8. package/ios/AttentiveReactNativeSdk.mm +317 -37
  9. package/ios/Bridging/ATTNNativeSDK.swift +506 -58
  10. package/ios/Bridging/AttentiveReactNativeSdk-Bridging-Header.h +3 -0
  11. package/ios/Bridging/AttentiveSDKManager.swift +83 -0
  12. package/ios/Podfile +3 -16
  13. package/lib/commonjs/NativeAttentiveReactNativeSdk.js +14 -0
  14. package/lib/commonjs/NativeAttentiveReactNativeSdk.js.map +1 -0
  15. package/lib/commonjs/eventTypes.js.map +1 -1
  16. package/lib/commonjs/index.js +362 -52
  17. package/lib/commonjs/index.js.map +1 -1
  18. package/lib/module/NativeAttentiveReactNativeSdk.js +7 -0
  19. package/lib/module/NativeAttentiveReactNativeSdk.js.map +1 -0
  20. package/lib/module/eventTypes.js.map +1 -1
  21. package/lib/module/index.js +345 -50
  22. package/lib/module/index.js.map +1 -1
  23. package/lib/typescript/NativeAttentiveReactNativeSdk.d.ts +103 -0
  24. package/lib/typescript/NativeAttentiveReactNativeSdk.d.ts.map +1 -0
  25. package/lib/typescript/eventTypes.d.ts +44 -17
  26. package/lib/typescript/eventTypes.d.ts.map +1 -1
  27. package/lib/typescript/index.d.ts +276 -41
  28. package/lib/typescript/index.d.ts.map +1 -1
  29. package/package.json +21 -7
  30. package/src/NativeAttentiveReactNativeSdk.ts +152 -0
  31. package/src/eventTypes.tsx +57 -20
  32. package/src/index.tsx +472 -96
  33. package/android/src/main/java/com/attentivereactnativesdk/AttentiveReactNativeSdkModule.java +0 -310
  34. package/android/src/main/java/com/attentivereactnativesdk/AttentiveReactNativeSdkPackage.java +0 -28
  35. package/ios/AttentiveReactNativeSdk.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -7
  36. package/ios/AttentiveReactNativeSdk.xcodeproj/project.xcworkspace/xcuserdata/zheref.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
  37. package/ios/AttentiveReactNativeSdk.xcodeproj/xcuserdata/zheref.xcuserdatad/xcschemes/xcschememanagement.plist +0 -14
@@ -9,6 +9,7 @@
9
9
  import Foundation
10
10
  import attentive_ios_sdk
11
11
  import UIKit
12
+ import UserNotifications
12
13
 
13
14
  // Debug Event structure for session history
14
15
  struct DebugEvent {
@@ -23,7 +24,7 @@ struct DebugEvent {
23
24
  self.eventType = eventType
24
25
  self.data = data
25
26
  }
26
-
27
+
27
28
  /**
28
29
  * Formats the debug event as a human-readable string for export
29
30
  * @return A formatted string containing timestamp, event type, and data
@@ -32,17 +33,17 @@ struct DebugEvent {
32
33
  let formatter = DateFormatter()
33
34
  formatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS"
34
35
  let timeString = formatter.string(from: timestamp)
35
-
36
+
36
37
  var output = "[\(timeString)] \(eventType)\n"
37
-
38
+
38
39
  // Add summary information if available
39
40
  let summary = getSummary()
40
41
  if !summary.isEmpty {
41
42
  output += "Summary: \(summary)\n"
42
43
  }
43
-
44
+
44
45
  output += "Data:\n"
45
-
46
+
46
47
  // Format data as JSON for better readability
47
48
  do {
48
49
  let jsonData = try JSONSerialization.data(withJSONObject: data, options: .prettyPrinted)
@@ -54,17 +55,17 @@ struct DebugEvent {
54
55
  } catch {
55
56
  output += "\(data)"
56
57
  }
57
-
58
+
58
59
  return output + "\n" + String(repeating: "=", count: 50) + "\n"
59
60
  }
60
-
61
+
61
62
  /**
62
63
  * Generates a summary of the debug event for quick overview
63
64
  * @return A brief summary string highlighting key information
64
65
  */
65
66
  private func getSummary() -> String {
66
67
  var summaryParts: [String] = []
67
-
68
+
68
69
  if let itemsCount = data["items_count"] as? String {
69
70
  summaryParts.append("Items: \(itemsCount)")
70
71
  }
@@ -77,10 +78,10 @@ struct DebugEvent {
77
78
  if let eventType = data["event_type"] as? String {
78
79
  summaryParts.append("Type: \(eventType)")
79
80
  }
80
-
81
+
81
82
  // Always show payload size info
82
83
  summaryParts.append("Payload: \(data.count) fields")
83
-
84
+
84
85
  return summaryParts.joined(separator: " • ")
85
86
  }
86
87
  }
@@ -94,17 +95,16 @@ struct DebugEvent {
94
95
  @objc(initWithDomain:mode:skipFatigueOnCreatives:enableDebugger:)
95
96
  public init(domain: String, mode: String, skipFatigueOnCreatives: Bool, enableDebugger: Bool) {
96
97
  self.sdk = ATTNSDK(domain: domain, mode: ATTNSDKMode(rawValue: mode) ?? .production)
97
- self.sdk.skipFatigueOnCreative = skipFatigueOnCreatives ?? false
98
-
98
+ self.sdk.skipFatigueOnCreative = skipFatigueOnCreatives
99
+
99
100
  // Only enable debugging if both enableDebugger is true AND the app is running in debug mode
100
- let enableDebuggerFromConfig = enableDebugger ?? false
101
101
  #if DEBUG
102
102
  let isDebugBuild = true
103
103
  #else
104
104
  let isDebugBuild = false
105
105
  #endif
106
- self.debuggingEnabled = enableDebuggerFromConfig && isDebugBuild
107
-
106
+ self.debuggingEnabled = enableDebugger && isDebugBuild
107
+
108
108
  ATTNEventTracker.setup(with: sdk)
109
109
  }
110
110
 
@@ -131,7 +131,14 @@ struct DebugEvent {
131
131
 
132
132
  @objc(identify:)
133
133
  public func identify(_ identifiers: [String: Any]) {
134
+ print("👤 [AttentiveSDK] identify called from React Native")
135
+ print(" Identifiers: \(identifiers)")
136
+
134
137
  sdk.identify(identifiers)
138
+
139
+ print("✅ [AttentiveSDK] identify completed")
140
+ print(" User is now identified with the SDK")
141
+ print(" SDK can now make network calls")
135
142
  }
136
143
 
137
144
  @objc
@@ -139,6 +146,346 @@ struct DebugEvent {
139
146
  sdk.clearUser()
140
147
  }
141
148
 
149
+ // MARK: - Push Notification Methods
150
+
151
+ /**
152
+ * Request push notification permission from the user.
153
+ * This will trigger the iOS permission dialog.
154
+ */
155
+ @objc
156
+ public func registerForPushNotifications() {
157
+ sdk.registerForPushNotifications()
158
+ if debuggingEnabled {
159
+ showDebugInfo(event: "Push Registration Requested", data: ["action": "registerForPushNotifications"])
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Register the device token received from APNs with the Attentive backend.
165
+ * @param token The hex-encoded device token string
166
+ * @param authorizationStatus Current push authorization status string
167
+ */
168
+ @objc(registerDeviceToken:authorizationStatus:)
169
+ public func registerDeviceToken(_ token: String, authorizationStatus: String) {
170
+ // Convert hex string back to Data
171
+ guard let tokenData = hexStringToData(token) else {
172
+ print("[AttentiveSDK] Invalid device token format")
173
+ return
174
+ }
175
+
176
+ // Map string to UNAuthorizationStatus
177
+ let status = mapAuthorizationStatus(authorizationStatus)
178
+
179
+ sdk.registerDeviceToken(tokenData, authorizationStatus: status)
180
+
181
+ if debuggingEnabled {
182
+ showDebugInfo(event: "Device Token Registered", data: [
183
+ "token": String(token.prefix(16)) + "...",
184
+ "authorizationStatus": authorizationStatus
185
+ ])
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Register the device token with callback for use in AppDelegate.
191
+ * This method is intended to be called directly from the host app's AppDelegate,
192
+ * not from React Native JavaScript.
193
+ *
194
+ * @param token The device token Data from APNs
195
+ * @param authorizationStatus Current push authorization status
196
+ * @param callback Completion handler called after registration attempt
197
+ */
198
+ @objc(registerDeviceTokenWithCallback:authorizationStatus:callback:)
199
+ public func registerDeviceToken(
200
+ _ token: Data,
201
+ authorizationStatus: UNAuthorizationStatus,
202
+ callback: @escaping (_ data: Data?, _ url: URL?, _ response: URLResponse?, _ error: Error?) -> Void
203
+ ) {
204
+ sdk.registerDeviceToken(token, authorizationStatus: authorizationStatus, callback: callback)
205
+
206
+ if debuggingEnabled {
207
+ let tokenString = token.map { String(format: "%02.2hhx", $0) }.joined()
208
+ showDebugInfo(event: "Device Token Registered (with callback)", data: [
209
+ "token": String(tokenString.prefix(16)) + "...",
210
+ "authorizationStatus": authorizationStatusToString(authorizationStatus)
211
+ ])
212
+ }
213
+ }
214
+
215
+ /**
216
+ * Handle a regular/direct app open event.
217
+ * This should be called after device token registration completes (success or failure).
218
+ *
219
+ * This is the TypeScript equivalent of the native iOS AppDelegate method:
220
+ * ```swift
221
+ * func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
222
+ * UNUserNotificationCenter.current().getNotificationSettings { [weak self] settings in
223
+ * guard let self = self else { return }
224
+ * let authStatus = settings.authorizationStatus
225
+ * attentiveSdk?.registerDeviceToken(deviceToken, authorizationStatus: authStatus, callback: { data, url, response, error in
226
+ * DispatchQueue.main.async {
227
+ * self.attentiveSdk?.handleRegularOpen(authorizationStatus: authStatus)
228
+ * }
229
+ * })
230
+ * }
231
+ * }
232
+ * ```
233
+ *
234
+ * @param authorizationStatus Current push authorization status
235
+ */
236
+ @objc(handleRegularOpen:)
237
+ public func handleRegularOpen(authorizationStatus: UNAuthorizationStatus) {
238
+ print("🌉 [AttentiveSDK] handleRegularOpen called from React Native")
239
+ print(" Authorization Status: \(authorizationStatusToString(authorizationStatus))")
240
+ print(" Calling underlying iOS SDK handleRegularOpen...")
241
+
242
+ // Call the underlying Attentive iOS SDK
243
+ sdk.handleRegularOpen(authorizationStatus: authorizationStatus)
244
+
245
+ print("✅ [AttentiveSDK] handleRegularOpen completed")
246
+ print(" This should trigger a network call to: https://mobile.attentivemobile.com/mtctrl")
247
+ print(" If you don't see the network call:")
248
+ print(" 1. Check that user is identified (call identify() before handleRegularOpen)")
249
+ print(" 2. Check your proxy debugger is configured for mobile.attentivemobile.com")
250
+ print(" 3. Verify SSL proxying is enabled")
251
+ print(" 4. Check device has internet connection")
252
+
253
+ if debuggingEnabled {
254
+ showDebugInfo(event: "Regular Open Event", data: [
255
+ "authorizationStatus": authorizationStatusToString(authorizationStatus),
256
+ "expectedEndpoint": "https://mobile.attentivemobile.com/mtctrl",
257
+ "note": "Check network logs to verify endpoint was called"
258
+ ])
259
+ }
260
+ }
261
+
262
+
263
+ /**
264
+ * Handle when a push notification is opened by the user.
265
+ * Note: SDK 2.0.8 changed the API to require UNNotificationResponse instead of userInfo dictionary.
266
+ * This method is kept for backward compatibility but has limited functionality.
267
+ * For full functionality, handle push notifications natively in AppDelegate.
268
+ *
269
+ * @param userInfo The notification payload
270
+ * @param applicationState The app state when notification was opened
271
+ * @param authorizationStatus Current push authorization status
272
+ */
273
+ @objc(handlePushOpened:applicationState:authorizationStatus:)
274
+ public func handlePushOpened(_ userInfo: [String: Any], applicationState: String, authorizationStatus: String) {
275
+ // Note: SDK 2.0.8 changed the API to require UNNotificationResponse
276
+ // Since React Native doesn't provide direct access to UNNotificationResponse,
277
+ // apps should handle push notifications natively in AppDelegate for full functionality
278
+ print("[AttentiveSDK] Warning: Push notification handling from React Native is limited in SDK 2.0.8")
279
+ print("[AttentiveSDK] The native SDK now requires UNNotificationResponse for push tracking")
280
+ print("[AttentiveSDK] Please implement push handling in AppDelegate for full functionality")
281
+
282
+ if debuggingEnabled {
283
+ showDebugInfo(event: "Push Opened (Limited)", data: [
284
+ "applicationState": applicationState,
285
+ "authorizationStatus": authorizationStatus,
286
+ "userInfo": userInfo,
287
+ "warning": "SDK 2.0.8 requires native UNNotificationResponse handling in AppDelegate"
288
+ ])
289
+ }
290
+ }
291
+
292
+ /**
293
+ * Handle when a push notification arrives while the app is in foreground.
294
+ * @param userInfo The notification payload
295
+ */
296
+ @objc(handleForegroundNotification:)
297
+ public func handleForegroundNotification(_ userInfo: [String: Any]) {
298
+ // Note: SDK 2.0.8 changed the API to require UNNotificationResponse
299
+ // Since React Native doesn't provide this, we'll log a warning
300
+ print("[AttentiveSDK] Warning: Foreground notification handling from React Native is limited in SDK 2.0.8")
301
+ print("[AttentiveSDK] Please handle foreground notifications natively in AppDelegate for full functionality")
302
+
303
+ if debuggingEnabled {
304
+ showDebugInfo(event: "Foreground Notification", data: [
305
+ "userInfo": userInfo,
306
+ "warning": "SDK 2.0.8 requires native UNNotificationResponse handling"
307
+ ])
308
+ }
309
+ }
310
+
311
+ /**
312
+ * Handle a push notification when the app is in the foreground (active state).
313
+ * This is the native equivalent that should be called from AppDelegate when the app is active.
314
+ *
315
+ * Equivalent to Swift AppDelegate code:
316
+ * ```swift
317
+ * case .active:
318
+ * self.attentiveSdk?.handleForegroundPush(response: response, authorizationStatus: authStatus)
319
+ * ```
320
+ *
321
+ * @param response The UNNotificationResponse from the notification center delegate
322
+ * @param authorizationStatus Current push authorization status
323
+ */
324
+ @objc(handleForegroundPush:authorizationStatus:)
325
+ public func handleForegroundPush(response: UNNotificationResponse, authorizationStatus: UNAuthorizationStatus) {
326
+ sdk.handleForegroundPush(response: response, authorizationStatus: authorizationStatus)
327
+
328
+ if debuggingEnabled {
329
+ let userInfo = response.notification.request.content.userInfo
330
+ showDebugInfo(event: "Foreground Push", data: [
331
+ "authorizationStatus": authorizationStatusToString(authorizationStatus),
332
+ "userInfo": userInfo as? [String: Any] ?? [:],
333
+ "actionIdentifier": response.actionIdentifier
334
+ ])
335
+ }
336
+ }
337
+
338
+ /**
339
+ * Handle when a push notification is opened by the user (app in background/inactive state).
340
+ * This is the native equivalent that should be called from AppDelegate when the app is background or inactive.
341
+ *
342
+ * Equivalent to Swift AppDelegate code:
343
+ * ```swift
344
+ * case .background, .inactive:
345
+ * self.attentiveSdk?.handlePushOpen(response: response, authorizationStatus: authStatus)
346
+ * ```
347
+ *
348
+ * @param response The UNNotificationResponse from the notification center delegate
349
+ * @param authorizationStatus Current push authorization status
350
+ */
351
+ @objc(handlePushOpen:authorizationStatus:)
352
+ public func handlePushOpen(response: UNNotificationResponse, authorizationStatus: UNAuthorizationStatus) {
353
+ sdk.handlePushOpen(response: response, authorizationStatus: authorizationStatus)
354
+
355
+ if debuggingEnabled {
356
+ let userInfo = response.notification.request.content.userInfo
357
+ showDebugInfo(event: "Push Open", data: [
358
+ "authorizationStatus": authorizationStatusToString(authorizationStatus),
359
+ "userInfo": userInfo as? [String: Any] ?? [:],
360
+ "actionIdentifier": response.actionIdentifier
361
+ ])
362
+ }
363
+ }
364
+
365
+ /**
366
+ * Handle a push notification when the app is in the foreground (active state) - React Native version.
367
+ * This method accepts userInfo dictionary instead of UNNotificationResponse, making it callable from React Native.
368
+ *
369
+ * Note: This is a limited version since we don't have access to the full UNNotificationResponse.
370
+ * For full functionality, use the native AppDelegate implementation.
371
+ *
372
+ * @param userInfo The notification payload dictionary
373
+ * @param authorizationStatus Current push authorization status string
374
+ */
375
+ @objc(handleForegroundPushFromRN:authorizationStatus:)
376
+ public func handleForegroundPushFromRN(_ userInfo: [String: Any], authorizationStatus: String) {
377
+ _ = mapAuthorizationStatus(authorizationStatus)
378
+
379
+ // Note: SDK 2.0.8 requires UNNotificationResponse, but we only have userInfo from React Native
380
+ // This is a workaround that logs the limitation
381
+ print("[AttentiveSDK] handleForegroundPush called from React Native (limited functionality)")
382
+ print("[AttentiveSDK] For full functionality, implement in native AppDelegate")
383
+
384
+ if debuggingEnabled {
385
+ showDebugInfo(event: "Foreground Push (React Native)", data: [
386
+ "authorizationStatus": authorizationStatus,
387
+ "userInfo": userInfo,
388
+ "note": "Limited functionality - UNNotificationResponse not available from React Native"
389
+ ])
390
+ }
391
+ }
392
+
393
+ /**
394
+ * Handle when a push notification is opened by the user (app in background/inactive state) - React Native version.
395
+ * This method accepts userInfo dictionary instead of UNNotificationResponse, making it callable from React Native.
396
+ *
397
+ * Note: This is a limited version since we don't have access to the full UNNotificationResponse.
398
+ * For full functionality, use the native AppDelegate implementation.
399
+ *
400
+ * @param userInfo The notification payload dictionary
401
+ * @param authorizationStatus Current push authorization status string
402
+ */
403
+ @objc(handlePushOpenFromRN:authorizationStatus:)
404
+ public func handlePushOpenFromRN(_ userInfo: [String: Any], authorizationStatus: String) {
405
+ _ = mapAuthorizationStatus(authorizationStatus)
406
+
407
+ // Note: SDK 2.0.8 requires UNNotificationResponse, but we only have userInfo from React Native
408
+ // This is a workaround that logs the limitation
409
+ print("[AttentiveSDK] handlePushOpen called from React Native (limited functionality)")
410
+ print("[AttentiveSDK] For full functionality, implement in native AppDelegate")
411
+
412
+ if debuggingEnabled {
413
+ showDebugInfo(event: "Push Open (React Native)", data: [
414
+ "authorizationStatus": authorizationStatus,
415
+ "userInfo": userInfo,
416
+ "note": "Limited functionality - UNNotificationResponse not available from React Native"
417
+ ])
418
+ }
419
+ }
420
+
421
+ // MARK: - Push Notification Helpers
422
+
423
+ private func hexStringToData(_ hexString: String) -> Data? {
424
+ var data = Data()
425
+ var hex = hexString
426
+
427
+ // Remove any non-hex characters
428
+ hex = hex.replacingOccurrences(of: " ", with: "")
429
+ hex = hex.replacingOccurrences(of: "<", with: "")
430
+ hex = hex.replacingOccurrences(of: ">", with: "")
431
+
432
+ var index = hex.startIndex
433
+ while index < hex.endIndex {
434
+ let nextIndex = hex.index(index, offsetBy: 2, limitedBy: hex.endIndex) ?? hex.endIndex
435
+ if nextIndex > hex.endIndex { break }
436
+
437
+ let byteString = String(hex[index..<nextIndex])
438
+ if let byte = UInt8(byteString, radix: 16) {
439
+ data.append(byte)
440
+ } else {
441
+ return nil
442
+ }
443
+ index = nextIndex
444
+ }
445
+
446
+ return data.isEmpty ? nil : data
447
+ }
448
+
449
+ private func mapAuthorizationStatus(_ status: String) -> UNAuthorizationStatus {
450
+ switch status.lowercased() {
451
+ case "authorized":
452
+ return .authorized
453
+ case "denied":
454
+ return .denied
455
+ case "notdetermined":
456
+ return .notDetermined
457
+ case "provisional":
458
+ return .provisional
459
+ case "ephemeral":
460
+ if #available(iOS 14.0, *) {
461
+ return .ephemeral
462
+ }
463
+ return .authorized
464
+ default:
465
+ return .notDetermined
466
+ }
467
+ }
468
+
469
+ private func authorizationStatusToString(_ status: UNAuthorizationStatus) -> String {
470
+ switch status {
471
+ case .authorized:
472
+ return "authorized"
473
+ case .denied:
474
+ return "denied"
475
+ case .notDetermined:
476
+ return "notDetermined"
477
+ case .provisional:
478
+ return "provisional"
479
+ case .ephemeral:
480
+ if #available(iOS 14.0, *) {
481
+ return "ephemeral"
482
+ }
483
+ return "authorized"
484
+ @unknown default:
485
+ return "notDetermined"
486
+ }
487
+ }
488
+
142
489
  @objc
143
490
  public func invokeAttentiveDebugHelper() {
144
491
  if debuggingEnabled {
@@ -157,7 +504,7 @@ struct DebugEvent {
157
504
  }
158
505
  }
159
506
  }
160
-
507
+
161
508
  }
162
509
 
163
510
  public extension ATTNNativeSDK {
@@ -170,36 +517,36 @@ public extension ATTNNativeSDK {
170
517
  guard debuggingEnabled else {
171
518
  return "Debug logging is not enabled. Please enable debugging to export logs."
172
519
  }
173
-
520
+
174
521
  if debugHistory.isEmpty {
175
522
  return "No debug events recorded in this session."
176
523
  }
177
-
524
+
178
525
  let formatter = DateFormatter()
179
526
  formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
180
527
  let exportDate = formatter.string(from: Date())
181
-
528
+
182
529
  var exportContent = """
183
530
  Attentive React Native SDK - Debug Session Export
184
531
  Generated: \(exportDate)
185
532
  Total Events: \(debugHistory.count)
186
-
533
+
187
534
  \(String(repeating: "=", count: 60))
188
-
535
+
189
536
  """
190
-
537
+
191
538
  // Add all events in chronological order (oldest first for better readability)
192
539
  for (index, event) in debugHistory.enumerated() {
193
540
  exportContent += "Event #\(index + 1)\n"
194
541
  exportContent += event.formatForExport()
195
542
  exportContent += "\n"
196
543
  }
197
-
544
+
198
545
  exportContent += """
199
546
  \(String(repeating: "=", count: 60))
200
547
  End of Debug Session Export
201
548
  """
202
-
549
+
203
550
  return exportContent
204
551
  }
205
552
 
@@ -211,7 +558,35 @@ public extension ATTNNativeSDK {
211
558
  ATTNEventTracker.sharedInstance()?.record(event: event)
212
559
 
213
560
  if debuggingEnabled {
214
- showDebugInfo(event: "Add To Cart Event", data: ["items_count": "\(items.count)", "deeplink": deeplink, "payload": attributes])
561
+ // Enhanced debug data to show parsed item details
562
+ var debugData: [String: Any] = [
563
+ "items_count": "\(items.count)",
564
+ "deeplink": deeplink,
565
+ "payload": attributes
566
+ ]
567
+
568
+ // Add item details to debug output
569
+ if let firstItem = items.first {
570
+ var itemDetails: [String: Any] = [
571
+ "productId": firstItem.productId,
572
+ "productVariantId": firstItem.productVariantId
573
+ ]
574
+ if let name = firstItem.name {
575
+ itemDetails["name"] = name
576
+ }
577
+ if let productImage = firstItem.productImage {
578
+ itemDetails["productImage"] = productImage
579
+ }
580
+ if let category = firstItem.category {
581
+ itemDetails["category"] = category
582
+ }
583
+ if let quantity = firstItem.quantity {
584
+ itemDetails["quantity"] = quantity
585
+ }
586
+ debugData["first_item"] = itemDetails
587
+ }
588
+
589
+ showDebugInfo(event: "Add To Cart Event", data: debugData)
215
590
  }
216
591
  }
217
592
 
@@ -223,7 +598,35 @@ public extension ATTNNativeSDK {
223
598
  ATTNEventTracker.sharedInstance()?.record(event: event)
224
599
 
225
600
  if debuggingEnabled {
226
- showDebugInfo(event: "Product View Event", data: ["items_count": "\(items.count)", "deeplink": deeplink, "payload": attributes])
601
+ // Enhanced debug data to show parsed item details
602
+ var debugData: [String: Any] = [
603
+ "items_count": "\(items.count)",
604
+ "deeplink": deeplink,
605
+ "payload": attributes
606
+ ]
607
+
608
+ // Add item details to debug output
609
+ if let firstItem = items.first {
610
+ var itemDetails: [String: Any] = [
611
+ "productId": firstItem.productId,
612
+ "productVariantId": firstItem.productVariantId
613
+ ]
614
+ if let name = firstItem.name {
615
+ itemDetails["name"] = name
616
+ }
617
+ if let productImage = firstItem.productImage {
618
+ itemDetails["productImage"] = productImage
619
+ }
620
+ if let category = firstItem.category {
621
+ itemDetails["category"] = category
622
+ }
623
+ if let quantity = firstItem.quantity {
624
+ itemDetails["quantity"] = quantity
625
+ }
626
+ debugData["first_item"] = itemDetails
627
+ }
628
+
629
+ showDebugInfo(event: "Product View Event", data: debugData)
227
630
  }
228
631
  }
229
632
 
@@ -237,7 +640,35 @@ public extension ATTNNativeSDK {
237
640
  ATTNEventTracker.sharedInstance()?.record(event: event)
238
641
 
239
642
  if debuggingEnabled {
240
- showDebugInfo(event: "Purchase Event", data: ["items_count": "\(items.count)", "order_id": orderId, "payload": attributes])
643
+ // Enhanced debug data to show parsed item details
644
+ var debugData: [String: Any] = [
645
+ "items_count": "\(items.count)",
646
+ "order_id": orderId,
647
+ "payload": attributes
648
+ ]
649
+
650
+ // Add item details to debug output
651
+ if let firstItem = items.first {
652
+ var itemDetails: [String: Any] = [
653
+ "productId": firstItem.productId,
654
+ "productVariantId": firstItem.productVariantId
655
+ ]
656
+ if let name = firstItem.name {
657
+ itemDetails["name"] = name
658
+ }
659
+ if let productImage = firstItem.productImage {
660
+ itemDetails["productImage"] = productImage
661
+ }
662
+ if let category = firstItem.category {
663
+ itemDetails["category"] = category
664
+ }
665
+ if let quantity = firstItem.quantity {
666
+ itemDetails["quantity"] = quantity
667
+ }
668
+ debugData["first_item"] = itemDetails
669
+ }
670
+
671
+ showDebugInfo(event: "Purchase Event", data: debugData)
241
672
  }
242
673
  }
243
674
 
@@ -259,21 +690,38 @@ private extension ATTNNativeSDK {
259
690
  var itemsToReturn: [ATTNItem] = []
260
691
 
261
692
  for rawItem in rawItems {
262
- if let rawPrice = rawItem["price"] as? [String: Any],
263
- let priceString = rawPrice["price"] as? String,
264
- let currency = rawPrice["currency"] as? String {
693
+ // Parse price - flattened structure (not nested)
694
+ guard let priceString = rawItem["price"] as? String,
695
+ let currency = rawItem["currency"] as? String,
696
+ let productId = rawItem["productId"] as? String,
697
+ let productVariantId = rawItem["productVariantId"] as? String else {
698
+ continue
699
+ }
265
700
 
266
- let price = NSDecimalNumber(string: priceString)
701
+ let price = NSDecimalNumber(string: priceString)
702
+ let attnPrice = ATTNPrice(price: price, currency: currency)
267
703
 
268
- let attnPrice = ATTNPrice(price: price, currency: currency)
704
+ let item = ATTNItem(productId: productId, productVariantId: productVariantId, price: attnPrice)
269
705
 
270
- if let productId = rawItem["productId"] as? String,
271
- let productVariantId = rawItem["productVariantId"] as? String {
706
+ // Parse optional fields to match Android implementation
707
+ if let productImage = rawItem["productImage"] as? String {
708
+ item.productImage = productImage
709
+ }
272
710
 
273
- let item = ATTNItem(productId: productId, productVariantId: productVariantId, price: attnPrice)
274
- itemsToReturn.append(item)
275
- }
711
+ if let name = rawItem["name"] as? String {
712
+ item.name = name
276
713
  }
714
+
715
+ // React Native bridges JS numbers as NSNumber, so accept NSNumber directly
716
+ if let quantity = rawItem["quantity"] as? NSNumber {
717
+ item.quantity = quantity
718
+ }
719
+
720
+ if let category = rawItem["category"] as? String {
721
+ item.category = category
722
+ }
723
+
724
+ itemsToReturn.append(item)
277
725
  }
278
726
 
279
727
  return itemsToReturn
@@ -380,7 +828,7 @@ class DebugOverlayViewController: UIViewController {
380
828
  shareButton.addTarget(self, action: #selector(shareButtonTapped), for: .touchUpInside)
381
829
  shareButton.translatesAutoresizingMaskIntoConstraints = false
382
830
  containerView.addSubview(shareButton)
383
-
831
+
384
832
  // X Close button in top-right corner
385
833
  let closeButton = UIButton(type: .system)
386
834
  closeButton.setTitle("✕", for: .normal)
@@ -538,10 +986,10 @@ class DebugOverlayViewController: UIViewController {
538
986
  @objc private func shareButtonTapped() {
539
987
  // Generate export content for the current history
540
988
  let exportContent = generateExportContent()
541
-
989
+
542
990
  // Create activity view controller for sharing
543
991
  let activityVC = UIActivityViewController(activityItems: [exportContent], applicationActivities: nil)
544
-
992
+
545
993
  // For iPad - prevent crash by setting popover presentation controller
546
994
  if let popover = activityVC.popoverPresentationController {
547
995
  // Find the share button view to anchor the popover
@@ -553,10 +1001,10 @@ class DebugOverlayViewController: UIViewController {
553
1001
  popover.sourceRect = CGRect(x: view.bounds.midX, y: view.bounds.midY, width: 0, height: 0)
554
1002
  }
555
1003
  }
556
-
1004
+
557
1005
  present(activityVC, animated: true)
558
1006
  }
559
-
1007
+
560
1008
  /**
561
1009
  * Generates formatted export content for sharing
562
1010
  * @return Formatted string containing all debug events
@@ -565,32 +1013,32 @@ class DebugOverlayViewController: UIViewController {
565
1013
  if history.isEmpty {
566
1014
  return "No debug events recorded in this session."
567
1015
  }
568
-
1016
+
569
1017
  let formatter = DateFormatter()
570
1018
  formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
571
1019
  let exportDate = formatter.string(from: Date())
572
-
1020
+
573
1021
  var exportContent = """
574
1022
  Attentive React Native SDK - Debug Session Export
575
1023
  Generated: \(exportDate)
576
1024
  Total Events: \(history.count)
577
-
1025
+
578
1026
  \(String(repeating: "=", count: 60))
579
-
1027
+
580
1028
  """
581
-
1029
+
582
1030
  // Add all events in chronological order (oldest first for better readability)
583
1031
  for (index, event) in history.enumerated() {
584
1032
  exportContent += "Event #\(index + 1)\n"
585
1033
  exportContent += event.formatForExport()
586
1034
  exportContent += "\n"
587
1035
  }
588
-
1036
+
589
1037
  exportContent += """
590
1038
  \(String(repeating: "=", count: 60))
591
1039
  End of Debug Session Export
592
1040
  """
593
-
1041
+
594
1042
  return exportContent
595
1043
  }
596
1044
 
@@ -772,7 +1220,7 @@ class EventDetailViewController: UIViewController {
772
1220
  shareButton.addTarget(self, action: #selector(shareEventButtonTapped), for: .touchUpInside)
773
1221
  shareButton.translatesAutoresizingMaskIntoConstraints = false
774
1222
  containerView.addSubview(shareButton)
775
-
1223
+
776
1224
  let closeButton = UIButton(type: .system)
777
1225
  closeButton.setTitle("✕", for: .normal)
778
1226
  closeButton.titleLabel?.font = UIFont.systemFont(ofSize: 18, weight: .medium)
@@ -834,19 +1282,19 @@ class EventDetailViewController: UIViewController {
834
1282
  */
835
1283
  @objc private func shareEventButtonTapped() {
836
1284
  let exportContent = generateSingleEventExport()
837
-
1285
+
838
1286
  // Create activity view controller for sharing
839
1287
  let activityVC = UIActivityViewController(activityItems: [exportContent], applicationActivities: nil)
840
-
1288
+
841
1289
  // For iPad - prevent crash by setting popover presentation controller
842
1290
  if let popover = activityVC.popoverPresentationController {
843
1291
  popover.sourceView = view
844
1292
  popover.sourceRect = CGRect(x: view.bounds.midX, y: view.bounds.midY, width: 0, height: 0)
845
1293
  }
846
-
1294
+
847
1295
  present(activityVC, animated: true)
848
1296
  }
849
-
1297
+
850
1298
  /**
851
1299
  * Generates formatted export content for a single event
852
1300
  * @return Formatted string containing the single debug event
@@ -855,18 +1303,18 @@ class EventDetailViewController: UIViewController {
855
1303
  let formatter = DateFormatter()
856
1304
  formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
857
1305
  let exportDate = formatter.string(from: Date())
858
-
1306
+
859
1307
  let eventContent = """
860
1308
  Attentive React Native SDK - Single Event Export
861
1309
  Generated: \(exportDate)
862
-
1310
+
863
1311
  \(String(repeating: "=", count: 60))
864
-
1312
+
865
1313
  \(event.formatForExport())
866
1314
  \(String(repeating: "=", count: 60))
867
1315
  End of Single Event Export
868
1316
  """
869
-
1317
+
870
1318
  return eventContent
871
1319
  }
872
1320