Ad and purchases enabled (still need actual testing)
This commit is contained in:
parent
2fcc6dcab7
commit
faf319322a
35 changed files with 1676 additions and 34 deletions
|
|
@ -1,3 +1,4 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
@ -6,6 +7,7 @@ import 'package:provider/provider.dart';
|
|||
|
||||
import '../services/app_state.dart';
|
||||
import '../services/receipt_parser.dart';
|
||||
import '../widgets/backup_reminder.dart';
|
||||
import 'receipt_image_screen.dart';
|
||||
|
||||
/// Shown right after a receipt photo is captured and OCR'd. Pre-fills
|
||||
|
|
@ -112,7 +114,8 @@ class _ConfirmFuelEntryScreenState extends State<ConfirmFuelEntryScreen> {
|
|||
|
||||
setState(() => _saving = true);
|
||||
try {
|
||||
await context.read<AppState>().addFuelEntry(
|
||||
final appState = context.read<AppState>();
|
||||
await appState.addFuelEntry(
|
||||
vehicleId: widget.vehicleId,
|
||||
date: _date,
|
||||
gallons: double.parse(_gallonsController.text),
|
||||
|
|
@ -120,7 +123,21 @@ class _ConfirmFuelEntryScreenState extends State<ConfirmFuelEntryScreen> {
|
|||
totalCost: double.parse(_totalController.text),
|
||||
receiptImage: widget.imageFile,
|
||||
);
|
||||
if (mounted) Navigator.of(context).pop();
|
||||
if (!mounted) return;
|
||||
|
||||
// Ask while this screen (and its context) is still fully alive,
|
||||
// before popping — simpler than trying to show a dialog against a
|
||||
// context whose widget is mid-removal.
|
||||
var wantsBackupSetup = false;
|
||||
if (!appState.hasCloudBackupConfigured) {
|
||||
wantsBackupSetup = await showBackupReminderDialog(context);
|
||||
if (!mounted) return;
|
||||
}
|
||||
|
||||
Navigator.of(context).pop();
|
||||
if (wantsBackupSetup) {
|
||||
unawaited(openCloudBackupSetup(context));
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _saving = false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -269,6 +269,11 @@ class _MainShellState extends State<MainShell> {
|
|||
if (navigator.canPop()) navigator.pop();
|
||||
_tourPushedDataScreen = false;
|
||||
}
|
||||
// Whether the tour finished naturally (on the Settings tab, since the
|
||||
// last step lives there) or was skipped mid-way through (on whichever
|
||||
// tab that step happened to be on), always land back on Receipts —
|
||||
// that's the tab a first-time user should actually start using.
|
||||
_jumpToTab(0);
|
||||
unawaited(context.read<AppState>().markOnboardingTourSeen());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../services/app_state.dart';
|
||||
|
|
@ -46,6 +47,49 @@ class SettingsScreen extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Ads', style: Theme.of(context).textTheme.titleMedium),
|
||||
const SizedBox(height: 8),
|
||||
if (appState.adsCurrentlyDisabled) ...[
|
||||
Text(
|
||||
'Ads are disabled until '
|
||||
'${DateFormat.yMMMd().format(appState.adFreeUntil!.toLocal())}.',
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
] else ...[
|
||||
Text(
|
||||
'This app shows a single ad, at most once per app session, right '
|
||||
'before syncing your data to the cloud.',
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
if (appState.purchaseError != null) ...[
|
||||
Text(
|
||||
appState.purchaseError!,
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.error),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
FilledButton.icon(
|
||||
onPressed: () => context.read<AppState>().buyAdFreeYear(),
|
||||
icon: const Icon(Icons.block),
|
||||
label: Text(
|
||||
appState.adFreeYearPriceLabel == null
|
||||
? 'Remove Ads for a Year'
|
||||
: 'Remove Ads for a Year — ${appState.adFreeYearPriceLabel}',
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
|
|
|
|||
128
lib/screens/user_agreement_screen.dart
Normal file
128
lib/screens/user_agreement_screen.dart
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../services/app_state.dart';
|
||||
|
||||
/// The very first thing shown on a fresh install — before the onboarding
|
||||
/// tour, before anything else — gating [MainShell] entirely until the user
|
||||
/// taps "I Agree" (see [AppRoot] in main.dart and
|
||||
/// [AppState.hasAcceptedUserAgreement]/[AppState.acceptUserAgreement]).
|
||||
/// Its whole purpose is the liability section below: this app stores data
|
||||
/// on the user's own device (and, optionally, their own cloud storage
|
||||
/// account) with no server or backend of ours involved, so we have no
|
||||
/// ability to recover anything for them if it's lost.
|
||||
class UserAgreementScreen extends StatelessWidget {
|
||||
const UserAgreementScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('User Agreement'), automaticallyImplyLeading: false),
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(20),
|
||||
children: [
|
||||
Text(
|
||||
'Please read and accept the following before using Show Me The Fuel '
|
||||
'Refund.',
|
||||
style: textTheme.bodyLarge,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
_Section(
|
||||
title: 'Your Data Lives on Your Device',
|
||||
body:
|
||||
'Every vehicle, fuel receipt, and photo you log is stored locally on '
|
||||
'this device. If you connect a cloud storage account (Google Drive, '
|
||||
'Dropbox, OneDrive, or your own WebDAV server) in Settings, a copy is '
|
||||
'also kept there — in storage you control, not on any server we run. '
|
||||
"We don't operate a backend, and we don't have a copy of your data.",
|
||||
),
|
||||
_Section(
|
||||
title: 'We Are Not Responsible for Your Data or Any Data Loss',
|
||||
body:
|
||||
'The developer is not responsible for your data, or for any loss, '
|
||||
'corruption, or unavailability of it, for any reason — including but '
|
||||
'not limited to device loss, theft, or damage; an uninstall or app/OS '
|
||||
'update; a bug in this app; or an outage or account issue with a cloud '
|
||||
'storage provider you connect. Backing up your data (by connecting a '
|
||||
'cloud storage account, and by keeping it connected and in good '
|
||||
'standing) is entirely your responsibility. If you never connect a '
|
||||
'backup, the only copy of your data is the one on this device.',
|
||||
),
|
||||
_Section(
|
||||
title: 'Provided As-Is',
|
||||
body:
|
||||
'This app is provided "as is," without warranty of any kind, express or '
|
||||
'implied. Fuel and gallon values are read automatically from your '
|
||||
'receipt photos and may be misread — always review them before relying '
|
||||
'on them, including for a refund claim.',
|
||||
),
|
||||
_Section(
|
||||
title: 'Not Tax or Legal Advice',
|
||||
body:
|
||||
"This app helps you organize receipts for Missouri's Motor Fuel Tax "
|
||||
'Refund program; it does not provide tax or legal advice, and does not '
|
||||
'guarantee your eligibility for any refund. The program itself is set '
|
||||
'by Missouri law, which can change at any time — always confirm current '
|
||||
'eligibility and rates with the Missouri Department of Revenue.',
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: () => SystemNavigator.pop(),
|
||||
child: const Text('Decline'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: FilledButton(
|
||||
onPressed: () => context.read<AppState>().acceptUserAgreement(),
|
||||
child: const Text('I Agree'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Section extends StatelessWidget {
|
||||
final String title;
|
||||
final String body;
|
||||
|
||||
const _Section({required this.title, required this.body});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title, style: textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700)),
|
||||
const SizedBox(height: 6),
|
||||
Text(body, style: textTheme.bodyMedium),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue