import 'dart:async'; import 'package:flutter/material.dart'; import '../app_navigator.dart'; /// How long a non-gating placeholder stays up before closing itself — /// short enough not to be a nuisance while testing, long enough to be /// clearly noticed rather than flickering past. const _autoDismissAfter = Duration(seconds: 4); /// Stands in for a real ad while `adsEnabled` is false — see /// lib/services/ad_config.dart for why that exists. /// /// [mustConfirm] mirrors the difference between this app's two kinds of ad: /// - `false` (the report-export and fuel-save interstitials): closes /// itself after [_autoDismissAfter], the same "you didn't have to do /// anything" feel as a plain interstitial. /// - `true` (the cloud-sync gate): waits for the user to tap Continue, /// standing in for a rewarded ad's watch-it-through requirement, since /// the upload genuinely shouldn't proceed until they've acted. /// /// Returns once the placeholder has been dismissed, so callers can treat it /// exactly like awaiting a real ad's show-and-dismiss cycle. A no-op (and /// an immediate return) if the navigator isn't mounted yet. Future showAdPlaceholder({required bool mustConfirm}) async { final navigator = navigatorKey.currentState; if (navigator == null) return; await navigator.push( PageRouteBuilder( opaque: true, transitionDuration: const Duration(milliseconds: 180), pageBuilder: (_, _, _) => AdPlaceholderScreen(mustConfirm: mustConfirm), ), ); } class AdPlaceholderScreen extends StatefulWidget { /// See [showAdPlaceholder] — true for the sync gate, false otherwise. final bool mustConfirm; const AdPlaceholderScreen({super.key, required this.mustConfirm}); @override State createState() => _AdPlaceholderScreenState(); } class _AdPlaceholderScreenState extends State { Timer? _ticker; late int _secondsLeft = _autoDismissAfter.inSeconds; @override void initState() { super.initState(); if (widget.mustConfirm) return; _ticker = Timer.periodic(const Duration(seconds: 1), (timer) { if (!mounted) { timer.cancel(); return; } setState(() => _secondsLeft--); if (_secondsLeft <= 0) _dismiss(); }); } @override void dispose() { _ticker?.cancel(); super.dispose(); } void _dismiss() { _ticker?.cancel(); if (mounted) Navigator.of(context).pop(); } @override Widget build(BuildContext context) { final theme = Theme.of(context); final colors = theme.colorScheme; return PopScope( // A real ad can't be dismissed with the system back gesture either; // letting it through here would let the sync gate be bypassed. canPop: false, child: Scaffold( backgroundColor: colors.surface, body: SafeArea( child: Padding( padding: const EdgeInsets.all(32), child: Column( children: [ Align( alignment: Alignment.topCenter, child: Text( 'ADVERTISEMENT', style: theme.textTheme.labelSmall?.copyWith( color: colors.onSurfaceVariant, letterSpacing: 1.6, fontWeight: FontWeight.w600, ), ), ), Expanded( child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Image.asset( 'assets/icon/hero_icon.png', height: 96, fit: BoxFit.contain, ), const SizedBox(height: 28), Text( 'An advertisement will appear here', textAlign: TextAlign.center, style: theme.textTheme.titleMedium ?.copyWith(fontWeight: FontWeight.w700), ), const SizedBox(height: 10), Text( 'Ads are not running yet. This placeholder stands in ' 'for one so the rest of the app behaves exactly as it ' 'will once they are.', textAlign: TextAlign.center, style: theme.textTheme.bodyMedium ?.copyWith(color: colors.onSurfaceVariant), ), ], ), ), ), if (widget.mustConfirm) SizedBox( width: double.infinity, child: FilledButton( onPressed: _dismiss, child: const Text('Continue'), ), ) else Text( 'Closing in $_secondsLeft…', style: theme.textTheme.bodySmall ?.copyWith(color: colors.onSurfaceVariant), ), ], ), ), ), ), ); } }