@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,512 @@
1
+ import Foundation
2
+ import UIKit
3
+ import GoogleMobileAds
4
+
5
+ /**
6
+ * Callback protocol for banner ad events
7
+ */
8
+ internal protocol BannerAdDelegate: AnyObject {
9
+ func bannerAdDidLoad(_ bannerView: GoogleMobileAds.BannerView)
10
+ func bannerAd(_ bannerView: GoogleMobileAds.BannerView, didFailToLoadWithError error: Error)
11
+ func bannerAdDidRecordClick(_ bannerView: GoogleMobileAds.BannerView)
12
+ func bannerAdDidRecordImpression(_ bannerView: GoogleMobileAds.BannerView)
13
+ }
14
+
15
+ /**
16
+ * Callback protocol for interstitial ad events
17
+ */
18
+ internal protocol InterstitialAdDelegate: AnyObject {
19
+ func interstitialAdDidLoad(_ interstitialAd: GoogleMobileAds.InterstitialAd)
20
+ func interstitialAd(didFailToLoadWithError error: Error)
21
+ func interstitialAdDidPresent(_ interstitialAd: GoogleMobileAds.InterstitialAd)
22
+ func interstitialAdDidDismiss(_ interstitialAd: GoogleMobileAds.InterstitialAd)
23
+ func interstitialAdDidRecordClick(_ interstitialAd: GoogleMobileAds.InterstitialAd)
24
+ func interstitialAdDidRecordImpression(_ interstitialAd: GoogleMobileAds.InterstitialAd)
25
+ }
26
+
27
+ /**
28
+ * Adapter for Google Mobile Ads SDK (Ad Manager)
29
+ *
30
+ * Wraps GMA SDK calls and translates callbacks to AnalyticsClient events.
31
+ * Handles banner, interstitial, and rewarded ad formats.
32
+ */
33
+ internal class GoogleAdsAdapter: NSObject {
34
+
35
+ private static let TAG = "GoogleAdsAdapter"
36
+
37
+ // Google's official iOS test ad unit IDs
38
+ // These are guaranteed to return test ads and will always fill
39
+ // See: https://developers.google.com/admob/ios/test-ads
40
+ private static let TEST_BANNER_AD_UNIT = "ca-app-pub-3940256099942544/2934735716"
41
+ private static let TEST_INTERSTITIAL_AD_UNIT = "ca-app-pub-3940256099942544/4411468910"
42
+ private static let TEST_REWARDED_AD_UNIT = "ca-app-pub-3940256099942544/1712485313"
43
+
44
+ private let analyticsClient: AnalyticsClient
45
+
46
+ init(analyticsClient: AnalyticsClient) {
47
+ self.analyticsClient = analyticsClient
48
+ super.init()
49
+ }
50
+
51
+ // MARK: - Ad Unit Resolution
52
+
53
+ /**
54
+ * Resolve the ad unit ID to use - test ID if useTestAds is true, otherwise production
55
+ *
56
+ * - Parameters:
57
+ * - placementConfig: The placement configuration
58
+ * - gamNetworkCode: The GAM network code from app config
59
+ * - useTestAds: Whether to use test ad units
60
+ * - Returns: The resolved ad unit ID
61
+ */
62
+ private func resolveAdUnitId(
63
+ placementConfig: PlacementConfig,
64
+ gamNetworkCode: String,
65
+ useTestAds: Bool
66
+ ) -> String {
67
+ if useTestAds {
68
+ let testAdUnit: String
69
+ switch placementConfig.format {
70
+ case "banner":
71
+ testAdUnit = GoogleAdsAdapter.TEST_BANNER_AD_UNIT
72
+ case "interstitial":
73
+ testAdUnit = GoogleAdsAdapter.TEST_INTERSTITIAL_AD_UNIT
74
+ case "rewarded":
75
+ testAdUnit = GoogleAdsAdapter.TEST_REWARDED_AD_UNIT
76
+ default:
77
+ testAdUnit = GoogleAdsAdapter.TEST_BANNER_AD_UNIT // fallback
78
+ }
79
+ BCLogger.debug("\(GoogleAdsAdapter.TAG): Using TEST ad unit for \(placementConfig.format): \(testAdUnit)")
80
+ return testAdUnit
81
+ } else {
82
+ // Production ad unit path
83
+ // - For GAM: /{networkCode}/{adUnit}
84
+ // - For AdMob (empty network code): just the ad unit ID directly
85
+ if gamNetworkCode.isEmpty {
86
+ return placementConfig.gamAdUnit
87
+ } else {
88
+ return "/\(gamNetworkCode)/\(placementConfig.gamAdUnit)"
89
+ }
90
+ }
91
+ }
92
+
93
+ // MARK: - Size Resolution
94
+
95
+ /// Resolve a BigCrunch AdSize to a Google AdSize.
96
+ /// For adaptive sizes, calculates the optimal ad size based on screen width.
97
+ private func resolveGoogleAdSize(_ bcAdSize: AdSize) -> GoogleMobileAds.AdSize {
98
+ if bcAdSize.isAdaptive {
99
+ let width: CGFloat = bcAdSize.width > 0
100
+ ? CGFloat(bcAdSize.width)
101
+ : UIScreen.main.bounds.width
102
+ BCLogger.debug("\(GoogleAdsAdapter.TAG): Resolving adaptive banner with width: \(width)pt")
103
+ return GoogleMobileAds.currentOrientationAnchoredAdaptiveBanner(width: width)
104
+ }
105
+ return GoogleMobileAds.adSizeFor(cgSize: CGSize(
106
+ width: CGFloat(bcAdSize.width),
107
+ height: CGFloat(bcAdSize.height)
108
+ ))
109
+ }
110
+
111
+ // MARK: - Banner Ads
112
+
113
+ /**
114
+ * Create and load a banner ad
115
+ *
116
+ * - Parameters:
117
+ * - placementConfig: Configuration for the placement
118
+ * - gamNetworkCode: The GAM network code from app config
119
+ * - request: Pre-configured GoogleMobileAds.Request (with S2S targeting)
120
+ * - rootViewController: View controller for presenting ad click actions
121
+ * - delegate: Delegate for ad events
122
+ * - useTestAds: Whether to use test ad units (default: false)
123
+ * - adSizeOverride: Optional size override (takes precedence over backend config)
124
+ * - Returns: The configured GoogleMobileAds.BannerView
125
+ */
126
+ @MainActor
127
+ func loadBannerAd(
128
+ placementConfig: PlacementConfig,
129
+ gamNetworkCode: String,
130
+ request: GoogleMobileAds.Request,
131
+ rootViewController: UIViewController,
132
+ delegate: BannerAdDelegate,
133
+ useTestAds: Bool = false,
134
+ adSizeOverride: AdSize? = nil,
135
+ refreshCount: Int = 0
136
+ ) -> GoogleMobileAds.BannerView {
137
+ BCLogger.debug("\(GoogleAdsAdapter.TAG): Loading banner ad for: \(placementConfig.placementId)")
138
+
139
+ // Resolve ad unit ID (test or production)
140
+ let adUnitID = resolveAdUnitId(
141
+ placementConfig: placementConfig,
142
+ gamNetworkCode: gamNetworkCode,
143
+ useTestAds: useTestAds
144
+ )
145
+
146
+ let bannerView = GoogleMobileAds.BannerView()
147
+ bannerView.adUnitID = adUnitID
148
+ bannerView.rootViewController = rootViewController
149
+
150
+ // Set ad size: use override first, then placement config, then default
151
+ if let overrideSize = adSizeOverride {
152
+ BCLogger.debug("\(GoogleAdsAdapter.TAG): Using size override: \(overrideSize.width)x\(overrideSize.height) (adaptive=\(overrideSize.isAdaptive))")
153
+ bannerView.adSize = resolveGoogleAdSize(overrideSize)
154
+ } else if let size = placementConfig.sizes?.first {
155
+ bannerView.adSize = resolveGoogleAdSize(size)
156
+ } else {
157
+ // Default to standard banner size
158
+ bannerView.adSize = GoogleMobileAds.AdSizeBanner
159
+ }
160
+
161
+ // Set up delegate wrapper to handle callbacks
162
+ let delegateWrapper = BannerDelegateWrapper(
163
+ placementConfig: placementConfig,
164
+ analyticsClient: analyticsClient,
165
+ delegate: delegate,
166
+ refreshCount: refreshCount
167
+ )
168
+ bannerView.delegate = delegateWrapper
169
+
170
+ // Store delegate wrapper to prevent deallocation
171
+ objc_setAssociatedObject(
172
+ bannerView,
173
+ &AssociatedKeys.delegateWrapper,
174
+ delegateWrapper,
175
+ .OBJC_ASSOCIATION_RETAIN_NONATOMIC
176
+ )
177
+
178
+ // Set up paid event handler for ILRD (Impression-Level Revenue Data)
179
+ bannerView.paidEventHandler = { [weak self, placementConfig] adValue in
180
+ BCLogger.debug("\(GoogleAdsAdapter.TAG): Banner paid event: \(adValue.value) \(adValue.currencyCode)")
181
+ // adValue.value is in currency units (e.g., dollars), convert to micros
182
+ // Must multiply before converting to Int64 to preserve precision
183
+ let valueMicros = adValue.value.multiplying(by: NSDecimalNumber(value: 1_000_000))
184
+ self?.analyticsClient.trackAdRevenue(
185
+ placementId: placementConfig.placementId,
186
+ format: placementConfig.format,
187
+ valueMicros: valueMicros.int64Value,
188
+ currency: adValue.currencyCode
189
+ )
190
+ }
191
+
192
+ // Load the ad
193
+ bannerView.load(request)
194
+
195
+ return bannerView
196
+ }
197
+
198
+ /**
199
+ * Destroy a banner ad view
200
+ *
201
+ * - Parameter bannerView: The GoogleMobileAds.BannerView to destroy
202
+ */
203
+ func destroyBannerAd(_ bannerView: GoogleMobileAds.BannerView) {
204
+ BCLogger.debug("\(GoogleAdsAdapter.TAG): Destroying banner ad")
205
+ bannerView.delegate = nil
206
+ bannerView.paidEventHandler = nil
207
+ bannerView.removeFromSuperview()
208
+
209
+ // Clear associated delegate wrapper
210
+ objc_setAssociatedObject(
211
+ bannerView,
212
+ &AssociatedKeys.delegateWrapper,
213
+ nil,
214
+ .OBJC_ASSOCIATION_RETAIN_NONATOMIC
215
+ )
216
+ }
217
+
218
+ // MARK: - Interstitial Ads
219
+
220
+ /**
221
+ * Load an interstitial ad
222
+ *
223
+ * - Parameters:
224
+ * - placementConfig: Configuration for the placement
225
+ * - gamNetworkCode: The GAM network code from app config
226
+ * - request: Pre-configured GoogleMobileAds.Request (with S2S targeting)
227
+ * - useTestAds: Whether to use test ad units (default: false)
228
+ * - Returns: Result containing loaded interstitial or error
229
+ */
230
+ func loadInterstitialAd(
231
+ placementConfig: PlacementConfig,
232
+ gamNetworkCode: String,
233
+ request: GoogleMobileAds.Request,
234
+ useTestAds: Bool = false
235
+ ) async -> Result<GoogleMobileAds.InterstitialAd, Error> {
236
+ BCLogger.debug("\(GoogleAdsAdapter.TAG): Loading interstitial ad for: \(placementConfig.placementId)")
237
+
238
+ // Resolve ad unit ID (test or production)
239
+ let adUnitID = resolveAdUnitId(
240
+ placementConfig: placementConfig,
241
+ gamNetworkCode: gamNetworkCode,
242
+ useTestAds: useTestAds
243
+ )
244
+
245
+ return await withCheckedContinuation { continuation in
246
+ GoogleMobileAds.InterstitialAd.load(
247
+ with: adUnitID,
248
+ request: request
249
+ ) { [weak self] interstitialAd, error in
250
+ if let error = error {
251
+ BCLogger.warning("\(GoogleAdsAdapter.TAG): Interstitial ad failed to load: \(error.localizedDescription)")
252
+ continuation.resume(returning: .failure(error))
253
+ return
254
+ }
255
+
256
+ guard let interstitialAd = interstitialAd else {
257
+ BCLogger.warning("\(GoogleAdsAdapter.TAG): Interstitial ad loaded but was nil")
258
+ continuation.resume(returning: .failure(NSError(
259
+ domain: "BigCrunchAds",
260
+ code: -1,
261
+ userInfo: [NSLocalizedDescriptionKey: "Interstitial ad was nil"]
262
+ )))
263
+ return
264
+ }
265
+
266
+ BCLogger.debug("\(GoogleAdsAdapter.TAG): Interstitial ad loaded: \(placementConfig.placementId)")
267
+
268
+ // Set up paid event handler for ILRD
269
+ interstitialAd.paidEventHandler = { [weak self, placementConfig] adValue in
270
+ BCLogger.debug("\(GoogleAdsAdapter.TAG): Interstitial paid event: \(adValue.value) \(adValue.currencyCode)")
271
+ // adValue.value is in currency units (e.g., dollars), convert to micros
272
+ // Must multiply before converting to Int64 to preserve precision
273
+ let valueMicros = adValue.value.multiplying(by: NSDecimalNumber(value: 1_000_000))
274
+ self?.analyticsClient.trackAdRevenue(
275
+ placementId: placementConfig.placementId,
276
+ format: placementConfig.format,
277
+ valueMicros: valueMicros.int64Value,
278
+ currency: adValue.currencyCode
279
+ )
280
+ }
281
+
282
+ continuation.resume(returning: .success(interstitialAd))
283
+ }
284
+ }
285
+ }
286
+
287
+ /**
288
+ * Show an interstitial ad
289
+ *
290
+ * - Parameters:
291
+ * - interstitialAd: The loaded interstitial ad
292
+ * - viewController: View controller to present from
293
+ * - placementConfig: Configuration for the placement
294
+ * - delegate: Delegate for ad events
295
+ */
296
+ func showInterstitialAd(
297
+ _ interstitialAd: GoogleMobileAds.InterstitialAd,
298
+ from viewController: UIViewController,
299
+ placementConfig: PlacementConfig,
300
+ delegate: InterstitialAdDelegate
301
+ ) {
302
+ BCLogger.debug("\(GoogleAdsAdapter.TAG): Showing interstitial ad: \(placementConfig.placementId)")
303
+
304
+ // Set up full screen content delegate
305
+ let delegateWrapper = InterstitialDelegateWrapper(
306
+ placementConfig: placementConfig,
307
+ analyticsClient: analyticsClient,
308
+ delegate: delegate
309
+ )
310
+ interstitialAd.fullScreenContentDelegate = delegateWrapper
311
+
312
+ // Store delegate wrapper to prevent deallocation
313
+ objc_setAssociatedObject(
314
+ interstitialAd,
315
+ &AssociatedKeys.delegateWrapper,
316
+ delegateWrapper,
317
+ .OBJC_ASSOCIATION_RETAIN_NONATOMIC
318
+ )
319
+
320
+ // Present the ad
321
+ interstitialAd.present(from: viewController)
322
+ }
323
+ }
324
+
325
+ // MARK: - Associated Object Keys
326
+
327
+ private struct AssociatedKeys {
328
+ static var delegateWrapper = "delegateWrapper"
329
+ }
330
+
331
+ // MARK: - Banner Delegate Wrapper
332
+
333
+ /**
334
+ * Internal delegate wrapper that handles GMA banner callbacks and
335
+ * forwards them to both AnalyticsClient and the public delegate.
336
+ */
337
+ private class BannerDelegateWrapper: NSObject, GoogleMobileAds.BannerViewDelegate {
338
+
339
+ private let placementConfig: PlacementConfig
340
+ private let analyticsClient: AnalyticsClient
341
+ private weak var delegate: BannerAdDelegate?
342
+ private let refreshCount: Int
343
+
344
+ init(
345
+ placementConfig: PlacementConfig,
346
+ analyticsClient: AnalyticsClient,
347
+ delegate: BannerAdDelegate,
348
+ refreshCount: Int = 0
349
+ ) {
350
+ self.placementConfig = placementConfig
351
+ self.analyticsClient = analyticsClient
352
+ self.delegate = delegate
353
+ self.refreshCount = refreshCount
354
+ }
355
+
356
+ func bannerViewDidReceiveAd(_ bannerView: GoogleMobileAds.BannerView) {
357
+ BCLogger.debug("GoogleAdsAdapter: Banner ad loaded: \(placementConfig.placementId) (refreshCount: \(refreshCount))")
358
+
359
+ // Extract GAM metadata from ResponseInfo
360
+ let responseInfo = bannerView.responseInfo
361
+ let adMetadata = GoogleAdsAdapter.extractAdMetadata(from: responseInfo)
362
+
363
+ analyticsClient.trackAdImpression(
364
+ placementId: placementConfig.placementId,
365
+ format: placementConfig.format,
366
+ refreshCount: refreshCount,
367
+ advertiserId: adMetadata["advertiser_id"] as? String,
368
+ campaignId: adMetadata["campaign_id"] as? String,
369
+ lineItemId: adMetadata["line_item_id"] as? String,
370
+ creativeId: adMetadata["creative_id"] as? String
371
+ )
372
+ delegate?.bannerAdDidLoad(bannerView)
373
+ }
374
+
375
+ func bannerView(_ bannerView: GoogleMobileAds.BannerView, didFailToReceiveAdWithError error: Error) {
376
+ BCLogger.warning("GoogleAdsAdapter: Banner ad failed to load: \(error.localizedDescription)")
377
+ delegate?.bannerAd(bannerView, didFailToLoadWithError: error)
378
+ }
379
+
380
+ func bannerViewDidRecordClick(_ bannerView: GoogleMobileAds.BannerView) {
381
+ BCLogger.debug("GoogleAdsAdapter: Banner ad clicked: \(placementConfig.placementId)")
382
+ analyticsClient.trackAdClick(
383
+ placementId: placementConfig.placementId,
384
+ format: placementConfig.format
385
+ )
386
+ delegate?.bannerAdDidRecordClick(bannerView)
387
+ }
388
+
389
+ func bannerViewDidRecordImpression(_ bannerView: GoogleMobileAds.BannerView) {
390
+ BCLogger.debug("GoogleAdsAdapter: Banner ad impression: \(placementConfig.placementId)")
391
+ delegate?.bannerAdDidRecordImpression(bannerView)
392
+ }
393
+ }
394
+
395
+ // MARK: - Interstitial Delegate Wrapper
396
+
397
+ /**
398
+ * Internal delegate wrapper that handles GMA interstitial callbacks and
399
+ * forwards them to both AnalyticsClient and the public delegate.
400
+ */
401
+ private class InterstitialDelegateWrapper: NSObject, GoogleMobileAds.FullScreenContentDelegate {
402
+
403
+ private let placementConfig: PlacementConfig
404
+ private let analyticsClient: AnalyticsClient
405
+ private weak var delegate: InterstitialAdDelegate?
406
+
407
+ init(
408
+ placementConfig: PlacementConfig,
409
+ analyticsClient: AnalyticsClient,
410
+ delegate: InterstitialAdDelegate
411
+ ) {
412
+ self.placementConfig = placementConfig
413
+ self.analyticsClient = analyticsClient
414
+ self.delegate = delegate
415
+ }
416
+
417
+ func adWillPresentFullScreenContent(_ ad: GoogleMobileAds.FullScreenPresentingAd) {
418
+ guard let interstitialAd = ad as? GoogleMobileAds.InterstitialAd else { return }
419
+ BCLogger.debug("GoogleAdsAdapter: Interstitial ad shown: \(placementConfig.placementId)")
420
+
421
+ // Extract GAM metadata from ResponseInfo
422
+ let responseInfo = interstitialAd.responseInfo
423
+ let adMetadata = GoogleAdsAdapter.extractAdMetadata(from: responseInfo)
424
+
425
+ analyticsClient.trackAdImpression(
426
+ placementId: placementConfig.placementId,
427
+ format: placementConfig.format,
428
+ advertiserId: adMetadata["advertiser_id"] as? String,
429
+ campaignId: adMetadata["campaign_id"] as? String,
430
+ lineItemId: adMetadata["line_item_id"] as? String,
431
+ creativeId: adMetadata["creative_id"] as? String
432
+ )
433
+ delegate?.interstitialAdDidPresent(interstitialAd)
434
+ }
435
+
436
+ func adDidDismissFullScreenContent(_ ad: GoogleMobileAds.FullScreenPresentingAd) {
437
+ guard let interstitialAd = ad as? GoogleMobileAds.InterstitialAd else { return }
438
+ BCLogger.debug("GoogleAdsAdapter: Interstitial ad dismissed: \(placementConfig.placementId)")
439
+ delegate?.interstitialAdDidDismiss(interstitialAd)
440
+ }
441
+
442
+ func ad(_ ad: GoogleMobileAds.FullScreenPresentingAd, didFailToPresentFullScreenContentWithError error: Error) {
443
+ BCLogger.warning("GoogleAdsAdapter: Interstitial ad failed to show: \(error.localizedDescription)")
444
+ delegate?.interstitialAd(didFailToLoadWithError: error)
445
+ }
446
+
447
+ func adDidRecordClick(_ ad: GoogleMobileAds.FullScreenPresentingAd) {
448
+ guard let interstitialAd = ad as? GoogleMobileAds.InterstitialAd else { return }
449
+ BCLogger.debug("GoogleAdsAdapter: Interstitial ad clicked: \(placementConfig.placementId)")
450
+ analyticsClient.trackAdClick(
451
+ placementId: placementConfig.placementId,
452
+ format: placementConfig.format
453
+ )
454
+ delegate?.interstitialAdDidRecordClick(interstitialAd)
455
+ }
456
+
457
+ func adDidRecordImpression(_ ad: GoogleMobileAds.FullScreenPresentingAd) {
458
+ guard let interstitialAd = ad as? GoogleMobileAds.InterstitialAd else { return }
459
+ BCLogger.debug("GoogleAdsAdapter: Interstitial ad impression: \(placementConfig.placementId)")
460
+ delegate?.interstitialAdDidRecordImpression(interstitialAd)
461
+ }
462
+ }
463
+
464
+ // MARK: - Metadata Extraction
465
+
466
+ extension GoogleAdsAdapter {
467
+ /**
468
+ * Extract GAM metadata from ResponseInfo
469
+ *
470
+ * Google Ad Manager provides limited metadata in ResponseInfo. The main fields available are:
471
+ * - adSourceID: Can contain advertiser ID or network ID
472
+ * - responseIdentifier: Unique response identifier (can be used as creative ID proxy)
473
+ *
474
+ * Note: GAM doesn't directly expose campaignId or lineItemId in mobile SDK ResponseInfo.
475
+ * These fields may be available in server-side logs but not in the client SDK.
476
+ *
477
+ * - Parameter responseInfo: The ResponseInfo from the ad response
478
+ * - Returns: Dictionary of available metadata fields
479
+ */
480
+ static func extractAdMetadata(from responseInfo: GoogleMobileAds.ResponseInfo?) -> [String: Any] {
481
+ guard let responseInfo = responseInfo else {
482
+ return [:]
483
+ }
484
+
485
+ var metadata: [String: Any] = [:]
486
+
487
+ // Extract adSourceID (often contains advertiser/network ID)
488
+ if let loadedAdapter = responseInfo.loadedAdNetworkResponseInfo {
489
+ if let adSourceID = loadedAdapter.adSourceID {
490
+ metadata["advertiser_id"] = adSourceID
491
+ }
492
+ if let adSourceInstanceID = loadedAdapter.adSourceInstanceID {
493
+ metadata["creative_id"] = adSourceInstanceID
494
+ }
495
+ }
496
+
497
+ // Response ID can serve as a creative identifier
498
+ if let responseIdentifier = responseInfo.responseIdentifier, !responseIdentifier.isEmpty {
499
+ // Use responseIdentifier as creative_id if we don't have a better one
500
+ if metadata["creative_id"] == nil {
501
+ metadata["creative_id"] = responseIdentifier
502
+ }
503
+ }
504
+
505
+ // Log extracted metadata for debugging
506
+ if !metadata.isEmpty {
507
+ BCLogger.debug("GoogleAdsAdapter: Extracted GAM metadata: \(metadata)")
508
+ }
509
+
510
+ return metadata
511
+ }
512
+ }