Temporary setup of alternative to advertisements

This commit is contained in:
Courtney Arnold 2026-09-19 20:45:59 -05:00
parent ad2d932842
commit 57a9793a40
14 changed files with 653 additions and 56 deletions

View file

@ -157,7 +157,7 @@ class AppState extends ChangeNotifier {
/// The earliest-known moment this app was ever used — null until [init]
/// loads it. Drives the new-user ad grace period (see
/// [_maybeShowFuelSaveAd]/[maybeShowReportAd]); see
/// [maybeShowFuelSaveAd]/[maybeShowReportAd]); see
/// [DatabaseService.getOrCreateFirstUsedAt] for why this is synced
/// rather than a local-only preference.
DateTime? firstUsedAt;
@ -247,34 +247,47 @@ class AppState extends ChangeNotifier {
return firstUsed == null || DateTime.now().difference(firstUsed) < _newUserAdGracePeriod;
}
/// Best-effort ad shown after a fuel entry is saved — see
/// [AdService.maybeShowFuelSaveAd]. A no-op for a user with an active ad
/// -free purchase, during the new-user grace period or the first
/// [_fuelSaveAdGraceSaves] saves, while a previously-watched fuel-save
/// ad's credit is still active, or — deliberately — whenever cloud
/// backup isn't configured: [showBackupReminderDialog] already claims
/// that same save's attention in that case (see its call site in
/// `ConfirmFuelEntryScreen._save`), and letting both compete for the
/// same moment is exactly the stacked-full-screen-surfaces problem this
/// scoping avoids. Skipping this way costs nothing — the credit/grace
/// state simply isn't touched, so the check is just as "due" next save.
Future<void> _maybeShowFuelSaveAd() async {
if (adsCurrentlyDisabled || !hasCloudBackupConfigured) return;
if (_inNewUserGracePeriod) return;
if (fuelEntries.length <= _fuelSaveAdGraceSaves) return;
/// Ad shown on saving a fuel entry — see [AdService.maybeShowFuelSaveAd].
///
/// Called by `ConfirmFuelEntryScreen._save` *after* [addFuelEntry] has
/// already committed the entry, and awaited before that screen pops. So
/// it reads to the user as "tap Save, see the ad, land back on the
/// list", while the entry itself is never actually waiting on the ad —
/// which matters both for their data (nothing is lost if the app dies
/// mid-ad) and for AdMob's rule that a plain interstitial must never
/// gate access to app functionality. That's the same rule that forced
/// the cloud-sync gate to a *rewarded* ad; the difference here is that
/// nothing is being withheld pending the ad.
///
/// A no-op for a user with an active ad-free purchase, during the
/// new-user grace period or the first [_fuelSaveAdGraceSaves] saves, or
/// while a previously-watched fuel-save ad's credit is still active.
///
/// Returns whether an ad actually showed. The caller uses that to hold
/// back [showBackupReminderDialog] for this one save: only ever one
/// full-screen surface per save (the "post-save arbitration" rule), but
/// the ad takes precedence rather than being suppressed outright — the
/// reminder recurs on the next save anyway, whereas suppressing meant a
/// user who never configured backup never saw this placement at all.
Future<bool> maybeShowFuelSaveAd() async {
if (adsCurrentlyDisabled) return false;
if (_inNewUserGracePeriod) return false;
if (fuelEntries.length <= _fuelSaveAdGraceSaves) return false;
final lastShown = _lastFuelSaveAdShownAt;
if (lastShown != null &&
DateTime.now().difference(lastShown) < _fuelSaveAdCreditDuration &&
_fuelSavesSinceAd < _fuelSaveAdCreditSaves) {
_fuelSavesSinceAd++;
return;
return false;
}
if (await adService.maybeShowFuelSaveAd()) {
_lastFuelSaveAdShownAt = DateTime.now();
_fuelSavesSinceAd = 0;
return true;
}
return false;
}
/// The store's own formatted, localized price for the ad-free-year
@ -703,7 +716,6 @@ class AppState extends ChangeNotifier {
);
await database.saveFuelEntry(entry);
await _persist();
unawaited(_maybeShowFuelSaveAd());
return entry;
}