@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.
- package/README.md +150 -0
- package/android/build.gradle +4 -0
- package/android/src/main/kotlin/com/attentivereactnativesdk/AttentiveReactNativeSdkModule.kt +384 -0
- package/android/src/main/kotlin/com/attentivereactnativesdk/AttentiveReactNativeSdkPackage.kt +36 -0
- package/android/src/main/kotlin/com/attentivereactnativesdk/debug/AttentiveDebugHelper.kt +438 -0
- package/android/src/main/kotlin/com/attentivereactnativesdk/debug/DebugEvent.kt +76 -0
- package/attentive-react-native-sdk.podspec +4 -5
- package/ios/AttentiveReactNativeSdk.h +6 -6
- package/ios/AttentiveReactNativeSdk.mm +325 -35
- package/ios/AttentiveReactNativeSdk.xcodeproj/project.pbxproj +2 -2
- package/ios/Bridging/ATTNNativeSDK.swift +1118 -3
- package/ios/Bridging/AttentiveReactNativeSdk-Bridging-Header.h +3 -0
- package/ios/Bridging/AttentiveSDKManager.swift +83 -0
- package/ios/Podfile +4 -17
- package/lib/commonjs/NativeAttentiveReactNativeSdk.js +14 -0
- package/lib/commonjs/NativeAttentiveReactNativeSdk.js.map +1 -0
- package/lib/commonjs/index.js +363 -39
- package/lib/commonjs/index.js.map +1 -1
- package/lib/module/NativeAttentiveReactNativeSdk.js +7 -0
- package/lib/module/NativeAttentiveReactNativeSdk.js.map +1 -0
- package/lib/module/index.js +346 -38
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/NativeAttentiveReactNativeSdk.d.ts +103 -0
- package/lib/typescript/NativeAttentiveReactNativeSdk.d.ts.map +1 -0
- package/lib/typescript/eventTypes.d.ts +44 -17
- package/lib/typescript/eventTypes.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +276 -33
- package/lib/typescript/index.d.ts.map +1 -1
- package/package.json +22 -8
- package/src/NativeAttentiveReactNativeSdk.ts +152 -0
- package/src/eventTypes.tsx +57 -20
- package/src/index.tsx +472 -82
- package/android/src/main/java/com/attentivereactnativesdk/AttentiveReactNativeSdkModule.java +0 -247
- 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
|
|
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
|
}
|