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

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.
@@ -8,25 +8,120 @@
8
8
 
9
9
  import Foundation
10
10
  import attentive_ios_sdk
11
+ import UIKit
12
+
13
+ // Debug Event structure for session history
14
+ struct DebugEvent {
15
+ let id: UUID
16
+ let timestamp: Date
17
+ let eventType: String
18
+ let data: [String: Any]
19
+
20
+ init(eventType: String, data: [String: Any]) {
21
+ self.id = UUID()
22
+ self.timestamp = Date()
23
+ self.eventType = eventType
24
+ self.data = data
25
+ }
26
+
27
+ /**
28
+ * Formats the debug event as a human-readable string for export
29
+ * @return A formatted string containing timestamp, event type, and data
30
+ */
31
+ func formatForExport() -> String {
32
+ let formatter = DateFormatter()
33
+ formatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS"
34
+ let timeString = formatter.string(from: timestamp)
35
+
36
+ var output = "[\(timeString)] \(eventType)\n"
37
+
38
+ // Add summary information if available
39
+ let summary = getSummary()
40
+ if !summary.isEmpty {
41
+ output += "Summary: \(summary)\n"
42
+ }
43
+
44
+ output += "Data:\n"
45
+
46
+ // Format data as JSON for better readability
47
+ do {
48
+ let jsonData = try JSONSerialization.data(withJSONObject: data, options: .prettyPrinted)
49
+ if let jsonString = String(data: jsonData, encoding: .utf8) {
50
+ output += jsonString
51
+ } else {
52
+ output += "\(data)"
53
+ }
54
+ } catch {
55
+ output += "\(data)"
56
+ }
57
+
58
+ return output + "\n" + String(repeating: "=", count: 50) + "\n"
59
+ }
60
+
61
+ /**
62
+ * Generates a summary of the debug event for quick overview
63
+ * @return A brief summary string highlighting key information
64
+ */
65
+ private func getSummary() -> String {
66
+ var summaryParts: [String] = []
67
+
68
+ if let itemsCount = data["items_count"] as? String {
69
+ summaryParts.append("Items: \(itemsCount)")
70
+ }
71
+ if let orderId = data["order_id"] as? String {
72
+ summaryParts.append("Order: \(orderId)")
73
+ }
74
+ if let creativeId = data["creativeId"] as? String {
75
+ summaryParts.append("Creative: \(creativeId)")
76
+ }
77
+ if let eventType = data["event_type"] as? String {
78
+ summaryParts.append("Type: \(eventType)")
79
+ }
80
+
81
+ // Always show payload size info
82
+ summaryParts.append("Payload: \(data.count) fields")
83
+
84
+ return summaryParts.joined(separator: " • ")
85
+ }
86
+ }
11
87
 
