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/widgets/session_drawer.dart |  159 +++++++++++++++++++++++++++++++++++-----------------
 1 files changed, 107 insertions(+), 52 deletions(-)

diff --git a/lib/widgets/session_drawer.dart b/lib/widgets/session_drawer.dart
index f3a5178..a9f6cd8 100644
--- a/lib/widgets/session_drawer.dart
+++ b/lib/widgets/session_drawer.dart
@@ -1,6 +1,7 @@
 import 'package:flutter/material.dart';
 
 import '../models/session.dart';
+import '../services/purchase_service.dart';
 import '../theme/app_theme.dart';
 
 /// Side drawer showing session list with reordering, unread badges, and controls.
@@ -8,24 +9,29 @@
   final List<Session> sessions;
   final String? activeSessionId;
   final Map<String, int> unreadCounts;
+  final bool isPro;
   final void Function(Session session) onSelect;
   final void Function(Session session) onRemove;
   final void Function(Session session, String newName) onRename;
   final void Function(int oldIndex, int newIndex) onReorder;
   final VoidCallback onNewSession;
   final VoidCallback onRefresh;
+  /// Called when the user taps the upgrade prompt in the drawer.
+  final VoidCallback? onUpgrade;
 
   const SessionDrawer({
     super.key,
     required this.sessions,
     required this.activeSessionId,
     required this.unreadCounts,
+    required this.isPro,
     required this.onSelect,
     required this.onRemove,
     required this.onRename,
     required this.onReorder,
     required this.onNewSession,
     required this.onRefresh,
+    this.onUpgrade,
   });
 
   @override
@@ -73,6 +79,10 @@
                         final session = sessions[index];
                         final isActive = session.id == activeSessionId;
                         final unread = unreadCounts[session.id] ?? 0;
