@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.
Files changed (57) 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/ios/BigCrunchAds/Sources/Adapters/GoogleAdsAdapter.swift +512 -0
  30. package/ios/BigCrunchAds/Sources/BigCrunchAds.swift +387 -0
  31. package/ios/BigCrunchAds/Sources/BigCrunchBannerView.swift +448 -0
  32. package/ios/BigCrunchAds/Sources/BigCrunchInterstitial.swift +412 -0
  33. package/ios/BigCrunchAds/Sources/BigCrunchRewarded.swift +523 -0
  34. package/ios/BigCrunchAds/Sources/Core/AdOrchestrator.swift +514 -0
  35. package/ios/BigCrunchAds/Sources/Core/AnalyticsClient.swift +874 -0
  36. package/ios/BigCrunchAds/Sources/Core/BidRequestClient.swift +344 -0
  37. package/ios/BigCrunchAds/Sources/Core/ConfigManager.swift +306 -0
  38. package/ios/BigCrunchAds/Sources/Core/DeviceContext.swift +284 -0
  39. package/ios/BigCrunchAds/Sources/Core/SessionManager.swift +392 -0
  40. package/ios/BigCrunchAds/Sources/Internal/HTTPClient.swift +146 -0
  41. package/ios/BigCrunchAds/Sources/Internal/Logger.swift +62 -0
  42. package/ios/BigCrunchAds/Sources/Internal/PrivacyStore.swift +129 -0
  43. package/ios/BigCrunchAds/Sources/Internal/Storage.swift +73 -0
  44. package/ios/BigCrunchAds/Sources/Models/AdEvent.swift +784 -0
  45. package/ios/BigCrunchAds/Sources/Models/AppConfig.swift +100 -0
  46. package/ios/BigCrunchAds/Sources/Models/DeviceData.swift +68 -0
  47. package/ios/BigCrunchAds/Sources/Models/PlacementConfig.swift +137 -0
  48. package/ios/BigCrunchAds/Sources/Models/SessionInfo.swift +48 -0
  49. package/ios/BigCrunchAdsModule.swift +0 -1
  50. package/ios/BigCrunchBannerViewManager.swift +0 -1
  51. package/lib/index.d.ts +1 -1
  52. package/lib/index.d.ts.map +1 -1
  53. package/lib/index.js +3 -2
  54. package/package.json +8 -2
  55. package/react-native-bigcrunch-ads.podspec +0 -1
  56. package/scripts/inject-version.js +55 -0
  57. package/src/index.ts +3 -2
