@bigcrunch/react-native-ads 0.3.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +5 -5
  2. package/android/bigcrunch-ads/com/bigcrunch/ads/BigCrunchAds.kt +434 -0
  3. package/android/bigcrunch-ads/com/bigcrunch/ads/BigCrunchBannerView.kt +484 -0
  4. package/android/bigcrunch-ads/com/bigcrunch/ads/BigCrunchInterstitial.kt +403 -0
  5. package/android/bigcrunch-ads/com/bigcrunch/ads/BigCrunchRewarded.kt +409 -0
  6. package/android/bigcrunch-ads/com/bigcrunch/ads/adapters/GoogleAdsAdapter.kt +592 -0
  7. package/android/bigcrunch-ads/com/bigcrunch/ads/core/AdOrchestrator.kt +623 -0
  8. package/android/bigcrunch-ads/com/bigcrunch/ads/core/AnalyticsClient.kt +719 -0
  9. package/android/bigcrunch-ads/com/bigcrunch/ads/core/BidRequestClient.kt +364 -0
  10. package/android/bigcrunch-ads/com/bigcrunch/ads/core/ConfigManager.kt +301 -0
  11. package/android/bigcrunch-ads/com/bigcrunch/ads/core/DeviceContext.kt +385 -0
  12. package/android/bigcrunch-ads/com/bigcrunch/ads/core/RewardedCallback.kt +42 -0
  13. package/android/bigcrunch-ads/com/bigcrunch/ads/core/SessionManager.kt +330 -0
  14. package/android/bigcrunch-ads/com/bigcrunch/ads/internal/DeviceHelper.kt +60 -0
  15. package/android/bigcrunch-ads/com/bigcrunch/ads/internal/HttpClient.kt +114 -0
  16. package/android/bigcrunch-ads/com/bigcrunch/ads/internal/Logger.kt +71 -0
  17. package/android/bigcrunch-ads/com/bigcrunch/ads/internal/PrivacyStore.kt +125 -0
  18. package/android/bigcrunch-ads/com/bigcrunch/ads/internal/Storage.kt +88 -0
  19. package/android/bigcrunch-ads/com/bigcrunch/ads/listeners/BannerAdListener.kt +55 -0
  20. package/android/bigcrunch-ads/com/bigcrunch/ads/listeners/InterstitialAdListener.kt +55 -0
  21. package/android/bigcrunch-ads/com/bigcrunch/ads/listeners/RewardedAdListener.kt +58 -0
  22. package/android/bigcrunch-ads/com/bigcrunch/ads/models/AdEvent.kt +880 -0
  23. package/android/bigcrunch-ads/com/bigcrunch/ads/models/AppConfig.kt +90 -0
  24. package/android/bigcrunch-ads/com/bigcrunch/ads/models/DeviceData.kt +18 -0
  25. package/android/bigcrunch-ads/com/bigcrunch/ads/models/PlacementConfig.kt +70 -0
  26. package/android/bigcrunch-ads/com/bigcrunch/ads/models/SessionInfo.kt +21 -0
  27. package/android/build.gradle +22 -10
  28. package/android/settings.gradle +2 -6
  29. package/android/src/main/java/com/bigcrunch/ads/react/BigCrunchAdsModule.kt +0 -23
  30. package/ios/BigCrunchAds/Sources/Adapters/GoogleAdsAdapter.swift +512 -0
  31. package/ios/BigCrunchAds/Sources/BigCrunchAds.swift +387 -0
  32. package/ios/BigCrunchAds/Sources/BigCrunchBannerView.swift +448 -0
  33. package/ios/BigCrunchAds/Sources/BigCrunchInterstitial.swift +412 -0
  34. package/ios/BigCrunchAds/Sources/BigCrunchRewarded.swift +523 -0
  35. package/ios/BigCrunchAds/Sources/Core/AdOrchestrator.swift +514 -0
  36. package/ios/BigCrunchAds/Sources/Core/AnalyticsClient.swift +874 -0
  37. package/ios/BigCrunchAds/Sources/Core/BidRequestClient.swift +344 -0
  38. package/ios/BigCrunchAds/Sources/Core/ConfigManager.swift +306 -0
  39. package/ios/BigCrunchAds/Sources/Core/DeviceContext.swift +284 -0
  40. package/ios/BigCrunchAds/Sources/Core/SessionManager.swift +392 -0
  41. package/ios/BigCrunchAds/Sources/Internal/HTTPClient.swift +146 -0
  42. package/ios/BigCrunchAds/Sources/Internal/Logger.swift +62 -0
  43. package/ios/BigCrunchAds/Sources/Internal/PrivacyStore.swift +129 -0
  44. package/ios/BigCrunchAds/Sources/Internal/Storage.swift +73 -0
  45. package/ios/BigCrunchAds/Sources/Models/AdEvent.swift +784 -0
  46. package/ios/BigCrunchAds/Sources/Models/AppConfig.swift +100 -0
  47. package/ios/BigCrunchAds/Sources/Models/DeviceData.swift +68 -0
  48. package/ios/BigCrunchAds/Sources/Models/PlacementConfig.swift +137 -0
  49. package/ios/BigCrunchAds/Sources/Models/SessionInfo.swift +48 -0
  50. package/ios/BigCrunchAdsModule.swift +5 -14
  51. package/ios/BigCrunchBannerViewManager.swift +0 -1
  52. package/lib/index.d.ts +1 -1
  53. package/lib/index.d.ts.map +1 -1
  54. package/lib/index.js +3 -2
  55. package/lib/types/config.d.ts +22 -9
  56. package/lib/types/config.d.ts.map +1 -1
  57. package/lib/types/events.d.ts +4 -4
  58. package/lib/types/events.d.ts.map +1 -1
  59. package/package.json +11 -4
  60. package/react-native-bigcrunch-ads.podspec +1 -3
  61. package/scripts/inject-version.js +55 -0
  62. package/src/index.ts +3 -2
  63. package/src/types/config.ts +23 -9
  64. package/src/types/events.ts +4 -4
