@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,592 @@
1
+ package com.bigcrunch.ads.adapters
2
+
3
+ import android.app.Activity
4
+ import android.content.Context
5
+ import android.view.Gravity
6
+ import android.view.View
7
+ import android.view.ViewGroup
8
+ import android.widget.FrameLayout
9
+ import com.bigcrunch.ads.core.AnalyticsClient
10
+ import com.bigcrunch.ads.internal.BCLogger
11
+ import com.bigcrunch.ads.models.PlacementConfig
12
+ import com.google.android.gms.ads.AdError
13
+ import com.google.android.gms.ads.AdListener
14
+ import com.google.android.gms.ads.AdSize
15
+ import com.google.android.gms.ads.FullScreenContentCallback
16
+ import com.google.android.gms.ads.LoadAdError
17
+ import com.google.android.gms.ads.OnPaidEventListener
18
+ import com.google.android.gms.ads.admanager.AdManagerAdRequest
19
+ import com.google.android.gms.ads.admanager.AdManagerAdView
20
+ import com.google.android.gms.ads.admanager.AdManagerInterstitialAd
21
+ import com.google.android.gms.ads.admanager.AdManagerInterstitialAdLoadCallback
22
+ import com.google.android.gms.ads.rewarded.RewardedAd
23
+ import com.google.android.gms.ads.rewarded.RewardedAdLoadCallback
24
+ import com.google.android.gms.ads.rewarded.RewardItem
25
+ import com.google.android.gms.ads.OnUserEarnedRewardListener
26
+ import kotlin.coroutines.resume
27
+ import kotlin.coroutines.suspendCoroutine
28
+
29
+ /**
30
+ * Callback interface for banner ad events
31
+ */
32
+ interface BannerAdCallback {
33
+ fun onAdLoaded()
34
+ fun onAdFailedToLoad(error: String)
35
+ fun onAdClicked()
36
+ fun onAdImpression()
37
+ }
38
+
39
+ /**
40
+ * Callback interface for interstitial ad events
41
+ */
42
+ interface InterstitialAdCallback {
43
+ fun onAdLoaded()
44
+ fun onAdFailedToLoad(error: String)
45
+ fun onAdShowed()
46
+ fun onAdDismissed()
47
+ fun onAdClicked()
48
+ fun onAdImpression()
49
+ }
50
+
51
+ /**
52
+ * Callback interface for rewarded ad events
53
+ */
54
+ interface RewardedAdCallback {
55
+ fun onAdLoaded()
56
+ fun onAdFailedToLoad(error: String)
57
+ fun onAdShowed()
58
+ fun onAdDismissed()
59
+ fun onAdClicked()
60
+ fun onAdImpression()
61
+ fun onUserEarnedReward(type: String, amount: Int)
62
+ }
63
+
64
+ /**
65
+ * Adapter for Google Mobile Ads SDK (Ad Manager)
66
+ *
67
+ * Wraps GMA SDK calls and translates callbacks to AnalyticsClient events.
68
+ * Handles banner, interstitial, and rewarded ad formats.
69
+ */
70
+ internal class GoogleAdsAdapter(
71
+ private val context: Context,
72
+ private val analyticsClient: AnalyticsClient
73
+ ) {
74
+ companion object {
75
+ private const val TAG = "GoogleAdsAdapter"
76
+
77
+ /**
78
+ * Safely execute a callback, catching any exceptions thrown by user code.
79
+ * This ensures SDK stability even if app's callback implementation throws.
80
+ */
81
+ private inline fun safeCallback(block: () -> Unit) {
82
+ try {
83
+ block()
84
+ } catch (e: Exception) {
85
+ BCLogger.e(TAG, "Callback threw exception - SDK will not propagate this to prevent app crash", e)
86
+ }
87
+ }
88
+
89
+ // Google's official test ad unit IDs
90
+ // These are guaranteed to return test ads and will always fill
91
+ private const val TEST_BANNER_AD_UNIT = "ca-app-pub-3940256099942544/6300978111"
92
+ private const val TEST_INTERSTITIAL_AD_UNIT = "ca-app-pub-3940256099942544/1033173712"
93
+ private const val TEST_REWARDED_AD_UNIT = "ca-app-pub-3940256099942544/5224354917"
94
+ }
95
+
96
+ /**
97
+ * Resolve the ad unit ID to use - test ID if useTestAds is true, otherwise production
98
+ */
99
+ private fun resolveAdUnitId(
100
+ placementConfig: PlacementConfig,
101
+ gamNetworkCode: String,
102
+ useTestAds: Boolean
103
+ ): String {
104
+ if (useTestAds) {
105
+ val testAdUnit = when (placementConfig.format) {
106
+ "banner" -> TEST_BANNER_AD_UNIT
107
+ "interstitial" -> TEST_INTERSTITIAL_AD_UNIT
108
+ "rewarded" -> TEST_REWARDED_AD_UNIT
109
+ else -> TEST_BANNER_AD_UNIT // fallback
110
+ }
111
+ BCLogger.d(TAG, "Using TEST ad unit for ${placementConfig.format}: $testAdUnit")
112
+ return testAdUnit
113
+ } else {
114
+ // Production ad unit path
115
+ return "/$gamNetworkCode/${placementConfig.gamAdUnit}"
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Resolve a BigCrunch AdSize to a Google AdSize.
121
+ * For adaptive sizes, calculates the optimal ad size based on screen width.
122
+ */
123
+ private fun resolveGoogleAdSize(
124
+ bcAdSize: com.bigcrunch.ads.models.AdSize
125
+ ): AdSize {
126
+ if (bcAdSize.isAdaptive) {
127
+ val widthDp = if (bcAdSize.width > 0) {
128
+ bcAdSize.width
129
+ } else {
130
+ val displayMetrics = context.resources.displayMetrics
131
+ (displayMetrics.widthPixels / displayMetrics.density).toInt()
132
+ }
133
+ BCLogger.d(TAG, "Resolving adaptive banner with width: ${widthDp}dp")
134
+ return AdSize.getCurrentOrientationAnchoredAdaptiveBannerAdSize(context, widthDp)
135
+ }
136
+ return AdSize(bcAdSize.width, bcAdSize.height)
137
+ }
138
+
139
+ /**
140
+ * Create and load a banner ad
141
+ *
142
+ * @param placementConfig Configuration for the placement
143
+ * @param gamNetworkCode The GAM network code from app config
144
+ * @param adRequest Pre-configured ad request (with S2S targeting)
145
+ * @param container ViewGroup to add the banner to
146
+ * @param callback Callback for ad events
147
+ * @param useTestAds Whether to use test ad units
148
+ * @param adSizeOverride Optional size override (takes precedence over backend config)
149
+ * @return The loaded AdManagerAdView
150
+ */
151
+ fun loadBannerAd(
152
+ placementConfig: PlacementConfig,
153
+ gamNetworkCode: String,
154
+ adRequest: AdManagerAdRequest,
155
+ container: ViewGroup,
156
+ callback: BannerAdCallback,
157
+ useTestAds: Boolean = false,
158
+ adSizeOverride: com.bigcrunch.ads.models.AdSize? = null,
159
+ refreshCount: Int = 0
160
+ ): AdManagerAdView {
161
+ BCLogger.d(TAG, "Loading banner ad for: ${placementConfig.placementId} with adSizeOverride: $adSizeOverride")
162
+
163
+ // Resolve ad unit ID (test or production)
164
+ val adUnitId = resolveAdUnitId(placementConfig, gamNetworkCode, useTestAds)
165
+
166
+ val adView = AdManagerAdView(context)
167
+ adView.adUnitId = adUnitId
168
+
169
+ // Set ad size: use override first, then placement config, then default
170
+ if (adSizeOverride != null) {
171
+ BCLogger.d(TAG, "Using size override: $adSizeOverride (adaptive=${adSizeOverride.isAdaptive})")
172
+ adView.setAdSize(resolveGoogleAdSize(adSizeOverride))
173
+ } else {
174
+ val size = placementConfig.sizes?.firstOrNull()
175
+ if (size != null) {
176
+ adView.setAdSize(resolveGoogleAdSize(size))
177
+ } else {
178
+ // Default to banner size
179
+ adView.setAdSize(AdSize.BANNER)
180
+ }
181
+ }
182
+
183
+ // Set up ad listener for callbacks
184
+ adView.adListener = object : AdListener() {
185
+ override fun onAdLoaded() {
186
+ BCLogger.d(TAG, "Banner ad loaded: ${placementConfig.placementId}")
187
+
188
+ // Manually measure and layout the ad view for React Native compatibility
189
+ // React Native suppresses normal Android layout passes, so we need to do this explicitly
190
+ val adSize = adView.adSize
191
+ if (adSize != null) {
192
+ val width = adSize.getWidthInPixels(context)
193
+ val height = adSize.getHeightInPixels(context)
194
+ val left = adView.left
195
+ val top = adView.top
196
+
197
+ BCLogger.d(TAG, """
198
+ === MANUAL LAYOUT IN onAdLoaded ===
199
+ Ad size: ${adSize.width}x${adSize.height} dp
200
+ Pixels: ${width}x${height}
201
+ Position: left=$left, top=$top
202
+ ====================================
203
+ """.trimIndent())
204
+
205
+ adView.measure(
206
+ View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
207
+ View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY)
208
+ )
209
+ adView.layout(left, top, left + width, top + height)
210
+ }
211
+
212
+ // Log ad view dimensions after manual layout
213
+ BCLogger.d(TAG, """
214
+ === AD VIEW AFTER LOAD (post-layout) ===
215
+ Ad View dimensions: ${adView.width}x${adView.height}
216
+ Ad View measured: ${adView.measuredWidth}x${adView.measuredHeight}
217
+ Ad View visibility: ${adView.visibility}
218
+ Ad View parent: ${adView.parent}
219
+ Container dimensions: ${container.width}x${container.height}
220
+ =========================================
221
+ """.trimIndent())
222
+
223
+ // Force container to re-layout its children (triggers measureAndLayout runnable)
224
+ container.requestLayout()
225
+ container.invalidate()
226
+
227
+ // Extract GAM metadata from ResponseInfo
228
+ val responseInfo = adView.responseInfo
229
+ val advertiserId = responseInfo?.loadedAdapterResponseInfo?.adSourceId
230
+ val adMetadata = extractAdMetadata(responseInfo)
231
+
232
+ analyticsClient.trackAdImpression(
233
+ placementId = placementConfig.placementId,
234
+ format = placementConfig.format,
235
+ advertiserId = adMetadata["advertiser_id"] as? String,
236
+ campaignId = adMetadata["campaign_id"] as? String,
237
+ lineItemId = adMetadata["line_item_id"] as? String,
238
+ creativeId = adMetadata["creative_id"] as? String,
239
+ refreshCount = refreshCount
240
+ )
241
+ safeCallback { callback.onAdLoaded() }
242
+ }
243
+
244
+ override fun onAdFailedToLoad(error: LoadAdError) {
245
+ BCLogger.w(TAG, "Banner ad failed to load: ${error.message}")
246
+ safeCallback { callback.onAdFailedToLoad(error.message) }
247
+ }
248
+
249
+ override fun onAdClicked() {
250
+ BCLogger.d(TAG, "Banner ad clicked: ${placementConfig.placementId}")
251
+ analyticsClient.trackAdClick(placementConfig.placementId, placementConfig.format)
252
+ safeCallback { callback.onAdClicked() }
253
+ }
254
+
255
+ override fun onAdImpression() {
256
+ BCLogger.d(TAG, "Banner ad impression: ${placementConfig.placementId}")
257
+ safeCallback { callback.onAdImpression() }
258
+ }
259
+ }
260
+
261
+ // Set up paid event listener for ILRD (Impression-Level Revenue Data)
262
+ adView.onPaidEventListener = OnPaidEventListener { adValue ->
263
+ BCLogger.d(TAG, "Banner paid event: ${adValue.valueMicros} ${adValue.currencyCode}")
264
+ // TODO: Track revenue separately when revenue tracking is implemented
265
+ // Revenue is included in the impression event via adPrice field
266
+ }
267
+
268
+ // Add to container with proper layout parameters
269
+ // Use actual pixel dimensions from the ad size
270
+ val adSize = adView.adSize
271
+ val widthPixels = if (adSize != null) {
272
+ adSize.getWidthInPixels(context)
273
+ } else {
274
+ // Default banner width in pixels (320dp)
275
+ (320 * context.resources.displayMetrics.density).toInt()
276
+ }
277
+ val heightPixels = if (adSize != null) {
278
+ adSize.getHeightInPixels(context)
279
+ } else {
280
+ // Default banner height in pixels (50dp)
281
+ (50 * context.resources.displayMetrics.density).toInt()
282
+ }
283
+
284
+ val layoutParams = FrameLayout.LayoutParams(widthPixels, heightPixels).apply {
285
+ gravity = Gravity.CENTER
286
+ }
287
+
288
+ // Ensure the AdView is visible
289
+ adView.visibility = View.VISIBLE
290
+
291
+ BCLogger.d(TAG, """
292
+ === BEFORE ADDING AD VIEW ===
293
+ Container: $container
294
+ Container children: ${container.childCount}
295
+ Ad View: $adView
296
+ Ad size: ${adSize?.width}x${adSize?.height}
297
+ Layout params: ${widthPixels}x${heightPixels}
298
+ ============================
299
+ """.trimIndent())
300
+
301
+ container.addView(adView, layoutParams)
302
+
303
+ // Trigger requestLayout on container to ensure child views get measured/laid out
304
+ // This is critical for React Native where layout passes are suppressed
305
+ container.requestLayout()
306
+
307
+ BCLogger.d(TAG, """
308
+ === AFTER ADDING AD VIEW ===
309
+ Container children: ${container.childCount}
310
+ Ad View parent: ${adView.parent}
311
+ Ad View visibility: ${adView.visibility}
312
+ Ad View dimensions: ${adView.width}x${adView.height}
313
+ ===========================
314
+ """.trimIndent())
315
+
316
+ adView.loadAd(adRequest)
317
+
318
+ return adView
319
+ }
320
+
321
+ /**
322
+ * Load an interstitial ad
323
+ *
324
+ * @param placementConfig Configuration for the placement
325
+ * @param gamNetworkCode The GAM network code from app config
326
+ * @param adRequest Pre-configured ad request (with S2S targeting)
327
+ * @return Result containing loaded interstitial or error
328
+ */
329
+ suspend fun loadInterstitialAd(
330
+ placementConfig: PlacementConfig,
331
+ gamNetworkCode: String,
332
+ adRequest: AdManagerAdRequest,
333
+ useTestAds: Boolean = false
334
+ ): Result<AdManagerInterstitialAd> = suspendCoroutine { continuation ->
335
+ BCLogger.d(TAG, "Loading interstitial ad for: ${placementConfig.placementId}")
336
+
337
+ // Resolve ad unit ID (test or production)
338
+ val adUnitId = resolveAdUnitId(placementConfig, gamNetworkCode, useTestAds)
339
+
340
+ AdManagerInterstitialAd.load(
341
+ context,
342
+ adUnitId,
343
+ adRequest,
344
+ object : AdManagerInterstitialAdLoadCallback() {
345
+ override fun onAdLoaded(interstitialAd: AdManagerInterstitialAd) {
346
+ BCLogger.d(TAG, "Interstitial ad loaded: ${placementConfig.placementId}")
347
+
348
+ // Set up paid event listener for ILRD
349
+ interstitialAd.onPaidEventListener = OnPaidEventListener { adValue ->
350
+ BCLogger.d(TAG, "Interstitial paid event: ${adValue.valueMicros} ${adValue.currencyCode}")
351
+ // TODO: Track revenue separately when revenue tracking is implemented
352
+ // Revenue is included in the impression event via adPrice field
353
+ }
354
+
355
+ continuation.resume(Result.success(interstitialAd))
356
+ }
357
+
358
+ override fun onAdFailedToLoad(error: LoadAdError) {
359
+ BCLogger.w(TAG, "Interstitial ad failed to load: ${error.message}")
360
+ continuation.resume(Result.failure(Exception(error.message)))
361
+ }
362
+ }
363
+ )
364
+ }
365
+
366
+ /**
367
+ * Show an interstitial ad
368
+ *
369
+ * @param interstitialAd The loaded interstitial ad
370
+ * @param activity Activity to show the ad from
371
+ * @param placementConfig Configuration for the placement
372
+ * @param callback Callback for ad events
373
+ */
374
+ fun showInterstitialAd(
375
+ interstitialAd: AdManagerInterstitialAd,
376
+ activity: Activity,
377
+ placementConfig: PlacementConfig,
378
+ callback: InterstitialAdCallback
379
+ ) {
380
+ BCLogger.d(TAG, "Showing interstitial ad: ${placementConfig.placementId}")
381
+
382
+ interstitialAd.fullScreenContentCallback = object : FullScreenContentCallback() {
383
+ override fun onAdShowedFullScreenContent() {
384
+ BCLogger.d(TAG, "Interstitial ad shown: ${placementConfig.placementId}")
385
+
386
+ // Extract GAM metadata from ResponseInfo
387
+ val responseInfo = interstitialAd.responseInfo
388
+ val adMetadata = extractAdMetadata(responseInfo)
389
+
390
+ analyticsClient.trackAdImpression(
391
+ placementId = placementConfig.placementId,
392
+ format = placementConfig.format,
393
+ advertiserId = adMetadata["advertiser_id"] as? String,
394
+ campaignId = adMetadata["campaign_id"] as? String,
395
+ lineItemId = adMetadata["line_item_id"] as? String,
396
+ creativeId = adMetadata["creative_id"] as? String
397
+ )
398
+ safeCallback { callback.onAdShowed() }
399
+ }
400
+
401
+ override fun onAdDismissedFullScreenContent() {
402
+ BCLogger.d(TAG, "Interstitial ad dismissed: ${placementConfig.placementId}")
403
+ safeCallback { callback.onAdDismissed() }
404
+ }
405
+
406
+ override fun onAdFailedToShowFullScreenContent(error: AdError) {
407
+ BCLogger.w(TAG, "Interstitial ad failed to show: ${error.message}")
408
+ safeCallback { callback.onAdFailedToLoad(error.message) }
409
+ }
410
+
411
+ override fun onAdClicked() {
412
+ BCLogger.d(TAG, "Interstitial ad clicked: ${placementConfig.placementId}")
413
+ analyticsClient.trackAdClick(placementConfig.placementId, placementConfig.format)
414
+ safeCallback { callback.onAdClicked() }
415
+ }
416
+
417
+ override fun onAdImpression() {
418
+ BCLogger.d(TAG, "Interstitial ad impression: ${placementConfig.placementId}")
419
+ safeCallback { callback.onAdImpression() }
420
+ }
421
+ }
422
+
423
+ interstitialAd.show(activity)
424
+ }
425
+
426
+ /**
427
+ * Load a rewarded ad
428
+ *
429
+ * @param placementConfig Configuration for the placement
430
+ * @param gamNetworkCode The GAM network code from app config
431
+ * @param adRequest Pre-configured ad request (with S2S targeting)
432
+ * @return Result containing the loaded RewardedAd or an error
433
+ */
434
+ suspend fun loadRewardedAd(
435
+ placementConfig: PlacementConfig,
436
+ gamNetworkCode: String,
437
+ adRequest: AdManagerAdRequest,
438
+ useTestAds: Boolean = false
439
+ ): Result<RewardedAd> = suspendCoroutine { continuation ->
440
+ BCLogger.d(TAG, "Loading rewarded ad for: ${placementConfig.placementId}")
441
+
442
+ // Resolve ad unit ID (test or production)
443
+ val adUnitId = resolveAdUnitId(placementConfig, gamNetworkCode, useTestAds)
444
+
445
+ RewardedAd.load(
446
+ context,
447
+ adUnitId,
448
+ adRequest,
449
+ object : RewardedAdLoadCallback() {
450
+ override fun onAdLoaded(ad: RewardedAd) {
451
+ BCLogger.d(TAG, "Rewarded ad loaded: ${placementConfig.placementId}")
452
+
453
+ // Set up paid event listener for ILRD
454
+ ad.onPaidEventListener = OnPaidEventListener { adValue ->
455
+ val valueMicros = adValue.valueMicros
456
+ val currencyCode = adValue.currencyCode
457
+ BCLogger.d(TAG, "Rewarded ad paid event: $valueMicros $currencyCode")
458
+ // TODO: Track revenue separately when revenue tracking is implemented
459
+ // Revenue is included in the impression event via adPrice field
460
+ }
461
+
462
+ continuation.resume(Result.success(ad))
463
+ }
464
+
465
+ override fun onAdFailedToLoad(error: LoadAdError) {
466
+ BCLogger.w(TAG, "Rewarded ad failed to load: ${error.message}")
467
+ continuation.resume(Result.failure(Exception(error.message)))
468
+ }
469
+ }
470
+ )
471
+ }
472
+
473
+ /**
474
+ * Show a rewarded ad
475
+ *
476
+ * @param rewardedAd The loaded rewarded ad to show
477
+ * @param activity The activity to show from
478
+ * @param placementConfig Configuration for the placement
479
+ * @param callback Callback for ad events
480
+ */
481
+ fun showRewardedAd(
482
+ rewardedAd: RewardedAd,
483
+ activity: Activity,
484
+ placementConfig: PlacementConfig,
485
+ callback: RewardedAdCallback
486
+ ) {
487
+ BCLogger.d(TAG, "Showing rewarded ad for: ${placementConfig.placementId}")
488
+
489
+ rewardedAd.fullScreenContentCallback = object : FullScreenContentCallback() {
490
+ override fun onAdShowedFullScreenContent() {
491
+ BCLogger.d(TAG, "Rewarded ad shown: ${placementConfig.placementId}")
492
+
493
+ // Extract GAM metadata from ResponseInfo
494
+ val responseInfo = rewardedAd.responseInfo
495
+ val adMetadata = extractAdMetadata(responseInfo)
496
+
497
+ analyticsClient.trackAdImpression(
498
+ placementId = placementConfig.placementId,
499
+ format = placementConfig.format,
500
+ advertiserId = adMetadata["advertiser_id"] as? String,
501
+ campaignId = adMetadata["campaign_id"] as? String,
502
+ lineItemId = adMetadata["line_item_id"] as? String,
503
+ creativeId = adMetadata["creative_id"] as? String
504
+ )
505
+ safeCallback { callback.onAdShowed() }
506
+ }
507
+
508
+ override fun onAdDismissedFullScreenContent() {
509
+ BCLogger.d(TAG, "Rewarded ad dismissed: ${placementConfig.placementId}")
510
+ safeCallback { callback.onAdDismissed() }
511
+ }
512
+
513
+ override fun onAdFailedToShowFullScreenContent(error: AdError) {
514
+ BCLogger.w(TAG, "Rewarded ad failed to show: ${error.message}")
515
+ safeCallback { callback.onAdFailedToLoad(error.message) }
516
+ }
517
+
518
+ override fun onAdClicked() {
519
+ BCLogger.d(TAG, "Rewarded ad clicked: ${placementConfig.placementId}")
520
+ analyticsClient.trackAdClick(placementConfig.placementId, placementConfig.format)
521
+ safeCallback { callback.onAdClicked() }
522
+ }
523
+
524
+ override fun onAdImpression() {
525
+ BCLogger.d(TAG, "Rewarded ad impression: ${placementConfig.placementId}")
526
+ safeCallback { callback.onAdImpression() }
527
+ }
528
+ }
529
+
530
+ // Show the ad with reward handler
531
+ rewardedAd.show(activity) { rewardItem: RewardItem ->
532
+ val rewardAmount = rewardItem.amount
533
+ val rewardType = rewardItem.type
534
+ BCLogger.d(TAG, "User earned reward: $rewardType x$rewardAmount")
535
+ safeCallback { callback.onUserEarnedReward(rewardType, rewardAmount) }
536
+ }
537
+ }
538
+
539
+ /**
540
+ * Destroy a banner ad view
541
+ *
542
+ * @param adView The AdManagerAdView to destroy
543
+ */
544
+ fun destroyBannerAd(adView: AdManagerAdView) {
545
+ BCLogger.d(TAG, "Destroying banner ad")
546
+ adView.destroy()
547
+ }
548
+
549
+ /**
550
+ * Extract GAM metadata from ResponseInfo
551
+ *
552
+ * Google Ad Manager provides limited metadata in ResponseInfo. The main fields available are:
553
+ * - adSourceId: Can contain advertiser ID or network ID
554
+ * - responseId: Unique response identifier (can be used as creative ID proxy)
555
+ *
556
+ * Note: GAM doesn't directly expose campaignId or lineItemId in mobile SDK ResponseInfo.
557
+ * These fields may be available in server-side logs but not in the client SDK.
558
+ *
559
+ * @param responseInfo The ResponseInfo from the ad response
560
+ * @return Map of available metadata fields
561
+ */
562
+ private fun extractAdMetadata(responseInfo: com.google.android.gms.ads.ResponseInfo?): Map<String, Any?> {
563
+ if (responseInfo == null) {
564
+ return emptyMap()
565
+ }
566
+
567
+ val metadata = mutableMapOf<String, Any?>()
568
+
569
+ // Extract adSourceId (often contains advertiser/network ID)
570
+ val loadedAdapter = responseInfo.loadedAdapterResponseInfo
571
+ if (loadedAdapter != null) {
572
+ metadata["advertiser_id"] = loadedAdapter.adSourceId
573
+ metadata["creative_id"] = loadedAdapter.adSourceInstanceId
574
+ }
575
+
576
+ // Response ID can serve as a creative identifier
577
+ val responseId = responseInfo.responseId
578
+ if (responseId != null && !responseId.isEmpty()) {
579
+ // Use responseId as creative_id if we don't have a better one
580
+ if (!metadata.containsKey("creative_id")) {
581
+ metadata["creative_id"] = responseId
582
+ }
583
+ }
584
+
585
+ // Log extracted metadata for debugging
586
+ if (metadata.isNotEmpty()) {
587
+ BCLogger.d(TAG, "Extracted GAM metadata: $metadata")
588
+ }
589
+
590
+ return metadata
591
+ }
592
+ }