12
88
  @objc public class ATTNNativeSDK: NSObject {
13
89
  private let sdk: ATTNSDK
90
+ private var debuggingEnabled: Bool = false
91
+ private var debugOverlayWindow: UIWindow?
92
+ private var debugHistory: [DebugEvent] = []
14
93
 
15
- @objc(initWithDomain:mode:skipFatigueOnCreatives:)
16
- public init(domain: String, mode: String, skipFatigueOnCreatives: Bool) {
94
+ @objc(initWithDomain:mode:skipFatigueOnCreatives:enableDebugger:)
95
+ public init(domain: String, mode: String, skipFatigueOnCreatives: Bool, enableDebugger: Bool) {
17
96
  self.sdk = ATTNSDK(domain: domain, mode: ATTNSDKMode(rawValue: mode) ?? .production)
18
97
  self.sdk.skipFatigueOnCreative = skipFatigueOnCreatives ?? false
98
+
99
+ // Only enable debugging if both enableDebugger is true AND the app is running in debug mode
100
+ let enableDebuggerFromConfig = enableDebugger ?? false
101
+ #if DEBUG
102
+ let isDebugBuild = true
103
+ #else
104
+ let isDebugBuild = false
105
+ #endif
106
+ self.debuggingEnabled = enableDebuggerFromConfig && 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:)
@@ -43,15 +138,81 @@ import attentive_ios_sdk
43
138
  public func clearUser() {
44
139
  sdk.clearUser()
45
140
  }
141
+
142
+ @objc
143
+ public func invokeAttentiveDebugHelper() {
144
+ if debuggingEnabled {
145
+ // Don't add to history - this is just for viewing existing debug data
146
+ DispatchQueue.main.async {
147
+ guard let keyWindow = UIApplication.shared.connectedScenes
148
+ .compactMap({ $0 as? UIWindowScene })
149
+ .first?.windows
150
+ .first(where: { $0.isKeyWindow }) else { return }
151
+
152
+ let debugVC = DebugOverlayViewController(currentEvent: "Manual Debug View", currentData: ["action": "manual_debug_call", "session_events": "\(self.debugHistory.count)"], history: self.debugHistory)
153
+ debugVC.modalPresentationStyle = .overFullScreen
154
+ debugVC.modalTransitionStyle = .crossDissolve
155
+
156
+ keyWindow.rootViewController?.present(debugVC, animated: true)
157
+ }
158
+ }
159
+ }
160
+
46
161
  }
47
162
 
48
163
  public extension ATTNNativeSDK {
164
+ /**
165
+ * Exports the current debug session logs as a formatted string
166
+ * @return A comprehensive formatted string containing all debug events in the current session
167
+ */
168
+ @objc
169
+ func exportDebugLogs() -> String {
170
+ guard debuggingEnabled else {
171
+ return "Debug logging is not enabled. Please enable debugging to export logs."
172
+ }
173
+
174
+ if debugHistory.isEmpty {
175
+ return "No debug events recorded in this session."
176
+ }
177
+
178
+ let formatter = DateFormatter()
179
+ formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
180
+ let exportDate = formatter.string(from: Date())
181
+
182
+ var exportContent = """
183
+ Attentive React Native SDK - Debug Session Export
184
+ Generated: \(exportDate)
185
+ Total Events: \(debugHistory.count)
186
+
187
+ \(String(repeating: "=", count: 60))
188
+
189
+ """
190
+
191
+ // Add all events in chronological order (oldest first for better readability)
192
+ for (index, event) in debugHistory.enumerated() {
193
+ exportContent += "Event #\(index + 1)\n"
194
+ exportContent += event.formatForExport()
195
+ exportContent += "\n"
196
+ }
197
+
198
+ exportContent += """
199
+ \(String(repeating: "=", count: 60))
200
+ End of Debug Session Export
201
+ """
202
+
203
+ return exportContent
204
+ }
205
+
49
206
  @objc
50
207
  func recordAddToCartEvent(_ attributes: [String: Any]) {
51
208
  let items = parseItems(attributes["items"] as? [[String : Any]] ?? [])
52
209
  let deeplink = attributes["deeplink"] as? String ?? ""
53
210
  let event = ATTNAddToCartEvent(items: items, deeplink: deeplink)
54
211
  ATTNEventTracker.sharedInstance()?.record(event: event)
212
+
213
+ if debuggingEnabled {
214
+ showDebugInfo(event: "Add To Cart Event", data: ["items_count": "\(items.count)", "deeplink": deeplink, "payload": attributes])
215
+ }
55
216
  }
56
217
 
57
218
  @objc
@@ -60,6 +221,10 @@ public extension ATTNNativeSDK {
60
221
  let deeplink = attributes["deeplink"] as? String ?? ""
61
222
  let event = ATTNProductViewEvent(items: items, deeplink: deeplink)
62
223
  ATTNEventTracker.sharedInstance()?.record(event: event)
224
+
225
+ if debuggingEnabled {
226
+ showDebugInfo(event: "Product View Event", data: ["items_count": "\(items.count)", "deeplink": deeplink, "payload": attributes])
227
+ }
63
228
  }
64
229
 
65
230
  @objc
@@ -70,6 +235,10 @@ public extension ATTNNativeSDK {
70
235
  let items = parseItems(attributes["items"] as? [[String : Any]] ?? [])
71
236
  let event = ATTNPurchaseEvent(items: items, order: order)
72
237
  ATTNEventTracker.sharedInstance()?.record(event: event)
238
+
239
+ if debuggingEnabled {
240
+ showDebugInfo(event: "Purchase Event", data: ["items_count": "\(items.count)", "order_id": orderId, "payload": attributes])
241
+ }
73
242
  }
74
243
 
75
244
  @objc
@@ -78,6 +247,10 @@ public extension ATTNNativeSDK {
78
247
  let properties = attributes["properties"] as? [String: String] ?? [:]
79
248
  guard let customEvent = ATTNCustomEvent(type: type, properties: properties) else { return }
80
249
  ATTNEventTracker.sharedInstance()?.record(event: customEvent)
250
+
251
+ if debuggingEnabled {
252
+ showDebugInfo(event: "Custom Event", data: ["event_type": type, "properties_count": "\(properties.count)", "payload": attributes])
253
+ }
81
254
  }
82
255
  }
83
256
 
@@ -105,4 +278,599 @@ private extension ATTNNativeSDK {
105
278
 
106
279
  return itemsToReturn
107
280
  }
281
+
282
+ func showDebugInfo(event: String, data: [String: Any]) {
283
+ // Add to debug history
284
+ let debugEvent = DebugEvent(eventType: event, data: data)
285
+ debugHistory.append(debugEvent)
286
+
287
+ DispatchQueue.main.async {
288
+ // Create debug overlay with history
289
+ guard let keyWindow = UIApplication.shared.connectedScenes
290
+ .compactMap({ $0 as? UIWindowScene })
291
+ .first?.windows
292
+ .first(where: { $0.isKeyWindow }) else { return }
293
+
294
+ let debugVC = DebugOverlayViewController(currentEvent: event, currentData: data, history: self.debugHistory)
295
+ debugVC.modalPresentationStyle = .overFullScreen
296
+ debugVC.modalTransitionStyle = .crossDissolve
297
+
298
+ keyWindow.rootViewController?.present(debugVC, animated: true)
299
+ }
300
+ }
301
+ }
302
+
303
+ // Debug Overlay View Controller
304
+ class DebugOverlayViewController: UIViewController {
305
+ private let currentEvent: String
306
+ private let currentData: [String: Any]
307
+ private let history: [DebugEvent]
308
+
309
+ private var segmentedControl: UISegmentedControl!
310
+ private var containerView: UIView!
311
+ private var currentEventView: UIView!
312
+ private var historyView: UIView!
313
+
314
+ init(currentEvent: String, currentData: [String: Any], history: [DebugEvent]) {
315
+ self.currentEvent = currentEvent
316
+ self.currentData = currentData
317
+ self.history = history
318
+ super.init(nibName: nil, bundle: nil)
319
+ }
320
+
321
+ required init?(coder: NSCoder) {
322
+ fatalError("init(coder:) has not been implemented")
323
+ }
324
+
325
+ override func viewDidLoad() {
326
+ super.viewDidLoad()
327
+ setupUI()
328
+ }
329
+
330
+ private func setupUI() {
331
+ view.backgroundColor = UIColor.black.withAlphaComponent(0.8)
332
+
333
+ // Main container
334
+ containerView = UIView()
335
+ containerView.backgroundColor = UIColor.systemBackground
336
+ containerView.layer.cornerRadius = 12
337
+ containerView.layer.shadowColor = UIColor.black.cgColor
338
+ containerView.layer.shadowOpacity = 0.3
339
+ containerView.layer.shadowOffset = CGSize(width: 0, height: 2)
340
+ containerView.layer.shadowRadius = 8
341
+ containerView.translatesAutoresizingMaskIntoConstraints = false
342
+ view.addSubview(containerView)
343
+
344
+ // Title
345
+ let titleLabel = UILabel()
346
+ titleLabel.text = "🐛 Attentive Debug Session"
347
+ titleLabel.font = UIFont.boldSystemFont(ofSize: 18)
348
+ titleLabel.textAlignment = .center
349
+ titleLabel.translatesAutoresizingMaskIntoConstraints = false
350
+ containerView.addSubview(titleLabel)
351
+
352
+ // Segmented control for Current/History
353
+ segmentedControl = UISegmentedControl(items: ["Current Event", "Session History (\(history.count))"])
354
+ segmentedControl.selectedSegmentIndex = 0
355
+ segmentedControl.addTarget(self, action: #selector(segmentChanged), for: .valueChanged)
356
+ segmentedControl.translatesAutoresizingMaskIntoConstraints = false
357
+ containerView.addSubview(segmentedControl)
358
+
359
+ // Content container for switching views
360
+ let contentContainer = UIView()
361
+ contentContainer.translatesAutoresizingMaskIntoConstraints = false
362
+ containerView.addSubview(contentContainer)
363
+
364
+ // Setup current event view
365
+ setupCurrentEventView()
366
+ contentContainer.addSubview(currentEventView)
367
+
368
+ // Setup history view
369
+ setupHistoryView()
370
+ contentContainer.addSubview(historyView)
371
+ historyView.isHidden = true
372
+
373
+ // Share button in top-right corner (left of close button)
374
+ let shareButton = UIButton(type: .system)
375
+ shareButton.setTitle("↗", for: .normal) // iOS-style share symbol
376
+ shareButton.titleLabel?.font = UIFont.systemFont(ofSize: 18, weight: .medium)
377
+ shareButton.setTitleColor(.systemBlue, for: .normal)
378
+ shareButton.backgroundColor = UIColor.systemGray5
379
+ shareButton.layer.cornerRadius = 15
380
+ shareButton.addTarget(self, action: #selector(shareButtonTapped), for: .touchUpInside)
381
+ shareButton.translatesAutoresizingMaskIntoConstraints = false
382
+ containerView.addSubview(shareButton)
383
+
384
+ // X Close button in top-right corner
385
+ let closeButton = UIButton(type: .system)
386
+ closeButton.setTitle("✕", for: .normal)
387
+ closeButton.titleLabel?.font = UIFont.systemFont(ofSize: 18, weight: .medium)
388
+ closeButton.setTitleColor(.secondaryLabel, for: .normal)
389
+ closeButton.backgroundColor = UIColor.systemGray5
390
+ closeButton.layer.cornerRadius = 15
391
+ closeButton.addTarget(self, action: #selector(closeButtonTapped), for: .touchUpInside)
392
+ closeButton.translatesAutoresizingMaskIntoConstraints = false
393
+ containerView.addSubview(closeButton)
394
+
395
+ // Layout constraints - position at bottom and make larger
396
+ NSLayoutConstraint.activate([
397
+ containerView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
398
+ containerView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
399
+ containerView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -16),
400
+ containerView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.65),
401
+
402
+ closeButton.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 12),
403
+ closeButton.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -12),
404
+ closeButton.widthAnchor.constraint(equalToConstant: 30),
405
+ closeButton.heightAnchor.constraint(equalToConstant: 30),
406
+
407
+ shareButton.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 12),
408
+ shareButton.trailingAnchor.constraint(equalTo: closeButton.leadingAnchor, constant: -8),
409
+ shareButton.widthAnchor.constraint(equalToConstant: 30),
410
+ shareButton.heightAnchor.constraint(equalToConstant: 30),
411
+
412
+ titleLabel.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 16),
413
+ titleLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 16),
414
+ titleLabel.trailingAnchor.constraint(equalTo: shareButton.leadingAnchor, constant: -8),
415
+
416
+ segmentedControl.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 16),
417
+ segmentedControl.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 16),
418
+ segmentedControl.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -16),
419
+
420
+ contentContainer.topAnchor.constraint(equalTo: segmentedControl.bottomAnchor, constant: 16),
421
+ contentContainer.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 16),
422
+ contentContainer.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -16),
423
+ contentContainer.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -16),
424
+
425
+ currentEventView.topAnchor.constraint(equalTo: contentContainer.topAnchor),
426
+ currentEventView.leadingAnchor.constraint(equalTo: contentContainer.leadingAnchor),
427
+ currentEventView.trailingAnchor.constraint(equalTo: contentContainer.trailingAnchor),
428
+ currentEventView.bottomAnchor.constraint(equalTo: contentContainer.bottomAnchor),
429
+
430
+ historyView.topAnchor.constraint(equalTo: contentContainer.topAnchor),
431
+ historyView.leadingAnchor.constraint(equalTo: contentContainer.leadingAnchor),
432
+ historyView.trailingAnchor.constraint(equalTo: contentContainer.trailingAnchor),
433
+ historyView.bottomAnchor.constraint(equalTo: contentContainer.bottomAnchor)
434
+ ])
435
+
436
+ // Auto-dismiss after 8 seconds (longer for history viewing)
437
+ DispatchQueue.main.asyncAfter(deadline: .now() + 8.0) {
438
+ self.dismiss(animated: true)
439
+ }
440
+ }
441
+
442
+ private func formatData(_ data: [String: Any]) -> String {
443
+ do {
444
+ let jsonData = try JSONSerialization.data(withJSONObject: data, options: .prettyPrinted)
445
+ return String(data: jsonData, encoding: .utf8) ?? "Unable to format data"
446
+ } catch {
447
+ return "Error formatting data: \(error.localizedDescription)"
448
+ }
449
+ }
450
+
451
+ private func setupCurrentEventView() {
452
+ currentEventView = UIView()
453
+ currentEventView.translatesAutoresizingMaskIntoConstraints = false
454
+
455
+ let eventLabel = UILabel()
456
+ eventLabel.text = "Event: \(currentEvent)"
457
+ eventLabel.font = UIFont.systemFont(ofSize: 16, weight: .medium)
458
+ eventLabel.translatesAutoresizingMaskIntoConstraints = false
459
+ currentEventView.addSubview(eventLabel)
460
+
461
+ let scrollView = UIScrollView()
462
+ scrollView.translatesAutoresizingMaskIntoConstraints = false
463
+ currentEventView.addSubview(scrollView)
464
+
465
+ let dataLabel = UILabel()
466
+ dataLabel.text = formatData(currentData)
467
+ dataLabel.font = UIFont.monospacedSystemFont(ofSize: 12, weight: .regular)
468
+ dataLabel.numberOfLines = 0
469
+ dataLabel.translatesAutoresizingMaskIntoConstraints = false
470
+ scrollView.addSubview(dataLabel)
471
+
472
+ NSLayoutConstraint.activate([
473
+ eventLabel.topAnchor.constraint(equalTo: currentEventView.topAnchor),
474
+ eventLabel.leadingAnchor.constraint(equalTo: currentEventView.leadingAnchor),
475
+ eventLabel.trailingAnchor.constraint(equalTo: currentEventView.trailingAnchor),
476
+
477
+ scrollView.topAnchor.constraint(equalTo: eventLabel.bottomAnchor, constant: 16),
478
+ scrollView.leadingAnchor.constraint(equalTo: currentEventView.leadingAnchor),
479
+ scrollView.trailingAnchor.constraint(equalTo: currentEventView.trailingAnchor),
480
+ scrollView.bottomAnchor.constraint(equalTo: currentEventView.bottomAnchor),
481
+
482
+ dataLabel.topAnchor.constraint(equalTo: scrollView.topAnchor),
483
+ dataLabel.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
484
+ dataLabel.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
485
+ dataLabel.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
486
+ dataLabel.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
487
+ ])
488
+ }
489
+
490
+ private func setupHistoryView() {
491
+ historyView = UIView()
492
+ historyView.translatesAutoresizingMaskIntoConstraints = false
493
+
494
+ if history.isEmpty {
495
+ let emptyLabel = UILabel()
496
+ emptyLabel.text = "No events recorded in this session yet."
497
+ emptyLabel.font = UIFont.systemFont(ofSize: 16)
498
+ emptyLabel.textColor = .secondaryLabel
499
+ emptyLabel.textAlignment = .center
500
+ emptyLabel.translatesAutoresizingMaskIntoConstraints = false
501
+ historyView.addSubview(emptyLabel)
502
+
503
+ NSLayoutConstraint.activate([
504
+ emptyLabel.centerXAnchor.constraint(equalTo: historyView.centerXAnchor),
505
+ emptyLabel.centerYAnchor.constraint(equalTo: historyView.centerYAnchor),
506
+ ])
507
+ } else {
508
+ let tableView = UITableView()
509
+ tableView.translatesAutoresizingMaskIntoConstraints = false
510
+ tableView.dataSource = self
511
+ tableView.delegate = self
512
+ tableView.register(DebugHistoryCell.self, forCellReuseIdentifier: "DebugHistoryCell")
513
+ tableView.backgroundColor = .clear
514
+ historyView.addSubview(tableView)
515
+
516
+ NSLayoutConstraint.activate([
517
+ tableView.topAnchor.constraint(equalTo: historyView.topAnchor),
518
+ tableView.leadingAnchor.constraint(equalTo: historyView.leadingAnchor),
519
+ tableView.trailingAnchor.constraint(equalTo: historyView.trailingAnchor),
520
+ tableView.bottomAnchor.constraint(equalTo: historyView.bottomAnchor),
521
+ ])
522
+ }
523
+ }
524
+
525
+ @objc private func segmentChanged(_ sender: UISegmentedControl) {
526
+ if sender.selectedSegmentIndex == 0 {
527
+ currentEventView.isHidden = false
528
+ historyView.isHidden = true
529
+ } else {
530
+ currentEventView.isHidden = true
531
+ historyView.isHidden = false
532
+ }
533
+ }
534
+
535
+ /**
536
+ * Handles the share button tap to export and share debug logs
537
+ */
538
+ @objc private func shareButtonTapped() {
539
+ // Generate export content for the current history
540
+ let exportContent = generateExportContent()
541
+
542
+ // Create activity view controller for sharing
543
+ let activityVC = UIActivityViewController(activityItems: [exportContent], applicationActivities: nil)
544
+
545
+ // For iPad - prevent crash by setting popover presentation controller
546
+ if let popover = activityVC.popoverPresentationController {
547
+ // Find the share button view to anchor the popover
548
+ if let shareButton = view.subviews.first(where: { $0.accessibilityLabel == "shareButton" }) {
549
+ popover.sourceView = shareButton
550
+ popover.sourceRect = shareButton.bounds
551
+ } else {
552
+ popover.sourceView = view
553
+ popover.sourceRect = CGRect(x: view.bounds.midX, y: view.bounds.midY, width: 0, height: 0)
554
+ }
555
+ }
556
+
557
+ present(activityVC, animated: true)
558
+ }
559
+
560
+ /**
561
+ * Generates formatted export content for sharing
562
+ * @return Formatted string containing all debug events
563
+ */
564
+ private func generateExportContent() -> String {
565
+ if history.isEmpty {
566
+ return "No debug events recorded in this session."
567
+ }
568
+
569
+ let formatter = DateFormatter()
570
+ formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
571
+ let exportDate = formatter.string(from: Date())
572
+
573
+ var exportContent = """
574
+ Attentive React Native SDK - Debug Session Export
575
+ Generated: \(exportDate)
576
+ Total Events: \(history.count)
577
+
578
+ \(String(repeating: "=", count: 60))
579
+
580
+ """
581
+
582
+ // Add all events in chronological order (oldest first for better readability)
583
+ for (index, event) in history.enumerated() {
584
+ exportContent += "Event #\(index + 1)\n"
585
+ exportContent += event.formatForExport()
586
+ exportContent += "\n"
587
+ }
588
+
589
+ exportContent += """
590
+ \(String(repeating: "=", count: 60))
591
+ End of Debug Session Export
592
+ """
593
+
594
+ return exportContent
595
+ }
596
+
597
+ @objc private func closeButtonTapped() {
598
+ dismiss(animated: true)
599
+ }
600
+ }
601
+
602
+ // MARK: - TableView DataSource and Delegate
603
+ extension DebugOverlayViewController: UITableViewDataSource, UITableViewDelegate {
604
+ func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
605
+ return history.count
606
+ }
607
+
608
+ func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
609
+ let cell = tableView.dequeueReusableCell(withIdentifier: "DebugHistoryCell", for: indexPath) as! DebugHistoryCell
610
+ let event = history[history.count - 1 - indexPath.row] // Show newest first
611
+ cell.configure(with: event)
612
+ return cell
613
+ }
614
+
615
+ func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
616
+ return UITableView.automaticDimension
617
+ }
618
+
619
+ func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
620
+ tableView.deselectRow(at: indexPath, animated: true)
621
+ let event = history[history.count - 1 - indexPath.row]
622
+
623
+ // Show detailed view of selected event
624
+ let detailVC = EventDetailViewController(event: event)
625
+ detailVC.modalPresentationStyle = .overFullScreen
626
+ present(detailVC, animated: true)
627
+ }
628
+ }
629
+
630
+ // MARK: - Debug History Cell
631
+ class DebugHistoryCell: UITableViewCell {
632
+ private let timeLabel = UILabel()
633
+ private let eventLabel = UILabel()
634
+ private let summaryLabel = UILabel()
635
+
636
+ override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
637
+ super.init(style: style, reuseIdentifier: reuseIdentifier)
638
+ setupUI()
639
+ }
640
+
641
+ required init?(coder: NSCoder) {
642
+ fatalError("init(coder:) has not been implemented")
643
+ }
644
+
645
+ private func setupUI() {
646
+ backgroundColor = .clear
647
+
648
+ timeLabel.font = UIFont.monospacedSystemFont(ofSize: 11, weight: .regular)
649
+ timeLabel.textColor = .secondaryLabel
650
+ timeLabel.translatesAutoresizingMaskIntoConstraints = false
651
+ contentView.addSubview(timeLabel)
652
+
653
+ eventLabel.font = UIFont.systemFont(ofSize: 14, weight: .medium)
654
+ eventLabel.translatesAutoresizingMaskIntoConstraints = false
655
+ contentView.addSubview(eventLabel)
656
+
657
+ summaryLabel.font = UIFont.systemFont(ofSize: 12)
658
+ summaryLabel.textColor = .secondaryLabel
659
+ summaryLabel.numberOfLines = 2
660
+ summaryLabel.translatesAutoresizingMaskIntoConstraints = false
661
+ contentView.addSubview(summaryLabel)
662
+
663
+ NSLayoutConstraint.activate([
664
+ timeLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8),
665
+ timeLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
666
+
667
+ eventLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8),
668
+ eventLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
669
+ eventLabel.trailingAnchor.constraint(equalTo: timeLabel.leadingAnchor, constant: -8),
670
+
671
+ summaryLabel.topAnchor.constraint(equalTo: eventLabel.bottomAnchor, constant: 4),
672
+ summaryLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
673
+ summaryLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
674
+ summaryLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -8),
675
+ ])
676
+ }
677
+
678
+ func configure(with event: DebugEvent) {
679
+ let formatter = DateFormatter()
680
+ formatter.dateFormat = "HH:mm:ss"
681
+ timeLabel.text = formatter.string(from: event.timestamp)
682
+
683
+ eventLabel.text = event.eventType
684
+
685
+ // Create summary from data - always show payload info
686
+ var summaryParts: [String] = []
687
+
688
+ // Add key-value summary
689
+ if let itemsCount = event.data["items_count"] as? String {
690
+ summaryParts.append("Items: \(itemsCount)")
691
+ }
692
+ if let orderId = event.data["order_id"] as? String {
693
+ summaryParts.append("Order: \(orderId)")
694
+ }
695
+ if let creativeId = event.data["creativeId"] as? String {
696
+ summaryParts.append("Creative: \(creativeId)")
697
+ }
698
+ if let eventType = event.data["event_type"] as? String {
699
+ summaryParts.append("Type: \(eventType)")
700
+ }
701
+
702
+ // Always show payload size info
703
+ let payloadInfo = "Payload: \(event.data.count) fields"
704
+ summaryParts.append(payloadInfo)
705
+
706
+ summaryLabel.text = summaryParts.joined(separator: " • ") + " (Tap for details)"
707
+ }
708
+ }
709
+
710
+ // MARK: - Event Detail View Controller
711
+ class EventDetailViewController: UIViewController {
712
+ private let event: DebugEvent
713
+
714
+ init(event: DebugEvent) {
715
+ self.event = event
716
+ super.init(nibName: nil, bundle: nil)
717
+ }
718
+
719
+ required init?(coder: NSCoder) {
720
+ fatalError("init(coder:) has not been implemented")
721
+ }
722
+
723
+ override func viewDidLoad() {
724
+ super.viewDidLoad()
725
+ setupUI()
726
+ }
727
+
728
+ private func setupUI() {
729
+ view.backgroundColor = UIColor.black.withAlphaComponent(0.8)
730
+
731
+ let containerView = UIView()
732
+ containerView.backgroundColor = UIColor.systemBackground
733
+ containerView.layer.cornerRadius = 12
734
+ containerView.translatesAutoresizingMaskIntoConstraints = false
735
+ view.addSubview(containerView)
736
+
737
+ let titleLabel = UILabel()
738
+ titleLabel.text = event.eventType
739
+ titleLabel.font = UIFont.boldSystemFont(ofSize: 18)
740
+ titleLabel.textAlignment = .center
741
+ titleLabel.translatesAutoresizingMaskIntoConstraints = false
742
+ containerView.addSubview(titleLabel)
743
+
744
+ let timeLabel = UILabel()
745
+ let formatter = DateFormatter()
746
+ formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
747
+ timeLabel.text = "Timestamp: \(formatter.string(from: event.timestamp))"
748
+ timeLabel.font = UIFont.systemFont(ofSize: 14)
749
+ timeLabel.textColor = .secondaryLabel
750
+ timeLabel.textAlignment = .center
751
+ timeLabel.translatesAutoresizingMaskIntoConstraints = false
752
+ containerView.addSubview(timeLabel)
753
+
754
+ let scrollView = UIScrollView()
755
+ scrollView.translatesAutoresizingMaskIntoConstraints = false
756
+ containerView.addSubview(scrollView)
757
+
758
+ let dataLabel = UILabel()
759
+ dataLabel.text = formatData(event.data)
760
+ dataLabel.font = UIFont.monospacedSystemFont(ofSize: 12, weight: .regular)
761
+ dataLabel.numberOfLines = 0
762
+ dataLabel.translatesAutoresizingMaskIntoConstraints = false
763
+ scrollView.addSubview(dataLabel)
764
+
765
+ // Share button for single event
766
+ let shareButton = UIButton(type: .system)
767
+ shareButton.setTitle("↗", for: .normal) // iOS-style share symbol
768
+ shareButton.titleLabel?.font = UIFont.systemFont(ofSize: 18, weight: .medium)
769
+ shareButton.setTitleColor(.systemBlue, for: .normal)
770
+ shareButton.backgroundColor = UIColor.systemGray5
771
+ shareButton.layer.cornerRadius = 15
772
+ shareButton.addTarget(self, action: #selector(shareEventButtonTapped), for: .touchUpInside)
773
+ shareButton.translatesAutoresizingMaskIntoConstraints = false
774
+ containerView.addSubview(shareButton)
775
+
776
+ let closeButton = UIButton(type: .system)
777
+ closeButton.setTitle("✕", for: .normal)
778
+ closeButton.titleLabel?.font = UIFont.systemFont(ofSize: 18, weight: .medium)
779
+ closeButton.setTitleColor(.secondaryLabel, for: .normal)
780
+ closeButton.backgroundColor = UIColor.systemGray5
781
+ closeButton.layer.cornerRadius = 15
782
+ closeButton.addTarget(self, action: #selector(closeButtonTapped), for: .touchUpInside)
783
+ closeButton.translatesAutoresizingMaskIntoConstraints = false
784
+ containerView.addSubview(closeButton)
785
+
786
+ NSLayoutConstraint.activate([
787
+ containerView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
788
+ containerView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
789
+ containerView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -16),
790
+ containerView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.65),
791
+
792
+ closeButton.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 12),
793
+ closeButton.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -12),
794
+ closeButton.widthAnchor.constraint(equalToConstant: 30),
795
+ closeButton.heightAnchor.constraint(equalToConstant: 30),
796
+
797
+ shareButton.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 12),
798
+ shareButton.trailingAnchor.constraint(equalTo: closeButton.leadingAnchor, constant: -8),
799
+ shareButton.widthAnchor.constraint(equalToConstant: 30),
800
+ shareButton.heightAnchor.constraint(equalToConstant: 30),
801
+
802
+ titleLabel.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 16),
803
+ titleLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 16),
804
+ titleLabel.trailingAnchor.constraint(equalTo: shareButton.leadingAnchor, constant: -8),
805
+
806
+ timeLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8),
807
+ timeLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 16),
808
+ timeLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -16),
809
+
810
+ scrollView.topAnchor.constraint(equalTo: timeLabel.bottomAnchor, constant: 16),
811
+ scrollView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 16),
812
+ scrollView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -16),
813
+ scrollView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -16),
814
+
815
+ dataLabel.topAnchor.constraint(equalTo: scrollView.topAnchor),
816
+ dataLabel.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
817
+ dataLabel.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
818
+ dataLabel.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
819
+ dataLabel.widthAnchor.constraint(equalTo: scrollView.widthAnchor)
820
+ ])
821
+ }
822
+
823
+ private func formatData(_ data: [String: Any]) -> String {
824
+ do {
825
+ let jsonData = try JSONSerialization.data(withJSONObject: data, options: .prettyPrinted)
826
+ return String(data: jsonData, encoding: .utf8) ?? "Unable to format data"
827
+ } catch {
828
+ return "Error formatting data: \(error.localizedDescription)"
829
+ }
830
+ }
831
+
832
+ /**
833
+ * Handles the share button tap to export and share a single debug event
834
+ */
835
+ @objc private func shareEventButtonTapped() {
836
+ let exportContent = generateSingleEventExport()
837
+
838
+ // Create activity view controller for sharing
839
+ let activityVC = UIActivityViewController(activityItems: [exportContent], applicationActivities: nil)
840
+
841
+ // For iPad - prevent crash by setting popover presentation controller
842
+ if let popover = activityVC.popoverPresentationController {
843
+ popover.sourceView = view
844
+ popover.sourceRect = CGRect(x: view.bounds.midX, y: view.bounds.midY, width: 0, height: 0)
845
+ }
846
+
847
+ present(activityVC, animated: true)
848
+ }
849
+
850
+ /**
851
+ * Generates formatted export content for a single event
852
+ * @return Formatted string containing the single debug event
853
+ */
854
+ private func generateSingleEventExport() -> String {
855
+ let formatter = DateFormatter()
856
+ formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
857
+ let exportDate = formatter.string(from: Date())
858
+
859
+ let eventContent = """
860
+ Attentive React Native SDK - Single Event Export
861
+ Generated: \(exportDate)
862
+
863
+ \(String(repeating: "=", count: 60))
864
+
865
+ \(event.formatForExport())
866
+ \(String(repeating: "=", count: 60))
867
+ End of Single Event Export
868
+ """
869
+
870
+ return eventContent
871
+ }
872
+
873
+ @objc private func closeButtonTapped() {
874
+ dismiss(animated: true)
875
+ }
108
876
  }