@@ -0,0 +1,284 @@
1
+ import Foundation
2
+ import UIKit
3
+
4
+ /**
5
+ * DeviceContext collects and provides device/app context for analytics
6
+ *
7
+ * Gathers information about:
8
+ * - Device type (phone, tablet)
9
+ * - Operating system and version
10
+ * - App name and version
11
+ * - Screen dimensions
12
+ * - Language and locale
13
+ * - Timezone
14
+ *
15
+ * All values are cached at initialization for consistent reporting.
16
+ */
17
+ internal final class DeviceContext {
18
+
19
+ // MARK: - Singleton
20
+
21
+ static let shared = DeviceContext()
22
+
23
+ // MARK: - Device Properties
24
+
25
+ /// Device type: "phone" or "tablet"
26
+ let deviceType: String
27
+
28
+ /// Device model (e.g., "iPhone14,2")
29
+ let deviceModel: String
30
+
31
+ /// Operating system name (always "iOS")
32
+ let osName: String = "iOS"
33
+
34
+ /// Operating system version (e.g., "17.0")
35
+ let osVersion: String
36
+
37
+ /// Screen width in points
38
+ let screenWidth: Int
39
+
40
+ /// Screen height in points
41
+ let screenHeight: Int
42
+
43
+ /// Device scale factor (e.g., 2.0 for Retina, 3.0 for Super Retina)
44
+ let screenScale: Double
45
+
46
+ // MARK: - App Properties
47
+
48
+ /// App bundle identifier (e.g., "com.example.app")
49
+ let appBundleId: String
50
+
51
+ /// App display name
52
+ let appName: String
53
+
54
+ /// App version (e.g., "1.2.3")
55
+ let appVersion: String
56
+
57
+ /// App build number (e.g., "42")
58
+ let appBuild: String
59
+
60
+ // MARK: - Locale Properties
61
+
62
+ /// User's preferred language code (e.g., "en")
63
+ let languageCode: String
64
+
65
+ /// User's country/region code (e.g., "US")
66
+ let countryCode: String
67
+
68
+ /// User's region/state (e.g., "CA", "NY") - may be empty if not available
69
+ let region: String
70
+
71
+ /// User's timezone identifier (e.g., "America/New_York")
72
+ let timezone: String
73
+
74
+ // MARK: - SDK Properties
75
+
76
+ /// SDK version
77
+ static let SDK_VERSION = "0.5.0"
78
+ let sdkVersion: String = SDK_VERSION
79
+
80
+ /// SDK platform (always "ios")
81
+ let sdkPlatform: String = "ios"
82
+
83
+ // MARK: - Initialization
84
+
85
+ private init() {
86
+ // Device type
87
+ let idiom = UIDevice.current.userInterfaceIdiom
88
+ self.deviceType = idiom == .pad ? "tablet" : "phone"
89
+
90
+ // Device model (hardware identifier)
91
+ var systemInfo = utsname()
92
+ uname(&systemInfo)
93
+ let machineMirror = Mirror(reflecting: systemInfo.machine)
94
+ self.deviceModel = machineMirror.children.reduce("") { identifier, element in
95
+ guard let value = element.value as? Int8, value != 0 else { return identifier }
96
+ return identifier + String(UnicodeScalar(UInt8(value)))
97
+ }
98
+
99
+ // OS version
100
+ self.osVersion = UIDevice.current.systemVersion
101
+
102
+ // Screen dimensions
103
+ let screenBounds = UIScreen.main.bounds
104
+ self.screenWidth = Int(screenBounds.width)
105
+ self.screenHeight = Int(screenBounds.height)
106
+ self.screenScale = Double(UIScreen.main.scale)
107
+
108
+ // App info from bundle
109
+ let bundle = Bundle.main
110
+ self.appBundleId = bundle.bundleIdentifier ?? "unknown"
111
+ self.appName = bundle.infoDictionary?["CFBundleDisplayName"] as? String
112
+ ?? bundle.infoDictionary?["CFBundleName"] as? String
113
+ ?? "unknown"
114
+ self.appVersion = bundle.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0.0"
115
+ self.appBuild = bundle.infoDictionary?["CFBundleVersion"] as? String ?? "0"
116
+
117
+ // Locale info (compatible with iOS 13+)
118
+ let locale = Locale.current
119
+ if #available(iOS 16, *) {
120
+ self.languageCode = locale.language.languageCode?.identifier ?? "en"
121
+ self.countryCode = locale.region?.identifier ?? "US"
122
+ } else {
123
+ self.languageCode = locale.languageCode ?? "en"
124
+ self.countryCode = locale.regionCode ?? "US"
125
+ }
126
+ self.timezone = TimeZone.current.identifier
127
+
128
+ // Region/state extraction - iOS doesn't provide direct access, leave empty for now
129
+ // Could be populated from IP geolocation or user settings in future
130
+ self.region = ""
131
+
132
+ BCLogger.debug("DeviceContext: Initialized - \(deviceType) \(deviceModel) iOS \(osVersion)")
133
+ }
134
+
135
+ // MARK: - Convenience Methods
136
+
137
+ /**
138
+ * Get web schema compatible fields (flat structure)
139
+ *
140
+ * - Returns: Dictionary of flattened web schema fields
141
+ */
142
+ func getWebSchemaFields() -> [String: Any] {
143
+ return [
144
+ "browser": getBrowserField(),
145
+ "device": deviceType, // "phone" or "tablet"
146
+ "os": osName, // "iOS"
147
+ "country": countryCode,
148
+ "region": region
149
+ ]
150
+ }
151
+
152
+ /**
153
+ * Get browser field for web schema (e.g., "BigCrunch iOS SDK 1.0.0")
154
+ */
155
+ func getBrowserField() -> String {
156
+ return "BigCrunch iOS SDK \(sdkVersion)"
157
+ }
158
+
159
+ /**
160
+ * Get mobile-specific device context fields (for optional inclusion)
161
+ *
162
+ * - Returns: Dictionary of mobile-specific context values
163
+ */
164
+ func getMobileSpecificFields() -> [String: Any] {
165
+ return [
166
+ "device_model": deviceModel,
167
+ "os_version": osVersion,
168
+ "screen_width": screenWidth,
169
+ "screen_height": screenHeight,
170
+ "screen_scale": screenScale,
171
+ "app_bundle_id": appBundleId,
172
+ "app_name": appName,
173
+ "app_version": appVersion,
174
+ "app_build": appBuild,
175
+ "language_code": languageCode,
176
+ "timezone": timezone,
177
+ "sdk_version": sdkVersion,
178
+ "sdk_platform": sdkPlatform
179
+ ]
180
+ }
181
+
182
+ /**
183
+ * Get a dictionary representation for analytics events (legacy format)
184
+ * DEPRECATED: Use getWebSchemaFields() for new analytics events
185
+ *
186
+ * - Returns: Dictionary of device context values
187
+ */
188
+ func toDictionary() -> [String: Any] {
189
+ return [
190
+ "device_type": deviceType,
191
+ "device_model": deviceModel,
192
+ "os_name": osName,
193
+ "os_version": osVersion,
194
+ "screen_width": screenWidth,
195
+ "screen_height": screenHeight,
196
+ "screen_scale": screenScale,
197
+ "app_bundle_id": appBundleId,
198
+ "app_name": appName,
199
+ "app_version": appVersion,
200
+ "app_build": appBuild,
201
+ "language_code": languageCode,
202
+ "country_code": countryCode,
203
+ "region": region,
204
+ "timezone": timezone,
205
+ "sdk_version": sdkVersion,
206
+ "sdk_platform": sdkPlatform
207
+ ]
208
+ }
209
+
210
+ /**
211
+ * Get the full OS string (e.g., "iOS 17.0")
212
+ */
213
+ var fullOSString: String {
214
+ return "\(osName) \(osVersion)"
215
+ }
216
+
217
+ /**
218
+ * Get screen dimensions string (e.g., "390x844")
219
+ */
220
+ var screenDimensionsString: String {
221
+ return "\(screenWidth)x\(screenHeight)"
222
+ }
223
+ }
224
+
225
+ /**
226
+ * Codable struct for including device context in analytics events
227
+ */
228
+ internal struct DeviceContextData: Codable {
229
+ let deviceType: String
230
+ let deviceModel: String
231
+ let osName: String
232
+ let osVersion: String
233
+ let screenWidth: Int
234
+ let screenHeight: Int
235
+ let appBundleId: String
236
+ let appName: String
237
+ let appVersion: String
238
+ let languageCode: String
239
+ let countryCode: String
240
+ let region: String
241
+ let timezone: String
242
+ let sdkVersion: String
243
+ let sdkPlatform: String
244
+
245
+ enum CodingKeys: String, CodingKey {
246
+ case deviceType = "device_type"
247
+ case deviceModel = "device_model"
248
+ case osName = "os_name"
249
+ case osVersion = "os_version"
250
+ case screenWidth = "screen_width"
251
+ case screenHeight = "screen_height"
252
+ case appBundleId = "app_bundle_id"
253
+ case appName = "app_name"
254
+ case appVersion = "app_version"
255
+ case languageCode = "language_code"
256
+ case countryCode = "country_code"
257
+ case region = "region"
258
+ case timezone = "timezone"
259
+ case sdkVersion = "sdk_version"
260
+ case sdkPlatform = "sdk_platform"
261
+ }
262
+
263
+ /// Create from the shared DeviceContext singleton
264
+ static func fromShared() -> DeviceContextData {
265
+ let ctx = DeviceContext.shared
266
+ return DeviceContextData(
267
+ deviceType: ctx.deviceType,
268
+ deviceModel: ctx.deviceModel,
269
+ osName: ctx.osName,
270
+ osVersion: ctx.osVersion,
271
+ screenWidth: ctx.screenWidth,
272
+ screenHeight: ctx.screenHeight,
273
+ appBundleId: ctx.appBundleId,
274
+ appName: ctx.appName,
275
+ appVersion: ctx.appVersion,
276
+ languageCode: ctx.languageCode,
277
+ countryCode: ctx.countryCode,
278
+ region: ctx.region,
279
+ timezone: ctx.timezone,
280
+ sdkVersion: ctx.sdkVersion,
281
+ sdkPlatform: ctx.sdkPlatform
282
+ )
283
+ }
284
+ }
@@ -0,0 +1,392 @@
1
+ import Foundation
2
+
3
+ /**
4
+ * SessionManager handles session and user identification for analytics
5
+ *
6
+ * Manages:
7
+ * - user_id: Persistent UUID stored across app launches (identifies a user/device)
8
+ * - session_id: New UUID generated per app launch (identifies a session)
9
+ * - session_depth: Counter incremented per screen view within a session
10
+ * - page_id: New UUID generated per screen/page view
11
+ *
12
+ * Thread-safe singleton pattern ensures consistent IDs across all analytics events.
13
+ */
14
+ internal final class SessionManager {
15
+
16
+ // MARK: - Singleton
17
+
18
+ static let shared = SessionManager()
19
+
20
+ // MARK: - Storage Keys
21
+
22
+ private enum Keys {
23
+ static let userId = "user_id"
24
+ static let sessionCount = "session_count"
25
+ static let sessionStartTime = "session_start_time"
26
+ static let utmSource = "utm_source"
27
+ static let utmMedium = "utm_medium"
28
+ static let utmCampaign = "utm_campaign"
29
+ static let utmTerm = "utm_term"
30
+ static let utmContent = "utm_content"
31
+ }
32
+
33
+ // MARK: - Properties
34
+
35
+ private let storage: KeyValueStore
36
+ private let lock = NSLock()
37
+
38
+ /// Persistent user ID (stored across app launches)
39
+ private(set) var userId: String
40
+
41
+ /// Current session ID (new per app launch)
42
+ private(set) var sessionId: String
43
+
44
+ /// Total number of sessions for this user (across all time)
45
+ private(set) var totalSessionCount: Int
46
+
47
+ /// True if this is the user's first session
48
+ var isNewUser: Bool {
49
+ return totalSessionCount == 1
50
+ }
51
+
52
+ /// Session start time (ISO 8601 string)
53
+ private(set) var sessionStartTime: String
54
+
55
+ /// UTM parameters (from deep links or attribution)
56
+ private(set) var utmSource: String?
57
+ private(set) var utmMedium: String?
58
+ private(set) var utmCampaign: String?
59
+ private(set) var utmTerm: String?
60
+ private(set) var utmContent: String?
61
+
62
+ /// Session attribution source (derived from UTM or defaults to "direct")
63
+ var sessionSource: String {
64
+ return utmSource ?? "direct"
65
+ }
66
+
67
+ /// Session attribution medium (derived from UTM or defaults to "none")
68
+ var sessionMedium: String {
69
+ return utmMedium ?? "none"
70
+ }
71
+
72
+ /// Number of page views in current session
73
+ private var _sessionDepth: Int = 0
74
+ var sessionDepth: Int {
75
+ lock.lock()
76
+ defer { lock.unlock() }
77
+ return _sessionDepth
78
+ }
79
+
80
+ /// Current page ID (new per page/screen view)
81
+ private var _currentPageId: String?
82
+ var currentPageId: String? {
83
+ lock.lock()
84
+ defer { lock.unlock() }
85
+ return _currentPageId
86
+ }
87
+
88
+ // MARK: - Ad Tracking Counters
89
+
90
+ /// Number of ad requests in current session
91
+ private var _adRequestCount: Int = 0
92
+ var adRequestCount: Int {
93
+ lock.lock()
94
+ defer { lock.unlock() }
95
+ return _adRequestCount
96
+ }
97
+
98
+ /// Number of ad impressions in current session
99
+ private var _adImpressionCount: Int = 0
100
+ var adImpressionCount: Int {
101
+ lock.lock()
102
+ defer { lock.unlock() }
103
+ return _adImpressionCount
104
+ }
105
+
106
+ /// Total revenue in micros for current session
107
+ private var _totalRevenueMicros: Int64 = 0
108
+ var totalRevenueMicros: Int64 {
109
+ lock.lock()
110
+ defer { lock.unlock() }
111
+ return _totalRevenueMicros
112
+ }
113
+
114
+ // MARK: - Initialization
115
+
116
+ private init() {
117
+ self.storage = UserDefaultsStore()
118
+
119
+ // Load or generate user_id
120
+ if let existingUserId = storage.getString(key: Keys.userId, default: nil) {
121
+ self.userId = existingUserId
122
+ BCLogger.debug("SessionManager: Loaded existing user_id: \(existingUserId)")
123
+ } else {
124
+ let newUserId = UUID().uuidString
125
+ storage.putString(key: Keys.userId, value: newUserId)
126
+ self.userId = newUserId
127
+ BCLogger.debug("SessionManager: Generated new user_id: \(newUserId)")
128
+ }
129
+
130
+ // Load session count and increment
131
+ let sessionCountStr = storage.getString(key: Keys.sessionCount, default: "0") ?? "0"
132
+ let previousCount = Int(sessionCountStr) ?? 0
133
+ self.totalSessionCount = previousCount + 1
134
+ storage.putString(key: Keys.sessionCount, value: String(totalSessionCount))
135
+
136
+ // Generate new session_id for this app launch
137
+ self.sessionId = UUID().uuidString
138
+
139
+ // Set session start time (ISO 8601 format)
140
+ self.sessionStartTime = ISO8601DateFormatter().string(from: Date())
141
+ storage.putString(key: Keys.sessionStartTime, value: sessionStartTime)
142
+
143
+ // Load UTM parameters (if set from deep link)
144
+ self.utmSource = storage.getString(key: Keys.utmSource, default: nil)
145
+ self.utmMedium = storage.getString(key: Keys.utmMedium, default: nil)
146
+ self.utmCampaign = storage.getString(key: Keys.utmCampaign, default: nil)
147
+ self.utmTerm = storage.getString(key: Keys.utmTerm, default: nil)
148
+ self.utmContent = storage.getString(key: Keys.utmContent, default: nil)
149
+
150
+ BCLogger.debug("SessionManager: Started session \(totalSessionCount) with session_id: \(sessionId)")
151
+ }
152
+
153
+ /// Internal initializer for testing
154
+ internal init(storage: KeyValueStore) {
155
+ self.storage = storage
156
+
157
+ // Load or generate user_id
158
+ if let existingUserId = storage.getString(key: Keys.userId, default: nil) {
159
+ self.userId = existingUserId
160
+ } else {
161
+ let newUserId = UUID().uuidString
162
+ storage.putString(key: Keys.userId, value: newUserId)
163
+ self.userId = newUserId
164
+ }
165
+
166
+ // Load session count and increment
167
+ let sessionCountStr = storage.getString(key: Keys.sessionCount, default: "0") ?? "0"
168
+ let previousCount = Int(sessionCountStr) ?? 0
169
+ self.totalSessionCount = previousCount + 1
170
+ storage.putString(key: Keys.sessionCount, value: String(totalSessionCount))
171
+
172
+ // Generate new session_id
173
+ self.sessionId = UUID().uuidString
174
+
175
+ // Set session start time (ISO 8601 format)
176
+ self.sessionStartTime = ISO8601DateFormatter().string(from: Date())
177
+ storage.putString(key: Keys.sessionStartTime, value: sessionStartTime)
178
+
179
+ // Load UTM parameters (if set from deep link)
180
+ self.utmSource = storage.getString(key: Keys.utmSource, default: nil)
181
+ self.utmMedium = storage.getString(key: Keys.utmMedium, default: nil)
182
+ self.utmCampaign = storage.getString(key: Keys.utmCampaign, default: nil)
183
+ self.utmTerm = storage.getString(key: Keys.utmTerm, default: nil)
184
+ self.utmContent = storage.getString(key: Keys.utmContent, default: nil)
185
+ }
186
+
187
+ // MARK: - Page Tracking
188
+
189
+ /**
190
+ * Start a new page view
191
+ *
192
+ * Generates a new page_id and increments session_depth.
193
+ * Should be called at the start of each screen/page view.
194
+ *
195
+ * - Returns: The new page_id
196
+ */
197
+ @discardableResult
198
+ func startPageView() -> String {
199
+ lock.lock()
200
+ defer { lock.unlock() }
201
+
202
+ _sessionDepth += 1
203
+ _currentPageId = UUID().uuidString
204
+
205
+ BCLogger.debug("SessionManager: Started page view \(_sessionDepth) with page_id: \(_currentPageId!)")
206
+
207
+ return _currentPageId!
208
+ }
209
+
210
+ /**
211
+ * Get the current page ID, generating one if needed
212
+ *
213
+ * - Returns: The current page_id (creates one if none exists)
214
+ */
215
+ func getOrCreatePageId() -> String {
216
+ lock.lock()
217
+ defer { lock.unlock() }
218
+
219
+ if let pageId = _currentPageId {
220
+ return pageId
221
+ }
222
+
223
+ // Auto-generate if no page view has been started
224
+ _sessionDepth += 1
225
+ _currentPageId = UUID().uuidString
226
+ return _currentPageId!
227
+ }
228
+
229
+ // MARK: - UTM Parameter Management
230
+
231
+ /**
232
+ * Set UTM parameters (typically from deep link)
233
+ *
234
+ * This persists UTM parameters for attribution tracking. These will be
235
+ * loaded for future sessions until cleared.
236
+ *
237
+ * - Parameters:
238
+ * - source: UTM source (e.g., "google", "facebook")
239
+ * - medium: UTM medium (e.g., "cpc", "email")
240
+ * - campaign: UTM campaign (e.g., "summer_sale")
241
+ * - term: UTM term (optional keyword)
242
+ * - content: UTM content (optional content variant)
243
+ */
244
+ func setUTMParameters(source: String?, medium: String?, campaign: String?, term: String?, content: String?) {
245
+ lock.lock()
246
+ defer { lock.unlock() }
247
+
248
+ self.utmSource = source
249
+ self.utmMedium = medium
250
+ self.utmCampaign = campaign
251
+ self.utmTerm = term
252
+ self.utmContent = content
253
+
254
+ // Persist to storage
255
+ if let source = source {
256
+ storage.putString(key: Keys.utmSource, value: source)
257
+ }
258
+ if let medium = medium {
259
+ storage.putString(key: Keys.utmMedium, value: medium)
260
+ }
261
+ if let campaign = campaign {
262
+ storage.putString(key: Keys.utmCampaign, value: campaign)
263
+ }
264
+ if let term = term {
265
+ storage.putString(key: Keys.utmTerm, value: term)
266
+ }
267
+ if let content = content {
268
+ storage.putString(key: Keys.utmContent, value: content)
269
+ }
270
+
271
+ BCLogger.debug("SessionManager: Set UTM parameters - source: \(source ?? "nil"), medium: \(medium ?? "nil"), campaign: \(campaign ?? "nil")")
272
+ }
273
+
274
+ /**
275
+ * Clear UTM parameters
276
+ */
277
+ func clearUTMParameters() {
278
+ lock.lock()
279
+ defer { lock.unlock() }
280
+
281
+ self.utmSource = nil
282
+ self.utmMedium = nil
283
+ self.utmCampaign = nil
284
+ self.utmTerm = nil
285
+ self.utmContent = nil
286
+
287
+ storage.putString(key: Keys.utmSource, value: nil)
288
+ storage.putString(key: Keys.utmMedium, value: nil)
289
+ storage.putString(key: Keys.utmCampaign, value: nil)
290
+ storage.putString(key: Keys.utmTerm, value: nil)
291
+ storage.putString(key: Keys.utmContent, value: nil)
292
+ }
293
+
294
+ // MARK: - Ad Tracking
295
+
296
+ /**
297
+ * Increment the ad request counter
298
+ */
299
+ func incrementAdRequestCount() {
300
+ lock.lock()
301
+ defer { lock.unlock() }
302
+ _adRequestCount += 1
303
+ BCLogger.debug("SessionManager: Ad request count: \(_adRequestCount)")
304
+ }
305
+
306
+ /**
307
+ * Increment the ad impression counter
308
+ */
309
+ func incrementAdImpressionCount() {
310
+ lock.lock()
311
+ defer { lock.unlock() }
312
+ _adImpressionCount += 1
313
+ BCLogger.debug("SessionManager: Ad impression count: \(_adImpressionCount)")
314
+ }
315
+
316
+ /**
317
+ * Add revenue to the session total
318
+ *
319
+ * - Parameter micros: Revenue amount in micros (1/1,000,000 of currency unit)
320
+ */
321
+ func addRevenue(_ micros: Int64) {
322
+ lock.lock()
323
+ defer { lock.unlock() }
324
+ _totalRevenueMicros += micros
325
+ BCLogger.debug("SessionManager: Added revenue \(micros) micros, total: \(_totalRevenueMicros)")
326
+ }
327
+
328
+ // MARK: - Session Management
329
+
330
+ /**
331
+ * Start a new session (resets all session counters)
332
+ *
333
+ * This generates a new session ID and resets all counters.
334
+ * Use this when you want to explicitly start a new session without restarting the app.
335
+ */
336
+ func startNewSession() {
337
+ lock.lock()
338
+ defer { lock.unlock() }
339
+
340
+ // Generate new session ID
341
+ sessionId = UUID().uuidString
342
+
343
+ // Update session start time
344
+ sessionStartTime = ISO8601DateFormatter().string(from: Date())
345
+ storage.putString(key: Keys.sessionStartTime, value: sessionStartTime)
346
+
347
+ // Increment total session count
348
+ totalSessionCount += 1
349
+ storage.putString(key: Keys.sessionCount, value: String(totalSessionCount))
350
+
351
+ // Reset session counters
352
+ _sessionDepth = 0
353
+ _currentPageId = nil
354
+ _adRequestCount = 0
355
+ _adImpressionCount = 0
356
+ _totalRevenueMicros = 0
357
+
358
+ BCLogger.debug("SessionManager: Started new session \(totalSessionCount) with session_id: \(sessionId)")
359
+ }
360
+
361
+ // MARK: - Timestamp Utilities
362
+
363
+ /// Date formatter for ISO 8601 timestamps with milliseconds
364
+ private static let timestampFormatter: DateFormatter = {
365
+ let formatter = DateFormatter()
366
+ formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
367
+ formatter.timeZone = TimeZone(identifier: "UTC")
368
+ formatter.locale = Locale(identifier: "en_US_POSIX")
369
+ return formatter
370
+ }()
371
+
372
+ /**
373
+ * Get current timestamp in ISO 8601 format with milliseconds
374
+ *
375
+ * - Returns: ISO 8601 timestamp string (e.g., "2025-01-15T10:30:00.000Z")
376
+ */
377
+ func getCurrentTimestamp() -> String {
378
+ return SessionManager.timestampFormatter.string(from: Date())
379
+ }
380
+
381
+ // MARK: - Reset (for testing)
382
+
383
+ /// Reset all session state (for testing only)
384
+ internal func resetForTesting() {
385
+ lock.lock()
386
+ defer { lock.unlock() }
387
+
388
+ storage.clear()
389
+ _sessionDepth = 0
390
+ _currentPageId = nil
391
+ }
392
+ }