+                        // Sessions beyond the free limit are locked for free users.
+                        // We still allow viewing them — just show the upgrade prompt.
+                        final isLocked =
+                            !isPro && index >= kFreeTierMaxSessions;
 
                         return Dismissible(
                           key: ValueKey(session.id),
@@ -111,87 +121,132 @@
                           onDismissed: (_) => onRemove(session),
                           child: ListTile(
                             key: ValueKey('tile_${session.id}'),
-                            leading: Text(
-                              session.icon,
-                              style: const TextStyle(fontSize: 20),
-                            ),
+                            leading: isLocked
+                                ? const Icon(Icons.lock_outline,
+                                    size: 20,
+                                    color: AppColors.darkTextTertiary)
+                                : Text(
+                                    session.icon,
+                                    style: const TextStyle(fontSize: 20),
+                                  ),
                             title: GestureDetector(
-                              onDoubleTap: () =>
-                                  _showRenameDialog(context, session),
+                              onDoubleTap: isLocked
+                                  ? null
+                                  : () => _showRenameDialog(context, session),
                               child: Text(
                                 session.name,
                                 style: TextStyle(
                                   fontWeight: isActive
                                       ? FontWeight.bold
                                       : FontWeight.normal,
-                                  color: isActive ? AppColors.accent : null,
+                                  color: isLocked
+                                      ? AppColors.darkTextTertiary
+                                      : isActive
+                                          ? AppColors.accent
+                                          : null,
                                 ),
                                 maxLines: 1,
                                 overflow: TextOverflow.ellipsis,
                               ),
                             ),
-                            trailing: Row(
-                              mainAxisSize: MainAxisSize.min,
-                              children: [
-                                if (unread > 0)
-                                  Container(
-                                    padding: const EdgeInsets.symmetric(
-                                        horizontal: 6, vertical: 2),
-                                    decoration: BoxDecoration(
-                                      color: AppColors.unreadBadge,
-                                      borderRadius:
-                                          BorderRadius.circular(10),
+                            trailing: isLocked
+                                ? TextButton(
+                                    onPressed: () {
+                                      Navigator.pop(context);
+                                      onUpgrade?.call();
+                                    },
+                                    style: TextButton.styleFrom(
+                                      foregroundColor: AppColors.accent,
+                                      padding: const EdgeInsets.symmetric(
+                                          horizontal: 8),
+                                      minimumSize: Size.zero,
+                                      tapTargetSize:
+                                          MaterialTapTargetSize.shrinkWrap,
                                     ),
-                                    child: Text(
-                                      '$unread',
-                                      style: const TextStyle(
-                                        color: Colors.white,
-                                        fontSize: 11,
-                                        fontWeight: FontWeight.bold,
+                                    child: const Text('Upgrade',
+                                        style: TextStyle(fontSize: 12)),
+                                  )
+                                : Row(
+                                    mainAxisSize: MainAxisSize.min,
+                                    children: [
+                                      if (unread > 0)
+                                        Container(
+                                          padding: const EdgeInsets.symmetric(
+                                              horizontal: 6, vertical: 2),
+                                          decoration: BoxDecoration(
+                                            color: AppColors.unreadBadge,
+                                            borderRadius:
+                                                BorderRadius.circular(10),
+                                          ),
+                                          child: Text(
+                                            '$unread',
+                                            style: const TextStyle(
+                                              color: Colors.white,
+                                              fontSize: 11,
+                                              fontWeight: FontWeight.bold,
+                                            ),
+                                          ),
+                                        ),
+                                      const SizedBox(width: 4),
+                                      ReorderableDragStartListener(
+                                        index: index,
+                                        child: Icon(
+                                          Icons.drag_handle,
+                                          color: isDark
+                                              ? AppColors.darkTextTertiary
+                                              : Colors.grey.shade400,
+                                          size: 20,
+                                        ),
                                       ),
-                                    ),
+                                    ],
                                   ),
-                                const SizedBox(width: 4),
-                                ReorderableDragStartListener(
-                                  index: index,
-                                  child: Icon(
-                                    Icons.drag_handle,
-                                    color: isDark
-                                        ? AppColors.darkTextTertiary
-                                        : Colors.grey.shade400,
-                                    size: 20,
-                                  ),
-                                ),
-                              ],
-                            ),
                             selected: isActive,
                             selectedTileColor: isDark
                                 ? Colors.white.withAlpha(10)
                                 : Colors.blue.withAlpha(15),
-                            onTap: () {
-                              onSelect(session);
-                              Navigator.pop(context);
-                            },
+                            onTap: isLocked
+                                ? () {
+                                    Navigator.pop(context);
+                                    onUpgrade?.call();
+                                  }
+                                : () {
+                                    onSelect(session);
+                                    Navigator.pop(context);
+                                  },
                           ),
                         );
                       },
                     ),
             ),
-            // New session button
+            // New session button (or upgrade prompt)
             const Divider(height: 1),
             Padding(
               padding: const EdgeInsets.all(12),
               child: SizedBox(
                 width: double.infinity,
-                child: ElevatedButton.icon(
-                  onPressed: onNewSession,
-                  icon: const Icon(Icons.add, size: 20),
-                  label: const Text('New Session'),
-                  style: ElevatedButton.styleFrom(
-                    backgroundColor: AppColors.accent,
-                    foregroundColor: Colors.white,
-                  ),
-                ),
+                child: !isPro &&
+                        sessions.length >= kFreeTierMaxSessions
+                    ? OutlinedButton.icon(
+                        onPressed: () {
+                          Navigator.pop(context);
+                          onUpgrade?.call();
+                        },
+                        icon: const Icon(Icons.lock_outline, size: 18),
+                        label: const Text('Upgrade for More Sessions'),
+                        style: OutlinedButton.styleFrom(
+                          foregroundColor: AppColors.accent,
+                          side: const BorderSide(color: AppColors.accent),
+                        ),
+                      )
+                    : ElevatedButton.icon(
+                        onPressed: onNewSession,
+                        icon: const Icon(Icons.add, size: 20),
+                        label: const Text('New Session'),
+                        style: ElevatedButton.styleFrom(
+                          backgroundColor: AppColors.accent,
+                          foregroundColor: Colors.white,
+                        ),
+                      ),
               ),
             ),
           ],

--
Gitblit v1.3.1