@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.
- 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/android/src/main/java/com/bigcrunch/ads/react/BigCrunchAdsModule.kt +0 -23
- 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 +5 -14
- 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/lib/types/config.d.ts +22 -9
- package/lib/types/config.d.ts.map +1 -1
- package/lib/types/events.d.ts +4 -4
- package/lib/types/events.d.ts.map +1 -1
- package/package.json +11 -4
- package/react-native-bigcrunch-ads.podspec +1 -3
- package/scripts/inject-version.js +55 -0
- package/src/index.ts +3 -2
- package/src/types/config.ts +23 -9
- 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
|
+
}
|