@@ -0,0 +1,330 @@
1
+ package com.bigcrunch.ads.core
2
+
3
+ import com.bigcrunch.ads.internal.BCLogger
4
+ import com.bigcrunch.ads.internal.KeyValueStore
5
+ import java.text.SimpleDateFormat
6
+ import java.util.Date
7
+ import java.util.Locale
8
+ import java.util.TimeZone
9
+ import java.util.UUID
10
+ import java.util.concurrent.atomic.AtomicInteger
11
+ import java.util.concurrent.atomic.AtomicLong
12
+ import java.util.concurrent.atomic.AtomicReference
13
+
14
+ /**
15
+ * SessionManager handles session and user identification for analytics
16
+ *
17
+ * Manages:
18
+ * - user_id: Persistent UUID stored across app launches (identifies a user/device)
19
+ * - session_id: New UUID generated per app launch (identifies a session)
20
+ * - session_depth: Counter incremented per screen view within a session
21
+ * - page_id: New UUID generated per screen/page view
22
+ *
23
+ * Thread-safe implementation ensures consistent IDs across all analytics events.
24
+ */
25
+ internal class SessionManager private constructor(private val storage: KeyValueStore) {
26
+
27
+ companion object {
28
+ private const val TAG = "SessionManager"
29
+
30
+ private const val KEY_USER_ID = "user_id"
31
+ private const val KEY_SESSION_COUNT = "session_count"
32
+ private const val KEY_SESSION_START_TIME = "session_start_time"
33
+ private const val KEY_UTM_SOURCE = "utm_source"
34
+ private const val KEY_UTM_MEDIUM = "utm_medium"
35
+ private const val KEY_UTM_CAMPAIGN = "utm_campaign"
36
+ private const val KEY_UTM_TERM = "utm_term"
37
+ private const val KEY_UTM_CONTENT = "utm_content"
38
+
39
+ @Volatile
40
+ private var instance: SessionManager? = null
41
+
42
+ /**
43
+ * Initialize the SessionManager singleton
44
+ *
45
+ * Must be called once during SDK initialization.
46
+ *
47
+ * @param storage Key-value storage for persistence
48
+ */
49
+ fun initialize(storage: KeyValueStore) {
50
+ if (instance == null) {
51
+ synchronized(this) {
52
+ if (instance == null) {
53
+ instance = SessionManager(storage)
54
+ }
55
+ }
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Get the SessionManager instance
61
+ *
62
+ * @return The singleton instance
63
+ * @throws IllegalStateException if not initialized
64
+ */
65
+ fun getInstance(): SessionManager {
66
+ return instance ?: throw IllegalStateException(
67
+ "SessionManager not initialized. Call SessionManager.initialize() first."
68
+ )
69
+ }
70
+
71
+ // Static accessors for tracking counters
72
+ fun getSessionId(): String = getInstance().sessionId
73
+ fun getSessionStartTime(): String = getInstance().sessionStartTime
74
+ fun getScreenViewCount(): Int = getInstance().screenViewCounter.get()
75
+ fun getAdRequestCount(): Int = getInstance().adRequestCounter.get()
76
+ fun getAdImpressionCount(): Int = getInstance().adImpressionCounter.get()
77
+ fun getTotalRevenueMicros(): Long = getInstance().totalRevenueMicrosCounter.get()
78
+
79
+ fun incrementScreenViewCount() {
80
+ getInstance().screenViewCounter.incrementAndGet()
81
+ }
82
+
83
+ fun incrementAdRequestCount() {
84
+ getInstance().adRequestCounter.incrementAndGet()
85
+ }
86
+
87
+ fun incrementAdImpressionCount() {
88
+ getInstance().adImpressionCounter.incrementAndGet()
89
+ }
90
+
91
+ fun addRevenueMicros(micros: Long) {
92
+ getInstance().totalRevenueMicrosCounter.addAndGet(micros)
93
+ }
94
+
95
+ fun startNewSession() {
96
+ getInstance().resetSessionCounters()
97
+ }
98
+
99
+ /**
100
+ * Reset the singleton instance (for testing only)
101
+ */
102
+ internal fun resetForTesting() {
103
+ synchronized(this) {
104
+ instance?.resetState()
105
+ instance = null
106
+ }
107
+ }
108
+ }
109
+
110
+ // MARK: - Properties
111
+
112
+ /** Persistent user ID (stored across app launches) */
113
+ val userId: String
114
+
115
+ /** Current session ID (new per app launch) */
116
+ val sessionId: String
117
+
118
+ /** Session start time (ISO 8601 string) */
119
+ val sessionStartTime: String
120
+
121
+ /** Total number of sessions for this user (across all time) */
122
+ val totalSessionCount: Int
123
+
124
+ /** True if this is the user's first session */
125
+ val isNewUser: Boolean
126
+ get() = totalSessionCount == 1
127
+
128
+ /** UTM parameters (from deep links or attribution) */
129
+ var utmSource: String? = null
130
+ private set
131
+ var utmMedium: String? = null
132
+ private set
133
+ var utmCampaign: String? = null
134
+ private set
135
+ var utmTerm: String? = null
136
+ private set
137
+ var utmContent: String? = null
138
+ private set
139
+
140
+ /** Session attribution source (derived from UTM or defaults to "direct") */
141
+ val sessionSource: String
142
+ get() = utmSource ?: "direct"
143
+
144
+ /** Session attribution medium (derived from UTM or defaults to "none") */
145
+ val sessionMedium: String
146
+ get() = utmMedium ?: "none"
147
+
148
+ /** Number of page views in current session */
149
+ private val sessionDepthCounter = AtomicInteger(0)
150
+ val sessionDepth: Int
151
+ get() = sessionDepthCounter.get()
152
+
153
+ /** Current page ID (new per page/screen view) */
154
+ private val currentPageIdRef = AtomicReference<String?>(null)
155
+ val currentPageId: String?
156
+ get() = currentPageIdRef.get()
157
+
158
+ /** Current screen name (for use in impression events) */
159
+ private val currentScreenNameRef = AtomicReference<String?>(null)
160
+ val currentScreenName: String?
161
+ get() = currentScreenNameRef.get()
162
+
163
+ // Analytics tracking counters
164
+ internal val screenViewCounter = AtomicInteger(0)
165
+ internal val adRequestCounter = AtomicInteger(0)
166
+ internal val adImpressionCounter = AtomicInteger(0)
167
+ internal val totalRevenueMicrosCounter = AtomicLong(0)
168
+
169
+ init {
170
+ // Load or generate user_id
171
+ val existingUserId = storage.getString(KEY_USER_ID)
172
+ if (existingUserId != null) {
173
+ userId = existingUserId
174
+ BCLogger.d(TAG, "Loaded existing user_id: $existingUserId")
175
+ } else {
176
+ val newUserId = UUID.randomUUID().toString()
177
+ storage.putString(KEY_USER_ID, newUserId)
178
+ userId = newUserId
179
+ BCLogger.d(TAG, "Generated new user_id: $newUserId")
180
+ }
181
+
182
+ // Load session count and increment
183
+ val sessionCountStr = storage.getString(KEY_SESSION_COUNT) ?: "0"
184
+ val previousCount = sessionCountStr.toIntOrNull() ?: 0
185
+ totalSessionCount = previousCount + 1
186
+ storage.putString(KEY_SESSION_COUNT, totalSessionCount.toString())
187
+
188
+ // Generate new session_id for this app launch
189
+ sessionId = UUID.randomUUID().toString()
190
+
191
+ // Set session start time (ISO 8601 format)
192
+ sessionStartTime = getCurrentTimestamp()
193
+ storage.putString(KEY_SESSION_START_TIME, sessionStartTime)
194
+
195
+ // Load UTM parameters (if set from deep link)
196
+ utmSource = storage.getString(KEY_UTM_SOURCE)
197
+ utmMedium = storage.getString(KEY_UTM_MEDIUM)
198
+ utmCampaign = storage.getString(KEY_UTM_CAMPAIGN)
199
+ utmTerm = storage.getString(KEY_UTM_TERM)
200
+ utmContent = storage.getString(KEY_UTM_CONTENT)
201
+
202
+ BCLogger.d(TAG, "Started session $totalSessionCount with session_id: $sessionId")
203
+ }
204
+
205
+ // MARK: - Page Tracking
206
+
207
+ /**
208
+ * Start a new page view
209
+ *
210
+ * Generates a new page_id and increments session_depth.
211
+ * Should be called at the start of each screen/page view.
212
+ *
213
+ * @param screenName Optional screen name to store for impression tracking
214
+ * @return The new page_id
215
+ */
216
+ fun startPageView(screenName: String? = null): String {
217
+ val depth = sessionDepthCounter.incrementAndGet()
218
+ val pageId = UUID.randomUUID().toString()
219
+ currentPageIdRef.set(pageId)
220
+ screenName?.let { currentScreenNameRef.set(it) }
221
+
222
+ BCLogger.d(TAG, "Started page view $depth with page_id: $pageId, screenName: $screenName")
223
+
224
+ return pageId
225
+ }
226
+
227
+ /**
228
+ * Get the current page ID, generating one if needed
229
+ *
230
+ * @return The current page_id (creates one if none exists)
231
+ */
232
+ fun getOrCreatePageId(): String {
233
+ var pageId = currentPageIdRef.get()
234
+ if (pageId == null) {
235
+ // Auto-generate if no page view has been started
236
+ sessionDepthCounter.incrementAndGet()
237
+ pageId = UUID.randomUUID().toString()
238
+ currentPageIdRef.set(pageId)
239
+ }
240
+ return pageId
241
+ }
242
+
243
+ // MARK: - Reset
244
+
245
+ /** Reset session counters (when starting a new session) */
246
+ internal fun resetSessionCounters() {
247
+ screenViewCounter.set(0)
248
+ adRequestCounter.set(0)
249
+ adImpressionCounter.set(0)
250
+ totalRevenueMicrosCounter.set(0)
251
+ sessionDepthCounter.set(0)
252
+ currentPageIdRef.set(null)
253
+ BCLogger.d(TAG, "Session counters reset")
254
+ }
255
+
256
+ /** Reset all session state (for testing only) */
257
+ private fun resetState() {
258
+ storage.clear()
259
+ sessionDepthCounter.set(0)
260
+ currentPageIdRef.set(null)
261
+ resetSessionCounters()
262
+ }
263
+
264
+ // MARK: - UTM Parameter Management
265
+
266
+ /**
267
+ * Set UTM parameters (typically from deep link)
268
+ *
269
+ * This persists UTM parameters for attribution tracking. These will be
270
+ * loaded for future sessions until cleared.
271
+ *
272
+ * @param source UTM source (e.g., "google", "facebook")
273
+ * @param medium UTM medium (e.g., "cpc", "email")
274
+ * @param campaign UTM campaign (e.g., "summer_sale")
275
+ * @param term UTM term (optional keyword)
276
+ * @param content UTM content (optional content variant)
277
+ */
278
+ fun setUTMParameters(
279
+ source: String? = null,
280
+ medium: String? = null,
281
+ campaign: String? = null,
282
+ term: String? = null,
283
+ content: String? = null
284
+ ) {
285
+ utmSource = source
286
+ utmMedium = medium
287
+ utmCampaign = campaign
288
+ utmTerm = term
289
+ utmContent = content
290
+
291
+ // Persist to storage
292
+ source?.let { storage.putString(KEY_UTM_SOURCE, it) }
293
+ medium?.let { storage.putString(KEY_UTM_MEDIUM, it) }
294
+ campaign?.let { storage.putString(KEY_UTM_CAMPAIGN, it) }
295
+ term?.let { storage.putString(KEY_UTM_TERM, it) }
296
+ content?.let { storage.putString(KEY_UTM_CONTENT, it) }
297
+
298
+ BCLogger.d(TAG, "Set UTM parameters - source: $source, medium: $medium, campaign: $campaign")
299
+ }
300
+
301
+ /**
302
+ * Clear UTM parameters
303
+ */
304
+ fun clearUTMParameters() {
305
+ utmSource = null
306
+ utmMedium = null
307
+ utmCampaign = null
308
+ utmTerm = null
309
+ utmContent = null
310
+
311
+ storage.remove(KEY_UTM_SOURCE)
312
+ storage.remove(KEY_UTM_MEDIUM)
313
+ storage.remove(KEY_UTM_CAMPAIGN)
314
+ storage.remove(KEY_UTM_TERM)
315
+ storage.remove(KEY_UTM_CONTENT)
316
+ }
317
+
318
+ // MARK: - Timestamp Utilities
319
+
320
+ /**
321
+ * Get current timestamp in ISO 8601 format with milliseconds
322
+ *
323
+ * @return ISO 8601 timestamp string (e.g., "2025-01-15T10:30:00.000Z")
324
+ */
325
+ fun getCurrentTimestamp(): String {
326
+ val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US)
327
+ dateFormat.timeZone = TimeZone.getTimeZone("UTC")
328
+ return dateFormat.format(Date())
329
+ }
330
+ }
@@ -0,0 +1,60 @@
1
+ package com.bigcrunch.ads.internal
2
+
3
+ import android.content.Context
4
+ import android.os.Build
5
+ import android.provider.Settings
6
+ import android.telephony.TelephonyManager
7
+ import android.util.DisplayMetrics
8
+ import com.bigcrunch.ads.models.DeviceData
9
+ import java.util.*
10
+
11
+ /**
12
+ * Helper class to get device information
13
+ */
14
+ internal object DeviceHelper {
15
+
16
+ fun getDeviceData(context: Context): DeviceData {
17
+ val displayMetrics = context.resources.displayMetrics
18
+ val telephonyManager = context.getSystemService(Context.TELEPHONY_SERVICE) as? TelephonyManager
19
+
20
+ return DeviceData(
21
+ deviceId = getDeviceId(context),
22
+ deviceModel = "${Build.MANUFACTURER} ${Build.MODEL}",
23
+ osVersion = Build.VERSION.RELEASE,
24
+ appVersion = getAppVersion(context),
25
+ screenWidth = displayMetrics.widthPixels,
26
+ screenHeight = displayMetrics.heightPixels,
27
+ language = Locale.getDefault().language,
28
+ country = Locale.getDefault().country,
29
+ carrier = telephonyManager?.networkOperatorName,
30
+ networkType = getNetworkType(context),
31
+ isTablet = isTablet(context)
32
+ )
33
+ }
34
+
35
+ private fun getDeviceId(context: Context): String {
36
+ return Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID) ?: "unknown"
37
+ }
38
+
39
+ private fun getAppVersion(context: Context): String {
40
+ return try {
41
+ val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)
42
+ packageInfo.versionName ?: "unknown"
43
+ } catch (e: Exception) {
44
+ "unknown"
45
+ }
46
+ }
47
+
48
+ private fun getNetworkType(context: Context): String? {
49
+ // TODO: Implement network type detection
50
+ return "unknown"
51
+ }
52
+
53
+ private fun isTablet(context: Context): Boolean {
54
+ val displayMetrics = context.resources.displayMetrics
55
+ val widthDp = displayMetrics.widthPixels / displayMetrics.density
56
+ val heightDp = displayMetrics.heightPixels / displayMetrics.density
57
+ val screenSize = Math.sqrt((widthDp * widthDp + heightDp * heightDp).toDouble())
58
+ return screenSize >= 600
59
+ }
60
+ }
@@ -0,0 +1,114 @@
1
+ package com.bigcrunch.ads.internal
2
+
3
+ import kotlinx.coroutines.Dispatchers
4
+ import kotlinx.coroutines.withContext
5
+ import okhttp3.MediaType.Companion.toMediaType
6
+ import okhttp3.OkHttpClient
7
+ import okhttp3.Request
8
+ import okhttp3.RequestBody.Companion.toRequestBody
9
+ import java.util.concurrent.TimeUnit
10
+
11
+ /**
12
+ * HTTP client for making network requests
13
+ *
14
+ * Uses OkHttp for reliable networking with automatic retries and connection pooling.
15
+ * All methods are suspend functions that run on the IO dispatcher.
16
+ */
17
+ internal class HttpClient {
18
+
19
+ private val client = OkHttpClient.Builder()
20
+ .connectTimeout(10, TimeUnit.SECONDS)
21
+ .readTimeout(10, TimeUnit.SECONDS)
22
+ .writeTimeout(10, TimeUnit.SECONDS)
23
+ .build()
24
+
25
+ /**
26
+ * Perform a GET request
27
+ *
28
+ * @param url The URL to fetch
29
+ * @param headers Optional HTTP headers
30
+ * @return Result containing response body or error
31
+ */
32
+ suspend fun get(
33
+ url: String,
34
+ headers: Map<String, String> = emptyMap()
35
+ ): Result<String> {
36
+ return withContext(Dispatchers.IO) {
37
+ try {
38
+ val requestBuilder = Request.Builder().url(url)
39
+
40
+ headers.forEach { (key, value) ->
41
+ requestBuilder.addHeader(key, value)
42
+ }
43
+
44
+ val response = client.newCall(requestBuilder.build()).execute()
45
+
46
+ if (response.isSuccessful) {
47
+ val body = response.body?.string() ?: ""
48
+ BCLogger.v("HttpClient", "GET success: $url (${body.length} bytes)")
49
+ Result.success(body)
50
+ } else {
51
+ val error = "HTTP ${response.code}: ${response.message}"
52
+ BCLogger.w("HttpClient", "GET failed: $url - $error")
53
+ Result.failure(Exception(error))
54
+ }
55
+ } catch (e: Exception) {
56
+ BCLogger.e("HttpClient", "GET request failed: $url", e)
57
+ Result.failure(e)
58
+ }
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Perform a POST request
64
+ *
65
+ * @param url The URL to post to
66
+ * @param body Request body (JSON string)
67
+ * @param headers Optional HTTP headers
68
+ * @return Result containing response body or error
69
+ */
70
+ suspend fun post(
71
+ url: String,
72
+ body: String,
73
+ headers: Map<String, String> = emptyMap()
74
+ ): Result<String> {
75
+ return withContext(Dispatchers.IO) {
76
+ try {
77
+ // Log the request body for debugging
78
+ BCLogger.d("HttpClient", "POST to: $url")
79
+ BCLogger.d("HttpClient", "Request body: $body")
80
+
81
+ val mediaType = "application/json".toMediaType()
82
+ val requestBody = body.toRequestBody(mediaType)
83
+
84
+ val requestBuilder = Request.Builder()
85
+ .url(url)
86
+ .post(requestBody)
87
+
88
+ headers.forEach { (key, value) ->
89
+ requestBuilder.addHeader(key, value)
90
+ }
91
+
92
+ val response = client.newCall(requestBuilder.build()).execute()
93
+
94
+ if (response.isSuccessful) {
95
+ val responseBody = response.body?.string() ?: ""
96
+ BCLogger.v("HttpClient", "POST success: $url")
97
+ Result.success(responseBody)
98
+ } else {
99
+ // Log response body for 4xx errors to see what the server says
100
+ val errorBody = response.body?.string() ?: ""
101
+ val error = "HTTP ${response.code}: ${response.message}"
102
+ BCLogger.w("HttpClient", "POST failed: $url - $error")
103
+ if (errorBody.isNotEmpty()) {
104
+ BCLogger.w("HttpClient", "Error response body: $errorBody")
105
+ }
106
+ Result.failure(Exception(error))
107
+ }
108
+ } catch (e: Exception) {
109
+ BCLogger.e("HttpClient", "POST request failed: $url", e)
110
+ Result.failure(e)
111
+ }
112
+ }
113
+ }
114
+ }
@@ -0,0 +1,71 @@
1
+ package com.bigcrunch.ads.internal
2
+
3
+ import android.util.Log
4
+
5
+ /**
6
+ * Internal logger for BigCrunch Ads SDK
7
+ *
8
+ * All logs are prefixed with "BCrunch:" for easy filtering.
9
+ * Logging can be disabled in production by setting isEnabled = false.
10
+ * Error logs are always shown regardless of isEnabled flag.
11
+ */
12
+ internal object BCLogger {
13
+
14
+ /**
15
+ * Enable/disable debug logging
16
+ * Defaults to false for production builds
17
+ */
18
+ var isEnabled = false
19
+
20
+ /**
21
+ * Verbose log - detailed debugging information
22
+ */
23
+ fun v(tag: String, msg: String) {
24
+ if (isEnabled) {
25
+ Log.v("BCrunch:$tag", msg)
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Debug log - general debugging information
31
+ */
32
+ fun d(tag: String, msg: String) {
33
+ if (isEnabled) {
34
+ Log.d("BCrunch:$tag", msg)
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Info log - informational messages
40
+ */
41
+ fun i(tag: String, msg: String) {
42
+ if (isEnabled) {
43
+ Log.i("BCrunch:$tag", msg)
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Warning log - potential issues
49
+ */
50
+ fun w(tag: String, msg: String, throwable: Throwable? = null) {
51
+ if (isEnabled) {
52
+ if (throwable != null) {
53
+ Log.w("BCrunch:$tag", msg, throwable)
54
+ } else {
55
+ Log.w("BCrunch:$tag", msg)
56
+ }
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Error log - critical errors
62
+ * Always logs regardless of isEnabled flag
63
+ */
64
+ fun e(tag: String, msg: String, throwable: Throwable? = null) {
65
+ if (throwable != null) {
66
+ Log.e("BCrunch:$tag", msg, throwable)
67
+ } else {
68
+ Log.e("BCrunch:$tag", msg)
69
+ }
70
+ }
71
+ }
@@ -0,0 +1,125 @@
1
+ package com.bigcrunch.ads.internal
2
+
3
+ import android.content.Context
4
+ import android.preference.PreferenceManager
5
+
6
+ /**
7
+ * Centralized privacy signal store
8
+ *
9
+ * Manages GDPR, CCPA, and COPPA privacy signals for the SDK.
10
+ * Reads IAB TCF/USP standard keys from SharedPreferences when available,
11
+ * and provides structured objects for bid request construction.
12
+ */
13
+ internal class PrivacyStore(private val context: Context) {
14
+
15
+ private val lock = Any()
16
+
17
+ // SDK-set values (from public API)
18
+ @Volatile private var _gdprConsentString: String? = null
19
+ @Volatile private var _gdprApplies: Boolean? = null
20
+ @Volatile private var _ccpaString: String? = null
21
+ @Volatile private var _coppaApplies: Boolean = false
22
+
23
+ companion object {
24
+ // IAB standard SharedPreferences keys
25
+ private const val IAB_TCF_CONSENT_KEY = "IABTCF_TCString"
26
+ private const val IAB_TCF_GDPR_APPLIES_KEY = "IABTCF_gdprApplies"
27
+ private const val IAB_USP_STRING_KEY = "IABUSPrivacy_String"
28
+ }
29
+
30
+ // MARK: - Setters (called from public API)
31
+
32
+ fun setGdprConsent(consent: String) {
33
+ synchronized(lock) {
34
+ _gdprApplies = true
35
+ _gdprConsentString = consent
36
+ }
37
+ BCLogger.d("PrivacyStore", "GDPR consent set")
38
+ }
39
+
40
+ fun setCcpaString(ccpa: String) {
41
+ synchronized(lock) {
42
+ _ccpaString = ccpa
43
+ }
44
+ // Also write to IAB standard key for CMP interop
45
+ PreferenceManager.getDefaultSharedPreferences(context)
46
+ .edit()
47
+ .putString(IAB_USP_STRING_KEY, ccpa)
48
+ .apply()
49
+ BCLogger.d("PrivacyStore", "CCPA string set")
50
+ }
51
+
52
+ fun setCoppaApplies(applies: Boolean) {
53
+ synchronized(lock) {
54
+ _coppaApplies = applies
55
+ }
56
+ BCLogger.d("PrivacyStore", "COPPA set to $applies")
57
+ }
58
+
59
+ // MARK: - Getters
60
+
61
+ /** Effective GDPR consent string (SDK-set takes priority, then IAB TCF) */
62
+ val gdprConsentString: String?
63
+ get() = synchronized(lock) {
64
+ _gdprConsentString
65
+ } ?: iabPrefs.getString(IAB_TCF_CONSENT_KEY, null)
66
+
67
+ /** Whether GDPR applies (SDK-set takes priority, then IAB TCF) */
68
+ val gdprApplies: Boolean?
69
+ get() {
70
+ synchronized(lock) {
71
+ _gdprApplies?.let { return it }
72
+ }
73
+ val prefs = iabPrefs
74
+ return if (prefs.contains(IAB_TCF_GDPR_APPLIES_KEY)) {
75
+ prefs.getInt(IAB_TCF_GDPR_APPLIES_KEY, 0) == 1
76
+ } else {
77
+ null
78
+ }
79
+ }
80
+
81
+ /** Effective CCPA/USP string (SDK-set takes priority, then IAB USP) */
82
+ val ccpaString: String?
83
+ get() = synchronized(lock) {
84
+ _ccpaString
85
+ } ?: iabPrefs.getString(IAB_USP_STRING_KEY, null)
86
+
87
+ val coppaApplies: Boolean
88
+ get() = synchronized(lock) { _coppaApplies }
89
+
90
+ private val iabPrefs
91
+ get() = PreferenceManager.getDefaultSharedPreferences(context)
92
+
93
+ // MARK: - Bid Request Helpers
94
+
95
+ /** Build the `regs` object for an OpenRTB bid request */
96
+ fun buildRegsMap(): Map<String, Any> {
97
+ val regs = mutableMapOf<String, Any>()
98
+ val ext = mutableMapOf<String, Any>()
99
+
100
+ regs["coppa"] = if (coppaApplies) 1 else 0
101
+
102
+ gdprApplies?.let { ext["gdpr"] = if (it) 1 else 0 }
103
+ ccpaString?.let { ext["us_privacy"] = it }
104
+
105
+ if (ext.isNotEmpty()) {
106
+ regs["ext"] = ext
107
+ }
108
+
109
+ return regs
110
+ }
111
+
112
+ /** Build the `user` object for an OpenRTB bid request */
113
+ fun buildUserMap(): Map<String, Any> {
114
+ val user = mutableMapOf<String, Any>()
115
+ val ext = mutableMapOf<String, Any>()
116
+
117
+ gdprConsentString?.let { ext["consent"] = it }
118
+
119
+ if (ext.isNotEmpty()) {
120
+ user["ext"] = ext
121
+ }
122
+
123
+ return user
124
+ }
125
+ }