| .. | .. |
|---|
| 1 | 1 | import 'package:flutter/material.dart'; |
|---|
| 2 | 2 | |
|---|
| 3 | 3 | import '../models/session.dart'; |
|---|
| 4 | +import '../services/purchase_service.dart'; |
|---|
| 4 | 5 | import '../theme/app_theme.dart'; |
|---|
| 5 | 6 | |
|---|
| 6 | 7 | /// Side drawer showing session list with reordering, unread badges, and controls. |
|---|
| .. | .. |
|---|
| 8 | 9 | final List<Session> sessions; |
|---|
| 9 | 10 | final String? activeSessionId; |
|---|
| 10 | 11 | final Map<String, int> unreadCounts; |
|---|
| 12 | + final bool isPro; |
|---|
| 11 | 13 | final void Function(Session session) onSelect; |
|---|
| 12 | 14 | final void Function(Session session) onRemove; |
|---|
| 13 | 15 | final void Function(Session session, String newName) onRename; |
|---|
| 14 | 16 | final void Function(int oldIndex, int newIndex) onReorder; |
|---|
| 15 | 17 | final VoidCallback onNewSession; |
|---|
| 16 | 18 | final VoidCallback onRefresh; |
|---|
| 19 | + /// Called when the user taps the upgrade prompt in the drawer. |
|---|
| 20 | + final VoidCallback? onUpgrade; |
|---|
| 17 | 21 | |
|---|
| 18 | 22 | const SessionDrawer({ |
|---|
| 19 | 23 | super.key, |
|---|
| 20 | 24 | required this.sessions, |
|---|
| 21 | 25 | required this.activeSessionId, |
|---|
| 22 | 26 | required this.unreadCounts, |
|---|
| 27 | + required this.isPro, |
|---|
| 23 | 28 | required this.onSelect, |
|---|
| 24 | 29 | required this.onRemove, |
|---|
| 25 | 30 | required this.onRename, |
|---|
| 26 | 31 | required this.onReorder, |
|---|
| 27 | 32 | required this.onNewSession, |
|---|
| 28 | 33 | required this.onRefresh, |
|---|
| 34 | + this.onUpgrade, |
|---|
| 29 | 35 | }); |
|---|
| 30 | 36 | |
|---|
| 31 | 37 | @override |
|---|
| .. | .. |
|---|
| 73 | 79 | final session = sessions[index]; |
|---|
| 74 | 80 | final isActive = session.id == activeSessionId; |
|---|
| 75 | 81 | final unread = unreadCounts[session.id] ?? 0; |
|---|
| 82 | + // Sessions beyond the free limit are locked for free users. |
|---|
| 83 | + // We still allow viewing them — just show the upgrade prompt. |
|---|
| 84 | + final isLocked = |
|---|
| 85 | + !isPro && index >= kFreeTierMaxSessions; |
|---|
| 76 | 86 | |
|---|
| 77 | 87 | return Dismissible( |
|---|
| 78 | 88 | key: ValueKey(session.id), |
|---|
| .. | .. |
|---|
| 111 | 121 | onDismissed: (_) => onRemove(session), |
|---|
| 112 | 122 | child: ListTile( |
|---|
| 113 | 123 | key: ValueKey('tile_${session.id}'), |
|---|
| 114 | | - leading: Text( |
|---|
| 115 | | - session.icon, |
|---|
| 116 | | - style: const TextStyle(fontSize: 20), |
|---|
| 117 | | - ), |
|---|
| 124 | + leading: isLocked |
|---|
| 125 | + ? const Icon(Icons.lock_outline, |
|---|
| 126 | + size: 20, |
|---|
| 127 | + color: AppColors.darkTextTertiary) |
|---|
| 128 | + : Text( |
|---|
| 129 | + session.icon, |
|---|
| 130 | + style: const TextStyle(fontSize: 20), |
|---|
| 131 | + ), |
|---|
| 118 | 132 | title: GestureDetector( |
|---|
| 119 | | - onDoubleTap: () => |
|---|
| 120 | | - _showRenameDialog(context, session), |
|---|
| 133 | + onDoubleTap: isLocked |
|---|
| 134 | + ? null |
|---|
| 135 | + : () => _showRenameDialog(context, session), |
|---|
| 121 | 136 | child: Text( |
|---|
| 122 | 137 | session.name, |
|---|
| 123 | 138 | style: TextStyle( |
|---|
| 124 | 139 | fontWeight: isActive |
|---|
| 125 | 140 | ? FontWeight.bold |
|---|
| 126 | 141 | : FontWeight.normal, |
|---|
| 127 | | - color: isActive ? AppColors.accent : null, |
|---|
| 142 | + color: isLocked |
|---|
| 143 | + ? AppColors.darkTextTertiary |
|---|
| 144 | + : isActive |
|---|
| 145 | + ? AppColors.accent |
|---|
| 146 | + : null, |
|---|
| 128 | 147 | ), |
|---|
| 129 | 148 | maxLines: 1, |
|---|
| 130 | 149 | overflow: TextOverflow.ellipsis, |
|---|
| 131 | 150 | ), |
|---|
| 132 | 151 | ), |
|---|
| 133 | | - trailing: Row( |
|---|
| 134 | | - mainAxisSize: MainAxisSize.min, |
|---|
| 135 | | - children: [ |
|---|
| 136 | | - if (unread > 0) |
|---|
| 137 | | - Container( |
|---|
| 138 | | - padding: const EdgeInsets.symmetric( |
|---|
| 139 | | - horizontal: 6, vertical: 2), |
|---|
| 140 | | - decoration: BoxDecoration( |
|---|
| 141 | | - color: AppColors.unreadBadge, |
|---|
| 142 | | - borderRadius: |
|---|
| 143 | | - BorderRadius.circular(10), |
|---|
| 152 | + trailing: isLocked |
|---|
| 153 | + ? TextButton( |
|---|
| 154 | + onPressed: () { |
|---|
| 155 | + Navigator.pop(context); |
|---|
| 156 | + onUpgrade?.call(); |
|---|
| 157 | + }, |
|---|
| 158 | + style: TextButton.styleFrom( |
|---|
| 159 | + foregroundColor: AppColors.accent, |
|---|
| 160 | + padding: const EdgeInsets.symmetric( |
|---|
| 161 | + horizontal: 8), |
|---|
| 162 | + minimumSize: Size.zero, |
|---|
| 163 | + tapTargetSize: |
|---|
| 164 | + MaterialTapTargetSize.shrinkWrap, |
|---|
| 144 | 165 | ), |
|---|
| 145 | | - child: Text( |
|---|
| 146 | | - '$unread', |
|---|
| 147 | | - style: const TextStyle( |
|---|
| 148 | | - color: Colors.white, |
|---|
| 149 | | - fontSize: 11, |
|---|
| 150 | | - fontWeight: FontWeight.bold, |
|---|
| 166 | + child: const Text('Upgrade', |
|---|
| 167 | + style: TextStyle(fontSize: 12)), |
|---|
| 168 | + ) |
|---|
| 169 | + : Row( |
|---|
| 170 | + mainAxisSize: MainAxisSize.min, |
|---|
| 171 | + children: [ |
|---|
| 172 | + if (unread > 0) |
|---|
| 173 | + Container( |
|---|
| 174 | + padding: const EdgeInsets.symmetric( |
|---|
| 175 | + horizontal: 6, vertical: 2), |
|---|
| 176 | + decoration: BoxDecoration( |
|---|
| 177 | + color: AppColors.unreadBadge, |
|---|
| 178 | + borderRadius: |
|---|
| 179 | + BorderRadius.circular(10), |
|---|
| 180 | + ), |
|---|
| 181 | + child: Text( |
|---|
| 182 | + '$unread', |
|---|
| 183 | + style: const TextStyle( |
|---|
| 184 | + color: Colors.white, |
|---|
| 185 | + fontSize: 11, |
|---|
| 186 | + fontWeight: FontWeight.bold, |
|---|
| 187 | + ), |
|---|
| 188 | + ), |
|---|
| 189 | + ), |
|---|
| 190 | + const SizedBox(width: 4), |
|---|
| 191 | + ReorderableDragStartListener( |
|---|
| 192 | + index: index, |
|---|
| 193 | + child: Icon( |
|---|
| 194 | + Icons.drag_handle, |
|---|
| 195 | + color: isDark |
|---|
| 196 | + ? AppColors.darkTextTertiary |
|---|
| 197 | + : Colors.grey.shade400, |
|---|
| 198 | + size: 20, |
|---|
| 199 | + ), |
|---|
| 151 | 200 | ), |
|---|
| 152 | | - ), |
|---|
| 201 | + ], |
|---|
| 153 | 202 | ), |
|---|
| 154 | | - const SizedBox(width: 4), |
|---|
| 155 | | - ReorderableDragStartListener( |
|---|
| 156 | | - index: index, |
|---|
| 157 | | - child: Icon( |
|---|
| 158 | | - Icons.drag_handle, |
|---|
| 159 | | - color: isDark |
|---|
| 160 | | - ? AppColors.darkTextTertiary |
|---|
| 161 | | - : Colors.grey.shade400, |
|---|
| 162 | | - size: 20, |
|---|
| 163 | | - ), |
|---|
| 164 | | - ), |
|---|
| 165 | | - ], |
|---|
| 166 | | - ), |
|---|
| 167 | 203 | selected: isActive, |
|---|
| 168 | 204 | selectedTileColor: isDark |
|---|
| 169 | 205 | ? Colors.white.withAlpha(10) |
|---|
| 170 | 206 | : Colors.blue.withAlpha(15), |
|---|
| 171 | | - onTap: () { |
|---|
| 172 | | - onSelect(session); |
|---|
| 173 | | - Navigator.pop(context); |
|---|
| 174 | | - }, |
|---|
| 207 | + onTap: isLocked |
|---|
| 208 | + ? () { |
|---|
| 209 | + Navigator.pop(context); |
|---|
| 210 | + onUpgrade?.call(); |
|---|
| 211 | + } |
|---|
| 212 | + : () { |
|---|
| 213 | + onSelect(session); |
|---|
| 214 | + Navigator.pop(context); |
|---|
| 215 | + }, |
|---|
| 175 | 216 | ), |
|---|
| 176 | 217 | ); |
|---|
| 177 | 218 | }, |
|---|
| 178 | 219 | ), |
|---|
| 179 | 220 | ), |
|---|
| 180 | | - // New session button |
|---|
| 221 | + // New session button (or upgrade prompt) |
|---|
| 181 | 222 | const Divider(height: 1), |
|---|
| 182 | 223 | Padding( |
|---|
| 183 | 224 | padding: const EdgeInsets.all(12), |
|---|
| 184 | 225 | child: SizedBox( |
|---|
| 185 | 226 | width: double.infinity, |
|---|
| 186 | | - child: ElevatedButton.icon( |
|---|
| 187 | | - onPressed: onNewSession, |
|---|
| 188 | | - icon: const Icon(Icons.add, size: 20), |
|---|
| 189 | | - label: const Text('New Session'), |
|---|
| 190 | | - style: ElevatedButton.styleFrom( |
|---|
| 191 | | - backgroundColor: AppColors.accent, |
|---|
| 192 | | - foregroundColor: Colors.white, |
|---|
| 193 | | - ), |
|---|
| 194 | | - ), |
|---|
| 227 | + child: !isPro && |
|---|
| 228 | + sessions.length >= kFreeTierMaxSessions |
|---|
| 229 | + ? OutlinedButton.icon( |
|---|
| 230 | + onPressed: () { |
|---|
| 231 | + Navigator.pop(context); |
|---|
| 232 | + onUpgrade?.call(); |
|---|
| 233 | + }, |
|---|
| 234 | + icon: const Icon(Icons.lock_outline, size: 18), |
|---|
| 235 | + label: const Text('Upgrade for More Sessions'), |
|---|
| 236 | + style: OutlinedButton.styleFrom( |
|---|
| 237 | + foregroundColor: AppColors.accent, |
|---|
| 238 | + side: const BorderSide(color: AppColors.accent), |
|---|
| 239 | + ), |
|---|
| 240 | + ) |
|---|
| 241 | + : ElevatedButton.icon( |
|---|
| 242 | + onPressed: onNewSession, |
|---|
| 243 | + icon: const Icon(Icons.add, size: 20), |
|---|
| 244 | + label: const Text('New Session'), |
|---|
| 245 | + style: ElevatedButton.styleFrom( |
|---|
| 246 | + backgroundColor: AppColors.accent, |
|---|
| 247 | + foregroundColor: Colors.white, |
|---|
| 248 | + ), |
|---|
| 249 | + ), |
|---|
| 195 | 250 | ), |
|---|
| 196 | 251 | ), |
|---|
| 197 | 252 | ], |
|---|