From 98e5695f9c77c594a103e9e81128798d41bae46a Mon Sep 17 00:00:00 2001
From: Matthias Nott <mnott@mnsoft.org>
Date: Wed, 01 Apr 2026 18:52:33 +0200
Subject: [PATCH] feat: StoreKit 2 IAP — free tier with 2 sessions and 15min message TTL
---
lib/screens/settings_screen.dart | 126 ++++++++++++++++++++++++++++++++++++++++++
1 files changed, 126 insertions(+), 0 deletions(-)
diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart
index 840db79..593b5dd 100644
--- a/lib/screens/settings_screen.dart
+++ b/lib/screens/settings_screen.dart
@@ -5,6 +5,7 @@
import '../models/server_config.dart';
import '../providers/providers.dart';
import '../services/mqtt_service.dart' show ConnectionStatus;
+import '../services/purchase_service.dart';
import '../services/wol_service.dart';
import '../theme/app_theme.dart';
import '../widgets/status_dot.dart';
@@ -25,10 +26,12 @@
late final TextEditingController _macController;
late final TextEditingController _mqttTokenController;
bool _isWaking = false;
+ bool _iapLoading = false;
@override
void initState() {
super.initState();
+ PurchaseService.instance.addListener(_onPurchaseChanged);
final config = ref.read(serverConfigProvider);
_localHostController =
TextEditingController(text: config?.localHost ?? '');
@@ -46,6 +49,7 @@
@override
void dispose() {
+ PurchaseService.instance.removeListener(_onPurchaseChanged);
_localHostController.dispose();
_vpnHostController.dispose();
_remoteHostController.dispose();
@@ -53,6 +57,45 @@
_macController.dispose();
_mqttTokenController.dispose();
super.dispose();
+ }
+
+ void _onPurchaseChanged() {
+ if (!mounted) return;
+ final isPro = PurchaseService.instance.isPro;
+ ref.read(isProProvider.notifier).state = isPro;
+ setState(() => _iapLoading = PurchaseService.instance.isLoading);
+ final error = PurchaseService.instance.errorMessage;
+ if (error != null && mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text(error)),
+ );
+ }
+ if (isPro && mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(
+ content: Text('PAILot Pro activated. Enjoy unlimited sessions!'),
+ duration: Duration(seconds: 3),
+ ),
+ );
+ }
+ }
+
+ Future<void> _handleUpgrade() async {
+ setState(() => _iapLoading = true);
+ await PurchaseService.instance.purchaseFullAccess();
+ }
+
+ Future<void> _handleRestore() async {
+ setState(() => _iapLoading = true);
+ await PurchaseService.instance.restorePurchases();
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(
+ content: Text('Checking for previous purchases...'),
+ duration: Duration(seconds: 2),
+ ),
+ );
+ }
}
Future<void> _save() async {
@@ -328,6 +371,89 @@
icon: const Icon(Icons.shield_outlined),
label: const Text('Reset Server Trust'),
),
+ const SizedBox(height: 24),
+
+ // --- PAILot Pro ---
+ const Divider(),
+ const SizedBox(height: 8),
+ Row(
+ children: [
+ const Icon(Icons.star, size: 18),
+ const SizedBox(width: 8),
+ Text(
+ 'PAILot Pro',
+ style: Theme.of(context).textTheme.titleMedium,
+ ),
+ const Spacer(),
+ Consumer(
+ builder: (ctx, ref, _) {
+ final isPro = ref.watch(isProProvider);
+ return Chip(
+ label: Text(
+ isPro ? 'Active' : 'Free Tier',
+ style: TextStyle(
+ fontSize: 11,
+ color: isPro ? Colors.white : null,
+ ),
+ ),
+ backgroundColor: isPro ? AppColors.accent : null,
+ padding: EdgeInsets.zero,
+ visualDensity: VisualDensity.compact,
+ );
+ },
+ ),
+ ],
+ ),
+ const SizedBox(height: 4),
+ Text(
+ 'Free: 2 sessions, messages expire after 15 min\n'
+ 'Pro: unlimited sessions, messages persist forever',
+ style: Theme.of(context)
+ .textTheme
+ .bodySmall
+ ?.copyWith(color: Colors.grey.shade500),
+ ),
+ const SizedBox(height: 12),
+ Consumer(
+ builder: (ctx, ref, _) {
+ final isPro = ref.watch(isProProvider);
+ if (isPro) {
+ return const Center(
+ child: Text(
+ 'Full access active',
+ style: TextStyle(color: AppColors.accent),
+ ),
+ );
+ }
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ ElevatedButton.icon(
+ onPressed: _iapLoading ? null : _handleUpgrade,
+ icon: _iapLoading
+ ? const SizedBox(
+ width: 16,
+ height: 16,
+ child:
+ CircularProgressIndicator(strokeWidth: 2),
+ )
+ : const Icon(Icons.upgrade),
+ label: const Text('Upgrade to Pro — \$4.99'),
+ style: ElevatedButton.styleFrom(
+ backgroundColor: AppColors.accent,
+ foregroundColor: Colors.white,
+ ),
+ ),
+ const SizedBox(height: 8),
+ OutlinedButton.icon(
+ onPressed: _iapLoading ? null : _handleRestore,
+ icon: const Icon(Icons.restore),
+ label: const Text('Restore Purchase'),
+ ),
+ ],
+ );
+ },
+ ),
const SizedBox(height: 12),
],
),
--
Gitblit v1.3.1