diff --git a/flutter_01.png b/flutter_01.png deleted file mode 100644 index 9afb358..0000000 Binary files a/flutter_01.png and /dev/null differ diff --git a/lib/core/l10n/app_strings.dart b/lib/core/l10n/app_strings.dart index e07751d..4003687 100644 --- a/lib/core/l10n/app_strings.dart +++ b/lib/core/l10n/app_strings.dart @@ -332,6 +332,19 @@ class AppStrings { _ru ? 'Предыдущие покупки не найдены' : 'No previous purchases found'; String get proTapToClose => _ru ? 'Нажмите чтобы закрыть' : 'Tap anywhere to close'; + String get proResetData => + _ru ? 'Сбросить тестовые данные' : 'Reset Test Data'; + String get proResetDataDesc => _ru + ? 'Локально отменить подписку для тестирования' + : 'Locally cancel subscription for testing'; + String get proResetDataConfirm => _ru + ? 'Сбросить подписку? Это локально очистит ваш премиум статус.' + : 'Reset subscription? This will locally clear your premium status.'; + String get proResetDataSuccess => _ru + ? 'Премиум статус сброшен' + : 'Premium status reset'; + String get proRestoreSuccessTitle => + _ru ? 'Покупки восстановлены!' : 'Purchases Restored!'; String get backupTitle => _ru ? 'Резервная копия' : 'Backup'; String get backupCreate => _ru ? 'Создать резервную копию' : 'Create Backup'; String get backupRestore => _ru ? 'Восстановить из копии' : 'Restore Backup'; diff --git a/lib/features/categories/provider.dart b/lib/features/categories/provider.dart index 5d83eb2..9c246e9 100644 --- a/lib/features/categories/provider.dart +++ b/lib/features/categories/provider.dart @@ -1,4 +1,5 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../shared/models/account.dart'; import '../../shared/models/transaction.dart'; import '../dashboard/provider.dart'; import '../settings/provider.dart'; @@ -35,81 +36,88 @@ class _StatsTimeFilterNotifier extends Notifier { }); } -String _statsTargetCurrency(Ref ref) { - final index = ref.watch(activeAccountIndexProvider); - final accountsAsync = ref.watch(accountsProvider); - final globalCurrency = ref.watch(currencyProvider).code; - - if (index > 0) { - final accounts = accountsAsync.value ?? []; - if (index <= accounts.length) { - return accounts[index - 1].currency; - } +String _resolveTargetCurrency( + int activeIndex, + List accounts, + String globalCurrency, +) { + if (activeIndex > 0 && activeIndex <= accounts.length) { + return accounts[activeIndex - 1].currency; } - return globalCurrency; } -CurrencyInfo _statsCurrencyInfo(Ref ref) { - final code = _statsTargetCurrency(ref); - return CurrencyInfo(currencyMap[code]?.symbol ?? '\$', code); -} - -List _statsScopedTransactions(Ref ref) { - final txs = ref.watch(accountFilteredTransactionsProvider); - final timeFilter = ref.watch(statsTimeFilterProvider); +List _filterScopedTransactions( + List txs, + StatsTimeFilter timeFilter, +) { var filtered = txs.where((t) => t.category != 'Transfer'); - if (timeFilter == StatsTimeFilter.month) { final now = DateTime.now(); filtered = filtered.where( (t) => t.date.year == now.year && t.date.month == now.month, ); } - return filtered.toList(); } -double _convertAmount(Ref ref, Transaction t) { - final exchange = ref.watch(exchangeRateServiceProvider); - return exchange.convert( - t.amount, - t.currencyCode, - _statsTargetCurrency(ref), - ); -} - final statsCurrencyProvider = Provider((ref) { - return _statsCurrencyInfo(ref); + final index = ref.watch(activeAccountIndexProvider); + final accountsAsync = ref.watch(accountsProvider); + final globalCurrency = ref.watch(currencyProvider).code; + final accounts = accountsAsync.value ?? []; + final code = _resolveTargetCurrency(index, accounts, globalCurrency); + return CurrencyInfo(currencyMap[code]?.symbol ?? '\$', code); }); - final statsScopedTransactionsProvider = Provider>((ref) { - return _statsScopedTransactions(ref); - }); +final statsScopedTransactionsProvider = Provider>((ref) { + final txs = ref.watch(accountFilteredTransactionsProvider); + final timeFilter = ref.watch(statsTimeFilterProvider); + return _filterScopedTransactions(txs, timeFilter); +}); final statsIncomeTotalProvider = Provider((ref) { + final exchange = ref.watch(exchangeRateServiceProvider); + final index = ref.watch(activeAccountIndexProvider); + final accountsAsync = ref.watch(accountsProvider); + final globalCurrency = ref.watch(currencyProvider).code; + final accounts = accountsAsync.value ?? []; + final target = _resolveTargetCurrency(index, accounts, globalCurrency); return ref .watch(statsScopedTransactionsProvider) .where((t) => t.type == TransactionType.income) - .fold(0.0, (sum, t) => sum + _convertAmount(ref, t)); + .fold(0.0, (sum, t) => sum + exchange.convert(t.amount, t.currencyCode, target)); }); final statsExpenseTotalProvider = Provider((ref) { + final exchange = ref.watch(exchangeRateServiceProvider); + final index = ref.watch(activeAccountIndexProvider); + final accountsAsync = ref.watch(accountsProvider); + final globalCurrency = ref.watch(currencyProvider).code; + final accounts = accountsAsync.value ?? []; + final target = _resolveTargetCurrency(index, accounts, globalCurrency); return ref .watch(statsScopedTransactionsProvider) .where((t) => t.type == TransactionType.expense) - .fold(0.0, (sum, t) => sum + _convertAmount(ref, t)); + .fold(0.0, (sum, t) => sum + exchange.convert(t.amount, t.currencyCode, target)); }); - final statsSummaryProvider = Provider((ref) { +final statsSummaryProvider = Provider((ref) { + final exchange = ref.watch(exchangeRateServiceProvider); + final index = ref.watch(activeAccountIndexProvider); + final accountsAsync = ref.watch(accountsProvider); + final globalCurrency = ref.watch(currencyProvider).code; + final accounts = accountsAsync.value ?? []; + final target = _resolveTargetCurrency(index, accounts, globalCurrency); final transactions = ref.watch(statsScopedTransactionsProvider); + var income = 0.0; var expense = 0.0; var incomeCount = 0; var expenseCount = 0; for (final transaction in transactions) { - final amount = _convertAmount(ref, transaction); + final amount = exchange.convert(transaction.amount, transaction.currencyCode, target); if (transaction.type == TransactionType.income) { income += amount; incomeCount++; @@ -128,22 +136,34 @@ final statsExpenseTotalProvider = Provider((ref) { averageIncome: incomeCount == 0 ? 0 : income / incomeCount, averageExpense: expenseCount == 0 ? 0 : expense / expenseCount, ); - }); +}); final categoryExpenseProvider = Provider>((ref) { + final exchange = ref.watch(exchangeRateServiceProvider); + final index = ref.watch(activeAccountIndexProvider); + final accountsAsync = ref.watch(accountsProvider); + final globalCurrency = ref.watch(currencyProvider).code; + final accounts = accountsAsync.value ?? []; + final target = _resolveTargetCurrency(index, accounts, globalCurrency); final map = {}; for (final t in ref.watch(statsScopedTransactionsProvider)) { if (t.type != TransactionType.expense) continue; - map[t.category] = (map[t.category] ?? 0) + _convertAmount(ref, t); + map[t.category] = (map[t.category] ?? 0) + exchange.convert(t.amount, t.currencyCode, target); } return map; }); final categoryIncomeProvider = Provider>((ref) { + final exchange = ref.watch(exchangeRateServiceProvider); + final index = ref.watch(activeAccountIndexProvider); + final accountsAsync = ref.watch(accountsProvider); + final globalCurrency = ref.watch(currencyProvider).code; + final accounts = accountsAsync.value ?? []; + final target = _resolveTargetCurrency(index, accounts, globalCurrency); final map = {}; for (final t in ref.watch(statsScopedTransactionsProvider)) { if (t.type != TransactionType.income) continue; - map[t.category] = (map[t.category] ?? 0) + _convertAmount(ref, t); + map[t.category] = (map[t.category] ?? 0) + exchange.convert(t.amount, t.currencyCode, target); } return map; }); @@ -151,7 +171,11 @@ final categoryIncomeProvider = Provider>((ref) { final monthlyBreakdownProvider = Provider>((ref) { final txs = ref.watch(accountFilteredTransactionsProvider); final exchange = ref.watch(exchangeRateServiceProvider); - final target = _statsTargetCurrency(ref); + final index = ref.watch(activeAccountIndexProvider); + final accountsAsync = ref.watch(accountsProvider); + final globalCurrency = ref.watch(currencyProvider).code; + final accounts = accountsAsync.value ?? []; + final target = _resolveTargetCurrency(index, accounts, globalCurrency); final now = DateTime.now(); final months = []; @@ -177,7 +201,11 @@ final monthlyBreakdownProvider = Provider>((ref) { final monthlyIncomeBreakdownProvider = Provider>((ref) { final txs = ref.watch(accountFilteredTransactionsProvider); final exchange = ref.watch(exchangeRateServiceProvider); - final target = _statsTargetCurrency(ref); + final index = ref.watch(activeAccountIndexProvider); + final accountsAsync = ref.watch(accountsProvider); + final globalCurrency = ref.watch(currencyProvider).code; + final accounts = accountsAsync.value ?? []; + final target = _resolveTargetCurrency(index, accounts, globalCurrency); final now = DateTime.now(); final months = []; diff --git a/lib/features/dashboard/widgets/account_editor_overlay/account_editor_overlay.dart b/lib/features/dashboard/widgets/account_editor_overlay/account_editor_overlay.dart index 9f9ad43..cec693a 100644 --- a/lib/features/dashboard/widgets/account_editor_overlay/account_editor_overlay.dart +++ b/lib/features/dashboard/widgets/account_editor_overlay/account_editor_overlay.dart @@ -63,9 +63,7 @@ class _AccountEditorOverlayState extends State { return; } - dash.setState(() { - dash.tempAccountName = _nameController.text; - }); + dash.tempAccountName = _nameController.text; dash.overlayEntry?.markNeedsBuild(); }); } @@ -411,9 +409,7 @@ class _AccountEditorOverlayState extends State { onTap: () { setState(() { _selectedCurrency = entry.$1; - dash.setState(() { - dash.tempAccountCurrency = entry.$1; - }); + dash.tempAccountCurrency = entry.$1; dash.overlayEntry?.markNeedsBuild(); _showCurrencyDropdown = false; }); diff --git a/lib/features/dashboard/widgets/account_editor_overlay/color_panel.dart b/lib/features/dashboard/widgets/account_editor_overlay/color_panel.dart index 55df861..9d208fd 100644 --- a/lib/features/dashboard/widgets/account_editor_overlay/color_panel.dart +++ b/lib/features/dashboard/widgets/account_editor_overlay/color_panel.dart @@ -57,16 +57,14 @@ class AccountColorPanel extends StatelessWidget { ); void onHSVChanged(HSVColor hsv) { + if (dashboardState.editingPrimary) { + dashboardState.tempPrimaryHSV = hsv; + dashboardState.tempPrimary = hsv.toColor(); + } else { + dashboardState.tempSecondaryHSV = hsv; + dashboardState.tempSecondary = hsv.toColor(); + } setPanelState(() {}); - dashboardState.setState(() { - if (dashboardState.editingPrimary) { - dashboardState.tempPrimaryHSV = hsv; - dashboardState.tempPrimary = hsv.toColor(); - } else { - dashboardState.tempSecondaryHSV = hsv; - dashboardState.tempSecondary = hsv.toColor(); - } - }); dashboardState.overlayEntry?.markNeedsBuild(); } @@ -106,18 +104,16 @@ class AccountColorPanel extends StatelessWidget { : dashboardState.tempPrimary, isDimmed: isSolid, onTap: () { - dashboardState.setState(() { - if (isSolid) - if (Theme.of(dashboardContext).brightness == - Brightness.dark) { - dashboardState.tempDarkGradientType = - CardColorService.defaultGradientDark; - } else { - dashboardState.tempLightGradientType = - CardColorService.defaultGradientLight; - } - dashboardState.editingPrimary = true; - }); + if (isSolid) + if (Theme.of(dashboardContext).brightness == + Brightness.dark) { + dashboardState.tempDarkGradientType = + GradientType.linear; + } else { + dashboardState.tempLightGradientType = + GradientType.linear; + } + dashboardState.editingPrimary = true; setPanelState(() {}); dashboardState.overlayEntry?.markNeedsBuild(); }, @@ -132,18 +128,16 @@ class AccountColorPanel extends StatelessWidget { color: dashboardState.tempSecondary, isDimmed: isSolid, onTap: () { - dashboardState.setState(() { - if (isSolid) - if (Theme.of(dashboardContext).brightness == - Brightness.dark) { - dashboardState.tempDarkGradientType = - CardColorService.defaultGradientDark; - } else { - dashboardState.tempLightGradientType = - CardColorService.defaultGradientLight; - } - dashboardState.editingPrimary = false; - }); + if (isSolid) + if (Theme.of(dashboardContext).brightness == + Brightness.dark) { + dashboardState.tempDarkGradientType = + GradientType.linear; + } else { + dashboardState.tempLightGradientType = + GradientType.linear; + } + dashboardState.editingPrimary = false; setPanelState(() {}); dashboardState.overlayEntry?.markNeedsBuild(); }, @@ -164,17 +158,15 @@ class AccountColorPanel extends StatelessWidget { onTap: isSolid ? null : () { - dashboardState.setState(() { - if (Theme.of(dashboardContext).brightness == - Brightness.dark) { - dashboardState.tempDarkGradientType = - GradientType.solid; - } else { - dashboardState.tempLightGradientType = - GradientType.solid; - } - dashboardState.editingPrimary = true; - }); + if (Theme.of(dashboardContext).brightness == + Brightness.dark) { + dashboardState.tempDarkGradientType = + GradientType.solid; + } else { + dashboardState.tempLightGradientType = + GradientType.solid; + } + dashboardState.editingPrimary = true; setPanelState(() {}); dashboardState.overlayEntry?.markNeedsBuild(); }, @@ -292,10 +284,7 @@ class AccountColorPanel extends StatelessWidget { children: [ GestureDetector( onTap: () { - dashboardState.setState( - () => dashboardState.editingPrimary = - true, - ); + dashboardState.editingPrimary = true; setPanelState(() {}); dashboardState.overlayEntry ?.markNeedsBuild(); @@ -350,11 +339,7 @@ class AccountColorPanel extends StatelessWidget { if (!isSolid) GestureDetector( onTap: () { - dashboardState.setState( - () => - dashboardState.editingPrimary = - false, - ); + dashboardState.editingPrimary = false; setPanelState(() {}); dashboardState.overlayEntry ?.markNeedsBuild(); @@ -420,13 +405,8 @@ class AccountColorPanel extends StatelessWidget { ), ), SizedBox(height: layout.controlSpacing), - IgnorePointer( - ignoring: isSolid, - child: AnimatedOpacity( - duration: const Duration(milliseconds: 200), - opacity: isSolid ? 0.3 : 1.0, - child: Row( - children: GradientType.values + Row( + children: GradientType.values .where((t) => t != GradientType.solid) .map((type) { final isSelected = activeGradientType == type; @@ -452,19 +432,15 @@ class AccountColorPanel extends StatelessWidget { padding: const EdgeInsets.only(right: 6), child: GestureDetector( onTap: () { - dashboardState.setState( - () { - if (Theme.of(dashboardContext) - .brightness == - Brightness.dark) { - dashboardState.tempDarkGradientType = - type; - } else { - dashboardState.tempLightGradientType = - type; - } - }, - ); + if (Theme.of(dashboardContext) + .brightness == + Brightness.dark) { + dashboardState.tempDarkGradientType = + type; + } else { + dashboardState.tempLightGradientType = + type; + } setPanelState(() {}); dashboardState.overlayEntry ?.markNeedsBuild(); @@ -526,10 +502,8 @@ class AccountColorPanel extends StatelessWidget { ), ), ); - }) - .toList(), - ), - ), + }) + .toList(), ), SizedBox(height: layout.controlSpacing), Row( @@ -546,19 +520,17 @@ class AccountColorPanel extends StatelessWidget { final defS = isDarkTheme ? CardColorService.defaultSecondary : CardColorService.defaultSecondaryLight; - dashboardState.setState(() { - dashboardState.tempPrimary = defP; - dashboardState.tempSecondary = defS; - dashboardState.tempPrimaryHSV = HSVColor.fromColor( - defP, - ); - dashboardState.tempSecondaryHSV = - HSVColor.fromColor(defS); + dashboardState.tempPrimary = defP; + dashboardState.tempSecondary = defS; + dashboardState.tempPrimaryHSV = HSVColor.fromColor( + defP, + ); + dashboardState.tempSecondaryHSV = + HSVColor.fromColor(defS); dashboardState.tempLightGradientType = CardColorService.defaultGradientLight; dashboardState.tempDarkGradientType = CardColorService.defaultGradientDark; - }); setPanelState(() {}); dashboardState.overlayEntry?.markNeedsBuild(); }, diff --git a/lib/features/dashboard/widgets/color_editor_overlay.dart b/lib/features/dashboard/widgets/color_editor_overlay.dart index 7e344ed..d09eb32 100644 --- a/lib/features/dashboard/widgets/color_editor_overlay.dart +++ b/lib/features/dashboard/widgets/color_editor_overlay.dart @@ -186,16 +186,14 @@ class _FullScreenBlurOverlayState extends State { ); void onHSVChanged(HSVColor hsv) { + if (dash.editingPrimary) { + dash.tempPrimaryHSV = hsv; + dash.tempPrimary = hsv.toColor(); + } else { + dash.tempSecondaryHSV = hsv; + dash.tempSecondary = hsv.toColor(); + } setPanelState(() {}); - dash.setState(() { - if (dash.editingPrimary) { - dash.tempPrimaryHSV = hsv; - dash.tempPrimary = hsv.toColor(); - } else { - dash.tempSecondaryHSV = hsv; - dash.tempSecondary = hsv.toColor(); - } - }); dash.overlayEntry?.markNeedsBuild(); } @@ -233,19 +231,17 @@ class _FullScreenBlurOverlayState extends State { : dash.tempPrimary, isDimmed: isSolid, onTap: () { - dash.setState(() { - if (isSolid) { - if (Theme.of(widget.context).brightness == - Brightness.dark) { - dash.tempDarkGradientType = - CardColorService.defaultGradientDark; - } else { - dash.tempLightGradientType = - CardColorService.defaultGradientLight; - } + if (isSolid) { + if (Theme.of(widget.context).brightness == + Brightness.dark) { + dash.tempDarkGradientType = + GradientType.linear; + } else { + dash.tempLightGradientType = + GradientType.linear; } - dash.editingPrimary = true; - }); + } + dash.editingPrimary = true; setPanelState(() {}); dash.overlayEntry?.markNeedsBuild(); }, @@ -259,19 +255,17 @@ class _FullScreenBlurOverlayState extends State { color: dash.tempSecondary, isDimmed: isSolid, onTap: () { - dash.setState(() { - if (isSolid) { - if (Theme.of(widget.context).brightness == - Brightness.dark) { - dash.tempDarkGradientType = - CardColorService.defaultGradientDark; - } else { - dash.tempLightGradientType = - CardColorService.defaultGradientLight; - } + if (isSolid) { + if (Theme.of(widget.context).brightness == + Brightness.dark) { + dash.tempDarkGradientType = + GradientType.linear; + } else { + dash.tempLightGradientType = + GradientType.linear; } - dash.editingPrimary = false; - }); + } + dash.editingPrimary = false; setPanelState(() {}); dash.overlayEntry?.markNeedsBuild(); }, @@ -291,17 +285,15 @@ class _FullScreenBlurOverlayState extends State { onTap: isSolid ? null : () { - dash.setState(() { - if (Theme.of(widget.context).brightness == - Brightness.dark) { - dash.tempDarkGradientType = - GradientType.solid; - } else { - dash.tempLightGradientType = - GradientType.solid; - } - dash.editingPrimary = true; - }); + if (Theme.of(widget.context).brightness == + Brightness.dark) { + dash.tempDarkGradientType = + GradientType.solid; + } else { + dash.tempLightGradientType = + GradientType.solid; + } + dash.editingPrimary = true; setPanelState(() {}); dash.overlayEntry?.markNeedsBuild(); }, @@ -418,9 +410,7 @@ class _FullScreenBlurOverlayState extends State { children: [ GestureDetector( onTap: () { - dash.setState( - () => dash.editingPrimary = true, - ); + dash.editingPrimary = true; setPanelState(() {}); dash.overlayEntry?.markNeedsBuild(); }, @@ -471,9 +461,7 @@ class _FullScreenBlurOverlayState extends State { if (!isSolid) GestureDetector( onTap: () { - dash.setState( - () => dash.editingPrimary = false, - ); + dash.editingPrimary = false; setPanelState(() {}); dash.overlayEntry?.markNeedsBuild(); }, @@ -532,13 +520,8 @@ class _FullScreenBlurOverlayState extends State { ), ), SizedBox(height: layout.controlSpacing), - IgnorePointer( - ignoring: isSolid, - child: AnimatedOpacity( - duration: const Duration(milliseconds: 200), - opacity: isSolid ? 0.3 : 1.0, - child: Row( - children: GradientType.values + Row( + children: GradientType.values .where((t) => t != GradientType.solid) .map((type) { final isSelected = activeGradientType == type; @@ -564,14 +547,12 @@ class _FullScreenBlurOverlayState extends State { padding: const EdgeInsets.only(right: 6), child: GestureDetector( onTap: () { - dash.setState(() { - if (Theme.of(widget.context).brightness == - Brightness.dark) { - dash.tempDarkGradientType = type; - } else { - dash.tempLightGradientType = type; - } - }); + if (Theme.of(widget.context).brightness == + Brightness.dark) { + dash.tempDarkGradientType = type; + } else { + dash.tempLightGradientType = type; + } setPanelState(() {}); dash.overlayEntry?.markNeedsBuild(); }, @@ -632,10 +613,8 @@ class _FullScreenBlurOverlayState extends State { ), ), ); - }) - .toList(), - ), - ), + }) + .toList(), ), SizedBox(height: layout.controlSpacing), Row( @@ -652,16 +631,14 @@ class _FullScreenBlurOverlayState extends State { final defS = isDarkTheme ? CardColorService.defaultSecondary : CardColorService.defaultSecondaryLight; - dash.setState(() { - dash.tempPrimary = defP; - dash.tempSecondary = defS; - dash.tempPrimaryHSV = HSVColor.fromColor(defP); - dash.tempSecondaryHSV = HSVColor.fromColor(defS); - dash.tempLightGradientType = - CardColorService.defaultGradientLight; - dash.tempDarkGradientType = - CardColorService.defaultGradientDark; - }); + dash.tempPrimary = defP; + dash.tempSecondary = defS; + dash.tempPrimaryHSV = HSVColor.fromColor(defP); + dash.tempSecondaryHSV = HSVColor.fromColor(defS); + dash.tempLightGradientType = + CardColorService.defaultGradientLight; + dash.tempDarkGradientType = + CardColorService.defaultGradientDark; setPanelState(() {}); dash.overlayEntry?.markNeedsBuild(); }, diff --git a/lib/features/settings/widgets/premium_section.dart b/lib/features/settings/widgets/premium_section.dart index 22612b3..eb4e6d3 100644 --- a/lib/features/settings/widgets/premium_section.dart +++ b/lib/features/settings/widgets/premium_section.dart @@ -2,9 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../core/constants.dart'; import '../../../core/l10n/locale_provider.dart'; -import '../../../core/services/haptic_service.dart'; import '../../../shared/providers/current_user_provider.dart'; -import '../../../shared/models/user_model.dart'; class PremiumSection extends ConsumerWidget { const PremiumSection({super.key}); @@ -66,16 +64,6 @@ class PremiumSection extends ConsumerWidget { ], ), ), - Switch( - value: user.isVip, - onChanged: (value) async { - HapticService.light(); - await ref.read(currentUserProvider.notifier).setPlan( - value ? UserPlan.vip : UserPlan.free, - ); - }, - activeThumbColor: const Color(0xFF7C6DED), - ), ], ), ], diff --git a/lib/shared/services/billing_service.dart b/lib/shared/services/billing_service.dart index 7c2f11b..39efe67 100644 --- a/lib/shared/services/billing_service.dart +++ b/lib/shared/services/billing_service.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'package:flutter/foundation.dart'; import 'package:in_app_purchase/in_app_purchase.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -15,6 +14,9 @@ class PurchaseResult { factory PurchaseResult.failed([String? error]) => PurchaseResult(success: false, error: error); + + factory PurchaseResult.cancelled() => + const PurchaseResult(success: false); } abstract class BillingService { @@ -23,24 +25,11 @@ abstract class BillingService { Future purchasePro(); Future restorePurchases(); Future queryPastPurchase(); - Future completePurchase(String purchaseToken); - Stream> get purchaseStream; Future dispose(); } class PlayBillingService implements BillingService { final InAppPurchase _inAppPurchase = InAppPurchase.instance; - late final StreamSubscription> _sub; - final _controller = StreamController>.broadcast(); - - PlayBillingService() { - _sub = _inAppPurchase.purchaseStream.listen((purchases) { - _controller.add(purchases); - }); - } - - @override - Stream> get purchaseStream => _controller.stream; @override Future purchasePro() async { @@ -52,51 +41,26 @@ class PlayBillingService implements BillingService { final response = await _inAppPurchase.queryProductDetails( {BillingService.proProductId}, ); + if (response.error != null) { + return PurchaseResult.failed(response.error!.message); + } if (response.productDetails.isEmpty) { return PurchaseResult.failed('Product not found'); } final product = response.productDetails.first; - final purchaseParam = PurchaseParam(productDetails: product); - - final started = await _inAppPurchase.buyNonConsumable( - purchaseParam: purchaseParam, - ); - if (!started) { - return PurchaseResult.failed('Could not start purchase'); - } - - final completer = Completer(); - late StreamSubscription sub; - sub = purchaseStream.timeout( - const Duration(seconds: 60), - onTimeout: (sink) { - if (!completer.isCompleted) { - completer.complete(PurchaseResult.failed('Purchase timed out')); + return _waitForPurchase( + timeout: const Duration(minutes: 2), + timeoutError: 'Purchase timed out', + start: () async { + final started = await _inAppPurchase.buyNonConsumable( + purchaseParam: PurchaseParam(productDetails: product), + ); + if (!started) { + throw StateError('Could not start purchase'); } - sub.cancel(); }, - ).listen((purchases) { - for (final p in purchases) { - if (p.productID == BillingService.proProductId && - p.status == PurchaseStatus.purchased) { - if (!completer.isCompleted) { - completer.complete(PurchaseResult.ok(p.verificationData.serverVerificationData)); - } - sub.cancel(); - return; - } - if (p.status == PurchaseStatus.error) { - if (!completer.isCompleted) { - completer.complete(PurchaseResult.failed(p.error?.message)); - } - sub.cancel(); - return; - } - } - }); - - return completer.future; + ); } @override @@ -106,102 +70,101 @@ class PlayBillingService implements BillingService { return PurchaseResult.failed('Billing not available'); } - await _inAppPurchase.restorePurchases(); - - final completer = Completer(); - late StreamSubscription sub; - sub = purchaseStream.timeout( - const Duration(seconds: 15), - onTimeout: (sink) { - if (!completer.isCompleted) { - completer.complete(PurchaseResult.failed('Restore timed out')); - } - sub.cancel(); - }, - ).listen((purchases) { - for (final p in purchases) { - if (p.productID == BillingService.proProductId && - (p.status == PurchaseStatus.restored || - p.status == PurchaseStatus.purchased)) { - if (!completer.isCompleted) { - completer.complete(PurchaseResult.ok(p.verificationData.serverVerificationData)); - } - sub.cancel(); - return; - } - } - }); - - return completer.future; - } - - @override - Future queryPastPurchase() async { - final available = await _inAppPurchase.isAvailable(); - if (!available) { - return const PurchaseResult(); - } - - final response = await _inAppPurchase.queryProductDetails( - {BillingService.proProductId}, + return _waitForPurchase( + timeout: const Duration(seconds: 30), + timeoutError: 'No purchases found', + start: _inAppPurchase.restorePurchases, ); - if (response.productDetails.isEmpty) { - return const PurchaseResult(); - } + } + @override + Future queryPastPurchase() => restorePurchases(); + + Future _waitForPurchase({ + required Future Function() start, + required Duration timeout, + required String timeoutError, + }) async { final completer = Completer(); - late StreamSubscription sub; - sub = purchaseStream.timeout( - const Duration(seconds: 10), - onTimeout: (sink) { - if (!completer.isCompleted) { - completer.complete(const PurchaseResult()); - } - sub.cancel(); - }, - ).listen((purchases) { - for (final p in purchases) { - if (p.productID == BillingService.proProductId && - (p.status == PurchaseStatus.restored || - p.status == PurchaseStatus.purchased)) { - if (!completer.isCompleted) { - completer.complete(PurchaseResult.ok(p.verificationData.serverVerificationData)); - } - sub.cancel(); - return; - } + var handlingPurchase = false; + late final StreamSubscription> subscription; + late final Timer timeoutTimer; + + void finish(PurchaseResult result) { + if (!completer.isCompleted) { + completer.complete(result); } - }); - - await _inAppPurchase.restorePurchases(); - - return completer.future; - } - - @override - Future completePurchase(String purchaseToken) async { - if (kDebugMode) { - print('completePurchase: $purchaseToken'); } + + Future handlePurchase(PurchaseDetails purchase) async { + if (purchase.productID != BillingService.proProductId) return; + + switch (purchase.status) { + case PurchaseStatus.pending: + return; + case PurchaseStatus.purchased: + case PurchaseStatus.restored: + if (handlingPurchase) return; + handlingPurchase = true; + try { + final token = purchase.verificationData.serverVerificationData; + if (token.isEmpty) { + finish(PurchaseResult.failed('Purchase verification data is empty')); + return; + } + if (purchase.pendingCompletePurchase) { + await _inAppPurchase.completePurchase(purchase); + } + finish(PurchaseResult.ok(token)); + } catch (error) { + finish(PurchaseResult.failed(error.toString())); + } finally { + handlingPurchase = false; + } + case PurchaseStatus.error: + finish(PurchaseResult.failed(purchase.error?.message)); + case PurchaseStatus.canceled: + finish(PurchaseResult.cancelled()); + } + } + + subscription = _inAppPurchase.purchaseStream.listen( + (purchases) { + for (final purchase in purchases) { + unawaited(handlePurchase(purchase)); + } + }, + onError: (Object error) { + finish(PurchaseResult.failed(error.toString())); + }, + ); + timeoutTimer = Timer( + timeout, + () => finish(PurchaseResult.failed(timeoutError)), + ); + + try { + await start(); + } catch (error) { + finish(PurchaseResult.failed(error.toString())); + } + + final result = await completer.future; + timeoutTimer.cancel(); + await subscription.cancel(); + return result; } @override - Future dispose() async { - await _sub.cancel(); - await _controller.close(); - } + Future dispose() async {} } class DebugBillingService implements BillingService { static const _key = 'debug_purchase_token'; final SharedPreferences _prefs; - final _controller = StreamController>.broadcast(); DebugBillingService(this._prefs); - @override - Stream> get purchaseStream => _controller.stream; - @override Future purchasePro() async { await Future.delayed(const Duration(seconds: 1)); @@ -230,10 +193,5 @@ class DebugBillingService implements BillingService { } @override - Future completePurchase(String purchaseToken) async {} - - @override - Future dispose() async { - await _controller.close(); - } + Future dispose() async {} } diff --git a/lib/shared/services/premium_manager.dart b/lib/shared/services/premium_manager.dart index 4f6851d..55af5b5 100644 --- a/lib/shared/services/premium_manager.dart +++ b/lib/shared/services/premium_manager.dart @@ -30,7 +30,6 @@ class PremiumManager { final result = await _billing.purchasePro(); if (result.success && result.purchaseToken != null) { await _setPremium(true, result.purchaseToken); - await _billing.completePurchase(result.purchaseToken!); } return result; } @@ -39,7 +38,6 @@ class PremiumManager { final result = await _billing.restorePurchases(); if (result.success && result.purchaseToken != null) { await _setPremium(true, result.purchaseToken); - await _billing.completePurchase(result.purchaseToken!); } return result; } @@ -49,7 +47,6 @@ class PremiumManager { final result = await _billing.queryPastPurchase(); if (result.success && result.purchaseToken != null) { await _setPremium(true, result.purchaseToken); - await _billing.completePurchase(result.purchaseToken!); } } diff --git a/lib/shared/widgets/pro_screen.dart b/lib/shared/widgets/pro_screen.dart index bf7966b..54680b3 100644 --- a/lib/shared/widgets/pro_screen.dart +++ b/lib/shared/widgets/pro_screen.dart @@ -5,7 +5,6 @@ import '../../core/l10n/app_strings.dart'; import '../../core/l10n/locale_provider.dart'; import '../../core/services/haptic_service.dart'; import '../providers/current_user_provider.dart'; -import '../providers/google_drive_provider.dart'; import '../providers/premium_provider.dart'; import 'error_snackbar.dart'; @@ -20,14 +19,16 @@ class _ProScreenState extends ConsumerState with SingleTickerProviderStateMixin { bool _purchasing = false; bool _restoring = false; + bool _resetting = false; bool _showSuccess = false; + String _successTitle = ''; late final AnimationController _successController; late final Animation _successScale; static const _gradientColors = [ - Color(0xFF1B5E20), - Color(0xFF2E7D32), - Color(0xFF4CAF8C), + Color(0xFF5B4DCC), + Color(0xFF7C6DED), + Color(0xFF9D8FF5), ]; @override @@ -51,8 +52,11 @@ class _ProScreenState extends ConsumerState super.dispose(); } - void _showSuccessOverlay() { - setState(() => _showSuccess = true); + void _showSuccessOverlay(String title) { + setState(() { + _successTitle = title; + _showSuccess = true; + }); _successController.forward(from: 0.0); } @@ -67,8 +71,6 @@ class _ProScreenState extends ConsumerState Widget build(BuildContext context) { final s = ref.watch(stringsProvider); final isPremium = ref.watch(isPremiumProvider); - final driveUserAsync = ref.watch(googleDriveUserProvider); - final driveUser = driveUserAsync.value; final colorScheme = Theme.of(context).colorScheme; return Stack( @@ -77,7 +79,7 @@ class _ProScreenState extends ConsumerState body: SafeArea( child: Column( children: [ - _buildHeader(context, s, colorScheme), + _buildHeader(context), Expanded( child: SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 20), @@ -85,13 +87,10 @@ class _ProScreenState extends ConsumerState crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 4), - _buildHeroBanner(context, s, colorScheme, isPremium), + _buildHeroBanner(context, s, isPremium), const SizedBox(height: 20), _buildFeatureList(context, s, colorScheme), const SizedBox(height: 16), - if (isPremium) ...[ - _buildProActiveSection(context, s, colorScheme, driveUser), - ], ], ), ), @@ -106,11 +105,7 @@ class _ProScreenState extends ConsumerState ); } - Widget _buildHeader( - BuildContext context, - AppStrings s, - ColorScheme colorScheme, - ) { + Widget _buildHeader(BuildContext context) { return Padding( padding: const EdgeInsets.fromLTRB(8, 4, 16, 0), child: Row( @@ -127,7 +122,6 @@ class _ProScreenState extends ConsumerState Widget _buildHeroBanner( BuildContext context, AppStrings s, - ColorScheme colorScheme, bool isVip, ) { return Container( @@ -209,7 +203,7 @@ class _ProScreenState extends ConsumerState ), const SizedBox(height: 20), Text( - s.proPurchaseSuccess, + _successTitle, style: Theme.of(context).textTheme.titleLarge?.copyWith( fontWeight: FontWeight.w800, color: colorScheme.onSurface, @@ -240,7 +234,6 @@ class _ProScreenState extends ConsumerState ColorScheme colorScheme, ) { final features = [ - (Icons.cloud_sync_rounded, s.proFeatureCloudSync, s.proFeatureCloudSyncDesc), (Icons.analytics_rounded, s.proFeatureAnalytics, s.proFeatureAnalyticsDesc), (Icons.palette_rounded, s.proFeatureCustomization, s.proFeatureCustomizationDesc), (Icons.fingerprint_rounded, s.proFeatureBiometric, s.proFeatureBiometricDesc), @@ -295,116 +288,12 @@ class _ProScreenState extends ConsumerState ); } - Widget _buildProActiveSection( - BuildContext context, - AppStrings s, - ColorScheme colorScheme, - dynamic googleUser, - ) { - return Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: colorScheme.surface, - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: colorScheme.primary.withOpacity(0.2), - width: 1, - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (googleUser != null) ...[ - Row( - children: [ - Icon(Icons.email_outlined, size: 18, color: colorScheme.onSurface.withOpacity(0.5)), - const SizedBox(width: 8), - Expanded( - child: Text( - googleUser.email, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurface, - ), - ), - ), - ], - ), - const SizedBox(height: 12), - Row( - children: [ - Icon(Icons.sync_rounded, size: 18, color: colorScheme.primary), - const SizedBox(width: 8), - Expanded( - child: Text( - s.proSyncEnabled, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurface, - ), - ), - ), - Icon(Icons.check_circle_rounded, color: colorScheme.primary, size: 20), - ], - ), - const SizedBox(height: 8), - Row( - children: [ - Icon(Icons.backup_outlined, size: 18, color: colorScheme.onSurface.withOpacity(0.5)), - const SizedBox(width: 8), - Text( - '${s.proLastBackup}: —', - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: colorScheme.onSurface.withOpacity(0.5), - ), - ), - ], - ), - const SizedBox(height: 16), - Row( - children: [ - Expanded( - child: OutlinedButton.icon( - onPressed: _handleGoogleSignOut, - icon: Icon(Icons.logout_rounded, color: colorScheme.error), - label: Text(s.proSignOut), - style: OutlinedButton.styleFrom( - foregroundColor: colorScheme.error, - ), - ), - ), - ], - ), - ] else ...[ - Text( - s.proSignInForSync, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurface.withOpacity(0.7), - ), - ), - const SizedBox(height: 16), - SizedBox( - width: double.infinity, - child: OutlinedButton.icon( - onPressed: _handleGoogleSignIn, - icon: Icon(Icons.login_rounded, color: colorScheme.primary), - label: Text(s.proSignInGoogle), - ), - ), - ], - ], - ), - ); - } - Widget _buildBottomBar( BuildContext context, AppStrings s, ColorScheme colorScheme, bool isVip, ) { - if (isVip) { - return const SizedBox.shrink(); - } - return Container( padding: const EdgeInsets.fromLTRB(20, 12, 20, 24), decoration: BoxDecoration( @@ -419,51 +308,124 @@ class _ProScreenState extends ConsumerState child: Column( mainAxisSize: MainAxisSize.min, children: [ - SizedBox( - width: double.infinity, - child: FilledButton( - onPressed: _purchasing ? null : _handlePurchase, - style: FilledButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), + if (!isVip) ...[ + SizedBox( + width: double.infinity, + child: FilledButton( + onPressed: _purchasing ? null : _handlePurchase, + style: FilledButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: _purchasing + ? SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + color: colorScheme.onPrimary, + ), + ) + : Text( + s.proBuy, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w700, + ), + ), + ), + ), + const SizedBox(height: 8), + SizedBox( + width: double.infinity, + child: TextButton( + onPressed: _restoring ? null : _handleRestore, + child: _restoring + ? SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + color: colorScheme.primary, + ), + ) + : Text(s.proRestorePurchases), + ), + ), + ] else ...[ + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), + decoration: BoxDecoration( + color: colorScheme.primary.withOpacity(0.08), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: colorScheme.primary.withOpacity(0.2), + width: 1, ), ), - child: _purchasing - ? SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - color: colorScheme.onPrimary, - ), - ) - : Text( - s.proBuy, - style: const TextStyle( - fontSize: 16, + child: Row( + children: [ + Icon( + Icons.verified_rounded, + color: colorScheme.primary, + size: 20, + ), + const SizedBox(width: 10), + Expanded( + child: Text( + s.proActive, + style: TextStyle( + color: colorScheme.primary, fontWeight: FontWeight.w700, + fontSize: 14, ), ), + ), + ], + ), ), - ), - const SizedBox(height: 8), - SizedBox( - width: double.infinity, - child: TextButton( - onPressed: _restoring ? null : _handleRestore, - child: _restoring - ? SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator( - strokeWidth: 2, - color: colorScheme.primary, + const SizedBox(height: 10), + SizedBox( + width: double.infinity, + child: OutlinedButton.icon( + onPressed: _resetting ? null : _handleResetData, + icon: _resetting + ? SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + color: colorScheme.onSurface.withOpacity(0.5), + ), + ) + : Icon( + Icons.restart_alt_rounded, + size: 18, + color: colorScheme.onSurface.withOpacity(0.6), ), - ) - : Text(s.proRestorePurchases), + label: Text( + s.proResetData, + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: colorScheme.onSurface.withOpacity(0.6), + ), + ), + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 12), + side: BorderSide( + color: colorScheme.onSurface.withOpacity(0.15), + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + ), ), - ), + ], ], ), ); @@ -478,7 +440,7 @@ class _ProScreenState extends ConsumerState if (result.success) { await ref.read(currentUserProvider.notifier).refreshFromPremium(); if (mounted) { - _showSuccessOverlay(); + _showSuccessOverlay(ref.read(stringsProvider).proPurchaseSuccess); HapticService.medium(); } } else { @@ -504,7 +466,7 @@ class _ProScreenState extends ConsumerState if (result.success) { await ref.read(currentUserProvider.notifier).refreshFromPremium(); if (mounted) { - _showSuccessOverlay(); + _showSuccessOverlay(ref.read(stringsProvider).proRestoreSuccessTitle); HapticService.medium(); } } else { @@ -521,27 +483,46 @@ class _ProScreenState extends ConsumerState } } - Future _handleGoogleSignIn() async { - HapticService.light(); - try { - final service = ref.read(googleDriveServiceProvider); - await service.signIn(); - } catch (e) { - if (mounted) { - showErrorSnackbar(context, e.toString()); - } - } - } - - Future _handleGoogleSignOut() async { - HapticService.light(); - try { - final service = ref.read(googleDriveServiceProvider); - await service.signOut(); - } catch (e) { - if (mounted) { - showErrorSnackbar(context, e.toString()); - } - } + Future _handleResetData() async { + final s = ref.read(stringsProvider); + showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: Text(s.proResetData), + content: Text(s.proResetDataConfirm), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: Text(s.cancel), + ), + TextButton( + onPressed: () async { + Navigator.pop(ctx); + HapticService.light(); + setState(() => _resetting = true); + try { + final manager = ref.read(premiumManagerProvider); + await manager.clear(); + await ref.read(currentUserProvider.notifier).refreshFromPremium(); + if (mounted) { + showSuccessSnackbar(context, s.proResetDataSuccess); + HapticService.medium(); + } + } catch (e) { + if (mounted) { + showErrorSnackbar(context, e.toString()); + } + } finally { + if (mounted) setState(() => _resetting = false); + } + }, + style: TextButton.styleFrom( + foregroundColor: const Color(0xFFE05C6B), + ), + child: Text(s.proResetData), + ), + ], + ), + ); } } diff --git a/lib/shared/widgets/pro_subscription_card.dart b/lib/shared/widgets/pro_subscription_card.dart index f831417..773c91b 100644 --- a/lib/shared/widgets/pro_subscription_card.dart +++ b/lib/shared/widgets/pro_subscription_card.dart @@ -3,169 +3,258 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../core/l10n/locale_provider.dart'; import '../../core/services/haptic_service.dart'; -import '../providers/google_drive_provider.dart'; import '../providers/premium_provider.dart'; -class ProSubscriptionCard extends ConsumerWidget { +class ProSubscriptionCard extends ConsumerStatefulWidget { const ProSubscriptionCard({super.key}); + @override + ConsumerState createState() => + _ProSubscriptionCardState(); +} + +class _ProSubscriptionCardState extends ConsumerState + with SingleTickerProviderStateMixin { + late final AnimationController _shimmerController; + static const _gradientColors = [ - Color(0xFF1B5E20), - Color(0xFF2E7D32), - Color(0xFF4CAF8C), + Color(0xFF5B4DCC), + Color(0xFF7C6DED), + Color(0xFF9D8FF5), ]; @override - Widget build(BuildContext context, WidgetRef ref) { + void initState() { + super.initState(); + _shimmerController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 2800), + )..repeat(); + } + + @override + void dispose() { + _shimmerController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { final s = ref.watch(stringsProvider); final isPremium = ref.watch(isPremiumProvider); - final driveUserAsync = ref.watch(googleDriveUserProvider); - final driveUser = driveUserAsync.value; - return Container( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24), - decoration: BoxDecoration( - gradient: const LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: _gradientColors, - ), - borderRadius: BorderRadius.circular(16), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + return ClipRRect( + borderRadius: BorderRadius.circular(20), + child: Stack( children: [ - Row( - children: [ - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.15), - borderRadius: BorderRadius.circular(12), - ), - child: Icon( - isPremium - ? Icons.verified_rounded - : Icons.workspace_premium_rounded, - color: Colors.white, - size: 28, - ), + Container( + padding: const EdgeInsets.fromLTRB(20, 22, 20, 18), + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: _gradientColors, ), - const SizedBox(width: 14), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( children: [ - Text( - s.proTitle, - style: - Theme.of(context).textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.w900, - color: Colors.white, - ), + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.18), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: Colors.white.withOpacity(0.25), + width: 1, + ), + ), + child: Icon( + isPremium + ? Icons.verified_rounded + : Icons.workspace_premium_rounded, + color: Colors.white, + size: 26, + ), ), - const SizedBox(height: 4), - Text( - isPremium ? s.proActive : s.proSubtitle, - style: isPremium - ? Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Colors.white.withOpacity(0.9), - fontWeight: FontWeight.w600, - ) - : Theme.of(context).textTheme.bodySmall?.copyWith( - color: Colors.white.withOpacity(0.7), - ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + s.proTitle, + style: Theme.of(context) + .textTheme + .titleLarge + ?.copyWith( + fontWeight: FontWeight.w800, + color: Colors.white, + letterSpacing: -0.3, + ), + ), + const SizedBox(height: 3), + Text( + isPremium ? s.proActive : s.proSubtitle, + style: isPremium + ? Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith( + color: Colors.white.withOpacity(0.95), + fontWeight: FontWeight.w600, + ) + : Theme.of(context) + .textTheme + .bodySmall + ?.copyWith( + color: Colors.white.withOpacity(0.75), + ), + ), + ], + ), ), + if (isPremium) + Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 5, + ), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: Colors.white.withOpacity(0.3), + width: 1, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.check_circle_rounded, + color: Colors.white.withOpacity(0.95), + size: 14, + ), + const SizedBox(width: 4), + Text( + 'Pro', + style: TextStyle( + color: Colors.white.withOpacity(0.95), + fontSize: 11, + fontWeight: FontWeight.w700, + ), + ), + ], + ), + ), ], ), - ), - ], - ), - const SizedBox(height: 16), - if (!isPremium) ...[ - Row( - children: [ - Expanded( - child: OutlinedButton( - onPressed: () { + const SizedBox(height: 18), + SizedBox( + width: double.infinity, + child: _GlassButton( + label: s.proAboutPro, + icon: Icons.arrow_forward_rounded, + onTap: () { HapticService.light(); context.push('/pro'); }, - style: OutlinedButton.styleFrom( - foregroundColor: Colors.white, - side: BorderSide( - color: Colors.white.withOpacity(0.4), width: 1.5), - padding: const EdgeInsets.symmetric(vertical: 12), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - child: Text( - s.proAboutPro, - style: const TextStyle( - fontWeight: FontWeight.w700, fontSize: 15), - ), ), ), ], ), - ] else ...[ - Row( - children: [ - Expanded( - child: OutlinedButton.icon( - onPressed: () { - HapticService.light(); - context.push('/backup'); + ), + Positioned.fill( + child: IgnorePointer( + child: AnimatedBuilder( + animation: _shimmerController, + builder: (context, _) { + final t = _shimmerController.value; + final startX = -1.5; + final endX = 2.5; + final x = startX + (endX - startX) * t; + return ShaderMask( + blendMode: BlendMode.srcOver, + shaderCallback: (Rect bounds) { + return LinearGradient( + begin: Alignment(x, 0), + end: Alignment(x + 0.5, 0), + colors: [ + Colors.white.withOpacity(0), + Colors.white.withOpacity(0.12), + Colors.white.withOpacity(0), + ], + stops: const [0.0, 0.5, 1.0], + ).createShader(bounds); }, - icon: Icon( - driveUser != null - ? Icons.cloud_done_rounded - : Icons.cloud_sync_rounded, - color: Colors.white, - size: 20, - ), - label: Text(s.backupSyncWithDrive), - style: OutlinedButton.styleFrom( - foregroundColor: Colors.white, - side: BorderSide( - color: Colors.white.withOpacity(0.4), width: 1.5), - padding: const EdgeInsets.symmetric(vertical: 12), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: Colors.white.withOpacity(0.15), + width: 1, + ), ), ), - ), - ), - ], - ), - if (driveUser == null) ...[ - const SizedBox(height: 10), - SizedBox( - width: double.infinity, - child: OutlinedButton.icon( - onPressed: () { - HapticService.light(); - ref.read(googleDriveServiceProvider).signIn(); - }, - icon: const Icon(Icons.login_rounded, - color: Colors.white, size: 18), - label: Text(s.proSignInGoogle), - style: OutlinedButton.styleFrom( - foregroundColor: Colors.white, - side: BorderSide( - color: Colors.white.withOpacity(0.3), width: 1), - padding: const EdgeInsets.symmetric(vertical: 10), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - ), + ); + }, ), - ], - ], + ), + ), ], ), ); } } + +class _GlassButton extends StatelessWidget { + final String label; + final IconData icon; + final VoidCallback onTap; + + const _GlassButton({ + required this.label, + required this.icon, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.white.withOpacity(0.15), + borderRadius: BorderRadius.circular(12), + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(12), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: Colors.white.withOpacity(0.25), + width: 1, + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + label, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.w700, + fontSize: 14, + ), + ), + const SizedBox(width: 6), + Icon(icon, color: Colors.white, size: 18), + ], + ), + ), + ), + ); + } +}