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