@bigcrunch/react-native-ads 0.4.0 → 0.6.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 +300 -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 +87 -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 +8 -2
- 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 +305 -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 +97 -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 +37 -9
- package/ios/BigCrunchBannerViewManager.swift +0 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +3 -2
- package/package.json +7 -1
- package/react-native-bigcrunch-ads.podspec +0 -1
- package/scripts/inject-version.js +55 -0
- package/src/index.ts +3 -2
|
@@ -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
|
+
}
|