96 lines
3.8 KiB
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: () {});
|
|
}
|
|
}
|