feat(iap): settings screen with remove-ads purchase and restore
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,7 @@ import '../widgets/banner_ad_slot.dart';
|
||||
import '../widgets/season_background.dart';
|
||||
import 'game_screen.dart';
|
||||
import 'season_map_screen.dart';
|
||||
import 'settings_screen.dart';
|
||||
|
||||
class HomeScreen extends ConsumerWidget {
|
||||
const HomeScreen({super.key});
|
||||
@@ -33,89 +34,103 @@ class HomeScreen extends ConsumerWidget {
|
||||
children: [
|
||||
const SeasonBackground(theme: SeasonTheme.fallback),
|
||||
SafeArea(
|
||||
child: Column(
|
||||
child: Stack(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_logoMark(),
|
||||
const SizedBox(height: 18),
|
||||
Text(
|
||||
l10n.appTitle,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.displaySmall
|
||||
?.copyWith(fontWeight: FontWeight.w900),
|
||||
),
|
||||
if (streak.current > 0) ...[
|
||||
const SizedBox(height: 10),
|
||||
Chip(
|
||||
avatar: const Icon(
|
||||
Icons.local_fire_department,
|
||||
color: Colors.deepOrange,
|
||||
size: 20,
|
||||
Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_logoMark(),
|
||||
const SizedBox(height: 18),
|
||||
Text(
|
||||
l10n.appTitle,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.displaySmall
|
||||
?.copyWith(fontWeight: FontWeight.w900),
|
||||
),
|
||||
label: Text(
|
||||
'${streak.current}',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 44),
|
||||
FilledButton(
|
||||
style: FilledButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 56, vertical: 18),
|
||||
textStyle: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
onPressed: () {
|
||||
if (!(ModalRoute.of(context)?.isCurrent ?? true)) return;
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const SeasonMapScreen()),
|
||||
);
|
||||
},
|
||||
child: Text(l10n.adventure),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
OutlinedButton(
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 40, vertical: 14),
|
||||
textStyle: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
onPressed: () {
|
||||
if (!(ModalRoute.of(context)?.isCurrent ?? true)) return;
|
||||
ref.read(seasonFlowProvider.notifier).clear();
|
||||
ref.read(analyticsProvider).endlessStart();
|
||||
ref.read(gameSessionProvider.notifier).startStage(
|
||||
StageConfig.endless(
|
||||
seed: DateTime.now().millisecondsSinceEpoch,
|
||||
),
|
||||
if (streak.current > 0) ...[
|
||||
const SizedBox(height: 10),
|
||||
Chip(
|
||||
avatar: const Icon(
|
||||
Icons.local_fire_department,
|
||||
color: Colors.deepOrange,
|
||||
size: 20,
|
||||
),
|
||||
label: Text(
|
||||
'${streak.current}',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 44),
|
||||
FilledButton(
|
||||
style: FilledButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 56, vertical: 18),
|
||||
textStyle: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
onPressed: () {
|
||||
if (!(ModalRoute.of(context)?.isCurrent ?? true)) return;
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const SeasonMapScreen()),
|
||||
);
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const GameScreen()),
|
||||
);
|
||||
},
|
||||
child: Text(l10n.classic),
|
||||
),
|
||||
if (best > 0) ...[
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
l10n.bestScore(best),
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.55),
|
||||
},
|
||||
child: Text(l10n.adventure),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
const SizedBox(height: 14),
|
||||
OutlinedButton(
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 40, vertical: 14),
|
||||
textStyle: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
onPressed: () {
|
||||
if (!(ModalRoute.of(context)?.isCurrent ?? true)) return;
|
||||
ref.read(seasonFlowProvider.notifier).clear();
|
||||
ref.read(analyticsProvider).endlessStart();
|
||||
ref.read(gameSessionProvider.notifier).startStage(
|
||||
StageConfig.endless(
|
||||
seed: DateTime.now().millisecondsSinceEpoch,
|
||||
),
|
||||
);
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const GameScreen()),
|
||||
);
|
||||
},
|
||||
child: Text(l10n.classic),
|
||||
),
|
||||
if (best > 0) ...[
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
l10n.bestScore(best),
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.55),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const BannerAdSlot(),
|
||||
],
|
||||
),
|
||||
Positioned(
|
||||
top: 8,
|
||||
right: 8,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.settings, color: Colors.white70),
|
||||
onPressed: () => Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (_) => const SettingsScreen()),
|
||||
),
|
||||
),
|
||||
),
|
||||
const BannerAdSlot(),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
// lib/ui/screens/settings_screen.dart
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../l10n/gen/app_localizations.dart';
|
||||
import '../../state/providers.dart';
|
||||
|
||||
class SettingsScreen extends ConsumerWidget {
|
||||
const SettingsScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final adsRemoved = ref.watch(adsRemovedProvider);
|
||||
final iap = ref.read(iapServiceProvider);
|
||||
|
||||
ref.listen<bool>(adsRemovedProvider, (prev, next) {
|
||||
if (next && !(prev ?? false)) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.adsRemovedThanks)),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(l10n.settings)),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(l10n.removeAds),
|
||||
subtitle: Text(l10n.removeAdsDescription),
|
||||
trailing: adsRemoved
|
||||
? const Icon(Icons.check_circle, color: Colors.green)
|
||||
: Text(iap.product?.price ?? ''),
|
||||
onTap: adsRemoved
|
||||
? null
|
||||
: () async {
|
||||
if (!iap.available || iap.product == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.purchaseUnavailable)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
await iap.buyRemoveAds();
|
||||
},
|
||||
),
|
||||
const Divider(),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.restore),
|
||||
title: Text(l10n.restorePurchases),
|
||||
onTap: () => iap.restorePurchases(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user