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