@attentive-mobile/attentive-react-native-sdk 1.0.3-beta.1 → 2.0.0-beta.1

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 (34) hide show
  1. package/README.md +150 -0
  2. package/android/build.gradle +4 -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 +438 -0
  6. package/android/src/main/kotlin/com/attentivereactnativesdk/debug/DebugEvent.kt +76 -0
  7. package/attentive-react-native-sdk.podspec +4 -5
  8. package/ios/AttentiveReactNativeSdk.h +6 -6
  9. package/ios/AttentiveReactNativeSdk.mm +325 -35
  10. package/ios/AttentiveReactNativeSdk.xcodeproj/project.pbxproj +2 -2
  11. package/ios/Bridging/ATTNNativeSDK.swift +1118 -3
  12. package/ios/Bridging/AttentiveReactNativeSdk-Bridging-Header.h +3 -0
  13. package/ios/Bridging/AttentiveSDKManager.swift +83 -0
  14. package/ios/Podfile +4 -17
  15. package/lib/commonjs/NativeAttentiveReactNativeSdk.js +14 -0
  16. package/lib/commonjs/NativeAttentiveReactNativeSdk.js.map +1 -0
  17. package/lib/commonjs/index.js +363 -39
  18. package/lib/commonjs/index.js.map +1 -1
  19. package/lib/module/NativeAttentiveReactNativeSdk.js +7 -0
  20. package/lib/module/NativeAttentiveReactNativeSdk.js.map +1 -0
  21. package/lib/module/index.js +346 -38
  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 -33
  28. package/lib/typescript/index.d.ts.map +1 -1
  29. package/package.json +22 -8
  30. package/src/NativeAttentiveReactNativeSdk.ts +152 -0
  31. package/src/eventTypes.tsx +57 -20
  32. package/src/index.tsx +472 -82
  33. package/android/src/main/java/com/attentivereactnativesdk/AttentiveReactNativeSdkModule.java +0 -247
  34. package/android/src/main/java/com/attentivereactnativesdk/AttentiveReactNativeSdkPackage.java +0 -28
@@ -8,25 +8,120 @@
8
8
 
9
9
  import Foundation
10
10
  import attentive_ios_sdk
11
+ import UIKit
12
+ import UserNotifications
13
+
14
+ // Debug Event structure for session history
15
+ struct DebugEvent {
16
+ let id: UUID
17
+ let timestamp: Date
18
+ let eventType: String
19
+ let data: [String: Any]
20
+
21
+ init(eventType: String, data: [String: Any]) {
22
+ self.id = UUID()
23
+ self.timestamp = Date()
24
+ self.eventType = eventType
25
+ self.data = data
26
+ }
27
+
28
+ /**
29
+ * Formats the debug event as a human-readable string for export
30
+ * @return A formatted string containing timestamp, event type, and data
31
+ */
32
+ func formatForExport() -> String {
33
+ let formatter = DateFormatter()
34
+ formatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS"
35
+ let timeString = formatter.string(from: timestamp)
36
+
37
+ var output = "[\(timeString)] \(eventType)\n"
38
+
39
+ // Add summary information if available
40
+ let summary = getSummary()
41
+ if !summary.isEmpty {
42
+ output += "Summary: \(summary)\n"
43
+ }
44
+
45
+ output += "Data:\n"
46
+
47
+ // Format data as JSON for better readability
48
+ do {
49
+ let jsonData = try JSONSerialization.data(withJSONObject: data, options: .prettyPrinted)
50
+ if let jsonString = String(data: jsonData, encoding: .utf8) {
51
+ output += jsonString
52
+ } else {
53
+ output += "\(data)"
54
+ }
55
+ } catch {
56
+ output += "\(data)"
57
+ }
58
+
59
+ return output + "\n" + String(repeating: "=", count: 50) + "\n"
60
+ }
61
+
62
+ /**
63
+ * Generates a summary of the debug event for quick overview
64
+ * @return A brief summary string highlighting key information
65
+ */
66
+ private func getSummary() -> String {
67
+ var summaryParts: [String] = []
68
+
69
+ if let itemsCount = data["items_count"] as? String {
70
+ summaryParts.append("Items: \(itemsCount)")
71
+ }
72
+ if let orderId = data["order_id"] as? String {
73
+ summaryParts.append("Order: \(orderId)")
74
+ }
75
+ if let creativeId = data["creativeId"] as? String {
76
+ summaryParts.append("Creative: \(creativeId)")
77
+ }
78
+ if let eventType = data["event_type"] as? String {
79
+ summaryParts.append("Type: \(eventType)")
80
+ }
81
+
82
+ // Always show payload size info
83
+ summaryParts.append("Payload: \(data.count) fields")
84
+
85
+ return summaryParts.joined(separator: " • ")
86
+ }
87
+ }
11
88
 
