import 'dart:async'; import 'package:flutter/foundation.dart' show debugPrint; import 'package:google_mobile_ads/google_mobile_ads.dart'; /// TODO(ads): official Google TEST ad unit ids — AdMob has not provisioned /// real ones yet (design.md Open Questions). Swap these for the real banner /// / interstitial unit ids once available; never ship the test ids to /// production. const bannerAdUnitIdPrueba = 'ca-app-pub-3940256099942544/6300978111'; const interstitialAdUnitIdPrueba = 'ca-app-pub-3940256099942544/1033173712'; /// Ads port + AdMob adapter (Design "Interfaces / Contracts", ADR-6): owns /// the entitlement gate for both surfaces, the interstitial's session /// frequency cap, and is the ONLY `google_mobile_ads` call site besides /// `banner_anuncio_superior.dart`'s `BannerAd` widget wrapper. The frequency /// cap and premium gating are pure/injectable (`ahora`, /// `mostrarInterstitialImpl`) so they are unit-testable with a fake clock /// and zero AdMob platform channels (Design Testing Strategy). class ServicioAnuncios { ServicioAnuncios({ bool Function()? esPremium, DateTime Function()? ahora, Future Function()? mostrarInterstitialImpl, }) : _esPremium = esPremium ?? (() => false), _ahora = ahora ?? DateTime.now, _mostrarInterstitialImpl = mostrarInterstitialImpl ?? _mostrarInterstitialAdMob; /// Session-scoped cap (ad-display spec "Interstitial Frequency Cap"): at /// most 2 interstitials per process lifetime. static const maxInterstitialsPorSesion = 2; /// Minimum spacing between two interstitials (ad-display spec, same /// requirement). static const separacionMinima = Duration(minutes: 3); final bool Function() _esPremium; final DateTime Function() _ahora; final Future Function() _mostrarInterstitialImpl; int _mostrados = 0; DateTime? _ultimoMostrado; /// Ad-display spec "Persistent Top Banner": absent entirely for premium. bool get debeMostrarBanner => !_esPremium(); bool _dentroDelCap() { if (_esPremium()) return false; if (_mostrados >= maxInterstitialsPorSesion) return false; final ultimo = _ultimoMostrado; if (ultimo != null && _ahora().difference(ultimo) < separacionMinima) { return false; } return true; } /// Attempts to show an interstitial for one of the two allowed CTAs (add /// station manually, add alarm). Callers are responsible for the ADR-6 /// ordering invariant themselves (cap-check-before-interstitial for /// add-alarm, so a refusal is never preceded by an ad) — this method only /// owns entitlement + frequency-cap gating, never the caller's own /// business-rule ordering. /// /// Returns whether an interstitial actually rendered. A failed/aborted ad /// load (network, no fill) does NOT consume the session cap — only a /// genuinely SHOWN ad does (Spec intent: the cap limits driver-facing /// interruptions, not load attempts). Future intentarInterstitial() async { if (!_dentroDelCap()) return false; final mostrado = await _mostrarInterstitialImpl(); if (mostrado) { _mostrados++; _ultimoMostrado = _ahora(); } return mostrado; } static Future _mostrarInterstitialAdMob() async { try { final cargaCompleter = Completer(); await InterstitialAd.load( adUnitId: interstitialAdUnitIdPrueba, request: const AdRequest(), adLoadCallback: InterstitialAdLoadCallback( onAdLoaded: (ad) { if (!cargaCompleter.isCompleted) cargaCompleter.complete(ad); }, onAdFailedToLoad: (error) { debugPrint('[PluriWave][anuncios] interstitial load ERROR $error'); if (!cargaCompleter.isCompleted) cargaCompleter.complete(null); }, ), ); final cargado = await cargaCompleter.future; if (cargado == null) return false; final cierreCompleter = Completer(); cargado.fullScreenContentCallback = FullScreenContentCallback( onAdDismissedFullScreenContent: (ad) { ad.dispose(); if (!cierreCompleter.isCompleted) cierreCompleter.complete(); }, onAdFailedToShowFullScreenContent: (ad, error) { ad.dispose(); if (!cierreCompleter.isCompleted) cierreCompleter.complete(); }, ); await cargado.show(); await cierreCompleter.future; return true; } catch (e) { debugPrint('[PluriWave][anuncios] interstitial ERROR $e'); return false; } } }