MO-Fuel-Tax-Back/lib/widgets/ad_placeholder_screen.dart

156 lines
5.3 KiB
Dart

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<void> showAdPlaceholder({required bool mustConfirm}) async {
final navigator = navigatorKey.currentState;
if (navigator == null) return;
await navigator.push(
PageRouteBuilder<void>(
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<AdPlaceholderScreen> createState() => _AdPlaceholderScreenState();
}
class _AdPlaceholderScreenState extends State<AdPlaceholderScreen> {
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),
),
],
),
),
),
),
);
}
}