12
89
  @objc public class ATTNNativeSDK: NSObject {
13
90
  private let sdk: ATTNSDK
91
+ private var debuggingEnabled: Bool = false
92
+ private var debugOverlayWindow: UIWindow?
93
+ private var debugHistory: [DebugEvent] = []
14
94
 
15
- @objc(initWithDomain:mode:skipFatigueOnCreatives:)
16
- public init(domain: String, mode: String, skipFatigueOnCreatives: Bool) {
95
+ @objc(initWithDomain:mode:skipFatigueOnCreatives:enableDebugger:)
96
+ public init(domain: String, mode: String, skipFatigueOnCreatives: Bool, enableDebugger: Bool) {
17
97
  self.sdk = ATTNSDK(domain: domain, mode: ATTNSDKMode(rawValue: mode) ?? .production)
18
- self.sdk.skipFatigueOnCreative = skipFatigueOnCreatives ?? false
98
+ self.sdk.skipFatigueOnCreative = skipFatigueOnCreatives
99
+
100
+ // Only enable debugging if both enableDebugger is true AND the app is running in debug mode
101
+ #if DEBUG
102
+ let isDebugBuild = true
103
+ #else
104
+ let isDebugBuild = false
105
+ #endif
106
+ self.debuggingEnabled = enableDebugger && isDebugBuild
107
+
19
108
  ATTNEventTracker.setup(with: sdk)
20
109
  }
21
110
 
22
111
  @objc(trigger:)
23
112
  public func trigger(_ view: UIView) {
24
113
  sdk.trigger(view)
114
+ if debuggingEnabled {
115
+ showDebugInfo(event: "Creative Triggered", data: ["type": "trigger", "creativeId": "default"])
116
+ }
25
117
  }
26
118
 
27
119
  @objc(trigger:creativeId:)
28
120
  public func trigger(_ view: UIView, creativeId: String) {
29
121
  sdk.trigger(view, creativeId:creativeId)
122
+ if debuggingEnabled {
123
+ showDebugInfo(event: "Creative Triggered", data: ["type": "trigger", "creativeId": creativeId])
124
+ }
30
125
  }
31
126
 
32
127
  @objc(updateDomain:)
@@ -36,22 +131,435 @@ import attentive_ios_sdk
36
131
 
37
132
  @objc(identify:)
38
133
  public func identify(_ identifiers: [String: Any]) {
134
+ print("👤 [AttentiveSDK] identify called from React Native")
135
+ print(" Identifiers: \(identifiers)")
136
+
39
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")
40
142
  }
41
143
 
42
144
  @objc
43
145
  public func clearUser() {
44
146
  sdk.clearUser()
45
147
  }
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
+
489
+ @objc
490
+ public func invokeAttentiveDebugHelper() {
491
+ if debuggingEnabled {
492
+ // Don't add to history - this is just for viewing existing debug data
493
+ DispatchQueue.main.async {
494
+ guard let keyWindow = UIApplication.shared.connectedScenes
495
+ .compactMap({ $0 as? UIWindowScene })
496
+ .first?.windows
497
+ .first(where: { $0.isKeyWindow }) else { return }
498
+
499
+ let debugVC = DebugOverlayViewController(currentEvent: "Manual Debug View", currentData: ["action": "manual_debug_call", "session_events": "\(self.debugHistory.count)"], history: self.debugHistory)
500
+ debugVC.modalPresentationStyle = .overFullScreen
501
+ debugVC.modalTransitionStyle = .crossDissolve
502
+
503
+ keyWindow.rootViewController?.present(debugVC, animated: true)
504
+ }
505
+ }
506
+ }
507
+
46
508
  }
47
509
 
48
510
  public extension ATTNNativeSDK {
511
+ /**
512
+ * Exports the current debug session logs as a formatted string
513
+ * @return A comprehensive formatted string containing all debug events in the current session
514
+ */
515
+ @objc
516
+ func exportDebugLogs() -> String {
517
+ guard debuggingEnabled else {
518
+ return "Debug logging is not enabled. Please enable debugging to export logs."
519
+ }
520
+
521
+ if debugHistory.isEmpty {
522
+ return "No debug events recorded in this session."
523
+ }
524
+
525
+ let formatter = DateFormatter()
526
+ formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
527
+ let exportDate = formatter.string(from: Date())
528
+
529
+ var exportContent = """
530
+ Attentive React Native SDK - Debug Session Export
531
+ Generated: \(exportDate)
532
+ Total Events: \(debugHistory.count)
533
+
534
+ \(String(repeating: "=", count: 60))
535
+
536
+ """
537
+
538
+ // Add all events in chronological order (oldest first for better readability)
539
+ for (index, event) in debugHistory.enumerated() {
540
+ exportContent += "Event #\(index + 1)\n"
541
+ exportContent += event.formatForExport()
542
+ exportContent += "\n"
543
+ }
544
+
545
+ exportContent += """
546
+ \(String(repeating: "=", count: 60))
547
+ End of Debug Session Export
548
+ """
549
+
550
+ return exportContent
551
+ }
552
+
49
553
  @objc
50
554
  func recordAddToCartEvent(_ attributes: [String: Any]) {
51
555
  let items = parseItems(attributes["items"] as? [[String : Any]] ?? [])
52
556
  let deeplink = attributes["deeplink"] as? String ?? ""
53
557
  let event = ATTNAddToCartEvent(items: items, deeplink: deeplink)
54
558
  ATTNEventTracker.sharedInstance()?.record(event: event)
559
+
560
+ if debuggingEnabled {
561
+ showDebugInfo(event: "Add To Cart Event", data: ["items_count": "\(items.count)", "deeplink": deeplink, "payload": attributes])
562
+ }
55
563
  }
56
564
 
57
565
  @objc
@@ -60,6 +568,10 @@ public extension ATTNNativeSDK {
60
568
  let deeplink = attributes["deeplink"] as? String ?? ""
61
569
  let event = ATTNProductViewEvent(items: items, deeplink: deeplink)
62
570
  ATTNEventTracker.sharedInstance()?.record(event: event)
571
+
572
+ if debuggingEnabled {
573
+ showDebugInfo(event: "Product View Event", data: ["items_count": "\(items.count)", "deeplink": deeplink, "payload": attributes])
574
+ }
63
575
  }
64
576
 
65
577
  @objc
@@ -70,6 +582,10 @@ public extension ATTNNativeSDK {
70
582
  let items = parseItems(attributes["items"] as? [[String : Any]] ?? [])
71
583
  let event = ATTNPurchaseEvent(items: items, order: order)
72
584
  ATTNEventTracker.sharedInstance()?.record(event: event)
585
+
586
+ if debuggingEnabled {
587
+ showDebugInfo(event: "Purchase Event", data: ["items_count": "\(items.count)", "order_id": orderId, "payload": attributes])
588
+ }
73
589
  }
74
590
 
75
591
  @objc
@@ -78,6 +594,10 @@ public extension ATTNNativeSDK {
78
594
  let properties = attributes["properties"] as? [String: String] ?? [:]
79
595
  guard let customEvent = ATTNCustomEvent(type: type, properties: properties) else { return }
80
596
  ATTNEventTracker.sharedInstance()?.record(event: customEvent)
597
+
598
+ if debuggingEnabled {
599
+ showDebugInfo(event: "Custom Event", data: ["event_type": type, "properties_count": "\(properties.count)", "payload": attributes])
600
+ }
81
601
  }
82
602
  }
83
603
 
@@ -105,4 +625,599 @@ private extension ATTNNativeSDK {
105
625
 
106
626
  return itemsToReturn
107
627
  }
628
+
629
+ func showDebugInfo(event: String, data: [String: Any]) {
630
+ // Add to debug history
631
+ let debugEvent = DebugEvent(eventType: event, data: data)
632
+ debugHistory.append(debugEvent)
633
+
634
+ DispatchQueue.main.async {
635
+ // Create debug overlay with history
636
+ guard let keyWindow = UIApplication.shared.connectedScenes
637
+ .compactMap({ $0 as? UIWindowScene })
638
+ .first?.windows
639
+ .first(where: { $0.isKeyWindow }) else { return }
640
+
641
+ let debugVC = DebugOverlayViewController(currentEvent: event, currentData: data, history: self.debugHistory)
642
+ debugVC.modalPresentationStyle = .overFullScreen
643
+ debugVC.modalTransitionStyle = .crossDissolve
644
+
645
+ keyWindow.rootViewController?.present(debugVC, animated: true)
646
+ }
647
+ }
648
+ }
649
+
650
+ // Debug Overlay View Controller
651
+ class DebugOverlayViewController: UIViewController {
652
+ private let currentEvent: String
653
+ private let currentData: [String: Any]
654
+ private let history: [DebugEvent]
655
+
656
+ private var segmentedControl: UISegmentedControl!
657
+ private var containerView: UIView!
658
+ private var currentEventView: UIView!
659
+ private var historyView: UIView!
660
+
661
+ init(currentEvent: String, currentData: [String: Any], history: [DebugEvent]) {
662
+ self.currentEvent = currentEvent
663
+ self.currentData = currentData
664
+ self.history = history
665
+ super.init(nibName: nil, bundle: nil)
666
+ }
667
+
668
+ required init?(coder: NSCoder) {
669
+ fatalError("init(coder:) has not been implemented")
670
+ }
671
+
672
+ override func viewDidLoad() {
673
+ super.viewDidLoad()
674
+ setupUI()
675
+ }
676
+
677
+ private func setupUI() {
678
+ view.backgroundColor = UIColor.black.withAlphaComponent(0.8)
679
+
680
+ // Main container
681
+ containerView = UIView()
682
+ containerView.backgroundColor = UIColor.systemBackground
683
+ containerView.layer.cornerRadius = 12
684
+ containerView.layer.shadowColor = UIColor.black.cgColor
685
+ containerView.layer.shadowOpacity = 0.3
686
+ containerView.layer.shadowOffset = CGSize(width: 0, height: 2)
687
+ containerView.layer.shadowRadius = 8
688
+ containerView.translatesAutoresizingMaskIntoConstraints = false
689
+ view.addSubview(containerView)
690
+
691
+ // Title
692
+ let titleLabel = UILabel()
693
+ titleLabel.text = "🐛 Attentive Debug Session"
694
+ titleLabel.font = UIFont.boldSystemFont(ofSize: 18)
695
+ titleLabel.textAlignment = .center
696
+ titleLabel.translatesAutoresizingMaskIntoConstraints = false
697
+ containerView.addSubview(titleLabel)
698
+
699
+ // Segmented control for Current/History
700
+ segmentedControl = UISegmentedControl(items: ["Current Event", "Session History (\(history.count))"])
701
+ segmentedControl.selectedSegmentIndex = 0
702
+ segmentedControl.addTarget(self, action: #selector(segmentChanged), for: .valueChanged)
703
+ segmentedControl.translatesAutoresizingMaskIntoConstraints = false
704
+ containerView.addSubview(segmentedControl)
705
+
706
+ // Content container for switching views
707
+ let contentContainer = UIView()
708
+ contentContainer.translatesAutoresizingMaskIntoConstraints = false
709
+ containerView.addSubview(contentContainer)
710
+
711
+ // Setup current event view
712
+ setupCurrentEventView()
713
+ contentContainer.addSubview(currentEventView)
714
+
715
+ // Setup history view
716
+ setupHistoryView()
717
+ contentContainer.addSubview(historyView)
718
+ historyView.isHidden = true
719
+
720
+ // Share button in top-right corner (left of close button)
721
+ let shareButton = UIButton(type: .system)
722
+ shareButton.setTitle("↗", for: .normal) // iOS-style share symbol
723
+ shareButton.titleLabel?.font = UIFont.systemFont(ofSize: 18, weight: .medium)
724
+ shareButton.setTitleColor(.systemBlue, for: .normal)
725
+ shareButton.backgroundColor = UIColor.systemGray5
726
+ shareButton.layer.cornerRadius = 15
727
+ shareButton.addTarget(self, action: #selector(shareButtonTapped), for: .touchUpInside)
728
+ shareButton.translatesAutoresizingMaskIntoConstraints = false
729
+ containerView.addSubview(shareButton)
730
+
731
+ // X Close button in top-right corner
732
+ let closeButton = UIButton(type: .system)
733
+ closeButton.setTitle("✕", for: .normal)
734
+ closeButton.titleLabel?.font = UIFont.systemFont(ofSize: 18, weight: .medium)
735
+ closeButton.setTitleColor(.secondaryLabel, for: .normal)
736
+ closeButton.backgroundColor = UIColor.systemGray5
737
+ closeButton.layer.cornerRadius = 15
738
+ closeButton.addTarget(self, action: #selector(closeButtonTapped), for: .touchUpInside)
739
+ closeButton.translatesAutoresizingMaskIntoConstraints = false
740
+ containerView.addSubview(closeButton)
741
+
742
+ // Layout constraints - position at bottom and make larger
743
+ NSLayoutConstraint.activate([
744
+ containerView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
745
+ containerView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
746
+ containerView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -16),
747
+ containerView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.65),
748
+
749
+ closeButton.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 12),
750
+ closeButton.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -12),
751
+ closeButton.widthAnchor.constraint(equalToConstant: 30),
752
+ closeButton.heightAnchor.constraint(equalToConstant: 30),
753
+
754
+ shareButton.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 12),
755
+ shareButton.trailingAnchor.constraint(equalTo: closeButton.leadingAnchor, constant: -8),
756
+ shareButton.widthAnchor.constraint(equalToConstant: 30),
757
+ shareButton.heightAnchor.constraint(equalToConstant: 30),
758
+
759
+ titleLabel.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 16),
760
+ titleLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 16),
761
+ titleLabel.trailingAnchor.constraint(equalTo: shareButton.leadingAnchor, constant: -8),
762
+
763
+ segmentedControl.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 16),
764
+ segmentedControl.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 16),
765
+ segmentedControl.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -16),
766
+
767
+ contentContainer.topAnchor.constraint(equalTo: segmentedControl.bottomAnchor, constant: 16),
768
+ contentContainer.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 16),
769
+ contentContainer.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -16),
770
+ contentContainer.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -16),
771
+
772
+ currentEventView.topAnchor.constraint(equalTo: contentContainer.topAnchor),
773
+ currentEventView.leadingAnchor.constraint(equalTo: contentContainer.leadingAnchor),
774
+ currentEventView.trailingAnchor.constraint(equalTo: contentContainer.trailingAnchor),
775
+ currentEventView.bottomAnchor.constraint(equalTo: contentContainer.bottomAnchor),
776
+
777
+ historyView.topAnchor.constraint(equalTo: contentContainer.topAnchor),
778
+ historyView.leadingAnchor.constraint(equalTo: contentContainer.leadingAnchor),
779
+ historyView.trailingAnchor.constraint(equalTo: contentContainer.trailingAnchor),
780
+ historyView.bottomAnchor.constraint(equalTo: contentContainer.bottomAnchor)
781
+ ])
782
+
783
+ // Auto-dismiss after 8 seconds (longer for history viewing)
784
+ DispatchQueue.main.asyncAfter(deadline: .now() + 8.0) {
785
+ self.dismiss(animated: true)
786
+ }
787
+ }
788
+
789
+ private func formatData(_ data: [String: Any]) -> String {
790
+ do {
791
+ let jsonData = try JSONSerialization.data(withJSONObject: data, options: .prettyPrinted)
792
+ return String(data: jsonData, encoding: .utf8) ?? "Unable to format data"
793
+ } catch {
794
+ return "Error formatting data: \(error.localizedDescription)"
795
+ }
796
+ }
797
+
798
+ private func setupCurrentEventView() {
799
+ currentEventView = UIView()
800
+ currentEventView.translatesAutoresizingMaskIntoConstraints = false
801
+
802
+ let eventLabel = UILabel()
803
+ eventLabel.text = "Event: \(currentEvent)"
804
+ eventLabel.font = UIFont.systemFont(ofSize: 16, weight: .medium)
805
+ eventLabel.translatesAutoresizingMaskIntoConstraints = false
806
+ currentEventView.addSubview(eventLabel)
807
+
808
+ let scrollView = UIScrollView()
809
+ scrollView.translatesAutoresizingMaskIntoConstraints = false
810
+ currentEventView.addSubview(scrollView)
811
+
812
+ let dataLabel = UILabel()
813
+ dataLabel.text = formatData(currentData)
814
+ dataLabel.font = UIFont.monospacedSystemFont(ofSize: 12, weight: .regular)
815
+ dataLabel.numberOfLines = 0
816
+ dataLabel.translatesAutoresizingMaskIntoConstraints = false
817
+ scrollView.addSubview(dataLabel)
818
+
819
+ NSLayoutConstraint.activate([
820
+ eventLabel.topAnchor.constraint(equalTo: currentEventView.topAnchor),
821
+ eventLabel.leadingAnchor.constraint(equalTo: currentEventView.leadingAnchor),
822
+ eventLabel.trailingAnchor.constraint(equalTo: currentEventView.trailingAnchor),
823
+
824
+ scrollView.topAnchor.constraint(equalTo: eventLabel.bottomAnchor, constant: 16),
825
+ scrollView.leadingAnchor.constraint(equalTo: currentEventView.leadingAnchor),
826
+ scrollView.trailingAnchor.constraint(equalTo: currentEventView.trailingAnchor),
827
+ scrollView.bottomAnchor.constraint(equalTo: currentEventView.bottomAnchor),
828
+
829
+ dataLabel.topAnchor.constraint(equalTo: scrollView.topAnchor),
830
+ dataLabel.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
831
+ dataLabel.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
832
+ dataLabel.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
833
+ dataLabel.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
834
+ ])
835
+ }
836
+
837
+ private func setupHistoryView() {
838
+ historyView = UIView()
839
+ historyView.translatesAutoresizingMaskIntoConstraints = false
840
+
841
+ if history.isEmpty {
842
+ let emptyLabel = UILabel()
843
+ emptyLabel.text = "No events recorded in this session yet."
844
+ emptyLabel.font = UIFont.systemFont(ofSize: 16)
845
+ emptyLabel.textColor = .secondaryLabel
846
+ emptyLabel.textAlignment = .center
847
+ emptyLabel.translatesAutoresizingMaskIntoConstraints = false
848
+ historyView.addSubview(emptyLabel)
849
+
850
+ NSLayoutConstraint.activate([
851
+ emptyLabel.centerXAnchor.constraint(equalTo: historyView.centerXAnchor),
852
+ emptyLabel.centerYAnchor.constraint(equalTo: historyView.centerYAnchor),
853
+ ])
854
+ } else {
855
+ let tableView = UITableView()
856
+ tableView.translatesAutoresizingMaskIntoConstraints = false
857
+ tableView.dataSource = self
858
+ tableView.delegate = self
859
+ tableView.register(DebugHistoryCell.self, forCellReuseIdentifier: "DebugHistoryCell")
860
+ tableView.backgroundColor = .clear
861
+ historyView.addSubview(tableView)
862
+
863
+ NSLayoutConstraint.activate([
864
+ tableView.topAnchor.constraint(equalTo: historyView.topAnchor),
865
+ tableView.leadingAnchor.constraint(equalTo: historyView.leadingAnchor),
866
+ tableView.trailingAnchor.constraint(equalTo: historyView.trailingAnchor),
867
+ tableView.bottomAnchor.constraint(equalTo: historyView.bottomAnchor),
868
+ ])
869
+ }
870
+ }
871
+
872
+ @objc private func segmentChanged(_ sender: UISegmentedControl) {
873
+ if sender.selectedSegmentIndex == 0 {
874
+ currentEventView.isHidden = false
875
+ historyView.isHidden = true
876
+ } else {
877
+ currentEventView.isHidden = true
878
+ historyView.isHidden = false
879
+ }
880
+ }
881
+
882
+ /**
883
+ * Handles the share button tap to export and share debug logs
884
+ */
885
+ @objc private func shareButtonTapped() {
886
+ // Generate export content for the current history
887
+ let exportContent = generateExportContent()
888
+
889
+ // Create activity view controller for sharing
890
+ let activityVC = UIActivityViewController(activityItems: [exportContent], applicationActivities: nil)
891
+
892
+ // For iPad - prevent crash by setting popover presentation controller
893
+ if let popover = activityVC.popoverPresentationController {
894
+ // Find the share button view to anchor the popover
895
+ if let shareButton = view.subviews.first(where: { $0.accessibilityLabel == "shareButton" }) {
896
+ popover.sourceView = shareButton
897
+ popover.sourceRect = shareButton.bounds
898
+ } else {
899
+ popover.sourceView = view
900
+ popover.sourceRect = CGRect(x: view.bounds.midX, y: view.bounds.midY, width: 0, height: 0)
901
+ }
902
+ }
903
+
904
+ present(activityVC, animated: true)
905
+ }
906
+
907
+ /**
908
+ * Generates formatted export content for sharing
909
+ * @return Formatted string containing all debug events
910
+ */
911
+ private func generateExportContent() -> String {
912
+ if history.isEmpty {
913
+ return "No debug events recorded in this session."
914
+ }
915
+
916
+ let formatter = DateFormatter()
917
+ formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
918
+ let exportDate = formatter.string(from: Date())
919
+
920
+ var exportContent = """
921
+ Attentive React Native SDK - Debug Session Export
922
+ Generated: \(exportDate)
923
+ Total Events: \(history.count)
924
+
925
+ \(String(repeating: "=", count: 60))
926
+
927
+ """
928
+
929
+ // Add all events in chronological order (oldest first for better readability)
930
+ for (index, event) in history.enumerated() {
931
+ exportContent += "Event #\(index + 1)\n"
932
+ exportContent += event.formatForExport()
933
+ exportContent += "\n"
934
+ }
935
+
936
+ exportContent += """
937
+ \(String(repeating: "=", count: 60))
938
+ End of Debug Session Export
939
+ """
940
+
941
+ return exportContent
942
+ }
943
+
944
+ @objc private func closeButtonTapped() {
945
+ dismiss(animated: true)
946
+ }
947
+ }
948
+
949
+ // MARK: - TableView DataSource and Delegate
950
+ extension DebugOverlayViewController: UITableViewDataSource, UITableViewDelegate {
951
+ func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
952
+ return history.count
953
+ }
954
+
955
+ func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
956
+ let cell = tableView.dequeueReusableCell(withIdentifier: "DebugHistoryCell", for: indexPath) as! DebugHistoryCell
957
+ let event = history[history.count - 1 - indexPath.row] // Show newest first
958
+ cell.configure(with: event)
959
+ return cell
960
+ }
961
+
962
+ func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
963
+ return UITableView.automaticDimension
964
+ }
965
+
966
+ func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
967
+ tableView.deselectRow(at: indexPath, animated: true)
968
+ let event = history[history.count - 1 - indexPath.row]
969
+
970
+ // Show detailed view of selected event
971
+ let detailVC = EventDetailViewController(event: event)
972
+ detailVC.modalPresentationStyle = .overFullScreen
973
+ present(detailVC, animated: true)
974
+ }
975
+ }
976
+
977
+ // MARK: - Debug History Cell
978
+ class DebugHistoryCell: UITableViewCell {
979
+ private let timeLabel = UILabel()
980
+ private let eventLabel = UILabel()
981
+ private let summaryLabel = UILabel()
982
+
983
+ override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
984
+ super.init(style: style, reuseIdentifier: reuseIdentifier)
985
+ setupUI()
986
+ }
987
+
988
+ required init?(coder: NSCoder) {
989
+ fatalError("init(coder:) has not been implemented")
990
+ }
991
+
992
+ private func setupUI() {
993
+ backgroundColor = .clear
994
+
995
+ timeLabel.font = UIFont.monospacedSystemFont(ofSize: 11, weight: .regular)
996
+ timeLabel.textColor = .secondaryLabel
997
+ timeLabel.translatesAutoresizingMaskIntoConstraints = false
998
+ contentView.addSubview(timeLabel)
999
+
1000
+ eventLabel.font = UIFont.systemFont(ofSize: 14, weight: .medium)
1001
+ eventLabel.translatesAutoresizingMaskIntoConstraints = false
1002
+ contentView.addSubview(eventLabel)
1003
+
1004
+ summaryLabel.font = UIFont.systemFont(ofSize: 12)
1005
+ summaryLabel.textColor = .secondaryLabel
1006
+ summaryLabel.numberOfLines = 2
1007
+ summaryLabel.translatesAutoresizingMaskIntoConstraints = false
1008
+ contentView.addSubview(summaryLabel)
1009
+
1010
+ NSLayoutConstraint.activate([
1011
+ timeLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8),
1012
+ timeLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
1013
+
1014
+ eventLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8),
1015
+ eventLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
1016
+ eventLabel.trailingAnchor.constraint(equalTo: timeLabel.leadingAnchor, constant: -8),
1017
+
1018
+ summaryLabel.topAnchor.constraint(equalTo: eventLabel.bottomAnchor, constant: 4),
1019
+ summaryLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
1020
+ summaryLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
1021
+ summaryLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -8),
1022
+ ])
1023
+ }
1024
+
1025
+ func configure(with event: DebugEvent) {
1026
+ let formatter = DateFormatter()
1027
+ formatter.dateFormat = "HH:mm:ss"
1028
+ timeLabel.text = formatter.string(from: event.timestamp)
1029
+
1030
+ eventLabel.text = event.eventType
1031
+
1032
+ // Create summary from data - always show payload info
1033
+ var summaryParts: [String] = []
1034
+
1035
+ // Add key-value summary
1036
+ if let itemsCount = event.data["items_count"] as? String {
1037
+ summaryParts.append("Items: \(itemsCount)")
1038
+ }
1039
+ if let orderId = event.data["order_id"] as? String {
1040
+ summaryParts.append("Order: \(orderId)")
1041
+ }
1042
+ if let creativeId = event.data["creativeId"] as? String {
1043
+ summaryParts.append("Creative: \(creativeId)")
1044
+ }
1045
+ if let eventType = event.data["event_type"] as? String {
1046
+ summaryParts.append("Type: \(eventType)")
1047
+ }
1048
+
1049
+ // Always show payload size info
1050
+ let payloadInfo = "Payload: \(event.data.count) fields"
1051
+ summaryParts.append(payloadInfo)
1052
+
1053
+ summaryLabel.text = summaryParts.joined(separator: " • ") + " (Tap for details)"
1054
+ }
1055
+ }
1056
+
1057
+ // MARK: - Event Detail View Controller
1058
+ class EventDetailViewController: UIViewController {
1059
+ private let event: DebugEvent
1060
+
1061
+ init(event: DebugEvent) {
1062
+ self.event = event
1063
+ super.init(nibName: nil, bundle: nil)
1064
+ }
1065
+
1066
+ required init?(coder: NSCoder) {
1067
+ fatalError("init(coder:) has not been implemented")
1068
+ }
1069
+
1070
+ override func viewDidLoad() {
1071
+ super.viewDidLoad()
1072
+ setupUI()
1073
+ }
1074
+
1075
+ private func setupUI() {
1076
+ view.backgroundColor = UIColor.black.withAlphaComponent(0.8)
1077
+
1078
+ let containerView = UIView()
1079
+ containerView.backgroundColor = UIColor.systemBackground
1080
+ containerView.layer.cornerRadius = 12
1081
+ containerView.translatesAutoresizingMaskIntoConstraints = false
1082
+ view.addSubview(containerView)
1083
+
1084
+ let titleLabel = UILabel()
1085
+ titleLabel.text = event.eventType
1086
+ titleLabel.font = UIFont.boldSystemFont(ofSize: 18)
1087
+ titleLabel.textAlignment = .center
1088
+ titleLabel.translatesAutoresizingMaskIntoConstraints = false
1089
+ containerView.addSubview(titleLabel)
1090
+
1091
+ let timeLabel = UILabel()
1092
+ let formatter = DateFormatter()
1093
+ formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
1094
+ timeLabel.text = "Timestamp: \(formatter.string(from: event.timestamp))"
1095
+ timeLabel.font = UIFont.systemFont(ofSize: 14)
1096
+ timeLabel.textColor = .secondaryLabel
1097
+ timeLabel.textAlignment = .center
1098
+ timeLabel.translatesAutoresizingMaskIntoConstraints = false
1099
+ containerView.addSubview(timeLabel)
1100
+
1101
+ let scrollView = UIScrollView()
1102
+ scrollView.translatesAutoresizingMaskIntoConstraints = false
1103
+ containerView.addSubview(scrollView)
1104
+
1105
+ let dataLabel = UILabel()
1106
+ dataLabel.text = formatData(event.data)
1107
+ dataLabel.font = UIFont.monospacedSystemFont(ofSize: 12, weight: .regular)
1108
+ dataLabel.numberOfLines = 0
1109
+ dataLabel.translatesAutoresizingMaskIntoConstraints = false
1110
+ scrollView.addSubview(dataLabel)
1111
+
1112
+ // Share button for single event
1113
+ let shareButton = UIButton(type: .system)
1114
+ shareButton.setTitle("↗", for: .normal) // iOS-style share symbol
1115
+ shareButton.titleLabel?.font = UIFont.systemFont(ofSize: 18, weight: .medium)
1116
+ shareButton.setTitleColor(.systemBlue, for: .normal)
1117
+ shareButton.backgroundColor = UIColor.systemGray5
1118
+ shareButton.layer.cornerRadius = 15
1119
+ shareButton.addTarget(self, action: #selector(shareEventButtonTapped), for: .touchUpInside)
1120
+ shareButton.translatesAutoresizingMaskIntoConstraints = false
1121
+ containerView.addSubview(shareButton)
1122
+
1123
+ let closeButton = UIButton(type: .system)
1124
+ closeButton.setTitle("✕", for: .normal)
1125
+ closeButton.titleLabel?.font = UIFont.systemFont(ofSize: 18, weight: .medium)
1126
+ closeButton.setTitleColor(.secondaryLabel, for: .normal)
1127
+ closeButton.backgroundColor = UIColor.systemGray5
1128
+ closeButton.layer.cornerRadius = 15
1129
+ closeButton.addTarget(self, action: #selector(closeButtonTapped), for: .touchUpInside)
1130
+ closeButton.translatesAutoresizingMaskIntoConstraints = false
1131
+ containerView.addSubview(closeButton)
1132
+
1133
+ NSLayoutConstraint.activate([
1134
+ containerView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
1135
+ containerView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
1136
+ containerView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -16),
1137
+ containerView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.65),
1138
+
1139
+ closeButton.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 12),
1140
+ closeButton.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -12),
1141
+ closeButton.widthAnchor.constraint(equalToConstant: 30),
1142
+ closeButton.heightAnchor.constraint(equalToConstant: 30),
1143
+
1144
+ shareButton.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 12),
1145
+ shareButton.trailingAnchor.constraint(equalTo: closeButton.leadingAnchor, constant: -8),
1146
+ shareButton.widthAnchor.constraint(equalToConstant: 30),
1147
+ shareButton.heightAnchor.constraint(equalToConstant: 30),
1148
+
1149
+ titleLabel.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 16),
1150
+ titleLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 16),
1151
+ titleLabel.trailingAnchor.constraint(equalTo: shareButton.leadingAnchor, constant: -8),
1152
+
1153
+ timeLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8),
1154
+ timeLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 16),
1155
+ timeLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -16),
1156
+
1157
+ scrollView.topAnchor.constraint(equalTo: timeLabel.bottomAnchor, constant: 16),
1158
+ scrollView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 16),
1159
+ scrollView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -16),
1160
+ scrollView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -16),
1161
+
1162
+ dataLabel.topAnchor.constraint(equalTo: scrollView.topAnchor),
1163
+ dataLabel.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
1164
+ dataLabel.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
1165
+ dataLabel.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
1166
+ dataLabel.widthAnchor.constraint(equalTo: scrollView.widthAnchor)
1167
+ ])
1168
+ }
1169
+
1170
+ private func formatData(_ data: [String: Any]) -> String {
1171
+ do {
1172
+ let jsonData = try JSONSerialization.data(withJSONObject: data, options: .prettyPrinted)
1173
+ return String(data: jsonData, encoding: .utf8) ?? "Unable to format data"
1174
+ } catch {
1175
+ return "Error formatting data: \(error.localizedDescription)"
1176
+ }
1177
+ }
1178
+
1179
+ /**
1180
+ * Handles the share button tap to export and share a single debug event
1181
+ */
1182
+ @objc private func shareEventButtonTapped() {
1183
+ let exportContent = generateSingleEventExport()
1184
+
1185
+ // Create activity view controller for sharing
1186
+ let activityVC = UIActivityViewController(activityItems: [exportContent], applicationActivities: nil)
1187
+
1188
+ // For iPad - prevent crash by setting popover presentation controller
1189
+ if let popover = activityVC.popoverPresentationController {
1190
+ popover.sourceView = view
1191
+ popover.sourceRect = CGRect(x: view.bounds.midX, y: view.bounds.midY, width: 0, height: 0)
1192
+ }
1193
+
1194
+ present(activityVC, animated: true)
1195
+ }
1196
+
1197
+ /**
1198
+ * Generates formatted export content for a single event
1199
+ * @return Formatted string containing the single debug event
1200
+ */
1201
+ private func generateSingleEventExport() -> String {
1202
+ let formatter = DateFormatter()
1203
+ formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
1204
+ let exportDate = formatter.string(from: Date())
1205
+
1206
+ let eventContent = """
1207
+ Attentive React Native SDK - Single Event Export
1208
+ Generated: \(exportDate)
1209
+
1210
+ \(String(repeating: "=", count: 60))
1211
+
1212
+ \(event.formatForExport())
1213
+ \(String(repeating: "=", count: 60))
1214
+ End of Single Event Export
1215
+ """
1216
+
1217
+ return eventContent
1218
+ }
1219
+
1220
+ @objc private func closeButtonTapped() {
1221
+ dismiss(animated: true)
1222
+ }
108
1223
  }