import Foundation import SwiftUI import Combine import GoogleMobileAds import UserMessagingPlatform import AppTrackingTransparency import AdSupport @MainActor class AdMobService: ObservableObject { // MARK: - Published Properties @Published var isConsentObtained = false @Published var canShowAds = false @Published var isLoading = false @Published var shouldRequestNonPersonalizedAds = false // MARK: - Ad Unit IDs // Ad Unit IDs #if DEBUG static let bannerAdUnitID = "ca-app-pub-3940256099942544/2934735716" // Test ad unit for development #else static let bannerAdUnitID = "ca-app-pub-1549720748100858/3342431241" // Production ad unit #endif // MARK: - Initialization init() { setupResetObserver() Task { await configureConsentAndRequestAdsIfNeeded() } } // MARK: - Consent Management func requestConsent() async { await configureConsentAndRequestAdsIfNeeded() } func presentPrivacyOptions() async { guard let root = topViewController() else { return } await withCheckedContinuation { continuation in ConsentForm.presentPrivacyOptionsForm(from: root) { _ in continuation.resume() } } updateConsentState() } func resetConsent() { ConsentInformation.shared.reset() UserDefaults.standard.removeObject(forKey: AppConstants.StorageKeys.adConsentObtained) isConsentObtained = false canShowAds = false shouldRequestNonPersonalizedAds = false } private func setupResetObserver() { NotificationCenter.default.addObserver( forName: .didResetData, object: nil, queue: .main ) { [weak self] _ in guard let self else { return } Task { @MainActor in self.resetConsent() await self.configureConsentAndRequestAdsIfNeeded() } } } private func configureConsentAndRequestAdsIfNeeded() async { isLoading = true let parameters = RequestParameters() await withCheckedContinuation { continuation in ConsentInformation.shared.requestConsentInfoUpdate(with: parameters) { _ in continuation.resume() } } if let root = topViewController() { await withCheckedContinuation { continuation in ConsentForm.loadAndPresentIfRequired(from: root) { _ in continuation.resume() } } } updateConsentState() if canShowAds { await requestTrackingIfNeeded() updateConsentState() } isLoading = false } private func updateConsentState() { let consentInfo = ConsentInformation.shared canShowAds = consentInfo.canRequestAds isConsentObtained = consentInfo.consentStatus == .obtained || consentInfo.consentStatus == .notRequired let trackingDenied: Bool if #available(iOS 14.5, *) { trackingDenied = ATTrackingManager.trackingAuthorizationStatus != .authorized } else { trackingDenied = false } shouldRequestNonPersonalizedAds = !isConsentObtained || trackingDenied UserDefaults.standard.set(isConsentObtained, forKey: AppConstants.StorageKeys.adConsentObtained) } private func requestTrackingIfNeeded() async { if #available(iOS 14.5, *) { _ = await ATTrackingManager.requestTrackingAuthorization() } } private func topViewController() -> UIViewController? { guard let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene, let root = scene.windows.first?.rootViewController else { return nil } var current = root while let presented = current.presentedViewController { current = presented } return current } // MARK: - Analytics func logAdImpression() { FirebaseService.shared.logAdImpression(adType: "banner") } func logAdClick() { FirebaseService.shared.logAdClick(adType: "banner") } } // MARK: - Banner Ad Coordinator class BannerAdCoordinator: NSObject, BannerViewDelegate { weak var adMobService: AdMobService? func bannerViewDidReceiveAd(_ bannerView: BannerView) { print("Banner ad received") adMobService?.logAdImpression() } func bannerView(_ bannerView: BannerView, didFailToReceiveAdWithError error: Error) { print("Banner ad failed to load: \(error.localizedDescription)") } func bannerViewDidRecordImpression(_ bannerView: BannerView) { // Impression recorded } func bannerViewDidRecordClick(_ bannerView: BannerView) { adMobService?.logAdClick() } func bannerViewWillPresentScreen(_ bannerView: BannerView) { // Ad will present full screen } func bannerViewWillDismissScreen(_ bannerView: BannerView) { // Ad will dismiss } func bannerViewDidDismissScreen(_ bannerView: BannerView) { // Ad dismissed } } // MARK: - UIKit Banner View Wrapper struct BannerAdView: UIViewRepresentable { @EnvironmentObject var adMobService: AdMobService func makeUIView(context: Context) -> BannerView { let bannerView = BannerView(adSize: AdSizeBanner) bannerView.adUnitID = AdMobService.bannerAdUnitID // Get root view controller if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, let rootViewController = windowScene.windows.first?.rootViewController { bannerView.rootViewController = rootViewController } bannerView.delegate = context.coordinator let request = Request() if adMobService.shouldRequestNonPersonalizedAds { let extras = Extras() extras.additionalParameters = ["npa": "1"] request.register(extras) } bannerView.load(request) return bannerView } func updateUIView(_ uiView: BannerView, context: Context) { // Banner updates automatically } func makeCoordinator() -> BannerAdCoordinator { let coordinator = BannerAdCoordinator() coordinator.adMobService = adMobService return coordinator } }