MO-Fuel-Tax-Back/lib/services/ad_service.dart

96 lines
3.8 KiB
Dart

import 'dart:async';
import 'package:google_mobile_ads/google_mobile_ads.dart';
/// The single ad placement this app has: one interstitial, shown at most
/// once per app session, right before the *upload-to-cloud* phase of the
/// first sync that reaches it (see [CloudSyncService.syncNow]'s
/// `beforeUpload` hook and [AppState.syncNow]) — deliberately not before
/// pulling/merging remote changes down, and never anywhere else in the app.
///
/// The Android AdMob App ID (android/app/src/main/AndroidManifest.xml) is
/// the real one from your AdMob console. [_interstitialAdUnitId] below is
/// TEMPORARILY back on Google's official test interstitial ID — your real
/// one (`ca-app-pub-9212406812117696/9482586380`) was returning no-fill
/// (error code 3), most likely just because it's brand new; this swap is
/// only to confirm the trigger/preload/display mechanism itself works
/// while that warms up. Swap the real one back in once it's serving.
/// ios/Runner/Info.plist's `GADApplicationIdentifier` is still Google's
/// test iOS App ID, though — an AdMob App ID is registered per-platform, so
/// it needs its own real iOS App ID (and a real iOS interstitial ad unit
/// ID here) from the AdMob console if this app ever ships on iOS.
class AdService {
static const _interstitialAdUnitId = 'ca-app-pub-3940256099942544/1033173712';
bool _sdkInitialized = false;
bool _shownThisSession = false;
InterstitialAd? _preloadedAd;
Completer<void>? _loadCompleter;
/// Starts the Mobile Ads SDK and begins preloading this session's one
/// interstitial. Idempotent (a no-op after the first call) and safe to
/// call speculatively — [AppState] calls this from [AppState.syncNow]
/// rather than unconditionally at app startup, so a user who never
/// connects cloud sync never triggers any ad-related network activity at
/// all.
Future<void> initialize() async {
if (_sdkInitialized) return;
_sdkInitialized = true;
await MobileAds.instance.initialize();
unawaited(_preload());
}
Future<void> _preload() {
final completer = Completer<void>();
_loadCompleter = completer;
InterstitialAd.load(
adUnitId: _interstitialAdUnitId,
request: const AdRequest(),
adLoadCallback: InterstitialAdLoadCallback(
onAdLoaded: (ad) {
_preloadedAd = ad;
if (!completer.isCompleted) completer.complete();
},
onAdFailedToLoad: (error) {
if (!completer.isCompleted) completer.complete();
},
),
);
return completer.future;
}
/// Shows the preloaded interstitial if this is the first call this app
/// session; every call after that (or if no ad ever became available) is
/// a no-op. Waits for the ad to actually be dismissed before returning,
/// so the caller — the sync engine, right before it starts uploading —
/// genuinely happens *after* the ad, not just alongside it; bounded so a
/// slow ad load or a stuck ad SDK callback can never block sync
/// indefinitely.
Future<void> maybeShowBeforeSync() async {
if (_shownThisSession) return;
_shownThisSession = true;
if (_preloadedAd == null) {
await _loadCompleter?.future.timeout(const Duration(seconds: 4), onTimeout: () {});
}
final ad = _preloadedAd;
_preloadedAd = null;
if (ad == null) return;
final dismissed = Completer<void>();
ad.fullScreenContentCallback = FullScreenContentCallback(
onAdDismissedFullScreenContent: (ad) {
ad.dispose();
if (!dismissed.isCompleted) dismissed.complete();
},
onAdFailedToShowFullScreenContent: (ad, error) {
ad.dispose();
if (!dismissed.isCompleted) dismissed.complete();
},
);
await ad.show();
await dismissed.future.timeout(const Duration(seconds: 15), onTimeout: () {});
}
}