From 4f56e4983c52de4ef5cdb746dc3eb62df5a194c4 Mon Sep 17 00:00:00 2001 From: kolo Date: Mon, 29 Jun 2026 16:04:15 +0300 Subject: [PATCH] step --- lib/app/router.dart | 5 + lib/app/theme.dart | 10 +- lib/core/l10n/app_strings.dart | 54 ++ lib/features/settings/screen.dart | 5 +- lib/shared/providers/billing_provider.dart | 8 + .../providers/google_auth_provider.dart | 12 + lib/shared/services/billing_service.dart | 34 ++ lib/shared/services/google_auth_service.dart | 21 + lib/shared/widgets/pro_screen.dart | 550 ++++++++++++++++++ lib/shared/widgets/pro_subscription_card.dart | 126 ++++ macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 48 ++ pubspec.yaml | 1 + 13 files changed, 869 insertions(+), 7 deletions(-) create mode 100644 lib/shared/providers/billing_provider.dart create mode 100644 lib/shared/providers/google_auth_provider.dart create mode 100644 lib/shared/services/billing_service.dart create mode 100644 lib/shared/services/google_auth_service.dart create mode 100644 lib/shared/widgets/pro_screen.dart create mode 100644 lib/shared/widgets/pro_subscription_card.dart diff --git a/lib/app/router.dart b/lib/app/router.dart index de175f8..a06e5b0 100644 --- a/lib/app/router.dart +++ b/lib/app/router.dart @@ -11,6 +11,7 @@ import '../features/onboarding/screen.dart'; import '../shared/models/transaction.dart'; import '../shared/paywall/paywall_screen.dart'; import '../shared/services/onboarding_service.dart'; +import '../shared/widgets/pro_screen.dart'; final _shellKey = GlobalKey(); @@ -67,6 +68,10 @@ final appRouter = GoRouter( path: '/paywall', builder: (context, state) => const PaywallScreen(), ), + GoRoute( + path: '/pro', + builder: (context, state) => const ProScreen(), + ), ], ); diff --git a/lib/app/theme.dart b/lib/app/theme.dart index d8ecb58..9c3a6f4 100644 --- a/lib/app/theme.dart +++ b/lib/app/theme.dart @@ -132,10 +132,12 @@ class AppTheme { static ThemeData get lightTheme { final base = ThemeData.light(useMaterial3: true); - final textTheme = GoogleFonts.poppinsTextTheme(base.textTheme).apply( - bodyColor: const Color(0xFF1A1A2E), - displayColor: const Color(0xFF1A1A2E), - fontFamilyFallback: ['Roboto'], + final textTheme = _withCyrillicFallback( + base.textTheme.apply( + fontFamily: 'Poppins', + bodyColor: const Color(0xFF1A1A2E), + displayColor: const Color(0xFF1A1A2E), + ), ); return base.copyWith( diff --git a/lib/core/l10n/app_strings.dart b/lib/core/l10n/app_strings.dart index b4e5d8c..309cb37 100644 --- a/lib/core/l10n/app_strings.dart +++ b/lib/core/l10n/app_strings.dart @@ -278,4 +278,58 @@ class AppStrings { ? 'Если готовы — свайпните вправо, чтобы открыть приложение' : "If you're ready — swipe right to open the app"; String get onboardingSwipeRight => _ru ? 'Свайп вправо' : 'Swipe Right'; + + String get proTitle => _ru ? 'Casha Pro' : 'Casha Pro'; + String get proAboutPro => _ru ? 'Подробнее' : 'About Pro'; + String get proSubtitle => _ru + ? 'Раскройте весь потенциал Casha' + : 'Unlock the full power of Casha'; + String get proBuy => _ru ? 'Купить Pro' : 'Buy Pro'; + String get proFeatureCloudSync => + _ru ? 'Облачная синхронизация' : 'Cloud Sync'; + String get proFeatureCloudSyncDesc => _ru + ? 'Синхронизация между всеми вашими устройствами' + : 'Sync across all your devices'; + String get proFeatureBiometric => + _ru ? 'Биометрическая защита' : 'Biometric Protection'; + String get proFeatureBiometricDesc => _ru + ? 'Защитите свои данные отпечатком пальца или Face ID' + : 'Secure your data with fingerprint or Face ID'; + String get proFeatureAnalytics => + _ru ? 'Детальная аналитика' : 'Detailed Analytics'; + String get proFeatureAnalyticsDesc => _ru + ? 'Графики и статистика по расходам и доходам' + : 'Charts and stats for income and spending'; + String get proFeatureCustomization => + _ru ? 'Кастомизация карточек' : 'Card Customization'; + String get proFeatureCustomizationDesc => _ru + ? 'Цвета, градиенты, высота — всё под вашим контролем' + : 'Colors, gradients, height — all under your control'; + String get proFeatureAccounts => + _ru ? 'До 8 счетов' : 'Up to 8 Accounts'; + String get proFeatureAccountsDesc => _ru + ? 'Создавайте больше счетов для разных целей' + : 'Create more accounts for different goals'; + String get proTryPro => _ru ? 'Попробовать Pro' : 'Try Pro'; + String get proRestorePurchases => + _ru ? 'Восстановить покупки' : 'Restore Purchases'; + String get proActive => _ru ? 'Pro активна' : 'Pro Active'; + String get proSignInForSync => + _ru ? 'Войдите, чтобы включить синхронизацию' : 'Sign in to enable sync'; + String get proSignInGoogle => + _ru ? 'Войти через Google' : 'Sign in with Google'; + String get proSyncEnabled => + _ru ? 'Синхронизация включена' : 'Sync enabled'; + String get proLastBackup => _ru ? 'Последняя резервная копия' : 'Last backup'; + String get proSignOut => _ru ? 'Выйти' : 'Sign Out'; + String get proPurchaseSuccess => + _ru ? 'Pro успешно активирована!' : 'Pro successfully activated!'; + String get proPurchaseFailed => + _ru ? 'Не удалось оформить подписку' : 'Purchase failed'; + String get proRestoreSuccess => + _ru ? 'Покупки восстановлены!' : 'Purchases restored!'; + String get proRestoreNotFound => + _ru ? 'Предыдущие покупки не найдены' : 'No previous purchases found'; + String get proTapToClose => + _ru ? 'Нажмите чтобы закрыть' : 'Tap anywhere to close'; } diff --git a/lib/features/settings/screen.dart b/lib/features/settings/screen.dart index 08b306f..80401a5 100644 --- a/lib/features/settings/screen.dart +++ b/lib/features/settings/screen.dart @@ -5,7 +5,6 @@ import '../../core/l10n/locale_provider.dart'; import '../../core/services/biometric_service.dart'; import '../../core/services/haptic_service.dart'; import '../dashboard/provider.dart'; -import 'provider.dart'; import 'widgets/theme_section.dart'; import 'widgets/card_text_color_section.dart'; import 'widgets/haptic_section.dart'; @@ -14,7 +13,7 @@ import 'widgets/language_section.dart'; import 'widgets/currency_section.dart'; import 'widgets/amount_format_section.dart'; import 'widgets/categories_section.dart'; -import 'widgets/premium_section.dart'; +import '../../shared/widgets/pro_subscription_card.dart'; class SettingsScreen extends ConsumerWidget { const SettingsScreen({super.key}); @@ -117,7 +116,7 @@ class SettingsScreen extends ConsumerWidget { physics: const ClampingScrollPhysics(), padding: const EdgeInsets.fromLTRB(20, 16, 20, 40), children: [ - const PremiumSection(), + const ProSubscriptionCard(), const SizedBox(height: 16), const ThemeSection(), const SizedBox(height: 16), diff --git a/lib/shared/providers/billing_provider.dart b/lib/shared/providers/billing_provider.dart new file mode 100644 index 0000000..4361573 --- /dev/null +++ b/lib/shared/providers/billing_provider.dart @@ -0,0 +1,8 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../features/dashboard/provider.dart'; +import '../services/billing_service.dart'; + +final billingServiceProvider = Provider((ref) { + final prefs = ref.watch(sharedPreferencesProvider); + return MockBillingService(prefs); +}); diff --git a/lib/shared/providers/google_auth_provider.dart b/lib/shared/providers/google_auth_provider.dart new file mode 100644 index 0000000..6a51679 --- /dev/null +++ b/lib/shared/providers/google_auth_provider.dart @@ -0,0 +1,12 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:google_sign_in/google_sign_in.dart'; +import '../services/google_auth_service.dart'; + +final googleAuthProvider = Provider((ref) { + return GoogleAuthService(); +}); + +final googleCurrentUserProvider = StreamProvider((ref) { + final service = ref.watch(googleAuthProvider); + return service.onCurrentUserChanged; +}); diff --git a/lib/shared/services/billing_service.dart b/lib/shared/services/billing_service.dart new file mode 100644 index 0000000..0b982a7 --- /dev/null +++ b/lib/shared/services/billing_service.dart @@ -0,0 +1,34 @@ +import 'package:shared_preferences/shared_preferences.dart'; +import '../models/user_model.dart'; + +abstract class BillingService { + Future purchasePro(); + Future restorePurchases(); + Future getCurrentPlan(); +} + +class MockBillingService implements BillingService { + static const _key = 'user_plan'; + final SharedPreferences _prefs; + + MockBillingService(this._prefs); + + @override + Future purchasePro() async { + await Future.delayed(const Duration(seconds: 1)); + await _prefs.setString(_key, 'vip'); + return true; + } + + @override + Future restorePurchases() async { + await Future.delayed(const Duration(seconds: 1)); + return false; + } + + @override + Future getCurrentPlan() async { + final value = _prefs.getString(_key); + return value == 'vip' ? UserPlan.vip : UserPlan.free; + } +} diff --git a/lib/shared/services/google_auth_service.dart b/lib/shared/services/google_auth_service.dart new file mode 100644 index 0000000..37760a1 --- /dev/null +++ b/lib/shared/services/google_auth_service.dart @@ -0,0 +1,21 @@ +import 'dart:async'; +import 'package:google_sign_in/google_sign_in.dart'; + +class GoogleAuthService { + final GoogleSignIn _googleSignIn; + + GoogleAuthService() : _googleSignIn = GoogleSignIn(scopes: ['email']); + + GoogleSignInAccount? get currentUser => _googleSignIn.currentUser; + + Stream get onCurrentUserChanged => + _googleSignIn.onCurrentUserChanged; + + Future signIn() async { + await _googleSignIn.signIn(); + } + + Future signOut() async { + await _googleSignIn.signOut(); + } +} diff --git a/lib/shared/widgets/pro_screen.dart b/lib/shared/widgets/pro_screen.dart new file mode 100644 index 0000000..e2553b3 --- /dev/null +++ b/lib/shared/widgets/pro_screen.dart @@ -0,0 +1,550 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import '../../core/l10n/app_strings.dart'; +import '../../core/l10n/locale_provider.dart'; +import '../../core/services/haptic_service.dart'; +import '../feature_flags/feature_flags_provider.dart'; +import '../models/user_model.dart'; +import '../providers/billing_provider.dart'; +import '../providers/current_user_provider.dart'; +import '../providers/google_auth_provider.dart'; +import 'error_snackbar.dart'; + +class ProScreen extends ConsumerStatefulWidget { + const ProScreen({super.key}); + + @override + ConsumerState createState() => _ProScreenState(); +} + +class _ProScreenState extends ConsumerState + with SingleTickerProviderStateMixin { + bool _purchasing = false; + bool _restoring = false; + bool _showSuccess = false; + late final AnimationController _successController; + late final Animation _successScale; + + static const _gradientColors = [ + Color(0xFF1A237E), + Color(0xFF6A1B9A), + Color(0xFFE65100), + ]; + + @override + void initState() { + super.initState(); + _successController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 400), + ); + _successScale = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation( + parent: _successController, + curve: Curves.elasticOut, + ), + ); + } + + @override + void dispose() { + _successController.dispose(); + super.dispose(); + } + + void _showSuccessOverlay() { + setState(() => _showSuccess = true); + _successController.forward(from: 0.0); + } + + void _dismissSuccessOverlay() { + _successController.stop(); + if (mounted) { + setState(() => _showSuccess = false); + } + } + + @override + Widget build(BuildContext context) { + final s = ref.watch(stringsProvider); + final flags = ref.watch(featureFlagsProvider); + final isVip = flags.maxAccounts != 3; + final googleUserAsync = ref.watch(googleCurrentUserProvider); + final googleUser = googleUserAsync.value; + final colorScheme = Theme.of(context).colorScheme; + + return Stack( + children: [ + Scaffold( + body: SafeArea( + child: Column( + children: [ + _buildHeader(context, s, colorScheme), + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 4), + _buildHeroBanner(context, s, colorScheme, isVip), + const SizedBox(height: 20), + _buildFeatureList(context, s, colorScheme), + const SizedBox(height: 16), + if (isVip) ...[ + _buildProActiveSection(context, s, colorScheme, googleUser), + ], + ], + ), + ), + ), + _buildBottomBar(context, s, colorScheme, isVip), + ], + ), + ), + ), + if (_showSuccess) _buildSuccessOverlay(context, s, colorScheme), + ], + ); + } + + Widget _buildHeader( + BuildContext context, + AppStrings s, + ColorScheme colorScheme, + ) { + return Padding( + padding: const EdgeInsets.fromLTRB(8, 4, 16, 0), + child: Row( + children: [ + IconButton( + icon: const Icon(Icons.arrow_back_rounded), + onPressed: () => context.pop(), + ), + ], + ), + ); + } + + Widget _buildHeroBanner( + BuildContext context, + AppStrings s, + ColorScheme colorScheme, + bool isVip, + ) { + return Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 20), + decoration: BoxDecoration( + gradient: const LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: _gradientColors, + ), + borderRadius: BorderRadius.circular(20), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + s.proTitle, + style: Theme.of(context).textTheme.displaySmall?.copyWith( + fontWeight: FontWeight.w900, + color: Colors.white, + letterSpacing: -0.5, + ), + ), + const SizedBox(height: 6), + Text( + isVip ? s.proActive : s.proSubtitle, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: Colors.white.withOpacity(0.9), + fontWeight: isVip ? FontWeight.w700 : FontWeight.w500, + ), + ), + ], + ), + ); + } + + Widget _buildSuccessOverlay( + BuildContext context, + AppStrings s, + ColorScheme colorScheme, + ) { + return GestureDetector( + onTap: _dismissSuccessOverlay, + child: Container( + color: Colors.black54, + child: Center( + child: GestureDetector( + onTap: _dismissSuccessOverlay, + child: AnimatedBuilder( + animation: _successController, + builder: (context, child) { + return Transform.scale( + scale: _successScale.value, + child: child, + ); + }, + child: Container( + padding: const EdgeInsets.all(32), + margin: const EdgeInsets.symmetric(horizontal: 40), + decoration: BoxDecoration( + color: colorScheme.surface, + borderRadius: BorderRadius.circular(24), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: colorScheme.primary.withOpacity(0.15), + shape: BoxShape.circle, + ), + child: Icon( + Icons.check_circle_rounded, + color: colorScheme.primary, + size: 64, + ), + ), + const SizedBox(height: 20), + Text( + s.proPurchaseSuccess, + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.w800, + color: colorScheme.onSurface, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 12), + Text( + s.proTapToClose, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: colorScheme.onSurface.withOpacity(0.5), + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + ), + ), + ), + ), + ); + } + + Widget _buildFeatureList( + BuildContext context, + AppStrings s, + 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), + (Icons.account_balance_wallet_rounded, s.proFeatureAccounts, s.proFeatureAccountsDesc), + ]; + + return Column( + children: features.map((f) { + return Padding( + padding: const EdgeInsets.only(bottom: 14), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: colorScheme.primary.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + f.$1, + color: colorScheme.primary, + size: 22, + ), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + f.$2, + style: Theme.of(context).textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w700, + color: colorScheme.onSurface, + ), + ), + const SizedBox(height: 4), + Text( + f.$3, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: colorScheme.onSurface.withOpacity(0.6), + ), + ), + ], + ), + ), + ], + ), + ); + }).toList(), + ); + } + + 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( + color: Theme.of(context).scaffoldBackgroundColor, + border: Border( + top: BorderSide( + color: colorScheme.outlineVariant.withOpacity(0.3), + width: 1, + ), + ), + ), + 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), + ), + ), + 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), + ), + ), + ], + ), + ); + } + + Future _handlePurchase() async { + HapticService.light(); + setState(() => _purchasing = true); + try { + final billing = ref.read(billingServiceProvider); + final success = await billing.purchasePro(); + if (success) { + await ref.read(currentUserProvider.notifier).setPlan(UserPlan.vip); + if (mounted) { + _showSuccessOverlay(); + HapticService.medium(); + } + } else { + if (mounted) { + showErrorSnackbar(context, ref.read(stringsProvider).proPurchaseFailed); + } + } + } catch (e) { + if (mounted) { + showErrorSnackbar(context, e.toString()); + } + } finally { + if (mounted) setState(() => _purchasing = false); + } + } + + Future _handleRestore() async { + HapticService.light(); + setState(() => _restoring = true); + try { + final billing = ref.read(billingServiceProvider); + final success = await billing.restorePurchases(); + if (success) { + await ref.read(currentUserProvider.notifier).setPlan(UserPlan.vip); + if (mounted) { + _showSuccessOverlay(); + HapticService.medium(); + } + } else { + if (mounted) { + showWarningSnackbar(context, ref.read(stringsProvider).proRestoreNotFound); + } + } + } catch (e) { + if (mounted) { + showErrorSnackbar(context, e.toString()); + } + } finally { + if (mounted) setState(() => _restoring = false); + } + } + + Future _handleGoogleSignIn() async { + HapticService.light(); + try { + final service = ref.read(googleAuthProvider); + await service.signIn(); + } catch (e) { + if (mounted) { + showErrorSnackbar(context, e.toString()); + } + } + } + + Future _handleGoogleSignOut() async { + HapticService.light(); + try { + final service = ref.read(googleAuthProvider); + await service.signOut(); + } catch (e) { + if (mounted) { + showErrorSnackbar(context, e.toString()); + } + } + } +} diff --git a/lib/shared/widgets/pro_subscription_card.dart b/lib/shared/widgets/pro_subscription_card.dart new file mode 100644 index 0000000..6a2dceb --- /dev/null +++ b/lib/shared/widgets/pro_subscription_card.dart @@ -0,0 +1,126 @@ +import 'package:flutter/material.dart'; +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 '../feature_flags/feature_flags_provider.dart'; +import '../providers/google_auth_provider.dart'; + +class ProSubscriptionCard extends ConsumerWidget { + const ProSubscriptionCard({super.key}); + + static const _gradientColors = [ + Color(0xFF1A237E), + Color(0xFF6A1B9A), + Color(0xFFE65100), + ]; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final s = ref.watch(stringsProvider); + final flags = ref.watch(featureFlagsProvider); + final isVip = flags.maxAccounts != 3; + final googleUserAsync = ref.watch(googleCurrentUserProvider); + final googleUser = googleUserAsync.value; + + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: const LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: _gradientColors, + ), + borderRadius: BorderRadius.circular(16), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.15), + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + isVip ? Icons.verified_rounded : Icons.workspace_premium_rounded, + color: Colors.white, + size: 24, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + s.proTitle, + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.w900, + color: Colors.white, + ), + ), + if (isVip) ...[ + const SizedBox(height: 2), + Text( + googleUser != null + ? '${s.proActive} \u2705' + : s.proActive, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Colors.white.withOpacity(0.9), + fontWeight: FontWeight.w600, + ), + ), + ], + ], + ), + ), + OutlinedButton( + onPressed: () { + 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(horizontal: 16, vertical: 8), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + ), + child: Text( + s.proAboutPro, + style: const TextStyle(fontWeight: FontWeight.w700), + ), + ), + ], + ), + if (isVip && googleUser == null) ...[ + const SizedBox(height: 14), + SizedBox( + width: double.infinity, + child: OutlinedButton.icon( + onPressed: () { + HapticService.light(); + ref.read(googleAuthProvider).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), + ), + ), + ), + ), + ], + ], + ), + ); + } +} diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 80dc39e..8edbf73 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,10 +5,12 @@ import FlutterMacOS import Foundation +import google_sign_in_ios import local_auth_darwin import shared_preferences_foundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FLTGoogleSignInPlugin.register(with: registry.registrar(forPlugin: "FLTGoogleSignInPlugin")) LocalAuthPlugin.register(with: registry.registrar(forPlugin: "LocalAuthPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 91599f4..f6e39e7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -376,6 +376,54 @@ packages: url: "https://pub.dev" source: hosted version: "8.1.0" + google_identity_services_web: + dependency: transitive + description: + name: google_identity_services_web + sha256: "5d187c46dc59e02646e10fe82665fc3884a9b71bc1c90c2b8b749316d33ee454" + url: "https://pub.dev" + source: hosted + version: "0.3.3+1" + google_sign_in: + dependency: "direct main" + description: + name: google_sign_in + sha256: d0a2c3bcb06e607bb11e4daca48bd4b6120f0bbc4015ccebbe757d24ea60ed2a + url: "https://pub.dev" + source: hosted + version: "6.3.0" + google_sign_in_android: + dependency: transitive + description: + name: google_sign_in_android + sha256: d5e23c56a4b84b6427552f1cf3f98f716db3b1d1a647f16b96dbb5b93afa2805 + url: "https://pub.dev" + source: hosted + version: "6.2.1" + google_sign_in_ios: + dependency: transitive + description: + name: google_sign_in_ios + sha256: "102005f498ce18442e7158f6791033bbc15ad2dcc0afa4cf4752e2722a516c96" + url: "https://pub.dev" + source: hosted + version: "5.9.0" + google_sign_in_platform_interface: + dependency: transitive + description: + name: google_sign_in_platform_interface + sha256: "5f6f79cf139c197261adb6ac024577518ae48fdff8e53205c5373b5f6430a8aa" + url: "https://pub.dev" + source: hosted + version: "2.5.0" + google_sign_in_web: + dependency: transitive + description: + name: google_sign_in_web + sha256: "460547beb4962b7623ac0fb8122d6b8268c951cf0b646dd150d60498430e4ded" + url: "https://pub.dev" + source: hosted + version: "0.12.4+4" graphs: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 649938f..5851a8c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -25,6 +25,7 @@ dependencies: drift: ^2.14.1 sqlite3_flutter_libs: ^0.6.0+eol path: ^1.8.3 + google_sign_in: ^6.2.1 flutter_launcher_icons: android: true