@bigcrunch/react-native-ads 0.4.0 → 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.
- package/README.md +5 -5
- package/android/bigcrunch-ads/com/bigcrunch/ads/BigCrunchAds.kt +434 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/BigCrunchBannerView.kt +484 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/BigCrunchInterstitial.kt +403 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/BigCrunchRewarded.kt +409 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/adapters/GoogleAdsAdapter.kt +592 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/core/AdOrchestrator.kt +623 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/core/AnalyticsClient.kt +719 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/core/BidRequestClient.kt +364 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/core/ConfigManager.kt +301 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/core/DeviceContext.kt +385 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/core/RewardedCallback.kt +42 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/core/SessionManager.kt +330 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/internal/DeviceHelper.kt +60 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/internal/HttpClient.kt +114 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/internal/Logger.kt +71 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/internal/PrivacyStore.kt +125 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/internal/Storage.kt +88 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/listeners/BannerAdListener.kt +55 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/listeners/InterstitialAdListener.kt +55 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/listeners/RewardedAdListener.kt +58 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/models/AdEvent.kt +880 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/models/AppConfig.kt +90 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/models/DeviceData.kt +18 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/models/PlacementConfig.kt +70 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/models/SessionInfo.kt +21 -0
- package/android/build.gradle +22 -10
- package/android/settings.gradle +2 -6
- package/ios/BigCrunchAds/Sources/Adapters/GoogleAdsAdapter.swift +512 -0
- package/ios/BigCrunchAds/Sources/BigCrunchAds.swift +387 -0
- package/ios/BigCrunchAds/Sources/BigCrunchBannerView.swift +448 -0
- package/ios/BigCrunchAds/Sources/BigCrunchInterstitial.swift +412 -0
- package/ios/BigCrunchAds/Sources/BigCrunchRewarded.swift +523 -0
- package/ios/BigCrunchAds/Sources/Core/AdOrchestrator.swift +514 -0
- package/ios/BigCrunchAds/Sources/Core/AnalyticsClient.swift +874 -0
- package/ios/BigCrunchAds/Sources/Core/BidRequestClient.swift +344 -0
- package/ios/BigCrunchAds/Sources/Core/ConfigManager.swift +306 -0
- package/ios/BigCrunchAds/Sources/Core/DeviceContext.swift +284 -0
- package/ios/BigCrunchAds/Sources/Core/SessionManager.swift +392 -0
- package/ios/BigCrunchAds/Sources/Internal/HTTPClient.swift +146 -0
- package/ios/BigCrunchAds/Sources/Internal/Logger.swift +62 -0
- package/ios/BigCrunchAds/Sources/Internal/PrivacyStore.swift +129 -0
- package/ios/BigCrunchAds/Sources/Internal/Storage.swift +73 -0
- package/ios/BigCrunchAds/Sources/Models/AdEvent.swift +784 -0
- package/ios/BigCrunchAds/Sources/Models/AppConfig.swift +100 -0
- package/ios/BigCrunchAds/Sources/Models/DeviceData.swift +68 -0
- package/ios/BigCrunchAds/Sources/Models/PlacementConfig.swift +137 -0
- package/ios/BigCrunchAds/Sources/Models/SessionInfo.swift +48 -0
- package/ios/BigCrunchAdsModule.swift +0 -1
- package/ios/BigCrunchBannerViewManager.swift +0 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +3 -2
- package/package.json +8 -2
- package/react-native-bigcrunch-ads.podspec +0 -1
- package/scripts/inject-version.js +55 -0
- package/src/index.ts +3 -2
|
@@ -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
|
+
}
|