mirror of
https://github.com/RevenueCat/purchases-ios
synced 2026-11-11 22:55:55 +05:45
* ci: test AdMob adapter deploy with hardcoded version 5.66.0 Temporarily hardcode version and simplify deploy-tag workflow to test the deploy-to-purchases-ios-admob job from the release-admob-adapter branch. * Revert "ci: test AdMob adapter deploy with hardcoded version 5.66.0" This reverts commit 104542f0ab0a0dbb590b19187f8e598ca8ece96c. * feat: add AdMobIntegrationSample example app xcodeproj and config Add the Xcode project, Info.plist, gitignore, and Package.resolved for the AdMob example app. Clean up duplicate RevenueCatAdMob package dependencies and fix project-level deployment target. * fix: allow example app Package.resolved through gitignore The root Package.resolved pattern matched at any depth, silently excluding the example app's Package.resolved from the deployed repo. Add a negation so the example app keeps reproducible builds. * fix: gitignore Package.resolved in example app instead of committing it * feat: add VanillaAdTrackingSample example app for manual ad tracking Adds a companion example to AdMobIntegrationSample that demonstrates manual ad event tracking via Purchases.shared.adTracker without the RevenueCatAdMob adapter library. Covers all ad formats (banner, interstitial, app open, rewarded, rewarded interstitial, native) with manual delegate callbacks and paidEventHandler wiring. * feat: add VanillaAdTrackingSample to Tuist workspace Adds Tuist project definition and workspace entry for the vanilla ad tracking example. Introduces vanillaAdTrackingPackages helper that provides RevenueCat (local) + GoogleMobileAds (remote) without the adapter dependency. * Fixup * docs: remove adapter comparison from VanillaAdTrackingSample README * fix: use dynamic placement in banner impression and click callbacks * fix: update Google Mobile Ads version constraint to allow v13 * fix: use configuration-based styling for CTA button in native ad view * fix: remove unused placement parameter from loadNativeAd * fix: inline Google Mobile Ads package in Project.swift Move the Google Mobile Ads package declaration directly into the VanillaAdTrackingSample Project.swift instead of using a shared helper, since it's only used by this one project. * fix: remove unused VanillaAdTrackingSample-Info.plist The info.plist values are already declared in Project.swift via infoPlist: .extendingDefault, making this file redundant. * fix: add didFailToPresentFullScreenContent delegate method Handle the case where a loaded full-screen ad fails to present by cleaning up metadata, resetting the ad reference and status. This prevents leaked metadata entries and stuck "Ready" states. * fix: mark AdMobManager as @MainActor for thread safety AdMob callbacks may fire off the main thread, but @Published properties are observed by SwiftUI and must be mutated on main. * docs: update build instructions to use tuist generate The project uses Tuist for workspace generation, not a standalone .xcodeproj file. * fix: move native ad delegate assignment before availability guard The delegate assignment is not iOS 15-dependent and should be set before the guard so impression/click callbacks work regardless of the consumer's deployment target. * refactor: extract shared full-screen ad cleanup into helper method Deduplicate the identical cleanup logic between adDidDismissFullScreenContent and ad(_:didFailToPresentFullScreenContentWithError:) into a single cleanUpFullScreenAd helper. * fix(ads): save adLoader before setting delegate * chore(ads): cleanup vanilla example and project file * fix(ads): remove hardcoded development team * fix(ads): move from local to remote package dep * fix(ads): revert tuist and fix project file * fix(ads): revert tuist and fix project file --------- Co-authored-by: Peter Porfy <peter.porfy@revenuecat.com>
603 lines
21 KiB
Swift
603 lines
21 KiB
Swift
// swiftlint:disable:next blanket_disable_command
|
|
// swiftlint:disable identifier_name
|
|
import Foundation
|
|
import GoogleMobileAds
|
|
import RevenueCat
|
|
@_spi(Experimental) import RevenueCat
|
|
|
|
// MARK: - AdMobManager
|
|
|
|
@MainActor
|
|
class AdMobManager: NSObject, ObservableObject {
|
|
|
|
var bannerView: BannerView?
|
|
var errorTestBannerView: BannerView?
|
|
var interstitialAd: InterstitialAd?
|
|
var appOpenAd: AppOpenAd?
|
|
var rewardedAd: RewardedAd?
|
|
var rewardedInterstitialAd: RewardedInterstitialAd?
|
|
var nativeAdLoader: AdLoader?
|
|
var nativeVideoAdLoader: AdLoader?
|
|
|
|
@Published var nativeAd: NativeAd?
|
|
@Published var nativeVideoAd: NativeAd?
|
|
|
|
@Published var interstitialStatus = "Not Loaded"
|
|
@Published var appOpenStatus = "Not Loaded"
|
|
@Published var rewardedStatus = "Not Loaded"
|
|
@Published var rewardedInterstitialStatus = "Not Loaded"
|
|
@Published var nativeAdStatus = "Not Loaded"
|
|
@Published var nativeVideoAdStatus = "Not Loaded"
|
|
|
|
/// Stored metadata for full-screen ads so delegate callbacks can construct tracking data.
|
|
private var fullScreenAdMetadata: [ObjectIdentifier: FullScreenAdMeta] = [:]
|
|
|
|
private struct FullScreenAdMeta {
|
|
let placement: String
|
|
let adUnitId: String
|
|
let adFormat: RevenueCat.AdFormat
|
|
let responseInfo: ResponseInfo?
|
|
}
|
|
|
|
// MARK: - Ad Tracker
|
|
|
|
@available(iOS 15.0, *)
|
|
private var adTracker: AdTracker { Purchases.shared.adTracker }
|
|
|
|
// MARK: - Helpers
|
|
|
|
private static let microsPerUnit = NSDecimalNumber(value: 1_000_000)
|
|
|
|
private func impressionId(from responseInfo: ResponseInfo?) -> String {
|
|
responseInfo?.responseIdentifier ?? ""
|
|
}
|
|
|
|
private func networkName(from responseInfo: ResponseInfo?) -> String {
|
|
responseInfo?.loadedAdNetworkResponseInfo?.adNetworkClassName ?? ""
|
|
}
|
|
|
|
private func revenueMicros(from adValue: NSDecimalNumber) -> Int {
|
|
let micros = adValue.multiplying(by: Self.microsPerUnit)
|
|
return Int(micros.int64Value)
|
|
}
|
|
|
|
private func mapPrecision(_ precision: AdValuePrecision) -> AdRevenue.Precision {
|
|
switch precision {
|
|
case .precise: return .exact
|
|
case .estimated: return .estimated
|
|
case .publisherProvided: return .publisherDefined
|
|
case .unknown: return .unknown
|
|
@unknown default: return .unknown
|
|
}
|
|
}
|
|
|
|
/// Creates a `paidEventHandler` closure that forwards revenue to RevenueCat.
|
|
@available(iOS 15.0, *)
|
|
private func makePaidEventHandler(
|
|
responseInfo: ResponseInfo?,
|
|
placement: String,
|
|
adUnitId: String,
|
|
adFormat: RevenueCat.AdFormat
|
|
) -> (AdValue) -> Void {
|
|
return { [weak self] adValue in
|
|
guard let self else { return }
|
|
self.adTracker.trackAdRevenue(AdRevenue(
|
|
networkName: self.networkName(from: responseInfo),
|
|
mediatorName: .adMob,
|
|
adFormat: adFormat,
|
|
placement: placement,
|
|
adUnitId: adUnitId,
|
|
impressionId: self.impressionId(from: responseInfo),
|
|
revenueMicros: self.revenueMicros(from: adValue.value),
|
|
currency: adValue.currencyCode,
|
|
precision: self.mapPrecision(adValue.precision)
|
|
))
|
|
}
|
|
}
|
|
|
|
// MARK: - Banner Ad
|
|
|
|
func loadBannerAd() -> BannerView {
|
|
let bannerSize = AdSize(size: CGSize(width: 320, height: 50), flags: 0)
|
|
let banner = BannerView(adSize: bannerSize)
|
|
banner.adUnitID = Constants.AdMob.bannerAdUnitID
|
|
banner.delegate = self
|
|
banner.load(Request())
|
|
self.bannerView = banner
|
|
return banner
|
|
}
|
|
|
|
// MARK: - Interstitial Ad
|
|
|
|
func loadInterstitialAd() {
|
|
interstitialStatus = "Loading..."
|
|
|
|
InterstitialAd.load(
|
|
with: Constants.AdMob.interstitialAdUnitID,
|
|
request: Request()
|
|
) { [weak self] ad, error in
|
|
guard let self else { return }
|
|
if let error {
|
|
self.handleFullScreenLoadFailure(
|
|
error: error, placement: "interstitial_main",
|
|
adUnitId: Constants.AdMob.interstitialAdUnitID,
|
|
adFormat: .interstitial, statusKeyPath: \.interstitialStatus
|
|
)
|
|
return
|
|
}
|
|
guard let ad else { return }
|
|
self.interstitialAd = ad
|
|
self.handleFullScreenLoadSuccess(
|
|
ad: ad, responseInfo: ad.responseInfo,
|
|
placement: "interstitial_main",
|
|
adUnitId: Constants.AdMob.interstitialAdUnitID,
|
|
adFormat: .interstitial, statusKeyPath: \.interstitialStatus
|
|
) { ad.paidEventHandler = $0 }
|
|
}
|
|
}
|
|
|
|
func showInterstitialAd(from viewController: UIViewController) {
|
|
guard let ad = interstitialAd else {
|
|
print("⚠️ Interstitial not ready")
|
|
return
|
|
}
|
|
ad.present(from: viewController)
|
|
}
|
|
|
|
// MARK: - App Open Ad
|
|
|
|
func loadAppOpenAd() {
|
|
appOpenStatus = "Loading..."
|
|
|
|
AppOpenAd.load(
|
|
with: Constants.AdMob.appOpenAdUnitID,
|
|
request: Request()
|
|
) { [weak self] ad, error in
|
|
guard let self else { return }
|
|
if let error {
|
|
self.handleFullScreenLoadFailure(
|
|
error: error, placement: "app_open_main",
|
|
adUnitId: Constants.AdMob.appOpenAdUnitID,
|
|
adFormat: .appOpen, statusKeyPath: \.appOpenStatus
|
|
)
|
|
return
|
|
}
|
|
guard let ad else { return }
|
|
self.appOpenAd = ad
|
|
self.handleFullScreenLoadSuccess(
|
|
ad: ad, responseInfo: ad.responseInfo,
|
|
placement: "app_open_main",
|
|
adUnitId: Constants.AdMob.appOpenAdUnitID,
|
|
adFormat: .appOpen, statusKeyPath: \.appOpenStatus
|
|
) { ad.paidEventHandler = $0 }
|
|
}
|
|
}
|
|
|
|
func showAppOpenAd(from viewController: UIViewController) {
|
|
guard let ad = appOpenAd else {
|
|
print("⚠️ App Open not ready")
|
|
return
|
|
}
|
|
ad.present(from: viewController)
|
|
}
|
|
|
|
// MARK: - Rewarded Ad
|
|
|
|
func loadRewardedAd() {
|
|
rewardedStatus = "Loading..."
|
|
|
|
RewardedAd.load(
|
|
with: Constants.AdMob.rewardedAdUnitID,
|
|
request: Request()
|
|
) { [weak self] ad, error in
|
|
guard let self else { return }
|
|
if let error {
|
|
self.handleFullScreenLoadFailure(
|
|
error: error, placement: "rewarded_main",
|
|
adUnitId: Constants.AdMob.rewardedAdUnitID,
|
|
adFormat: .rewarded, statusKeyPath: \.rewardedStatus
|
|
)
|
|
return
|
|
}
|
|
guard let ad else { return }
|
|
self.rewardedAd = ad
|
|
self.handleFullScreenLoadSuccess(
|
|
ad: ad, responseInfo: ad.responseInfo,
|
|
placement: "rewarded_main",
|
|
adUnitId: Constants.AdMob.rewardedAdUnitID,
|
|
adFormat: .rewarded, statusKeyPath: \.rewardedStatus
|
|
) { ad.paidEventHandler = $0 }
|
|
}
|
|
}
|
|
|
|
func showRewardedAd(from viewController: UIViewController, userDidEarnRewardHandler: @escaping () -> Void) {
|
|
guard let ad = rewardedAd else {
|
|
print("⚠️ Rewarded not ready")
|
|
return
|
|
}
|
|
ad.present(from: viewController, userDidEarnRewardHandler: {
|
|
print("✅ User earned reward")
|
|
userDidEarnRewardHandler()
|
|
})
|
|
}
|
|
|
|
// MARK: - Rewarded Interstitial Ad
|
|
|
|
func loadRewardedInterstitialAd() {
|
|
rewardedInterstitialStatus = "Loading..."
|
|
|
|
RewardedInterstitialAd.load(
|
|
with: Constants.AdMob.rewardedInterstitialAdUnitID,
|
|
request: Request()
|
|
) { [weak self] ad, error in
|
|
guard let self else { return }
|
|
if let error {
|
|
self.handleFullScreenLoadFailure(
|
|
error: error, placement: "rewarded_interstitial_main",
|
|
adUnitId: Constants.AdMob.rewardedInterstitialAdUnitID,
|
|
adFormat: .rewardedInterstitial, statusKeyPath: \.rewardedInterstitialStatus
|
|
)
|
|
return
|
|
}
|
|
guard let ad else { return }
|
|
self.rewardedInterstitialAd = ad
|
|
self.handleFullScreenLoadSuccess(
|
|
ad: ad, responseInfo: ad.responseInfo,
|
|
placement: "rewarded_interstitial_main",
|
|
adUnitId: Constants.AdMob.rewardedInterstitialAdUnitID,
|
|
adFormat: .rewardedInterstitial, statusKeyPath: \.rewardedInterstitialStatus
|
|
) { ad.paidEventHandler = $0 }
|
|
}
|
|
}
|
|
|
|
// swiftlint:disable:next line_length
|
|
func showRewardedInterstitialAd(from viewController: UIViewController, userDidEarnRewardHandler: @escaping () -> Void) {
|
|
guard let ad = rewardedInterstitialAd else {
|
|
print("⚠️ Rewarded Interstitial not ready")
|
|
return
|
|
}
|
|
ad.present(from: viewController, userDidEarnRewardHandler: {
|
|
print("✅ User earned reward (rewarded interstitial)")
|
|
userDidEarnRewardHandler()
|
|
})
|
|
}
|
|
|
|
// MARK: - Native Ad
|
|
|
|
func loadNativeAd(adUnitID: String = Constants.AdMob.nativeAdUnitID) {
|
|
if adUnitID == Constants.AdMob.nativeAdUnitID {
|
|
nativeAdStatus = "Loading..."
|
|
} else if adUnitID == Constants.AdMob.nativeVideoAdUnitID {
|
|
nativeVideoAdStatus = "Loading..."
|
|
}
|
|
|
|
let adLoader = AdLoader(
|
|
adUnitID: adUnitID,
|
|
rootViewController: nil,
|
|
adTypes: [.native],
|
|
options: nil
|
|
)
|
|
|
|
if adUnitID == Constants.AdMob.nativeAdUnitID {
|
|
nativeAdLoader = adLoader
|
|
} else if adUnitID == Constants.AdMob.nativeVideoAdUnitID {
|
|
nativeVideoAdLoader = adLoader
|
|
}
|
|
|
|
adLoader.delegate = self
|
|
adLoader.load(Request())
|
|
}
|
|
|
|
// MARK: - Error Testing
|
|
|
|
func loadAdWithError() {
|
|
print("Loading ad with invalid ID to test error tracking")
|
|
|
|
let bannerSize = AdSize(size: CGSize(width: 320, height: 50), flags: 0)
|
|
let banner = BannerView(adSize: bannerSize)
|
|
banner.adUnitID = Constants.AdMob.invalidAdUnitID
|
|
banner.delegate = self
|
|
self.errorTestBannerView = banner
|
|
banner.load(Request())
|
|
}
|
|
|
|
// MARK: - Full-Screen Load Helper
|
|
|
|
@available(iOS 15.0, *)
|
|
private func handleFullScreenLoadSuccess(
|
|
ad: some FullScreenPresentingAd & AnyObject,
|
|
responseInfo: ResponseInfo?,
|
|
placement: String,
|
|
adUnitId: String,
|
|
adFormat: RevenueCat.AdFormat,
|
|
statusKeyPath: ReferenceWritableKeyPath<AdMobManager, String>,
|
|
setPaidEventHandler: (@escaping (AdValue) -> Void) -> Void
|
|
) {
|
|
print("✅ \(adFormat.rawValue) loaded")
|
|
|
|
adTracker.trackAdLoaded(AdLoaded(
|
|
networkName: networkName(from: responseInfo),
|
|
mediatorName: .adMob,
|
|
adFormat: adFormat,
|
|
placement: placement,
|
|
adUnitId: adUnitId,
|
|
impressionId: impressionId(from: responseInfo)
|
|
))
|
|
|
|
ad.fullScreenContentDelegate = self
|
|
fullScreenAdMetadata[ObjectIdentifier(ad)] = FullScreenAdMeta(
|
|
placement: placement,
|
|
adUnitId: adUnitId,
|
|
adFormat: adFormat,
|
|
responseInfo: responseInfo
|
|
)
|
|
|
|
setPaidEventHandler(makePaidEventHandler(
|
|
responseInfo: responseInfo,
|
|
placement: placement,
|
|
adUnitId: adUnitId,
|
|
adFormat: adFormat
|
|
))
|
|
|
|
self[keyPath: statusKeyPath] = "Ready"
|
|
}
|
|
|
|
@available(iOS 15.0, *)
|
|
private func handleFullScreenLoadFailure(
|
|
error: Error,
|
|
placement: String,
|
|
adUnitId: String,
|
|
adFormat: RevenueCat.AdFormat,
|
|
statusKeyPath: ReferenceWritableKeyPath<AdMobManager, String>
|
|
) {
|
|
print("❌ \(adFormat.rawValue) failed: \(error.localizedDescription)")
|
|
self[keyPath: statusKeyPath] = "Failed"
|
|
adTracker.trackAdFailedToLoad(AdFailedToLoad(
|
|
mediatorName: .adMob,
|
|
adFormat: adFormat,
|
|
placement: placement,
|
|
adUnitId: adUnitId,
|
|
mediatorErrorCode: (error as NSError).code
|
|
))
|
|
}
|
|
}
|
|
|
|
// MARK: - BannerViewDelegate
|
|
|
|
extension AdMobManager: BannerViewDelegate {
|
|
func bannerViewDidReceiveAd(_ bannerView: BannerView) {
|
|
guard #available(iOS 15.0, *) else { return }
|
|
let responseInfo = bannerView.responseInfo
|
|
let adUnitId = bannerView.adUnitID ?? ""
|
|
let placement = bannerView === errorTestBannerView ? "error_test" : "home_banner"
|
|
|
|
adTracker.trackAdLoaded(AdLoaded(
|
|
networkName: networkName(from: responseInfo),
|
|
mediatorName: .adMob,
|
|
adFormat: .banner,
|
|
placement: placement,
|
|
adUnitId: adUnitId,
|
|
impressionId: impressionId(from: responseInfo)
|
|
))
|
|
|
|
bannerView.paidEventHandler = makePaidEventHandler(
|
|
responseInfo: responseInfo,
|
|
placement: placement,
|
|
adUnitId: adUnitId,
|
|
adFormat: .banner
|
|
)
|
|
}
|
|
|
|
func bannerViewDidRecordImpression(_ bannerView: BannerView) {
|
|
guard #available(iOS 15.0, *) else { return }
|
|
let responseInfo = bannerView.responseInfo
|
|
let placement = bannerView === errorTestBannerView ? "error_test" : "home_banner"
|
|
|
|
adTracker.trackAdDisplayed(AdDisplayed(
|
|
networkName: networkName(from: responseInfo),
|
|
mediatorName: .adMob,
|
|
adFormat: .banner,
|
|
placement: placement,
|
|
adUnitId: bannerView.adUnitID ?? "",
|
|
impressionId: impressionId(from: responseInfo)
|
|
))
|
|
}
|
|
|
|
func bannerViewDidRecordClick(_ bannerView: BannerView) {
|
|
guard #available(iOS 15.0, *) else { return }
|
|
let responseInfo = bannerView.responseInfo
|
|
let placement = bannerView === errorTestBannerView ? "error_test" : "home_banner"
|
|
|
|
adTracker.trackAdOpened(AdOpened(
|
|
networkName: networkName(from: responseInfo),
|
|
mediatorName: .adMob,
|
|
adFormat: .banner,
|
|
placement: placement,
|
|
adUnitId: bannerView.adUnitID ?? "",
|
|
impressionId: impressionId(from: responseInfo)
|
|
))
|
|
}
|
|
|
|
func bannerView(_ bannerView: BannerView, didFailToReceiveAdWithError error: Error) {
|
|
guard #available(iOS 15.0, *) else { return }
|
|
let adUnitId = bannerView.adUnitID ?? ""
|
|
let placement = bannerView === errorTestBannerView ? "error_test" : "home_banner"
|
|
|
|
print("❌ Banner failed: \(error.localizedDescription)")
|
|
|
|
adTracker.trackAdFailedToLoad(AdFailedToLoad(
|
|
mediatorName: .adMob,
|
|
adFormat: .banner,
|
|
placement: placement,
|
|
adUnitId: adUnitId,
|
|
mediatorErrorCode: (error as NSError).code
|
|
))
|
|
}
|
|
}
|
|
|
|
// MARK: - FullScreenContentDelegate
|
|
|
|
extension AdMobManager: FullScreenContentDelegate {
|
|
func adDidRecordImpression(_ ad: any FullScreenPresentingAd) {
|
|
guard #available(iOS 15.0, *) else { return }
|
|
guard let meta = fullScreenAdMetadata[ObjectIdentifier(ad as AnyObject)] else { return }
|
|
|
|
adTracker.trackAdDisplayed(AdDisplayed(
|
|
networkName: networkName(from: meta.responseInfo),
|
|
mediatorName: .adMob,
|
|
adFormat: meta.adFormat,
|
|
placement: meta.placement,
|
|
adUnitId: meta.adUnitId,
|
|
impressionId: impressionId(from: meta.responseInfo)
|
|
))
|
|
}
|
|
|
|
func adDidRecordClick(_ ad: any FullScreenPresentingAd) {
|
|
guard #available(iOS 15.0, *) else { return }
|
|
guard let meta = fullScreenAdMetadata[ObjectIdentifier(ad as AnyObject)] else { return }
|
|
|
|
adTracker.trackAdOpened(AdOpened(
|
|
networkName: networkName(from: meta.responseInfo),
|
|
mediatorName: .adMob,
|
|
adFormat: meta.adFormat,
|
|
placement: meta.placement,
|
|
adUnitId: meta.adUnitId,
|
|
impressionId: impressionId(from: meta.responseInfo)
|
|
))
|
|
}
|
|
|
|
func ad(_ ad: any FullScreenPresentingAd, didFailToPresentFullScreenContentWithError error: Error) {
|
|
print("❌ Full-screen ad failed to present: \(error.localizedDescription)")
|
|
cleanUpFullScreenAd(ad)
|
|
}
|
|
|
|
func adDidDismissFullScreenContent(_ ad: any FullScreenPresentingAd) {
|
|
cleanUpFullScreenAd(ad)
|
|
}
|
|
|
|
private func cleanUpFullScreenAd(_ ad: any FullScreenPresentingAd) {
|
|
let key = ObjectIdentifier(ad as AnyObject)
|
|
fullScreenAdMetadata.removeValue(forKey: key)
|
|
|
|
if ad is InterstitialAd {
|
|
interstitialAd = nil
|
|
interstitialStatus = "Not Loaded"
|
|
} else if ad is AppOpenAd {
|
|
appOpenAd = nil
|
|
appOpenStatus = "Not Loaded"
|
|
} else if ad is RewardedAd {
|
|
rewardedAd = nil
|
|
rewardedStatus = "Not Loaded"
|
|
} else if ad is RewardedInterstitialAd {
|
|
rewardedInterstitialAd = nil
|
|
rewardedInterstitialStatus = "Not Loaded"
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - NativeAdLoaderDelegate / AdLoaderDelegate
|
|
|
|
extension AdMobManager: NativeAdLoaderDelegate, AdLoaderDelegate {
|
|
func adLoader(_ adLoader: AdLoader, didReceive nativeAd: NativeAd) {
|
|
let isNativeVideo = adLoader === self.nativeVideoAdLoader
|
|
let isNative = adLoader === self.nativeAdLoader
|
|
guard isNativeVideo || isNative else { return }
|
|
|
|
print("✅ \(isNativeVideo ? "Native video" : "Native") ad loaded")
|
|
|
|
if isNativeVideo {
|
|
self.nativeVideoAd = nativeAd
|
|
nativeVideoAdStatus = "Ready"
|
|
} else {
|
|
self.nativeAd = nativeAd
|
|
nativeAdStatus = "Ready"
|
|
}
|
|
|
|
nativeAd.delegate = self
|
|
|
|
guard #available(iOS 15.0, *) else { return }
|
|
let responseInfo = nativeAd.responseInfo
|
|
let adUnitId = adLoader.adUnitID
|
|
let placement = isNativeVideo ? "native_video_main" : "native_main"
|
|
|
|
adTracker.trackAdLoaded(AdLoaded(
|
|
networkName: networkName(from: responseInfo),
|
|
mediatorName: .adMob,
|
|
adFormat: .native,
|
|
placement: placement,
|
|
adUnitId: adUnitId,
|
|
impressionId: impressionId(from: responseInfo)
|
|
))
|
|
|
|
nativeAd.paidEventHandler = makePaidEventHandler(
|
|
responseInfo: responseInfo,
|
|
placement: placement,
|
|
adUnitId: adUnitId,
|
|
adFormat: .native
|
|
)
|
|
}
|
|
|
|
func adLoader(_ adLoader: AdLoader, didFailToReceiveAdWithError error: Error) {
|
|
let isNativeVideo = adLoader === self.nativeVideoAdLoader
|
|
let isNative = adLoader === self.nativeAdLoader
|
|
guard isNativeVideo || isNative else { return }
|
|
|
|
print("❌ \(isNativeVideo ? "Native video" : "Native") ad failed: \(error.localizedDescription)")
|
|
|
|
if isNativeVideo {
|
|
nativeVideoAdStatus = "Failed"
|
|
} else {
|
|
nativeAdStatus = "Failed"
|
|
}
|
|
|
|
guard #available(iOS 15.0, *) else { return }
|
|
let placement = isNativeVideo ? "native_video_main" : "native_main"
|
|
|
|
adTracker.trackAdFailedToLoad(AdFailedToLoad(
|
|
mediatorName: .adMob,
|
|
adFormat: .native,
|
|
placement: placement,
|
|
adUnitId: adLoader.adUnitID,
|
|
mediatorErrorCode: (error as NSError).code
|
|
))
|
|
}
|
|
}
|
|
|
|
// MARK: - NativeAdDelegate
|
|
|
|
extension AdMobManager: NativeAdDelegate {
|
|
func nativeAdDidRecordImpression(_ nativeAd: NativeAd) {
|
|
guard #available(iOS 15.0, *) else { return }
|
|
let responseInfo = nativeAd.responseInfo
|
|
let isVideo = self.nativeVideoAd === nativeAd
|
|
let placement = isVideo ? "native_video_main" : "native_main"
|
|
let adUnitId = (isVideo ? nativeVideoAdLoader : nativeAdLoader)?.adUnitID ?? ""
|
|
|
|
adTracker.trackAdDisplayed(AdDisplayed(
|
|
networkName: networkName(from: responseInfo),
|
|
mediatorName: .adMob,
|
|
adFormat: .native,
|
|
placement: placement,
|
|
adUnitId: adUnitId,
|
|
impressionId: impressionId(from: responseInfo)
|
|
))
|
|
}
|
|
|
|
func nativeAdDidRecordClick(_ nativeAd: NativeAd) {
|
|
guard #available(iOS 15.0, *) else { return }
|
|
let responseInfo = nativeAd.responseInfo
|
|
let isVideo = self.nativeVideoAd === nativeAd
|
|
let placement = isVideo ? "native_video_main" : "native_main"
|
|
let adUnitId = (isVideo ? nativeVideoAdLoader : nativeAdLoader)?.adUnitID ?? ""
|
|
|
|
adTracker.trackAdOpened(AdOpened(
|
|
networkName: networkName(from: responseInfo),
|
|
mediatorName: .adMob,
|
|
adFormat: .native,
|
|
placement: placement,
|
|
adUnitId: adUnitId,
|
|
impressionId: impressionId(from: responseInfo)
|
|
))
|
|
}
|
|
}
|