From e6e870c991831c11341a4b78590d9af84be972bc Mon Sep 17 00:00:00 2001 From: Developer Date: Sat, 5 Sep 2026 04:29:43 -0400 Subject: [PATCH] feat: unify settings account and pro experience --- lib/core/theme/evolution_theme_config.dart | 45 - lib/features/entitlements/pro_screen.dart | 916 ++--- lib/features/settings/account_screen.dart | 1879 +++++----- lib/features/settings/settings_screen.dart | 3098 +++++++++-------- .../settings/widgets/current_plan_card.dart | 134 +- .../widgets/evolution_settings_view.dart | 2187 ------------ ..._settings_account_pro_responsive_test.dart | 386 ++ test/evolution_theme_test.dart | 23 - test/pro_screen_test.dart | 8 +- test/theme_test.dart | 23 +- 10 files changed, 3540 insertions(+), 5159 deletions(-) delete mode 100644 lib/core/theme/evolution_theme_config.dart delete mode 100644 lib/features/settings/widgets/evolution_settings_view.dart create mode 100644 test/canonical_settings_account_pro_responsive_test.dart diff --git a/lib/core/theme/evolution_theme_config.dart b/lib/core/theme/evolution_theme_config.dart deleted file mode 100644 index de88638..0000000 --- a/lib/core/theme/evolution_theme_config.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import '../../features/records/records_service.dart'; - -/// Legacy dashboard style generation. -/// -/// Deprecated in UI-2A in favor of unified responsive layouts with ThemeMode appearance. -@Deprecated('Use canonical MotorSignal responsive layout with ThemeMode instead') -enum DashboardThemeMode { classic, evolution } - -/// StateNotifier for legacy [DashboardThemeMode]. -@Deprecated('Use canonical MotorSignal responsive layout with ThemeMode instead') -class DashboardThemeModeNotifier extends StateNotifier { - final SharedPreferences _prefs; - static const String _key = 'app_dashboard_theme_mode'; - - DashboardThemeModeNotifier(this._prefs) : super(_loadInitial(_prefs)); - - static DashboardThemeMode _loadInitial(SharedPreferences prefs) { - final val = prefs.getString(_key); - if (val == 'evolution') { - return DashboardThemeMode.evolution; - } - return DashboardThemeMode.classic; - } - - void setThemeMode(DashboardThemeMode mode) { - state = mode; - _prefs.setString(_key, mode.name); - } - - void toggleTheme() { - final newMode = state == DashboardThemeMode.classic - ? DashboardThemeMode.evolution - : DashboardThemeMode.classic; - setThemeMode(newMode); - } -} - -/// Provider for legacy [DashboardThemeMode]. -@Deprecated('Use canonical MotorSignal responsive layout with ThemeMode instead') -final dashboardThemeModeProvider = StateNotifierProvider((ref) { - final prefs = ref.watch(sharedPreferencesProvider); - return DashboardThemeModeNotifier(prefs); -}); diff --git a/lib/features/entitlements/pro_screen.dart b/lib/features/entitlements/pro_screen.dart index 121e2b1..0c7cc00 100644 --- a/lib/features/entitlements/pro_screen.dart +++ b/lib/features/entitlements/pro_screen.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../core/responsive/viewport_size.dart'; +import '../../core/theme/theme_extensions.dart'; import '../../services/billing/billing_models.dart'; import '../../services/billing/billing_providers.dart'; import '../../services/billing/product_catalog.dart'; @@ -72,7 +74,6 @@ class _ProScreenState extends ConsumerState { }); } } else { - // Flow handed over to PlayBillingService purchaseStream -> EntitlementCoordinator if (mounted) { setState(() { _currentStep = ProPurchaseStep.idle; @@ -90,7 +91,6 @@ class _ProScreenState extends ConsumerState { try { final billing = ref.read(billingServiceProvider); await billing.restorePurchases(); - // Allow brief moment for store emission await Future.delayed(const Duration(seconds: 1)); if (mounted) { @@ -126,6 +126,10 @@ class _ProScreenState extends ConsumerState { @override Widget build(BuildContext context) { final entitlement = ref.watch(entitlementProvider); + final colors = context.motorColors; + final snapshot = context.responsive; + final isWide = snapshot.isMedium || snapshot.isExpanded; + final monthlyProd = _products .where((p) => p.id == ProductCatalog.proMonthly) .firstOrNull; @@ -158,293 +162,395 @@ class _ProScreenState extends ConsumerState { ), ), actions: [ - TextButton( - onPressed: _currentStep != ProPurchaseStep.idle - ? null - : _handleRestore, - child: const Text( - 'RESTORE', - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: Color(0xFF00E5FF), + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: TextButton( + style: TextButton.styleFrom( + minimumSize: const Size(48, 48), + padding: const EdgeInsets.symmetric(horizontal: 12), + ), + onPressed: _currentStep != ProPurchaseStep.idle + ? null + : _handleRestore, + child: Text( + 'RESTORE', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: colors.cyberCyan, + ), ), ), ), ], ), body: SafeArea( - child: ListView( - padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 12.0), - children: [ - // Status Banner if present - if (_statusBanner != null) ...[ - Container( - padding: const EdgeInsets.all(12), - margin: const EdgeInsets.only(bottom: 16), - decoration: BoxDecoration( - color: _isBannerError - ? Colors.redAccent.withValues(alpha: 0.15) - : Colors.greenAccent.withValues(alpha: 0.15), - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: _isBannerError - ? Colors.redAccent - : Colors.greenAccent, - width: 0.8, - ), - ), - child: Row( - children: [ - Icon( - _isBannerError - ? Icons.error_outline - : Icons.check_circle_outline, - color: _isBannerError - ? Colors.redAccent - : Colors.greenAccent, - size: 20, - ), - const SizedBox(width: 10), - Expanded( - child: Text( - _statusBanner!, - style: TextStyle( - fontSize: 12, - color: _isBannerError - ? Colors.redAccent - : Colors.greenAccent, - fontWeight: FontWeight.w600, - ), - ), - ), - ], - ), - ), - ], - - // Active Pro Banner if user is already verified Pro - if (entitlement.isPro) ...[ - Container( - padding: const EdgeInsets.all(16), - margin: const EdgeInsets.only(bottom: 20), - decoration: BoxDecoration( - color: const Color(0xFF00E5FF).withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(12), - border: Border.all(color: const Color(0xFF00E5FF)), - ), - child: Row( - children: [ - const Icon( - Icons.verified, - color: Color(0xFF00E5FF), - size: 28, - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - entitlement.plan == MotorSignalPlan.proLifetime - ? 'MOTORSIGNAL PRO LIFETIME ACTIVE' - : 'MOTORSIGNAL PRO ACTIVE', - style: const TextStyle( - fontFamily: 'Sora', - fontSize: 13, - fontWeight: FontWeight.w900, - color: Color(0xFF00E5FF), - letterSpacing: 0.5, - ), - ), - const SizedBox(height: 2), - Text( - entitlement.plan == MotorSignalPlan.proLifetime - ? 'Permanent access enabled. Thank you for supporting MotorSignal.' - : (entitlement.expiresAt != null - ? 'Subscription active. Renews ${entitlement.expiresAt!.toLocal().toString().split(' ')[0]}.' - : 'Subscription active.'), - style: const TextStyle( - fontSize: 11, - color: Colors.white70, - ), - ), - ], - ), - ), - ], - ), - ), - ], - - // Header Pitch - const Text( - 'PRECISION TELEMETRY.\nUNLIMITED RIDE DATA.', - style: TextStyle( - fontFamily: 'Sora', - fontSize: 22, - fontWeight: FontWeight.w900, - color: Colors.white, - letterSpacing: 0.2, - height: 1.2, + child: SingleChildScrollView( + padding: EdgeInsets.symmetric( + horizontal: isWide ? 24.0 : 20.0, + vertical: 12.0, + ), + child: Center( + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: isWide ? 1100 : 640, ), + child: isWide + ? _buildWideLayout(entitlement, monthlyProd, lifetimeProd) + : _buildCompactLayout(entitlement, monthlyProd, lifetimeProd), ), - const SizedBox(height: 8), - const Text( - 'Unlock 20Hz deep telemetry analysis, multiple motorcycle profiles, unlimited cloud sync, and forensic data exports.', - style: TextStyle( - fontSize: 13, - color: Colors.white70, - height: 1.4, - ), - ), - const SizedBox(height: 24), - - // Plan Cards - if (_isLoadingProducts) ...[ - const Center( - child: Padding( - padding: EdgeInsets.all(32.0), - child: CircularProgressIndicator(color: Color(0xFF00E5FF)), - ), - ), - ] else if (!_storeAvailable) ...[ - _buildStoreUnavailableCard(), - ] else ...[ - // Lifetime Plan Card - _buildPlanCard( - title: 'PRO LIFETIME', - subtitle: 'One-time purchase • Permanent access', - priceDisplay: lifetimeProd?.price ?? 'Unavailable', - badgeText: 'BEST VALUE', - isHighlighted: true, - isLoading: _currentStep == ProPurchaseStep.purchasing, - isCurrentPlan: - entitlement.plan == MotorSignalPlan.proLifetime && - entitlement.isPro, - onTap: lifetimeProd != null - ? () => _handleBuy(ProductCatalog.proLifetime) - : null, - ), - const SizedBox(height: 14), - - // Monthly Plan Card - _buildPlanCard( - title: 'PRO MONTHLY', - subtitle: 'Recurring subscription • Cancel anytime', - priceDisplay: monthlyProd != null - ? '${monthlyProd.price}/month' - : 'Unavailable', - badgeText: null, - isHighlighted: false, - isLoading: _currentStep == ProPurchaseStep.purchasing, - isCurrentPlan: - entitlement.plan == MotorSignalPlan.proMonthly && - entitlement.isPro, - onTap: monthlyProd != null - ? () => _handleBuy(ProductCatalog.proMonthly) - : null, - ), - ], - - const SizedBox(height: 28), - const Divider(color: Colors.white12), - const SizedBox(height: 16), - - // Benefits Checklist - const Text( - 'WHAT IS INCLUDED IN PRO', - style: TextStyle( - fontFamily: 'Sora', - fontSize: 12, - fontWeight: FontWeight.w800, - color: Color(0xFF00E5FF), - letterSpacing: 1.0, - ), - ), - const SizedBox(height: 16), - - _buildBenefitRow( - icon: Icons.speed, - title: 'Advanced Acceleration & Braking', - desc: - 'G-force timeline, sprint intervals, roll-on speed, and deceleration thresholds.', - ), - _buildBenefitRow( - icon: Icons.screen_rotation, - title: 'Advanced Lean Angle Analysis', - desc: - 'Left and right apex lean distributions, transition dynamics, and turn stability.', - ), - _buildBenefitRow( - icon: Icons.show_chart, - title: '20Hz Sub-Second Telemetry', - desc: - 'High-frequency forensic sensor fusion playback with interactive scrub control.', - ), - _buildBenefitRow( - icon: Icons.history, - title: 'Unlimited Ride History', - desc: - 'Complete persistent ledger of every ride without duration or count limitations.', - ), - _buildBenefitRow( - icon: Icons.table_chart_outlined, - title: 'Forensic CSV & PDF Export', - desc: - 'Export structured engineering data and printable performance run sheets.', - ), - _buildBenefitRow( - icon: Icons.two_wheeler, - title: 'Multiple Motorcycle Garage', - desc: - 'Log and compare telemetry across your entire personal motorcycle fleet.', - ), - _buildBenefitRow( - icon: Icons.cloud_done_outlined, - title: 'Unlimited MotorCloud Sync', - desc: - 'Automatic encrypted cloud backup with multi-device restore capability.', - ), - _buildBenefitRow( - icon: Icons.alt_route, - title: 'Route Planning & Group Rides', - desc: - 'Multi-waypoint scenic routing and group pack coordination features.', - ), - - const SizedBox(height: 24), - const Divider(color: Colors.white12), - const SizedBox(height: 12), - - // Terms & Clarity Note - const Text( - 'Basic ride recording, cockpit instruments, offline history, local SQLite storage, and crash detection remain 100% free forever. MotorSignal Pro subscriptions can be managed or canceled anytime in Google Play Store settings.', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 11, - color: Colors.white38, - height: 1.4, - ), - ), - const SizedBox(height: 24), - ], + ), ), ), ); } + Widget _buildWideLayout( + EntitlementState entitlement, + StoreProduct? monthlyProd, + StoreProduct? lifetimeProd, + ) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + flex: 5, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (_statusBanner != null) ...[ + _buildStatusBanner(), + const SizedBox(height: 16), + ], + if (entitlement.isPro) ...[ + _buildActiveProBanner(entitlement), + const SizedBox(height: 20), + ], + _buildPitchHeader(), + const SizedBox(height: 24), + _buildPlanSection(entitlement, monthlyProd, lifetimeProd), + ], + ), + ), + const SizedBox(width: 32), + Expanded( + flex: 6, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildBenefitsSection(), + const SizedBox(height: 20), + _buildDisclaimer(), + ], + ), + ), + ], + ); + } + + Widget _buildCompactLayout( + EntitlementState entitlement, + StoreProduct? monthlyProd, + StoreProduct? lifetimeProd, + ) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (_statusBanner != null) ...[ + _buildStatusBanner(), + const SizedBox(height: 16), + ], + if (entitlement.isPro) ...[ + _buildActiveProBanner(entitlement), + const SizedBox(height: 20), + ], + _buildPitchHeader(), + const SizedBox(height: 24), + _buildPlanSection(entitlement, monthlyProd, lifetimeProd), + const SizedBox(height: 28), + const Divider(color: Colors.white12), + const SizedBox(height: 16), + _buildBenefitsSection(), + const SizedBox(height: 24), + const Divider(color: Colors.white12), + const SizedBox(height: 12), + _buildDisclaimer(), + const SizedBox(height: 24), + ], + ); + } + + Widget _buildStatusBanner() { + final colors = context.motorColors; + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: _isBannerError + ? colors.raceRed.withValues(alpha: 0.15) + : colors.launchGreen.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: _isBannerError ? colors.raceRed : colors.launchGreen, + width: 0.8, + ), + ), + child: Row( + children: [ + Icon( + _isBannerError + ? Icons.error_outline + : Icons.check_circle_outline, + color: _isBannerError ? colors.raceRed : colors.launchGreen, + size: 20, + ), + const SizedBox(width: 10), + Expanded( + child: Text( + _statusBanner!, + style: TextStyle( + fontSize: 12, + color: _isBannerError ? colors.raceRed : colors.launchGreen, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ); + } + + Widget _buildActiveProBanner(EntitlementState entitlement) { + final colors = context.motorColors; + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: colors.cyberCyan.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: colors.cyberCyan), + ), + child: Row( + children: [ + Icon( + Icons.verified, + color: colors.cyberCyan, + size: 28, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + entitlement.plan == MotorSignalPlan.proLifetime + ? 'MOTORSIGNAL PRO LIFETIME ACTIVE' + : 'MOTORSIGNAL PRO ACTIVE', + style: TextStyle( + fontFamily: 'Sora', + fontSize: 13, + fontWeight: FontWeight.w900, + color: colors.cyberCyan, + letterSpacing: 0.5, + ), + ), + const SizedBox(height: 2), + Text( + entitlement.plan == MotorSignalPlan.proLifetime + ? 'Permanent access enabled. Thank you for supporting MotorSignal.' + : (entitlement.expiresAt != null + ? 'Subscription active. Renews ${entitlement.expiresAt!.toLocal().toString().split(' ')[0]}.' + : 'Subscription active.'), + style: const TextStyle( + fontSize: 11, + color: Colors.white70, + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildPitchHeader() { + return const Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'PRECISION TELEMETRY.\nUNLIMITED RIDE DATA.', + style: TextStyle( + fontFamily: 'Sora', + fontSize: 22, + fontWeight: FontWeight.w900, + color: Colors.white, + letterSpacing: 0.2, + height: 1.2, + ), + ), + SizedBox(height: 8), + Text( + 'Unlock 20Hz deep telemetry analysis, multiple motorcycle profiles, unlimited cloud sync, and forensic data exports.', + style: TextStyle( + fontSize: 13, + color: Colors.white70, + height: 1.4, + ), + ), + ], + ); + } + + Widget _buildPlanSection( + EntitlementState entitlement, + StoreProduct? monthlyProd, + StoreProduct? lifetimeProd, + ) { + if (_isLoadingProducts) { + return Center( + child: Padding( + padding: const EdgeInsets.all(32.0), + child: CircularProgressIndicator(color: context.motorColors.cyberCyan), + ), + ); + } + + if (!_storeAvailable) { + return _buildStoreUnavailableCard(); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Lifetime Plan Card + _buildPlanCard( + title: 'PRO LIFETIME', + subtitle: 'One-time purchase • Permanent access', + priceDisplay: lifetimeProd?.price ?? 'Unavailable', + badgeText: 'BEST VALUE', + isHighlighted: true, + isLoading: _currentStep == ProPurchaseStep.purchasing, + isCurrentPlan: + entitlement.plan == MotorSignalPlan.proLifetime && + entitlement.isPro, + onTap: lifetimeProd != null + ? () => _handleBuy(ProductCatalog.proLifetime) + : null, + ), + const SizedBox(height: 14), + + // Monthly Plan Card + _buildPlanCard( + title: 'PRO MONTHLY', + subtitle: 'Recurring subscription • Cancel anytime', + priceDisplay: monthlyProd != null + ? '${monthlyProd.price}/month' + : 'Unavailable', + badgeText: null, + isHighlighted: false, + isLoading: _currentStep == ProPurchaseStep.purchasing, + isCurrentPlan: + entitlement.plan == MotorSignalPlan.proMonthly && + entitlement.isPro, + onTap: monthlyProd != null + ? () => _handleBuy(ProductCatalog.proMonthly) + : null, + ), + ], + ); + } + + Widget _buildBenefitsSection() { + final colors = context.motorColors; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'WHAT IS INCLUDED IN PRO', + style: TextStyle( + fontFamily: 'Sora', + fontSize: 12, + fontWeight: FontWeight.w800, + color: colors.cyberCyan, + letterSpacing: 1.0, + ), + ), + const SizedBox(height: 16), + _buildBenefitRow( + icon: Icons.speed, + title: 'Advanced Acceleration & Braking', + desc: + 'G-force timeline, sprint intervals, roll-on speed, and deceleration thresholds.', + ), + _buildBenefitRow( + icon: Icons.screen_rotation, + title: 'Advanced Lean Angle Analysis', + desc: + 'Left and right apex lean distributions, transition dynamics, and turn stability.', + ), + _buildBenefitRow( + icon: Icons.show_chart, + title: '20Hz Sub-Second Telemetry', + desc: + 'High-frequency forensic sensor fusion playback with interactive scrub control.', + ), + _buildBenefitRow( + icon: Icons.history, + title: 'Unlimited Ride History', + desc: + 'Complete persistent ledger of every ride without duration or count limitations.', + ), + _buildBenefitRow( + icon: Icons.table_chart_outlined, + title: 'Forensic CSV & PDF Export', + desc: + 'Export structured engineering data and printable performance run sheets.', + ), + _buildBenefitRow( + icon: Icons.two_wheeler, + title: 'Multiple Motorcycle Garage', + desc: + 'Log and compare telemetry across your entire personal motorcycle fleet.', + ), + _buildBenefitRow( + icon: Icons.cloud_done_outlined, + title: 'Unlimited MotorCloud Sync', + desc: + 'Automatic encrypted cloud backup with multi-device restore capability.', + ), + _buildBenefitRow( + icon: Icons.alt_route, + title: 'Route Planning & Group Rides', + desc: + 'Multi-waypoint scenic routing and group pack coordination features.', + ), + ], + ); + } + + Widget _buildDisclaimer() { + return const Text( + 'Basic ride recording, cockpit instruments, offline history, local SQLite storage, and crash detection remain 100% free forever. MotorSignal Pro subscriptions can be managed or canceled anytime in Google Play Store settings.', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 11, + color: Colors.white38, + height: 1.4, + ), + ); + } + Widget _buildStoreUnavailableCard() { + final colors = context.motorColors; return Container( padding: const EdgeInsets.all(18), decoration: BoxDecoration( - color: const Color(0xFF141414), + color: colors.surfaceContainer, borderRadius: BorderRadius.circular(12), - border: Border.all(color: Colors.white12), + border: Border.all(color: colors.outlineVariant), ), child: Column( children: [ - const Icon(Icons.cloud_off, color: Colors.amberAccent, size: 32), + Icon(Icons.cloud_off, color: colors.warningAmber, size: 32), const SizedBox(height: 12), const Text( 'Google Play Billing Unavailable', @@ -458,6 +564,7 @@ class _ProScreenState extends ConsumerState { ), const SizedBox(height: 14), OutlinedButton( + style: OutlinedButton.styleFrom(minimumSize: const Size(120, 48)), onPressed: _loadStoreProducts, child: const Text('RETRY CONNECTION'), ), @@ -476,143 +583,143 @@ class _ProScreenState extends ConsumerState { required bool isCurrentPlan, required VoidCallback? onTap, }) { - return Container( - decoration: BoxDecoration( - color: isHighlighted - ? const Color(0xFF001A1E) - : const Color(0xFF0F0F0F), + final colors = context.motorColors; + return Material( + color: Colors.transparent, + child: InkWell( borderRadius: BorderRadius.circular(14), - border: Border.all( - color: isCurrentPlan - ? Colors.greenAccent - : (isHighlighted - ? const Color(0xFF00E5FF) - : const Color(0xFF222222)), - width: isHighlighted || isCurrentPlan ? 1.6 : 1.0, - ), - ), - child: Material( - color: Colors.transparent, - child: InkWell( - borderRadius: BorderRadius.circular(14), - onTap: isCurrentPlan ? null : onTap, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - spacing: 8, - runSpacing: 4, - children: [ - Text( - title, - style: const TextStyle( - fontFamily: 'Sora', - fontSize: 15, - fontWeight: FontWeight.w900, - color: Colors.white, - letterSpacing: 0.5, - ), - ), - if (badgeText != null) - Container( - padding: const EdgeInsets.symmetric( - horizontal: 6, - vertical: 2, - ), - decoration: BoxDecoration( - color: const Color(0xFF00E5FF), - borderRadius: BorderRadius.circular(4), - ), - child: Text( - badgeText, - style: const TextStyle( - fontSize: 9, - fontWeight: FontWeight.w900, - color: Colors.black, - letterSpacing: 0.5, - ), - ), - ), - if (isCurrentPlan) - Container( - padding: const EdgeInsets.symmetric( - horizontal: 6, - vertical: 2, - ), - decoration: BoxDecoration( - color: Colors.greenAccent.withValues( - alpha: 0.2, - ), - borderRadius: BorderRadius.circular(4), - border: Border.all(color: Colors.greenAccent), - ), - child: const Text( - 'CURRENT PLAN', - style: TextStyle( - fontSize: 9, - fontWeight: FontWeight.w900, - color: Colors.greenAccent, - letterSpacing: 0.5, - ), - ), - ), - ], - ), - const SizedBox(height: 4), - Text( - subtitle, - style: const TextStyle( - fontSize: 11, - color: Colors.white60, - ), - ), - ], - ), - ), - const SizedBox(width: 12), - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 130), - child: Column( - crossAxisAlignment: CrossAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: [ - FittedBox( - fit: BoxFit.scaleDown, - child: Text( - priceDisplay, - style: TextStyle( - fontFamily: 'JetBrains Mono', - fontSize: 17, - fontWeight: FontWeight.w900, - color: isHighlighted - ? const Color(0xFF00E5FF) - : Colors.white, - ), - ), - ), - const SizedBox(height: 2), - Text( - isCurrentPlan ? 'Active' : 'Select', - style: TextStyle( - fontSize: 11, - color: isHighlighted - ? const Color(0xFF00E5FF) - : Colors.white54, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ), - ], + onTap: isCurrentPlan ? null : onTap, + child: Container( + constraints: const BoxConstraints(minHeight: 64), + decoration: BoxDecoration( + color: isHighlighted + ? colors.cyberCyan.withValues(alpha: 0.08) + : colors.surfaceContainer, + borderRadius: BorderRadius.circular(14), + border: Border.all( + color: isCurrentPlan + ? colors.launchGreen + : (isHighlighted + ? colors.cyberCyan + : colors.outlineVariant), + width: isHighlighted || isCurrentPlan ? 1.6 : 1.0, ), ), + padding: const EdgeInsets.all(16.0), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + spacing: 8, + runSpacing: 4, + children: [ + Text( + title, + style: const TextStyle( + fontFamily: 'Sora', + fontSize: 15, + fontWeight: FontWeight.w900, + color: Colors.white, + letterSpacing: 0.5, + ), + ), + if (badgeText != null) + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: colors.cyberCyan, + borderRadius: BorderRadius.circular(4), + ), + child: const Text( + 'BEST VALUE', + style: TextStyle( + fontSize: 9, + fontWeight: FontWeight.w900, + color: Colors.black, + letterSpacing: 0.5, + ), + ), + ), + if (isCurrentPlan) + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: colors.launchGreen.withValues( + alpha: 0.2, + ), + borderRadius: BorderRadius.circular(4), + border: Border.all(color: colors.launchGreen), + ), + child: Text( + 'CURRENT PLAN', + style: TextStyle( + fontSize: 9, + fontWeight: FontWeight.w900, + color: colors.launchGreen, + letterSpacing: 0.5, + ), + ), + ), + ], + ), + const SizedBox(height: 4), + Text( + subtitle, + style: const TextStyle( + fontSize: 11, + color: Colors.white60, + ), + ), + ], + ), + ), + const SizedBox(width: 12), + ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 130), + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + FittedBox( + fit: BoxFit.scaleDown, + child: Text( + priceDisplay, + style: TextStyle( + fontFamily: 'JetBrains Mono', + fontSize: 17, + fontWeight: FontWeight.w900, + color: isHighlighted + ? colors.cyberCyan + : Colors.white, + ), + ), + ), + const SizedBox(height: 2), + Text( + isCurrentPlan ? 'Active' : 'Select', + style: TextStyle( + fontSize: 11, + color: isHighlighted + ? colors.cyberCyan + : Colors.white54, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ], + ), ), ), ); @@ -623,6 +730,7 @@ class _ProScreenState extends ConsumerState { required String title, required String desc, }) { + final colors = context.motorColors; return Padding( padding: const EdgeInsets.only(bottom: 16.0), child: Row( @@ -631,10 +739,10 @@ class _ProScreenState extends ConsumerState { Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( - color: const Color(0xFF00E5FF).withValues(alpha: 0.1), + color: colors.cyberCyan.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8), ), - child: Icon(icon, color: const Color(0xFF00E5FF), size: 20), + child: Icon(icon, color: colors.cyberCyan, size: 20), ), const SizedBox(width: 14), Expanded( diff --git a/lib/features/settings/account_screen.dart b/lib/features/settings/account_screen.dart index 4b9a5e0..625d7ef 100644 --- a/lib/features/settings/account_screen.dart +++ b/lib/features/settings/account_screen.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../core/theme/evolution_theme_config.dart'; +import '../../core/responsive/viewport_size.dart'; +import '../../core/theme/theme_extensions.dart'; import '../../database/database.dart'; import '../../services/auth/auth_providers.dart'; import '../../services/auth/auth_state.dart'; @@ -49,447 +50,433 @@ class _AccountScreenState extends ConsumerState { @override Widget build(BuildContext context) { final authState = ref.watch(authNotifierProvider); - final themeMode = ref.watch(dashboardThemeModeProvider); - final isEvolution = themeMode == DashboardThemeMode.evolution; - final isLandscape = - MediaQuery.of(context).orientation == Orientation.landscape; - - final bgColor = isEvolution - ? Colors.black - : Theme.of(context).scaffoldBackgroundColor; - final titleFont = isEvolution ? 'Sora' : null; + final snapshot = context.responsive; + final isWide = snapshot.isMedium || snapshot.isExpanded; return Scaffold( - backgroundColor: bgColor, appBar: AppBar( - backgroundColor: isEvolution ? Colors.transparent : null, - elevation: 0, - title: Text( + title: const Text( 'Account & Cloud Identity', - style: TextStyle( - fontFamily: titleFont, - fontWeight: FontWeight.w800, - fontSize: isLandscape ? 14 : 18, - ), + style: TextStyle(fontWeight: FontWeight.w800, letterSpacing: 0.5), ), ), body: SafeArea( - child: LayoutBuilder( - builder: (context, constraints) { - final isWide = constraints.maxWidth > 700; - - Widget content; - if (authState.isAuthenticated) { - content = _buildSignedInView( - context, - authState.user!, - isEvolution, - isWide, - ); - } else if (authState.isAnonymous && !_showAnonymousUpgrade) { - content = _buildAnonymousView( - context, - authState.user!, - isEvolution, - isWide, - ); - } else { - content = _buildSignedOutView( - context, - authState, - isEvolution, - isWide, - ); - } - - return SingleChildScrollView( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 12.0, + child: SingleChildScrollView( + keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, + padding: EdgeInsets.symmetric( + horizontal: isWide ? 24.0 : 16.0, + vertical: 14.0, + ), + child: Center( + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: isWide ? 1100 : 640, ), - child: Center( - child: ConstrainedBox( - constraints: BoxConstraints( - maxWidth: isWide ? 640 : double.infinity, - ), - child: content, - ), - ), - ); - }, + child: _buildBodyContent(context, authState, isWide), + ), + ), ), ), ); } + Widget _buildBodyContent(BuildContext context, AuthState authState, bool isWide) { + if (authState.isAuthenticated) { + return _buildSignedInView(context, authState.user!, isWide); + } else if (authState.isAnonymous && !_showAnonymousUpgrade) { + return _buildAnonymousView(context, authState.user!, isWide); + } else { + return _buildSignedOutView(context, authState, isWide); + } + } + Widget _buildSignedOutView( BuildContext context, AuthState authState, - bool isEvolution, bool isWide, ) { - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - _buildOfflineNoticeCard(isEvolution, authState.isFirebaseConfigured), - const SizedBox(height: 16), - _buildAuthCard( - isEvolution: isEvolution, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, + final formCard = _buildAuthCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( children: [ - Row( - children: [ - Icon( - Icons.account_circle_outlined, - color: const Color(0xFF00E5FF), - size: 22, - ), - const SizedBox(width: 10), - Text( - _formMode == _AuthFormMode.signIn - ? 'SIGN IN TO MOTORCLOUD' - : (_formMode == _AuthFormMode.register - ? 'CREATE ACCOUNT' - : 'RESET PASSWORD'), - style: TextStyle( - fontFamily: isEvolution ? 'Sora' : null, - fontSize: 13, - fontWeight: FontWeight.w800, - color: const Color(0xFF00E5FF), - letterSpacing: 0.8, - ), - ), - ], + Icon( + Icons.account_circle_outlined, + color: context.motorColors.cyberCyan, + size: 22, ), - const SizedBox(height: 8), - Text( - 'Cloud sign-in is completely optional. All telemetry, HUD instruments, sensor fusion, and ride history are 100% functional without an account.', - style: TextStyle( - fontFamily: isEvolution ? 'Inter' : null, - fontSize: 11, - color: Colors.white70, - height: 1.35, - ), - ), - const SizedBox(height: 16), - if (authState.hasError) ...[ - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.redAccent.withValues(alpha: 0.12), - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: Colors.redAccent.withValues(alpha: 0.6), - ), - ), - child: Row( - children: [ - const Icon( - Icons.error_outline, - color: Colors.redAccent, - size: 18, - ), - const SizedBox(width: 8), - Expanded( - child: Text( - authState.errorMessage!, - style: const TextStyle( - fontSize: 11, - color: Colors.redAccent, - ), - ), - ), - ], + const SizedBox(width: 10), + Expanded( + child: Text( + _formMode == _AuthFormMode.signIn + ? 'SIGN IN TO MOTORCLOUD' + : (_formMode == _AuthFormMode.register + ? 'CREATE ACCOUNT' + : 'RESET PASSWORD'), + style: TextStyle( + fontFamily: 'Sora', + fontSize: 13, + fontWeight: FontWeight.w800, + color: context.motorColors.cyberCyan, + letterSpacing: 0.8, ), ), - const SizedBox(height: 16), - ], - if (_statusMessage != null) ...[ - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.greenAccent.withValues(alpha: 0.12), - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: Colors.greenAccent.withValues(alpha: 0.6), - ), - ), - child: Row( - children: [ - const Icon( - Icons.check_circle_outline, - color: Colors.greenAccent, - size: 18, - ), - const SizedBox(width: 8), - Expanded( - child: Text( - _statusMessage!, - style: const TextStyle( - fontSize: 11, - color: Colors.greenAccent, - ), - ), - ), - ], - ), - ), - const SizedBox(height: 16), - ], - if (_formMode != _AuthFormMode.forgotPassword) ...[ - ElevatedButton.icon( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.white, - foregroundColor: Colors.black87, - padding: const EdgeInsets.symmetric(vertical: 12), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - icon: const Icon( - Icons.g_mobiledata, - size: 24, - color: Colors.redAccent, - ), - label: const Text( - 'Continue with Google', - style: TextStyle(fontSize: 13, fontWeight: FontWeight.bold), - ), - onPressed: authState.isLoading - ? null - : () => _handleGoogleSignIn(), - ), - const SizedBox(height: 16), - const Row( - children: [ - Expanded(child: Divider(color: Colors.white24)), - Padding( - padding: EdgeInsets.symmetric(horizontal: 10), - child: Text( - 'OR', - style: TextStyle( - color: Colors.white38, - fontSize: 10, - fontWeight: FontWeight.bold, - ), - ), - ), - Expanded(child: Divider(color: Colors.white24)), - ], - ), - const SizedBox(height: 16), - ], - Form( - key: _formKey, - child: Column( - children: [ - if (_formMode == _AuthFormMode.register) ...[ - TextFormField( - controller: _displayNameController, - decoration: InputDecoration( - labelText: 'Display Name (Optional)', - prefixIcon: const Icon( - Icons.person_outline, - size: 18, - ), - filled: true, - fillColor: isEvolution - ? const Color(0xFF141414) - : Colors.white10, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - ), - isDense: true, - ), - style: const TextStyle(fontSize: 13), - ), - const SizedBox(height: 12), - ], - TextFormField( - controller: _emailController, - keyboardType: TextInputType.emailAddress, - decoration: InputDecoration( - labelText: 'Email Address', - prefixIcon: const Icon(Icons.email_outlined, size: 18), - filled: true, - fillColor: isEvolution - ? const Color(0xFF141414) - : Colors.white10, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - ), - isDense: true, - ), - style: const TextStyle(fontSize: 13), - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'Please enter an email address'; - } - if (!value.contains('@') || !value.contains('.')) { - return 'Please enter a valid email address'; - } - return null; - }, - ), - if (_formMode != _AuthFormMode.forgotPassword) ...[ - const SizedBox(height: 12), - TextFormField( - controller: _passwordController, - obscureText: _obscurePassword, - decoration: InputDecoration( - labelText: 'Password', - prefixIcon: const Icon(Icons.lock_outline, size: 18), - suffixIcon: IconButton( - icon: Icon( - _obscurePassword - ? Icons.visibility_off - : Icons.visibility, - size: 18, - color: Colors.white54, - ), - onPressed: () => setState( - () => _obscurePassword = !_obscurePassword, - ), - ), - filled: true, - fillColor: isEvolution - ? const Color(0xFF141414) - : Colors.white10, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - ), - isDense: true, - ), - style: const TextStyle(fontSize: 13), - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter your password'; - } - if (_formMode == _AuthFormMode.register && - value.length < 6) { - return 'Password must be at least 6 characters'; - } - return null; - }, - ), - ], - ], - ), - ), - const SizedBox(height: 16), - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF00E5FF), - foregroundColor: Colors.black, - padding: const EdgeInsets.symmetric(vertical: 12), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - onPressed: authState.isLoading ? null : () => _submitForm(), - child: authState.isLoading - ? const SizedBox( - width: 18, - height: 18, - child: CircularProgressIndicator( - strokeWidth: 2, - color: Colors.black, - ), - ) - : Text( - _formMode == _AuthFormMode.signIn - ? 'SIGN IN' - : (_formMode == _AuthFormMode.register - ? 'CREATE ACCOUNT' - : 'SEND RESET LINK'), - style: const TextStyle( - fontWeight: FontWeight.w900, - letterSpacing: 0.8, - ), - ), - ), - const SizedBox(height: 12), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - if (_formMode == _AuthFormMode.signIn) ...[ - TextButton( - onPressed: () => setState(() { - _formMode = _AuthFormMode.forgotPassword; - _statusMessage = null; - }), - child: const Text( - 'Forgot Password?', - style: TextStyle(fontSize: 11, color: Colors.white70), - ), - ), - TextButton( - onPressed: () => setState(() { - _formMode = _AuthFormMode.register; - _statusMessage = null; - }), - child: const Text( - 'Create Account', - style: TextStyle( - fontSize: 11, - color: Color(0xFF00E5FF), - fontWeight: FontWeight.bold, - ), - ), - ), - ] else if (_formMode == _AuthFormMode.register) ...[ - TextButton( - onPressed: () => setState(() { - _formMode = _AuthFormMode.signIn; - _statusMessage = null; - }), - child: const Text( - 'Already have an account? Sign In', - style: TextStyle( - fontSize: 11, - color: Color(0xFF00E5FF), - ), - ), - ), - ] else ...[ - TextButton( - onPressed: () => setState(() { - _formMode = _AuthFormMode.signIn; - _statusMessage = null; - }), - child: const Text( - 'Back to Sign In', - style: TextStyle(fontSize: 11, color: Colors.white70), - ), - ), - ], - ], - ), - const Divider(color: Colors.white12, height: 24), - OutlinedButton.icon( - style: OutlinedButton.styleFrom( - foregroundColor: Colors.white70, - side: const BorderSide(color: Colors.white24), - padding: const EdgeInsets.symmetric(vertical: 10), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - icon: const Icon(Icons.person_outline, size: 18), - label: const Text( - 'Continue as Guest / Anonymous Rider', - style: TextStyle(fontSize: 11), - ), - onPressed: authState.isLoading - ? null - : () => ref - .read(authNotifierProvider.notifier) - .signInAnonymously(), ), ], ), - ), + const SizedBox(height: 8), + Text( + 'Cloud sign-in is completely optional. All telemetry, HUD instruments, sensor fusion, and ride history are 100% functional without an account.', + style: TextStyle( + fontSize: 11, + color: Theme.of(context).colorScheme.onSurfaceVariant, + height: 1.35, + ), + ), + const SizedBox(height: 16), + if (authState.hasError) ...[ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: context.motorColors.raceRed.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: context.motorColors.raceRed.withValues(alpha: 0.6), + ), + ), + child: Row( + children: [ + Icon( + Icons.error_outline, + color: context.motorColors.raceRed, + size: 18, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + authState.errorMessage!, + style: TextStyle( + fontSize: 11, + color: context.motorColors.raceRed, + ), + ), + ), + ], + ), + ), + const SizedBox(height: 16), + ], + if (_statusMessage != null) ...[ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: context.motorColors.launchGreen.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: context.motorColors.launchGreen.withValues(alpha: 0.6), + ), + ), + child: Row( + children: [ + Icon( + Icons.check_circle_outline, + color: context.motorColors.launchGreen, + size: 18, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + _statusMessage!, + style: TextStyle( + fontSize: 11, + color: context.motorColors.launchGreen, + ), + ), + ), + ], + ), + ), + const SizedBox(height: 16), + ], + if (_formMode != _AuthFormMode.forgotPassword) ...[ + ElevatedButton.icon( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: Colors.black87, + minimumSize: const Size(double.infinity, 48), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + icon: const Icon( + Icons.g_mobiledata, + size: 24, + color: Colors.redAccent, + ), + label: const Text( + 'Continue with Google', + style: TextStyle(fontSize: 13, fontWeight: FontWeight.bold), + ), + onPressed: authState.isLoading + ? null + : () => _handleGoogleSignIn(), + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded(child: Divider(color: context.motorColors.outlineVariant)), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Text( + 'OR', + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontSize: 10, + fontWeight: FontWeight.bold, + ), + ), + ), + Expanded(child: Divider(color: context.motorColors.outlineVariant)), + ], + ), + const SizedBox(height: 16), + ], + Form( + key: _formKey, + child: Column( + children: [ + if (_formMode == _AuthFormMode.register) ...[ + TextFormField( + controller: _displayNameController, + decoration: InputDecoration( + labelText: 'Display Name (Optional)', + prefixIcon: const Icon( + Icons.person_outline, + size: 18, + ), + filled: true, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + isDense: true, + ), + style: const TextStyle(fontSize: 13), + ), + const SizedBox(height: 12), + ], + TextFormField( + controller: _emailController, + keyboardType: TextInputType.emailAddress, + decoration: InputDecoration( + labelText: 'Email Address', + prefixIcon: const Icon(Icons.email_outlined, size: 18), + filled: true, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + isDense: true, + ), + style: const TextStyle(fontSize: 13), + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'Please enter an email address'; + } + if (!value.contains('@') || !value.contains('.')) { + return 'Please enter a valid email address'; + } + return null; + }, + ), + if (_formMode != _AuthFormMode.forgotPassword) ...[ + const SizedBox(height: 12), + TextFormField( + controller: _passwordController, + obscureText: _obscurePassword, + decoration: InputDecoration( + labelText: 'Password', + prefixIcon: const Icon(Icons.lock_outline, size: 18), + suffixIcon: IconButton( + icon: Icon( + _obscurePassword + ? Icons.visibility_off + : Icons.visibility, + size: 18, + ), + onPressed: () => setState( + () => _obscurePassword = !_obscurePassword, + ), + ), + filled: true, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + isDense: true, + ), + style: const TextStyle(fontSize: 13), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter your password'; + } + if (_formMode == _AuthFormMode.register && + value.length < 6) { + return 'Password must be at least 6 characters'; + } + return null; + }, + ), + ], + ], + ), + ), + const SizedBox(height: 16), + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: context.motorColors.cyberCyan, + foregroundColor: Colors.black, + minimumSize: const Size(double.infinity, 48), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + onPressed: authState.isLoading ? null : () => _submitForm(), + child: authState.isLoading + ? const SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.black, + ), + ) + : Text( + _formMode == _AuthFormMode.signIn + ? 'SIGN IN' + : (_formMode == _AuthFormMode.register + ? 'CREATE ACCOUNT' + : 'SEND RESET LINK'), + style: const TextStyle( + fontWeight: FontWeight.w900, + letterSpacing: 0.8, + ), + ), + ), + const SizedBox(height: 12), + Wrap( + alignment: WrapAlignment.spaceBetween, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + if (_formMode == _AuthFormMode.signIn) ...[ + TextButton( + onPressed: () => setState(() { + _formMode = _AuthFormMode.forgotPassword; + _statusMessage = null; + }), + child: Text( + 'Forgot Password?', + style: TextStyle( + fontSize: 11, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ), + TextButton( + onPressed: () => setState(() { + _formMode = _AuthFormMode.register; + _statusMessage = null; + }), + child: Text( + 'Create Account', + style: TextStyle( + fontSize: 11, + color: context.motorColors.cyberCyan, + fontWeight: FontWeight.bold, + ), + ), + ), + ] else if (_formMode == _AuthFormMode.register) ...[ + TextButton( + onPressed: () => setState(() { + _formMode = _AuthFormMode.signIn; + _statusMessage = null; + }), + child: Text( + 'Already have an account? Sign In', + style: TextStyle( + fontSize: 11, + color: context.motorColors.cyberCyan, + ), + ), + ), + ] else ...[ + TextButton( + onPressed: () => setState(() { + _formMode = _AuthFormMode.signIn; + _statusMessage = null; + }), + child: Text( + 'Back to Sign In', + style: TextStyle( + fontSize: 11, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ), + ], + ], + ), + const Divider(height: 24), + OutlinedButton.icon( + style: OutlinedButton.styleFrom( + foregroundColor: Theme.of(context).colorScheme.onSurface, + side: BorderSide(color: context.motorColors.outlineVariant), + minimumSize: const Size(double.infinity, 48), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + icon: const Icon(Icons.person_outline, size: 18), + label: const Text( + 'Continue as Guest / Anonymous Rider', + style: TextStyle(fontSize: 11), + ), + onPressed: authState.isLoading + ? null + : () => ref + .read(authNotifierProvider.notifier) + .signInAnonymously(), + ), + ], + ), + ); + + if (isWide) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(child: formCard), + const SizedBox(width: 24), + Expanded( + child: Column( + children: [ + _buildOfflineNoticeCard(authState.isFirebaseConfigured), + const SizedBox(height: 16), + _buildLocalRidingActiveCard(), + ], + ), + ), + ], + ); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildOfflineNoticeCard(authState.isFirebaseConfigured), + const SizedBox(height: 16), + formCard, + const SizedBox(height: 16), + _buildLocalRidingActiveCard(), ], ); } @@ -497,319 +484,344 @@ class _AccountScreenState extends ConsumerState { Widget _buildSignedInView( BuildContext context, AuthUser user, - bool isEvolution, bool isWide, ) { - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - _buildAuthCard( - isEvolution: isEvolution, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - CircleAvatar( - radius: 28, - backgroundColor: const Color( - 0xFF00E5FF, - ).withValues(alpha: 0.2), - backgroundImage: user.photoUrl != null - ? NetworkImage(user.photoUrl!) - : null, - child: user.photoUrl == null - ? const Icon( - Icons.person, - color: Color(0xFF00E5FF), - size: 32, - ) - : null, - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - user.displayLabel, - style: TextStyle( - fontFamily: isEvolution ? 'Sora' : null, - fontSize: 18, - fontWeight: FontWeight.w800, - color: Colors.white, - ), - ), - const SizedBox(height: 2), - Text( - user.email ?? 'No email associated', - style: const TextStyle( - fontSize: 12, - color: Colors.white70, - ), - ), - const SizedBox(height: 6), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 2, - ), - decoration: BoxDecoration( - color: const Color( - 0xFF00E5FF, - ).withValues(alpha: 0.15), - borderRadius: BorderRadius.circular(4), - border: Border.all( - color: const Color(0xFF00E5FF), - width: 0.8, - ), - ), - child: Text( - 'AUTHENTICATED (${user.providerId ?? "cloud"})', - style: const TextStyle( - fontFamily: 'Inter', - fontSize: 9, - fontWeight: FontWeight.w900, - color: Color(0xFF00E5FF), - letterSpacing: 0.5, - ), - ), - ), - ], - ), - ), - ], - ), - const Divider(height: 32, color: Colors.white12), - _buildInfoRow('User ID', user.uid, isMonospace: true), - if (user.providerId != null) - _buildInfoRow('Provider', user.providerId!), - if (user.createdAt != null) - _buildInfoRow( - 'Account Created', - user.createdAt!.toLocal().toString().split('.')[0], - ), - if (user.lastSignInTime != null) - _buildInfoRow( - 'Last Sign-In', - user.lastSignInTime!.toLocal().toString().split('.')[0], - ), - if (user.providerId == 'password' && !user.emailVerified) ...[ - const SizedBox(height: 12), - Container( - padding: const EdgeInsets.all(10), - decoration: BoxDecoration( - color: Colors.amberAccent.withValues(alpha: 0.12), - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: Colors.amberAccent.withValues(alpha: 0.5), - ), - ), - child: Row( - children: [ - const Expanded( - child: Text( - 'Your email is not verified. Verify it to protect account recovery.', - style: TextStyle(fontSize: 11, color: Colors.white), - ), - ), - TextButton( - onPressed: () => _sendVerification(), - child: const Text('VERIFY'), - ), - IconButton( - tooltip: 'Refresh verification status', - onPressed: () => _reloadUser(), - icon: const Icon(Icons.refresh, size: 18), - ), - ], - ), - ), - ], - const SizedBox(height: 20), - Row( - children: [ - Expanded( - child: OutlinedButton.icon( - style: OutlinedButton.styleFrom( - foregroundColor: const Color(0xFF00E5FF), - side: const BorderSide(color: Color(0xFF00E5FF)), - padding: const EdgeInsets.symmetric(vertical: 12), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - icon: const Icon(Icons.person_outline, size: 18), - label: const Text( - 'RIDER PROFILE', - style: TextStyle( - fontWeight: FontWeight.bold, - letterSpacing: 0.5, - fontSize: 12, - ), - ), - onPressed: () => context.push('/settings/profile'), - ), - ), - const SizedBox(width: 10), - Expanded( - child: OutlinedButton.icon( - style: OutlinedButton.styleFrom( - foregroundColor: const Color(0xFF00E5FF), - side: const BorderSide(color: Color(0xFF00E5FF)), - padding: const EdgeInsets.symmetric(vertical: 12), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - icon: const Icon(Icons.people_outline, size: 18), - label: const Text( - 'FRIENDS', - style: TextStyle( - fontWeight: FontWeight.bold, - letterSpacing: 0.5, - fontSize: 12, - ), - ), - onPressed: () => context.push('/settings/friends'), - ), - ), - ], - ), - const SizedBox(height: 10), - SizedBox( - width: double.infinity, - child: ElevatedButton.icon( - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF00E5FF).withValues(alpha: 0.15), - foregroundColor: const Color(0xFF00E5FF), - side: const BorderSide(color: Color(0xFF00E5FF)), - padding: const EdgeInsets.symmetric(vertical: 12), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - icon: const Icon(Icons.groups_outlined, size: 18), - label: const Text( - 'GROUP RIDES', - style: TextStyle( - fontWeight: FontWeight.bold, - letterSpacing: 0.5, - fontSize: 12, - ), - ), - onPressed: () => context.push('/group-rides'), - ), - ), - const SizedBox(height: 20), - SizedBox( - width: double.infinity, - child: OutlinedButton.icon( - style: OutlinedButton.styleFrom( - foregroundColor: Colors.redAccent, - side: const BorderSide(color: Colors.redAccent), - padding: const EdgeInsets.symmetric(vertical: 12), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - icon: const Icon(Icons.logout, size: 18), - label: const Text( - 'SIGN OUT', - style: TextStyle( - fontWeight: FontWeight.bold, - letterSpacing: 0.8, - ), - ), - onPressed: () => - ref.read(authNotifierProvider.notifier).signOut(), - ), - ), - const SizedBox(height: 10), - SizedBox( - width: double.infinity, - child: TextButton.icon( - style: TextButton.styleFrom( - foregroundColor: Colors.redAccent, - ), - icon: const Icon(Icons.delete_forever_outlined, size: 18), - label: const Text('DELETE ACCOUNT'), - onPressed: () => _handleDeleteAccount(user), - ), - ), - ], - ), - ), - const SizedBox(height: 16), - CurrentPlanCard(isEvolution: isEvolution), - const SizedBox(height: 16), - _buildCloudSyncSection( - context: context, - isEvolution: isEvolution, - isAuthenticated: true, - ), - const SizedBox(height: 16), - _buildGroupRidePrivacyCard(isEvolution), - const SizedBox(height: 16), - _buildLocalRidingActiveCard(isEvolution), - ], - ); - } - - Widget _buildGroupRidePrivacyCard(bool isEvolution) { - final isAllowed = ref.watch(groupRideLiveSharingAllowedProvider); - return _buildAuthCard( - isEvolution: isEvolution, + final leftColumn = _buildAuthCard( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ - const Icon(Icons.share_location, color: Color(0xFF00E5FF), size: 20), + CircleAvatar( + radius: 28, + backgroundColor: context.motorColors.cyberCyan.withValues(alpha: 0.2), + backgroundImage: user.photoUrl != null + ? NetworkImage(user.photoUrl!) + : null, + child: user.photoUrl == null + ? Icon( + Icons.person, + color: context.motorColors.cyberCyan, + size: 32, + ) + : null, + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + user.displayLabel, + style: const TextStyle( + fontFamily: 'Sora', + fontSize: 18, + fontWeight: FontWeight.w800, + ), + ), + const SizedBox(height: 2), + Text( + user.email ?? 'No email associated', + style: TextStyle( + fontSize: 12, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 6), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 2, + ), + decoration: BoxDecoration( + color: context.motorColors.cyberCyan.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(4), + border: Border.all( + color: context.motorColors.cyberCyan, + width: 0.8, + ), + ), + child: Text( + 'AUTHENTICATED (${user.providerId ?? "cloud"})', + style: TextStyle( + fontFamily: 'Inter', + fontSize: 9, + fontWeight: FontWeight.w900, + color: context.motorColors.cyberCyan, + letterSpacing: 0.5, + ), + ), + ), + ], + ), + ), + ], + ), + const Divider(height: 32), + _buildInfoRow('User ID', user.uid, isMonospace: true), + if (user.providerId != null) + _buildInfoRow('Provider', user.providerId!), + if (user.createdAt != null) + _buildInfoRow( + 'Account Created', + user.createdAt!.toLocal().toString().split('.')[0], + ), + if (user.lastSignInTime != null) + _buildInfoRow( + 'Last Sign-In', + user.lastSignInTime!.toLocal().toString().split('.')[0], + ), + if (user.providerId == 'password' && !user.emailVerified) ...[ + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: context.motorColors.warningAmber.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: context.motorColors.warningAmber.withValues(alpha: 0.5), + ), + ), + child: Row( + children: [ + Expanded( + child: Text( + 'Your email is not verified. Verify it to protect account recovery.', + style: TextStyle( + fontSize: 11, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ), + TextButton( + onPressed: () => _sendVerification(), + child: const Text('VERIFY'), + ), + IconButton( + tooltip: 'Refresh verification status', + onPressed: () => _reloadUser(), + icon: const Icon(Icons.refresh, size: 18), + ), + ], + ), + ), + ], + const SizedBox(height: 20), + Row( + children: [ + Expanded( + child: OutlinedButton.icon( + style: OutlinedButton.styleFrom( + foregroundColor: context.motorColors.cyberCyan, + side: BorderSide(color: context.motorColors.cyberCyan), + minimumSize: const Size(0, 48), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + icon: const Icon(Icons.person_outline, size: 18), + label: const Text( + 'RIDER PROFILE', + style: TextStyle( + fontWeight: FontWeight.bold, + letterSpacing: 0.5, + fontSize: 11, + ), + ), + onPressed: () => context.push('/settings/profile'), + ), + ), const SizedBox(width: 8), - Text( - 'GROUP RIDE LIVE LOCATION', + Expanded( + child: OutlinedButton.icon( + style: OutlinedButton.styleFrom( + foregroundColor: context.motorColors.cyberCyan, + side: BorderSide(color: context.motorColors.cyberCyan), + minimumSize: const Size(0, 48), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + icon: const Icon(Icons.people_outline, size: 18), + label: const Text( + 'FRIENDS', + style: TextStyle( + fontWeight: FontWeight.bold, + letterSpacing: 0.5, + fontSize: 11, + ), + ), + onPressed: () => context.push('/settings/friends'), + ), + ), + ], + ), + const SizedBox(height: 10), + SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + style: ElevatedButton.styleFrom( + backgroundColor: context.motorColors.cyberCyan.withValues(alpha: 0.15), + foregroundColor: context.motorColors.cyberCyan, + side: BorderSide(color: context.motorColors.cyberCyan), + minimumSize: const Size(double.infinity, 48), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + icon: const Icon(Icons.groups_outlined, size: 18), + label: const Text( + 'GROUP RIDES', style: TextStyle( - fontFamily: isEvolution ? 'Sora' : null, - fontSize: 14, - fontWeight: FontWeight.w800, - color: Colors.white, + fontWeight: FontWeight.bold, letterSpacing: 0.5, + fontSize: 12, + ), + ), + onPressed: () => context.push('/group-rides'), + ), + ), + const SizedBox(height: 20), + SizedBox( + width: double.infinity, + child: OutlinedButton.icon( + style: OutlinedButton.styleFrom( + foregroundColor: context.motorColors.raceRed, + side: BorderSide(color: context.motorColors.raceRed), + minimumSize: const Size(double.infinity, 48), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + icon: const Icon(Icons.logout, size: 18), + label: const Text( + 'SIGN OUT', + style: TextStyle( + fontWeight: FontWeight.bold, + letterSpacing: 0.8, + ), + ), + onPressed: () => + ref.read(authNotifierProvider.notifier).signOut(), + ), + ), + const SizedBox(height: 10), + SizedBox( + width: double.infinity, + child: TextButton.icon( + style: TextButton.styleFrom( + foregroundColor: context.motorColors.raceRed, + minimumSize: const Size(double.infinity, 48), + ), + icon: const Icon(Icons.delete_forever_outlined, size: 18), + label: const Text('DELETE ACCOUNT'), + onPressed: () => _handleDeleteAccount(user), + ), + ), + ], + ), + ); + + final rightColumn = Column( + children: [ + const CurrentPlanCard(), + const SizedBox(height: 16), + _buildCloudSyncSection( + context: context, + isAuthenticated: true, + ), + const SizedBox(height: 16), + _buildGroupRidePrivacyCard(), + const SizedBox(height: 16), + _buildLocalRidingActiveCard(), + ], + ); + + if (isWide) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(child: leftColumn), + const SizedBox(width: 24), + Expanded(child: rightColumn), + ], + ); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + leftColumn, + const SizedBox(height: 16), + rightColumn, + ], + ); + } + + Widget _buildGroupRidePrivacyCard() { + final colors = context.motorColors; + final isAllowed = ref.watch(groupRideLiveSharingAllowedProvider); + return _buildAuthCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.share_location, color: colors.cyberCyan, size: 20), + const SizedBox(width: 8), + Expanded( + child: Text( + 'GROUP RIDE LIVE LOCATION', + style: TextStyle( + fontFamily: 'Sora', + fontSize: 13, + fontWeight: FontWeight.w800, + color: Theme.of(context).colorScheme.onSurface, + letterSpacing: 0.5, + ), ), ), ], ), const SizedBox(height: 8), - const Text( + Text( 'Control whether MotorSignal can broadcast your live location to other accepted riders during an active Group Ride.', - style: TextStyle(color: Colors.white70, fontSize: 12, height: 1.35), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontSize: 12, + height: 1.35, + ), ), const SizedBox(height: 12), SwitchListTile( contentPadding: EdgeInsets.zero, title: const Text( 'Allow Live Location Sharing', - style: TextStyle(color: Colors.white, fontSize: 13, fontWeight: FontWeight.w600), + style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600), ), subtitle: Text( isAllowed ? 'Permitted when opted in inside individual rides' : 'Master block: disabled for all group rides', style: TextStyle( - color: isAllowed ? const Color(0xFF00E5FF) : Colors.white38, + color: isAllowed ? colors.cyberCyan : colors.outlineVariant, fontSize: 11, ), ), value: isAllowed, - activeThumbColor: const Color(0xFF00E5FF), + activeThumbColor: colors.cyberCyan, onChanged: (val) { ref.read(groupRideLiveSharingAllowedProvider.notifier).state = val; }, ), const SizedBox(height: 6), - const Text( + Text( 'Privacy contract: Live location is ephemeral, shared only with accepted riders while a ride is active, and never saved to historical telemetry or rider profiles.', - style: TextStyle(color: Colors.white38, fontSize: 10, height: 1.3), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant.withValues(alpha: 0.6), + fontSize: 10, + height: 1.3, + ), ), ], ), @@ -819,132 +831,147 @@ class _AccountScreenState extends ConsumerState { Widget _buildAnonymousView( BuildContext context, AuthUser user, - bool isEvolution, bool isWide, ) { - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - _buildAuthCard( - isEvolution: isEvolution, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + final colors = context.motorColors; + final leftColumn = _buildAuthCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( children: [ - Row( - children: [ - CircleAvatar( - radius: 28, - backgroundColor: Colors.amberAccent.withValues(alpha: 0.2), - child: const Icon( - Icons.person_outline, - color: Colors.amberAccent, - size: 32, - ), - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Guest / Anonymous Rider', - style: TextStyle( - fontFamily: isEvolution ? 'Sora' : null, - fontSize: 17, - fontWeight: FontWeight.w800, - color: Colors.white, - ), - ), - const SizedBox(height: 4), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 2, - ), - decoration: BoxDecoration( - color: Colors.amberAccent.withValues(alpha: 0.15), - borderRadius: BorderRadius.circular(4), - border: Border.all( - color: Colors.amberAccent, - width: 0.8, - ), - ), - child: const Text( - 'OFFLINE GUEST SESSION', - style: TextStyle( - fontFamily: 'Inter', - fontSize: 9, - fontWeight: FontWeight.w900, - color: Colors.amberAccent, - letterSpacing: 0.5, - ), - ), - ), - ], - ), - ), - ], - ), - const Divider(height: 24, color: Colors.white12), - _buildInfoRow('Session ID', user.uid, isMonospace: true), - const SizedBox(height: 16), - const Text( - 'You are currently using MotorSignal in local guest mode. All rides are stored locally in your SQLite database. You can link a full cloud account anytime.', - style: TextStyle( - fontSize: 11, - color: Colors.white70, - height: 1.4, + CircleAvatar( + radius: 28, + backgroundColor: colors.warningAmber.withValues(alpha: 0.2), + child: Icon( + Icons.person_outline, + color: colors.warningAmber, + size: 32, ), ), - const SizedBox(height: 20), - SizedBox( - width: double.infinity, - child: ElevatedButton.icon( - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF00E5FF), - foregroundColor: Colors.black, - padding: const EdgeInsets.symmetric(vertical: 12), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Guest / Anonymous Rider', + style: TextStyle( + fontFamily: 'Sora', + fontSize: 17, + fontWeight: FontWeight.w800, + ), ), - ), - icon: const Icon(Icons.link, size: 18), - label: const Text( - 'SIGN IN WITH FULL ACCOUNT', - style: TextStyle(fontWeight: FontWeight.bold), - ), - onPressed: () => setState(() => _showAnonymousUpgrade = true), + const SizedBox(height: 4), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 2, + ), + decoration: BoxDecoration( + color: colors.warningAmber.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(4), + border: Border.all( + color: colors.warningAmber, + width: 0.8, + ), + ), + child: Text( + 'OFFLINE GUEST SESSION', + style: TextStyle( + fontFamily: 'Inter', + fontSize: 9, + fontWeight: FontWeight.w900, + color: colors.warningAmber, + letterSpacing: 0.5, + ), + ), + ), + ], ), ), ], ), - ), - const SizedBox(height: 16), - CurrentPlanCard(isEvolution: isEvolution), + const Divider(height: 24), + _buildInfoRow('Session ID', user.uid, isMonospace: true), + const SizedBox(height: 16), + Text( + 'You are currently using MotorSignal in local guest mode. All rides are stored locally in your SQLite database. You can link a full cloud account anytime.', + style: TextStyle( + fontSize: 11, + color: Theme.of(context).colorScheme.onSurfaceVariant, + height: 1.4, + ), + ), + const SizedBox(height: 20), + SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + style: ElevatedButton.styleFrom( + backgroundColor: colors.cyberCyan, + foregroundColor: Colors.black, + minimumSize: const Size(double.infinity, 48), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + icon: const Icon(Icons.link, size: 18), + label: const Text( + 'SIGN IN WITH FULL ACCOUNT', + style: TextStyle(fontWeight: FontWeight.bold), + ), + onPressed: () => setState(() => _showAnonymousUpgrade = true), + ), + ), + ], + ), + ); + + final rightColumn = Column( + children: [ + const CurrentPlanCard(), const SizedBox(height: 16), _buildCloudSyncSection( context: context, - isEvolution: isEvolution, isAuthenticated: false, ), const SizedBox(height: 16), - _buildLocalRidingActiveCard(isEvolution), + _buildLocalRidingActiveCard(), + ], + ); + + if (isWide) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(child: leftColumn), + const SizedBox(width: 24), + Expanded(child: rightColumn), + ], + ); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + leftColumn, + const SizedBox(height: 16), + rightColumn, ], ); } Widget _buildCloudSyncSection({ required BuildContext context, - required bool isEvolution, required bool isAuthenticated, }) { + final colors = context.motorColors; final syncState = ref.watch(syncStateProvider); final syncManager = ref.watch(syncManagerProvider); final restoreState = ref.watch(cloudRestoreControllerProvider); final restoreController = ref.read(cloudRestoreControllerProvider.notifier); return _buildAuthCard( - isEvolution: isEvolution, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -955,41 +982,37 @@ class _AccountScreenState extends ConsumerState { children: [ Icon( Icons.cloud_sync_outlined, - color: isEvolution - ? const Color(0xFF00E5FF) - : Colors.blueAccent, + color: colors.cyberCyan, size: 20, ), const SizedBox(width: 8), Text( 'MOTORCLOUD SYNC', style: TextStyle( - fontFamily: isEvolution ? 'Sora' : null, + fontFamily: 'Sora', fontSize: 12, fontWeight: FontWeight.w800, - color: isEvolution - ? const Color(0xFF00E5FF) - : Colors.white, + color: colors.cyberCyan, letterSpacing: 0.5, ), ), ], ), if (syncState.isProcessing) - const Row( + Row( children: [ SizedBox( width: 12, height: 12, child: CircularProgressIndicator( strokeWidth: 2, - color: Color(0xFF00E5FF), + color: colors.cyberCyan, ), ), - SizedBox(width: 6), + const SizedBox(width: 6), Text( 'Syncing...', - style: TextStyle(fontSize: 10, color: Colors.white70), + style: TextStyle(fontSize: 10, color: Theme.of(context).colorScheme.onSurfaceVariant), ), ], ), @@ -997,11 +1020,11 @@ class _AccountScreenState extends ConsumerState { ), const SizedBox(height: 12), if (!isAuthenticated) ...[ - const Text( + Text( 'Sign in to MotorCloud to back up eligible rides and restore them on another device. Local rides remain available on this device.', style: TextStyle( fontSize: 11, - color: Colors.white70, + color: Theme.of(context).colorScheme.onSurfaceVariant, height: 1.4, ), ), @@ -1009,9 +1032,9 @@ class _AccountScreenState extends ConsumerState { Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: Colors.white.withValues(alpha: 0.04), + color: colors.surfaceContainerHighest.withValues(alpha: 0.4), borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.white12), + border: Border.all(color: colors.outlineVariant), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, @@ -1019,17 +1042,17 @@ class _AccountScreenState extends ConsumerState { _buildSyncStatItem( 'Synced', '${syncState.syncedCount}', - Colors.greenAccent, + colors.launchGreen, ), _buildSyncStatItem( 'Pending', '${syncState.pendingCount}', - Colors.amberAccent, + colors.warningAmber, ), _buildSyncStatItem( 'Failed', '${syncState.failedCount}', - Colors.redAccent, + colors.raceRed, ), ], ), @@ -1042,17 +1065,17 @@ class _AccountScreenState extends ConsumerState { vertical: 6, ), decoration: BoxDecoration( - color: Colors.redAccent.withValues(alpha: 0.15), + color: colors.raceRed.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(6), border: Border.all( - color: Colors.redAccent.withValues(alpha: 0.4), + color: colors.raceRed.withValues(alpha: 0.4), ), ), child: Row( children: [ - const Icon( + Icon( Icons.warning_amber_rounded, - color: Colors.redAccent, + color: colors.raceRed, size: 16, ), const SizedBox(width: 8), @@ -1061,14 +1084,14 @@ class _AccountScreenState extends ConsumerState { '${syncState.failedCount} ride(s) need attention. Tap Retry to process.', style: const TextStyle( fontSize: 11, - color: Colors.white, + fontWeight: FontWeight.w600, ), ), ), TextButton( style: TextButton.styleFrom( visualDensity: VisualDensity.compact, - foregroundColor: Colors.redAccent, + foregroundColor: colors.raceRed, ), onPressed: () => syncManager.retryAllPending(), child: const Text( @@ -1100,9 +1123,9 @@ class _AccountScreenState extends ConsumerState { Expanded( child: ElevatedButton.icon( style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF00E5FF), + backgroundColor: colors.cyberCyan, foregroundColor: Colors.black, - padding: const EdgeInsets.symmetric(vertical: 10), + minimumSize: const Size(0, 48), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), @@ -1134,9 +1157,9 @@ class _AccountScreenState extends ConsumerState { Expanded( child: OutlinedButton.icon( style: OutlinedButton.styleFrom( - foregroundColor: Colors.white, - side: const BorderSide(color: Colors.white24), - padding: const EdgeInsets.symmetric(vertical: 10), + foregroundColor: Theme.of(context).colorScheme.onSurface, + side: BorderSide(color: colors.outlineVariant), + minimumSize: const Size(0, 48), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), @@ -1156,11 +1179,11 @@ class _AccountScreenState extends ConsumerState { ], ), const SizedBox(height: 12), - const Text( + Text( 'Download your backed-up ride history and telemetry to this device.', style: TextStyle( fontSize: 11, - color: Colors.white70, + color: Theme.of(context).colorScheme.onSurfaceVariant, height: 1.35, ), ), @@ -1169,20 +1192,20 @@ class _AccountScreenState extends ConsumerState { width: double.infinity, child: OutlinedButton.icon( style: OutlinedButton.styleFrom( - foregroundColor: Colors.white, - side: const BorderSide(color: Colors.white24), - padding: const EdgeInsets.symmetric(vertical: 10), + foregroundColor: Theme.of(context).colorScheme.onSurface, + side: BorderSide(color: colors.outlineVariant), + minimumSize: const Size(double.infinity, 48), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), icon: restoreState.isRestoring - ? const SizedBox( + ? SizedBox( width: 14, height: 14, child: CircularProgressIndicator( strokeWidth: 2, - color: Colors.white, + color: Theme.of(context).colorScheme.onSurface, ), ) : const Icon(Icons.cloud_download_outlined, size: 16), @@ -1233,7 +1256,6 @@ class _AccountScreenState extends ConsumerState { restoreState.message!, style: const TextStyle( fontSize: 11, - color: Colors.white, height: 1.3, ), ), @@ -1262,10 +1284,10 @@ class _AccountScreenState extends ConsumerState { const SizedBox(height: 2), Text( label.toUpperCase(), - style: const TextStyle( + style: TextStyle( fontSize: 9, fontWeight: FontWeight.w700, - color: Colors.white60, + color: Theme.of(context).colorScheme.onSurfaceVariant, letterSpacing: 0.5, ), ), @@ -1463,18 +1485,19 @@ class _AccountScreenState extends ConsumerState { } Color _restoreStatusColor(CloudRestoreStatus status) { + final colors = context.motorColors; switch (status) { case CloudRestoreStatus.success: - return Colors.greenAccent; + return colors.launchGreen; case CloudRestoreStatus.partialFailure: - return Colors.amberAccent; + return colors.warningAmber; case CloudRestoreStatus.authRequired: case CloudRestoreStatus.unavailable: case CloudRestoreStatus.failure: - return Colors.redAccent; + return colors.raceRed; case CloudRestoreStatus.idle: case CloudRestoreStatus.restoring: - return const Color(0xFF00E5FF); + return colors.cyberCyan; } } @@ -1494,119 +1517,128 @@ class _AccountScreenState extends ConsumerState { } } - Widget _buildOfflineNoticeCard(bool isEvolution, bool isFirebaseConfigured) { - return Container( - padding: const EdgeInsets.all(14), - decoration: BoxDecoration( - color: isEvolution ? const Color(0xFF0D0D0D) : const Color(0xFF1B2838), + Widget _buildOfflineNoticeCard(bool isFirebaseConfigured) { + final colors = context.motorColors; + return Material( + color: colors.surfaceContainer, + shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), - border: Border.all( + side: BorderSide( color: isFirebaseConfigured - ? const Color(0xFF00E5FF).withValues(alpha: 0.3) - : Colors.amberAccent.withValues(alpha: 0.4), + ? colors.cyberCyan.withValues(alpha: 0.3) + : colors.warningAmber.withValues(alpha: 0.4), width: 1.2, ), ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Icon( - isFirebaseConfigured - ? Icons.cloud_done_outlined - : Icons.offline_bolt_outlined, - color: isFirebaseConfigured - ? const Color(0xFF00E5FF) - : Colors.amberAccent, - size: 20, - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - isFirebaseConfigured - ? 'OFFLINE-FIRST ARCHITECTURE' - : 'STANDALONE OFFLINE MODE', - style: TextStyle( - fontFamily: isEvolution ? 'Sora' : null, - fontSize: 11, - fontWeight: FontWeight.w800, - color: isFirebaseConfigured - ? const Color(0xFF00E5FF) - : Colors.amberAccent, - letterSpacing: 0.6, - ), - ), - const SizedBox(height: 4), - Text( - isFirebaseConfigured - ? 'Signing in enables optional multi-device account sync. Offline riding, telemetry acquisition, local SQLite persistence, and forensic exports are always available without an account.' - : 'MotorSignal is operating in local mode. All instruments, 20Hz sensor fusion, garage storage, and ride recordings remain 100% operational.', - style: const TextStyle( - fontSize: 11, - color: Colors.white70, - height: 1.35, - ), - ), - ], + child: Padding( + padding: const EdgeInsets.all(14), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + isFirebaseConfigured + ? Icons.cloud_done_outlined + : Icons.offline_bolt_outlined, + color: isFirebaseConfigured + ? colors.cyberCyan + : colors.warningAmber, + size: 20, ), - ), - ], + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + isFirebaseConfigured + ? 'OFFLINE-FIRST ARCHITECTURE' + : 'STANDALONE OFFLINE MODE', + style: TextStyle( + fontFamily: 'Sora', + fontSize: 11, + fontWeight: FontWeight.w800, + color: isFirebaseConfigured + ? colors.cyberCyan + : colors.warningAmber, + letterSpacing: 0.6, + ), + ), + const SizedBox(height: 4), + Text( + isFirebaseConfigured + ? 'Signing in enables optional multi-device account sync. Offline riding, telemetry acquisition, local SQLite persistence, and forensic exports are always available without an account.' + : 'MotorSignal is operating in local mode. All instruments, 20Hz sensor fusion, garage storage, and ride recordings remain 100% operational.', + style: TextStyle( + fontSize: 11, + color: Theme.of(context).colorScheme.onSurfaceVariant, + height: 1.35, + ), + ), + ], + ), + ), + ], + ), ), ); } - Widget _buildLocalRidingActiveCard(bool isEvolution) { - return Container( - padding: const EdgeInsets.all(14), - decoration: BoxDecoration( - color: isEvolution ? const Color(0xFF0D0D0D) : const Color(0xFF141414), + Widget _buildLocalRidingActiveCard() { + final colors = context.motorColors; + return Material( + color: colors.surfaceContainer, + shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), - border: Border.all(color: const Color(0xFF1F1F1F), width: 1.2), + side: BorderSide(color: colors.outlineVariant, width: 1.2), ), - child: const Row( - children: [ - Icon(Icons.shield_outlined, color: Colors.greenAccent, size: 20), - SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'LOCAL DATA ISOLATION ACTIVE', - style: TextStyle( - fontSize: 11, - fontWeight: FontWeight.w800, - color: Colors.greenAccent, - letterSpacing: 0.6, + child: Padding( + padding: const EdgeInsets.all(14), + child: Row( + children: [ + Icon(Icons.shield_outlined, color: colors.launchGreen, size: 20), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'LOCAL DATA ISOLATION ACTIVE', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w800, + color: colors.launchGreen, + letterSpacing: 0.6, + ), ), - ), - SizedBox(height: 2), - Text( - 'Your local ride history remains available on this device. MotorCloud backup is optional and never required for recording or offline use.', - style: TextStyle(fontSize: 11, color: Colors.white60), - ), - ], + const SizedBox(height: 2), + Text( + 'Your local ride history remains available on this device. MotorCloud backup is optional and never required for recording or offline use.', + style: TextStyle(fontSize: 11, color: Theme.of(context).colorScheme.onSurfaceVariant), + ), + ], + ), ), - ), - ], + ], + ), ), ); } - Widget _buildAuthCard({required bool isEvolution, required Widget child}) { - return Container( - padding: const EdgeInsets.all(18), - decoration: BoxDecoration( - color: isEvolution ? const Color(0xFF0D0D0D) : null, + Widget _buildAuthCard({required Widget child}) { + final colors = context.motorColors; + return Material( + color: colors.surfaceContainer, + shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(14), - border: Border.all( - color: isEvolution ? const Color(0xFF1F1F1F) : Colors.white12, + side: BorderSide( + color: colors.outlineVariant, width: 1.2, ), ), - child: child, + child: Padding( + padding: const EdgeInsets.all(18), + child: child, + ), ); } @@ -1618,7 +1650,7 @@ class _AccountScreenState extends ConsumerState { children: [ Text( label, - style: const TextStyle(fontSize: 11, color: Colors.white54), + style: TextStyle(fontSize: 11, color: Theme.of(context).colorScheme.onSurfaceVariant), ), Flexible( child: Text( @@ -1629,7 +1661,6 @@ class _AccountScreenState extends ConsumerState { fontFamily: isMonospace ? 'monospace' : null, fontSize: 11, fontWeight: FontWeight.w600, - color: Colors.white, ), ), ), diff --git a/lib/features/settings/settings_screen.dart b/lib/features/settings/settings_screen.dart index 3d36034..1957e1b 100644 --- a/lib/features/settings/settings_screen.dart +++ b/lib/features/settings/settings_screen.dart @@ -2,21 +2,21 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../services/sensor_fusion_service.dart'; -import '../../services/map/map_provider_interface.dart'; -import '../../services/map/map_providers.dart'; +import '../../core/app_info.dart'; +import '../../core/responsive/viewport_size.dart'; +import '../../core/theme/theme_extensions.dart'; +import '../../core/theme/theme_mode_provider.dart'; +import '../../database/database.dart'; +import '../../features/rides/ride_state_machine.dart'; import '../../services/alerts/road_alert_models.dart'; import '../../services/alerts/road_alert_providers.dart'; import '../../services/alerts/road_alert_service.dart'; -import '../../features/rides/ride_state_machine.dart'; -import '../../database/database.dart'; -import '../../core/app_info.dart'; -import '../../services/field_test/field_test_logger.dart'; -import '../../core/theme/evolution_theme_config.dart'; -import '../../core/theme/theme_mode_provider.dart'; -import '../../services/osm_oauth_service.dart'; import '../../services/auth/auth_providers.dart'; -import 'widgets/evolution_settings_view.dart'; +import '../../services/field_test/field_test_logger.dart'; +import '../../services/map/map_provider_interface.dart'; +import '../../services/map/map_providers.dart'; +import '../../services/osm_oauth_service.dart'; +import '../../services/sensor_fusion_service.dart'; import 'widgets/current_plan_card.dart'; class SettingsScreen extends ConsumerStatefulWidget { @@ -57,1464 +57,1599 @@ class _SettingsScreenState extends ConsumerState { @override Widget build(BuildContext context) { - final themeMode = ref.watch(dashboardThemeModeProvider); - if (themeMode == DashboardThemeMode.evolution) { - return const EvolutionSettingsView(); - } - - final mapType = ref.watch(mapProviderTypeNotifierProvider); - final roadAlertsEnabled = ref.watch(roadAlertsEnabledProvider); - final audioAlertsEnabled = ref.watch(audioAlertsEnabledProvider); - final alertRadiusMeters = ref.watch(alertRadiusMetersProvider); - final enabledCategories = ref.watch(enabledCategoriesProvider); - final tomtomStatus = ref.watch(tomtomStatusProvider); - final communityStatus = ref.watch(communityStatusProvider); + final snapshot = context.responsive; + final isWide = snapshot.isExpanded && !snapshot.isShortViewport; return Scaffold( - appBar: AppBar(title: const Text('Settings')), - body: ListView( - padding: const EdgeInsets.all(16.0), - children: [ - _buildSectionHeader('Membership & Entitlement'), - const SizedBox(height: 12), - const CurrentPlanCard(), - const SizedBox(height: 24), - _buildSectionHeader('Account & Cloud Identity'), - const SizedBox(height: 12), - Card( - clipBehavior: Clip.antiAlias, - child: Consumer( - builder: (context, ref, _) { - final authState = ref.watch(authNotifierProvider); - final isAuthed = authState.isAuthenticated; - final user = authState.user; - - return InkWell( - key: const ValueKey('settings_account_card'), - onTap: () => context.go('/settings/account'), - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Row( - children: [ - Icon( - isAuthed - ? Icons.account_circle - : Icons.account_circle_outlined, - color: Colors.cyanAccent, - size: 24, - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - isAuthed - ? (user?.displayLabel ?? 'Rider Account') - : 'Sign In / Cloud Account', - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(height: 2), - Text( - isAuthed - ? (user?.email ?? - 'Cloud Identity Active • Offline rides protected') - : 'Optional cloud identity • Local riding always active', - style: const TextStyle( - fontSize: 12, - color: Colors.white70, - ), - ), - ], - ), - ), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 3, - ), - decoration: BoxDecoration( - color: isAuthed - ? Colors.cyanAccent.withValues(alpha: 0.15) - : Colors.white10, - borderRadius: BorderRadius.circular(6), - border: Border.all( - color: isAuthed - ? Colors.cyanAccent - : Colors.white24, - ), - ), - child: Text( - isAuthed ? 'SIGNED IN' : 'LOCAL', - style: TextStyle( - fontSize: 10, - fontWeight: FontWeight.bold, - color: isAuthed - ? Colors.cyanAccent - : Colors.white54, - ), - ), - ), - const SizedBox(width: 6), - const Icon( - Icons.chevron_right, - color: Colors.white54, - size: 20, - ), - ], - ), - ), - ); - }, - ), - ), - const SizedBox(height: 24), - _buildSectionHeader('Appearance & Display Theme'), - const SizedBox(height: 12), - Card( - clipBehavior: Clip.antiAlias, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Row( - children: [ - Icon( - Icons.brightness_medium_outlined, - color: Colors.cyanAccent, - size: 20, - ), - SizedBox(width: 10), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Display Appearance', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - ), - ), - SizedBox(height: 2), - Text( - 'System default, Daylight Light mode, or Cockpit OLED Dark mode.', - style: TextStyle( - fontSize: 12, - color: Colors.white70, - height: 1.3, - ), - ), - ], - ), - ), - ], - ), - const SizedBox(height: 16), - Consumer( - builder: (context, ref, _) { - final appThemeMode = ref.watch(appThemeModeProvider); - return SizedBox( - width: double.infinity, - child: SegmentedButton( - segments: const [ - ButtonSegment( - value: ThemeMode.system, - label: Text('System', style: TextStyle(fontSize: 12)), - icon: Icon(Icons.settings_brightness, size: 18), - ), - ButtonSegment( - value: ThemeMode.light, - label: Text('Light', style: TextStyle(fontSize: 12)), - icon: Icon(Icons.light_mode, size: 18), - ), - ButtonSegment( - value: ThemeMode.dark, - label: Text('Dark', style: TextStyle(fontSize: 12)), - icon: Icon(Icons.dark_mode, size: 18), - ), - ], - selected: {appThemeMode}, - onSelectionChanged: (Set selection) { - ref - .read(appThemeModeProvider.notifier) - .setThemeMode(selection.first); - }, - ), - ); - }, - ), - const Divider(height: 32), - const Row( - children: [ - Icon( - Icons.dashboard_customize_outlined, - color: Colors.cyanAccent, - size: 20, - ), - SizedBox(width: 10), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Dashboard Layout Generation', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - ), - ), - SizedBox(height: 2), - Text( - 'Legacy cockpit design layout selector (Classic / Evolution).', - style: TextStyle( - fontSize: 12, - color: Colors.white70, - height: 1.3, - ), - ), - ], - ), - ), - ], - ), - const SizedBox(height: 16), - SizedBox( - width: double.infinity, - child: SegmentedButton( - segments: const [ - ButtonSegment( - value: DashboardThemeMode.classic, - label: Text( - 'Classic Cockpit', - style: TextStyle(fontSize: 12), - ), - icon: Icon( - Icons.dashboard_customize_outlined, - size: 18, - ), - ), - ButtonSegment( - value: DashboardThemeMode.evolution, - label: Text( - 'MotorSignal Evolution', - style: TextStyle(fontSize: 12), - ), - icon: Icon(Icons.speed, size: 18), - ), - ], - selected: {themeMode}, - onSelectionChanged: (Set selection) { - ref - .read(dashboardThemeModeProvider.notifier) - .setThemeMode(selection.first); - }, - ), - ), - ], - ), - ), - ), - const SizedBox(height: 24), - _buildSectionHeader('Maps & Navigation Engine'), - const SizedBox(height: 12), - Card( - clipBehavior: Clip.antiAlias, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Row( - children: [ - Icon(Icons.map, color: Colors.cyanAccent, size: 20), - SizedBox(width: 10), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Active Map Engine', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - ), - ), - SizedBox(height: 2), - Text( - 'Select map rendering backend for live map, route preview, and replay.', - style: TextStyle( - fontSize: 12, - color: Colors.white70, - height: 1.3, - ), - ), - ], - ), - ), - ], - ), - const SizedBox(height: 16), - SizedBox( - width: double.infinity, - child: SegmentedButton( - segments: const [ - ButtonSegment( - value: AppMapType.openStreetMap, - label: Text( - 'OpenStreetMap', - style: TextStyle(fontSize: 12), - ), - icon: Icon(Icons.public, size: 18), - ), - ButtonSegment( - value: AppMapType.googleMaps, - label: Text( - 'Google Maps', - style: TextStyle(fontSize: 12), - ), - icon: Icon(Icons.map_outlined, size: 18), - ), - ], - selected: {mapType}, - onSelectionChanged: (Set selection) { - ref - .read(mapProviderTypeNotifierProvider.notifier) - .setMapProviderType(selection.first); - }, - ), - ), - const SizedBox(height: 16), - const Divider(), - const SizedBox(height: 8), - Consumer( - builder: (context, ref, _) { - final osmAuth = ref.watch(osmOAuthServiceProvider); - final isAuthed = osmAuth.isAuthenticated; - final user = osmAuth.user; - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Expanded( - child: Text( - 'OpenStreetMap Account', - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontWeight: FontWeight.w600, - fontSize: 13, - ), - ), - ), - const SizedBox(width: 8), - Chip( - label: Text( - isAuthed ? 'Connected' : 'Disconnected', - style: TextStyle( - fontSize: 10, - fontWeight: FontWeight.bold, - color: isAuthed - ? Colors.cyanAccent - : Colors.grey, - ), - ), - backgroundColor: isAuthed - ? Colors.cyanAccent.withValues(alpha: 0.15) - : Colors.white10, - visualDensity: VisualDensity.compact, - padding: EdgeInsets.zero, - ), - ], - ), - const SizedBox(height: 6), - if (isAuthed && user != null) ...[ - ListTile( - contentPadding: EdgeInsets.zero, - leading: CircleAvatar( - backgroundImage: user.avatarUrl != null - ? NetworkImage(user.avatarUrl!) - : null, - child: user.avatarUrl == null - ? const Icon(Icons.person) - : null, - ), - title: Text( - user.displayName, - style: const TextStyle( - fontWeight: FontWeight.bold, - ), - ), - subtitle: Text( - 'Changesets: ${user.changesetCount} • GPS Trace Sync Enabled', - style: const TextStyle(fontSize: 11), - ), - trailing: TextButton.icon( - style: TextButton.styleFrom( - foregroundColor: Colors.redAccent, - ), - icon: const Icon(Icons.link_off, size: 16), - label: const Text( - 'Disconnect', - style: TextStyle(fontSize: 12), - ), - onPressed: () => ref - .read(osmOAuthServiceProvider.notifier) - .logout(), - ), - ), - ] else ...[ - const Text( - 'Sign in to OpenStreetMap via OAuth 2.0 PKCE to upload ride GPX tracks and contribute traces.', - style: TextStyle( - fontSize: 11, - color: Colors.white60, - ), - ), - const SizedBox(height: 10), - ElevatedButton.icon( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.cyanAccent, - foregroundColor: Colors.black, - minimumSize: const Size(double.infinity, 38), - ), - icon: const Icon(Icons.login, size: 18), - label: const Text( - 'Connect OpenStreetMap', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 12, - ), - ), - onPressed: osmAuth.isLoading - ? null - : () => ref - .read(osmOAuthServiceProvider.notifier) - .startLoginFlow(), - ), - ], - ], - ); - }, - ), - ], - ), - ), - ), - - const SizedBox(height: 24), - _buildSectionHeader('Road Alerts & Incidents'), - const SizedBox(height: 12), - Card( - clipBehavior: Clip.antiAlias, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SwitchListTile( - activeThumbColor: Colors.cyanAccent, - contentPadding: const EdgeInsets.symmetric( - horizontal: 12.0, - vertical: 0.0, - ), - title: const Text( - 'Road Alerts', - style: TextStyle( - fontWeight: FontWeight.w600, - fontSize: 14, - ), - ), - subtitle: const Text( - 'Display real-time police, hazards, and traffic incidents on map.', - style: TextStyle(fontSize: 11), - ), - value: roadAlertsEnabled, - onChanged: (val) { - ref - .read(roadAlertsEnabledProvider.notifier) - .setEnabled(val); - }, - ), - const Divider(), - SwitchListTile( - activeThumbColor: Colors.cyanAccent, - contentPadding: const EdgeInsets.symmetric( - horizontal: 12.0, - vertical: 0.0, - ), - title: const Text( - 'Audio Warnings (TTS)', - style: TextStyle( - fontWeight: FontWeight.w600, - fontSize: 14, - ), - ), - subtitle: const Text( - 'Announce approaching hazards and enforcement over helmet headset.', - style: TextStyle(fontSize: 11), - ), - value: audioAlertsEnabled, - onChanged: roadAlertsEnabled - ? (val) { - ref - .read(audioAlertsEnabledProvider.notifier) - .setEnabled(val); - } - : null, - ), - const Divider(), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 8.0, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Alert Radius', - style: TextStyle( - fontWeight: FontWeight.w600, - fontSize: 14, - ), - ), - SizedBox(height: 4), - Text( - 'Distance to detect upcoming incidents', - style: TextStyle( - fontSize: 11, - color: Colors.white70, - ), - ), - ], - ), - ), - const SizedBox(width: 12), - DropdownButton( - value: alertRadiusMeters, - items: const [ - DropdownMenuItem( - value: 152.4, - child: Text('500 ft'), - ), - DropdownMenuItem( - value: 304.8, - child: Text('1,000 ft'), - ), - DropdownMenuItem( - value: 402.3, - child: Text('1/4 Mile'), - ), - DropdownMenuItem( - value: 804.7, - child: Text('1/2 Mile'), - ), - DropdownMenuItem( - value: 1609.3, - child: Text('1 Mile'), - ), - ], - onChanged: roadAlertsEnabled - ? (val) { - if (val != null) { - ref - .read( - alertRadiusMetersProvider.notifier, - ) - .setRadius(val); - } - } - : null, - ), - ], - ), - ), - const Divider(), - const Padding( - padding: EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 8.0, - ), - child: Text( - 'Alert Categories', - style: TextStyle( - fontWeight: FontWeight.w600, - fontSize: 14, - ), - ), - ), - ...RoadAlertCategory.values.map((cat) { - final isChecked = enabledCategories.contains(cat); - return CheckboxListTile( - activeColor: Colors.cyanAccent, - checkColor: Colors.black, - dense: true, - visualDensity: VisualDensity.compact, - contentPadding: const EdgeInsets.symmetric( - horizontal: 12.0, - vertical: 0.0, - ), - title: Text( - cat.displayName, - style: const TextStyle( - fontSize: 13, - fontWeight: FontWeight.w500, - ), - ), - secondary: Icon(cat.icon, color: cat.color, size: 20), - value: isChecked, - onChanged: roadAlertsEnabled - ? (val) { - ref - .read(enabledCategoriesProvider.notifier) - .toggleCategory(cat, val ?? true); - } - : null, - ); - }), - ], - ), - ), - ), - - const SizedBox(height: 16), - // Community Traffic Provider Card - Card( - clipBehavior: Clip.antiAlias, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildProviderHeader( - icon: Icons.local_police, - iconColor: Colors.blueAccent, - title: 'Community Traffic Provider', - subtitle: - 'Supplies Police, Speed Cameras, and Crowd Hazards.', - status: communityStatus, - ), - const SizedBox(height: 16), - TextField( - controller: _communityKeyController, - decoration: const InputDecoration( - labelText: 'Community / RapidAPI Key', - hintText: - 'Enter OpenWebNinja, RapidAPI, or Community API Key', - helperText: - 'Supplies Police, Speed Cameras, and Crowd Hazards.', - helperMaxLines: 2, - border: OutlineInputBorder(), - isDense: true, - ), - style: const TextStyle(fontSize: 14), - onChanged: (val) { - ref.read(communityApiKeyProvider.notifier).setApiKey(val); - }, - ), - const SizedBox(height: 10), - TextField( - controller: _communityEndpointController, - decoration: const InputDecoration( - labelText: 'Custom Endpoint URL (Optional)', - hintText: - 'e.g. https://google-map-places.p.rapidapi.com or custom URL', - helperText: - 'Leave blank for automatic OpenWebNinja / RapidAPI detection.', - helperMaxLines: 2, - border: OutlineInputBorder(), - isDense: true, - ), - style: const TextStyle(fontSize: 13), - onChanged: (val) { - ref - .read(communityEndpointUrlProvider.notifier) - .setEndpoint(val); - }, - ), - const SizedBox(height: 12), - Align( - alignment: Alignment.centerRight, - child: ElevatedButton.icon( - onPressed: _isTestingCommunity - ? null - : () async { - setState(() => _isTestingCommunity = true); - final messenger = ScaffoldMessenger.of(context); - final provider = ref.read( - communityProviderInstance, - ); - final status = await provider.testConnection( - _communityKeyController.text, - ); - ref.read(communityStatusProvider.notifier).state = - status; - setState(() => _isTestingCommunity = false); - - if (mounted) { - String msg = ''; - if (status == - RoadAlertProviderStatus.connected) { - msg = - 'Community / OpenWebNinja API Connected successfully!'; - } else if (status == - RoadAlertProviderStatus.authError) { - msg = - 'Authentication Error: Invalid or unauthorized key.'; - } else if (status == - RoadAlertProviderStatus.notConfigured) { - msg = 'API Key is empty.'; - } else if (status == - RoadAlertProviderStatus.networkError) { - msg = - 'Network Error: Check internet connection or endpoint.'; - } else if (status == - RoadAlertProviderStatus.rateLimited) { - msg = 'Rate Limited: Plan limit reached.'; - } else { - msg = 'Status: ${status.name}'; - } - messenger.showSnackBar( - SnackBar(content: Text(msg)), - ); - } - }, - icon: _isTestingCommunity - ? const SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : const Icon(Icons.sync), - label: const Text('Test Connection'), - ), - ), - ], - ), - ), - ), - - const SizedBox(height: 16), - // TomTom Provider API Key Card - Card( - clipBehavior: Clip.antiAlias, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildProviderHeader( - icon: Icons.security, - iconColor: Colors.cyanAccent, - title: 'TomTom Traffic Provider', - subtitle: - 'Supplies Accidents, Hazards, Closures & Construction data.', - status: tomtomStatus, - ), - const SizedBox(height: 16), - TextField( - controller: _tomtomKeyController, - decoration: const InputDecoration( - labelText: 'TomTom API Key', - hintText: 'Enter your free TomTom API Key', - helperText: - 'Supplies Accidents, Hazards, Closures & Construction data.', - helperMaxLines: 2, - border: OutlineInputBorder(), - isDense: true, - ), - style: const TextStyle(fontSize: 14), - onChanged: (val) { - ref.read(tomtomApiKeyProvider.notifier).setApiKey(val); - }, - ), - const SizedBox(height: 12), - Align( - alignment: Alignment.centerRight, - child: ElevatedButton.icon( - onPressed: _isTestingTomTom - ? null - : () async { - setState(() => _isTestingTomTom = true); - final messenger = ScaffoldMessenger.of(context); - final provider = ref.read(tomtomProviderInstance); - final status = await provider.testConnection( - _tomtomKeyController.text, - ); - ref.read(tomtomStatusProvider.notifier).state = - status; - setState(() => _isTestingTomTom = false); - - if (mounted) { - String msg = ''; - if (status == - RoadAlertProviderStatus.connected) { - msg = 'TomTom API Connected successfully!'; - } else if (status == - RoadAlertProviderStatus.authError) { - msg = 'Authentication Error: Invalid Key.'; - } else if (status == - RoadAlertProviderStatus.notConfigured) { - msg = 'Key is empty.'; - } else if (status == - RoadAlertProviderStatus.networkError) { - msg = - 'Network Error: Failed to connect to API.'; - } else if (status == - RoadAlertProviderStatus.rateLimited) { - msg = 'Rate Limited: Too many requests.'; - } else { - msg = 'Status: ${status.name}'; - } - messenger.showSnackBar( - SnackBar(content: Text(msg)), - ); - } - }, - icon: _isTestingTomTom - ? const SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : const Icon(Icons.sync), - label: const Text('Test Connection'), - ), - ), - ], - ), - ), - ), - - // Developer Test Mode Section (Debug Mode Only) - if (kDebugMode) ...[ - const SizedBox(height: 32), - const Text( - 'Developer Options', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: Colors.orangeAccent, - ), - ), - const SizedBox(height: 16), - Card( - color: Colors.orange.withValues(alpha: 0.1), - shape: RoundedRectangleBorder( - side: const BorderSide(color: Colors.orangeAccent), - borderRadius: BorderRadius.circular(12), - ), - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Row( - children: [ - Icon( - Icons.bug_report, - color: Colors.orangeAccent, - size: 20, - ), - SizedBox(width: 10), - Text( - 'Road Alert Test Mode', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - color: Colors.orangeAccent, - ), - ), - ], - ), - const SizedBox(height: 8), - const Text( - 'Generate simulated test alerts for real-device testing (Debug Build Only).', - style: TextStyle(fontSize: 12, color: Colors.white70), - ), - const SizedBox(height: 16), - Wrap( - spacing: 8, - runSpacing: 8, - children: [ - ElevatedButton.icon( - icon: const Icon( - Icons.local_police, - color: Colors.blueAccent, - size: 18, - ), - label: const Text('Test Police (100m)'), - onPressed: () { - final alert = RoadAlert( - id: 'sim_police', - providerId: 'mock', - category: RoadAlertCategory.police, - description: 'Simulated police alert', - latitude: 0, - longitude: 0, - distanceFromUser: 100.0, - reportedAt: DateTime.now(), - lastUpdated: DateTime.now(), - source: 'Test', - ); - ref - .read(roadAlertServiceProvider) - .simulateAlert(alert); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text( - 'Simulated Police Alert sent to Dashboard.', - ), - ), - ); - }, - ), - ElevatedButton.icon( - icon: const Icon( - Icons.warning, - color: Colors.orangeAccent, - size: 18, - ), - label: const Text('Test Hazard (250m)'), - onPressed: () { - final alert = RoadAlert( - id: 'sim_hazard', - providerId: 'mock', - category: RoadAlertCategory.hazard, - description: 'Simulated hazard alert', - latitude: 0, - longitude: 0, - distanceFromUser: 250.0, - reportedAt: DateTime.now(), - lastUpdated: DateTime.now(), - source: 'Test', - ); - ref - .read(roadAlertServiceProvider) - .simulateAlert(alert); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text( - 'Simulated Hazard Alert sent to Dashboard.', - ), - ), - ); - }, - ), - ], - ), - ], - ), - ), - ), - ], - - const SizedBox(height: 24), - _buildSectionHeader('Sensors'), - const SizedBox(height: 12), - Card( - clipBehavior: Clip.antiAlias, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Icon( - Icons.screen_rotation, - color: Colors.cyanAccent, - size: 20, - ), - const SizedBox(width: 10), - const Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Calibrate Mount Angle', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - ), - ), - SizedBox(height: 2), - Text( - 'Place the motorcycle perfectly upright and tap this to zero the lean angle sensor.', - style: TextStyle( - fontSize: 12, - color: Colors.white70, - height: 1.3, - ), - ), - ], - ), - ), - ], - ), - const SizedBox(height: 16), - Align( - alignment: Alignment.centerRight, - child: ElevatedButton( - onPressed: () async { - final session = ref.read(rideStateMachineProvider); - if (session.state == RideState.riding) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text( - 'Cannot calibrate while a ride is actively recording.', - ), - ), - ); - return; - } - ref - .read(sensorFusionServiceProvider) - .calibrateUpright(); - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Sensors Calibrated!'), - ), - ); - } - }, - child: const Text('Calibrate'), - ), - ), - ], - ), - ), - ), - const SizedBox(height: 24), - _buildSectionHeader('Data'), - const SizedBox(height: 12), - Card( - clipBehavior: Clip.antiAlias, - child: InkWell( - onTap: () => context.go('/settings/diagnostics'), - child: const Padding( - padding: EdgeInsets.all(16.0), - child: Row( - children: [ - Icon( - Icons.monitor_heart, - color: Colors.cyanAccent, - size: 20, - ), - SizedBox(width: 10), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Sensor Diagnostics', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - ), - ), - SizedBox(height: 2), - Text( - 'View raw IMU and GPS values.', - style: TextStyle( - fontSize: 12, - color: Colors.white70, - ), - ), - ], - ), - ), - Icon(Icons.chevron_right, color: Colors.white54, size: 20), - ], - ), - ), - ), - ), - const SizedBox(height: 16), - Card( - clipBehavior: Clip.antiAlias, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Icon( - Icons.delete_forever, - color: Colors.redAccent, - size: 20, - ), - const SizedBox(width: 10), - const Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Clear Ride History', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - ), - ), - SizedBox(height: 2), - Text( - 'Permanently delete all recorded rides and performance runs.', - style: TextStyle( - fontSize: 12, - color: Colors.white70, - height: 1.3, - ), - ), - ], - ), - ), - ], - ), - const SizedBox(height: 16), - Align( - alignment: Alignment.centerRight, - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.red.withValues(alpha: 0.2), - ), - onPressed: () async { - final session = ref.read(rideStateMachineProvider); - if (session.state == RideState.riding) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text( - 'Cannot clear history while a ride is actively recording.', - ), - ), - ); - return; - } - - final confirmed = await showDialog( - context: context, - builder: (ctx) => AlertDialog( - title: const Text('Clear All Ride History?'), - content: const Text( - 'This will permanently delete all saved rides, telemetry points, and events. This action cannot be undone.', - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(ctx, false), - child: const Text('CANCEL'), - ), - TextButton( - onPressed: () => Navigator.pop(ctx, true), - child: const Text( - 'CLEAR ALL', - style: TextStyle( - color: Colors.redAccent, - fontWeight: FontWeight.bold, - ), - ), - ), - ], - ), - ); - - if (confirmed == true && mounted) { - final db = ref.read(databaseProvider); - await db.transaction(() async { - await db.delete(db.telemetrySamples).go(); - await db.delete(db.rideEvents).go(); - await db.delete(db.performanceRuns).go(); - await db.delete(db.rides).go(); - }); - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('All ride history cleared.'), - ), - ); - } - } - }, - child: const Text( - 'Clear', - style: TextStyle(color: Colors.redAccent), - ), - ), - ), - ], - ), - ), - ), - const SizedBox(height: 24), - _buildFieldTestSection(context, ref), - const SizedBox(height: 24), - _AppVersionFooter(), - ], + appBar: AppBar( + title: const Text( + 'Settings', + style: TextStyle(fontWeight: FontWeight.w800, letterSpacing: 0.5), + ), + ), + body: SafeArea( + child: isWide ? _buildWideLayout(context) : _buildCompactLayout(context), ), ); } - Widget _buildFieldTestSection(BuildContext context, WidgetRef ref) { - final isEnabled = ref.watch(fieldTestModeEnabledProvider); - final activeSession = ref.watch(activeFieldTestSessionProvider); - final logger = ref.watch(fieldTestLoggerProvider); - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildSectionHeader('Engineering & Diagnostics'), - const SizedBox(height: 12), - Card( - clipBehavior: Clip.antiAlias, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Expanded( - child: Row( - children: [ - Icon( - Icons.biotech, - color: isEnabled - ? Colors.greenAccent - : Colors.white54, - size: 24, - ), - const SizedBox(width: 10), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Field Test Mode', - style: TextStyle( - fontSize: 15, - fontWeight: FontWeight.bold, - ), - ), - Text( - 'Silent black-box diagnostic recorder', - style: const TextStyle( - fontSize: 11, - color: Colors.white60, - ), - overflow: TextOverflow.ellipsis, - ), - ], - ), - ), - ], - ), + Widget _buildWideLayout(BuildContext context) { + return SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 16.0), + child: Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 1200), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Left Column: Account, Plan, Appearance, Sensors + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildSectionHeader('Membership & Entitlement'), + const SizedBox(height: 10), + const CurrentPlanCard(), + const SizedBox(height: 24), + _buildSectionHeader('Account & Cloud Identity'), + const SizedBox(height: 10), + _buildAccountCard(context), + const SizedBox(height: 12), + _buildRiderSocialLinks(context), + const SizedBox(height: 24), + _buildSectionHeader('Appearance & Display Theme'), + const SizedBox(height: 10), + _buildAppearanceCard(context), + const SizedBox(height: 24), + _buildSectionHeader('Sensors & Calibration'), + const SizedBox(height: 10), + _buildSensorsCard(context), + ], ), - const SizedBox(width: 8), - Switch( - value: isEnabled, - activeThumbColor: Colors.greenAccent, - onChanged: (val) { - ref - .read(fieldTestModeEnabledProvider.notifier) - .setEnabled(val); - }, - ), - ], - ), - const Divider(height: 24, color: Colors.white12), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - 'Logger Status:', - style: TextStyle(color: Colors.white70, fontSize: 13), - ), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 2, - ), - decoration: BoxDecoration( - color: activeSession != null - ? Colors.greenAccent.withValues(alpha: 0.15) - : (isEnabled - ? Colors.cyanAccent.withValues(alpha: 0.15) - : Colors.white10), - borderRadius: BorderRadius.circular(6), - border: Border.all( - color: activeSession != null - ? Colors.greenAccent - : (isEnabled - ? Colors.cyanAccent - : Colors.white30), - ), - ), - child: Text( - activeSession != null - ? 'RECORDING (Ride #$activeSession)' - : (isEnabled - ? 'ARMED (Auto-starts on Ride)' - : 'OFF'), - style: TextStyle( - fontSize: 11, - fontWeight: FontWeight.bold, - color: activeSession != null - ? Colors.greenAccent - : (isEnabled - ? Colors.cyanAccent - : Colors.white54), - ), - ), - ), - ], - ), - const SizedBox(height: 16), - Row( - children: [ - Expanded( - child: ElevatedButton.icon( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.greenAccent.withValues( - alpha: 0.2, - ), - foregroundColor: Colors.greenAccent, - side: const BorderSide(color: Colors.greenAccent), - ), - icon: const Icon(Icons.fact_check, size: 18), - label: const Text( - 'Pre-Ride Check', - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - ), - ), - onPressed: () => - context.go('/settings/field-test-pre-ride-check'), - ), - ), - const SizedBox(width: 8), - Expanded( - child: ElevatedButton.icon( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.cyanAccent.withValues( - alpha: 0.15, - ), - foregroundColor: Colors.cyanAccent, - side: const BorderSide(color: Colors.cyanAccent), - ), - icon: const Icon(Icons.assessment, size: 18), - label: const Text( - 'View Reports', - style: TextStyle(fontSize: 12), - ), - onPressed: () => - context.go('/settings/field-test-report'), - ), - ), - const SizedBox(width: 8), - Expanded( - child: OutlinedButton.icon( - icon: const Icon(Icons.receipt_long, size: 18), - label: const Text( - 'Live Log', - style: TextStyle(fontSize: 12), - ), - onPressed: () => - context.go('/settings/field-test-live-log'), - ), - ), - ], - ), - const SizedBox(height: 8), - Align( - alignment: Alignment.centerRight, - child: TextButton.icon( - style: TextButton.styleFrom( - foregroundColor: Colors.redAccent, - ), - icon: const Icon(Icons.delete_sweep, size: 16), - label: const Text( - 'Clear Field Test Logs', - style: TextStyle(fontSize: 11), - ), - onPressed: () async { - final session = ref.read(rideStateMachineProvider); - if (session.state == RideState.riding) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text( - 'Cannot clear logs while a ride is recording.', - ), - ), - ); - return; - } - - final confirm = await showDialog( - context: context, - builder: (ctx) => AlertDialog( - title: const Text('Clear All Field Test Logs?'), - content: const Text( - 'This will delete all stored black-box diagnostic sessions. Standard ride history will NOT be affected.', - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(ctx, false), - child: const Text('CANCEL'), - ), - TextButton( - onPressed: () => Navigator.pop(ctx, true), - child: const Text( - 'CLEAR', - style: TextStyle(color: Colors.redAccent), - ), - ), - ], - ), - ); - - if (confirm == true && context.mounted) { - await logger.clearAllSessions(); - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Field test logs cleared.'), - ), - ); - } - } - }, ), - ), - ], - ), + const SizedBox(width: 24), + // Right Column: Maps, Road Alerts, Data & Diagnostics + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildSectionHeader('Maps & Navigation Engine'), + const SizedBox(height: 10), + _buildMapsCard(context), + const SizedBox(height: 24), + _buildSectionHeader('Road Alerts & Incidents'), + const SizedBox(height: 10), + _buildRoadAlertsCard(context), + const SizedBox(height: 16), + _buildCommunityProviderCard(context), + const SizedBox(height: 16), + _buildTomTomProviderCard(context), + if (kDebugMode) ...[ + const SizedBox(height: 16), + _buildDeveloperAlertsCard(context), + ], + const SizedBox(height: 24), + _buildSectionHeader('Data & Diagnostics'), + const SizedBox(height: 10), + _buildDiagnosticsLinkCard(context), + const SizedBox(height: 12), + _buildClearHistoryCard(context), + const SizedBox(height: 16), + _buildFieldTestSection(context), + ], + ), + ), + ], + ), + const SizedBox(height: 32), + const _AppVersionFooter(), + const SizedBox(height: 16), + ], ), ), + ), + ); + } + + Widget _buildCompactLayout(BuildContext context) { + return ListView( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 14.0), + children: [ + _buildSectionHeader('Membership & Entitlement'), + const SizedBox(height: 10), + const CurrentPlanCard(), + const SizedBox(height: 24), + _buildSectionHeader('Account & Cloud Identity'), + const SizedBox(height: 10), + _buildAccountCard(context), + const SizedBox(height: 12), + _buildRiderSocialLinks(context), + const SizedBox(height: 24), + _buildSectionHeader('Appearance & Display Theme'), + const SizedBox(height: 10), + _buildAppearanceCard(context), + const SizedBox(height: 24), + _buildSectionHeader('Sensors & Calibration'), + const SizedBox(height: 10), + _buildSensorsCard(context), + const SizedBox(height: 24), + _buildSectionHeader('Maps & Navigation Engine'), + const SizedBox(height: 10), + _buildMapsCard(context), + const SizedBox(height: 24), + _buildSectionHeader('Road Alerts & Incidents'), + const SizedBox(height: 10), + _buildRoadAlertsCard(context), + const SizedBox(height: 16), + _buildCommunityProviderCard(context), + const SizedBox(height: 16), + _buildTomTomProviderCard(context), + if (kDebugMode) ...[ + const SizedBox(height: 16), + _buildDeveloperAlertsCard(context), + ], + const SizedBox(height: 24), + _buildSectionHeader('Data & Diagnostics'), + const SizedBox(height: 10), + _buildDiagnosticsLinkCard(context), + const SizedBox(height: 12), + _buildClearHistoryCard(context), + const SizedBox(height: 16), + _buildFieldTestSection(context), + const SizedBox(height: 28), + const _AppVersionFooter(), + const SizedBox(height: 16), ], ); } Widget _buildSectionHeader(String title) { return Text( - title, - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - letterSpacing: 0.4, - color: Colors.cyanAccent, + title.toUpperCase(), + style: TextStyle( + fontFamily: 'Sora', + fontSize: 12, + fontWeight: FontWeight.w800, + letterSpacing: 0.8, + color: context.motorColors.cyberCyan, + ), + ); + } + + Widget _buildAccountCard(BuildContext context) { + final colors = context.motorColors; + return Card( + clipBehavior: Clip.antiAlias, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14), + side: BorderSide(color: colors.outlineVariant), + ), + child: Consumer( + builder: (context, ref, _) { + final authState = ref.watch(authNotifierProvider); + final isAuthed = authState.isAuthenticated; + final user = authState.user; + + return InkWell( + key: const ValueKey('settings_account_card'), + onTap: () => context.go('/settings/account'), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + children: [ + Icon( + isAuthed ? Icons.account_circle : Icons.account_circle_outlined, + color: isAuthed ? colors.cyberCyan : colors.outline, + size: 28, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + isAuthed + ? (user?.displayLabel ?? 'Rider Account') + : 'Sign In / Cloud Account', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(height: 2), + Text( + isAuthed + ? (user?.email ?? 'Cloud Identity Active • Offline rides protected') + : 'Optional cloud identity • Local riding always active', + style: TextStyle( + fontSize: 12, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + decoration: BoxDecoration( + color: isAuthed + ? colors.cyberCyan.withValues(alpha: 0.15) + : colors.outlineVariant.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(6), + border: Border.all( + color: isAuthed ? colors.cyberCyan : colors.outlineVariant, + ), + ), + child: Text( + isAuthed ? 'SIGNED IN' : 'LOCAL', + style: TextStyle( + fontFamily: 'Inter', + fontSize: 10, + fontWeight: FontWeight.bold, + color: isAuthed ? colors.cyberCyan : colors.outline, + letterSpacing: 0.5, + ), + ), + ), + const SizedBox(width: 6), + Icon( + Icons.chevron_right, + color: colors.outline, + size: 20, + ), + ], + ), + ), + ); + }, + ), + ); + } + + Widget _buildRiderSocialLinks(BuildContext context) { + final colors = context.motorColors; + return LayoutBuilder( + builder: (context, constraints) { + final useRow = constraints.maxWidth > 340; + final buttons = [ + OutlinedButton.icon( + style: OutlinedButton.styleFrom( + foregroundColor: Theme.of(context).colorScheme.onSurface, + side: BorderSide(color: colors.outlineVariant), + minimumSize: const Size(0, 48), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + icon: const Icon(Icons.person_outline, size: 16), + label: const FittedBox( + fit: BoxFit.scaleDown, + child: Text( + 'PROFILE', + style: TextStyle(fontSize: 11, fontWeight: FontWeight.bold), + ), + ), + onPressed: () => context.push('/settings/profile'), + ), + OutlinedButton.icon( + style: OutlinedButton.styleFrom( + foregroundColor: Theme.of(context).colorScheme.onSurface, + side: BorderSide(color: colors.outlineVariant), + minimumSize: const Size(0, 48), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + icon: const Icon(Icons.people_outline, size: 16), + label: const FittedBox( + fit: BoxFit.scaleDown, + child: Text( + 'FRIENDS', + style: TextStyle(fontSize: 11, fontWeight: FontWeight.bold), + ), + ), + onPressed: () => context.push('/settings/friends'), + ), + OutlinedButton.icon( + style: OutlinedButton.styleFrom( + foregroundColor: colors.cyberCyan, + side: BorderSide(color: colors.cyberCyan.withValues(alpha: 0.6)), + minimumSize: const Size(0, 48), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + icon: const Icon(Icons.groups_outlined, size: 16), + label: const FittedBox( + fit: BoxFit.scaleDown, + child: Text( + 'GROUP RIDES', + style: TextStyle(fontSize: 11, fontWeight: FontWeight.bold), + ), + ), + onPressed: () => context.push('/group-rides'), + ), + ]; + + if (useRow) { + return Row( + children: [ + Expanded(child: buttons[0]), + const SizedBox(width: 8), + Expanded(child: buttons[1]), + const SizedBox(width: 8), + Expanded(child: buttons[2]), + ], + ); + } + + return Wrap( + spacing: 8, + runSpacing: 8, + children: buttons + .map((b) => SizedBox( + width: (constraints.maxWidth - 8) / 2, + child: b, + )) + .toList(), + ); + }, + ); + } + + Widget _buildAppearanceCard(BuildContext context) { + final colors = context.motorColors; + return Card( + clipBehavior: Clip.antiAlias, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14), + side: BorderSide(color: colors.outlineVariant), + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.brightness_medium_outlined, + color: colors.cyberCyan, + size: 20, + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Display Appearance', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(height: 2), + Text( + 'System default, Daylight Light mode, or Cockpit OLED Dark mode.', + style: TextStyle( + fontSize: 12, + color: Theme.of(context).colorScheme.onSurfaceVariant, + height: 1.3, + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 16), + Consumer( + builder: (context, ref, _) { + final appThemeMode = ref.watch(appThemeModeProvider); + return SizedBox( + width: double.infinity, + child: SegmentedButton( + showSelectedIcon: false, + style: const ButtonStyle( + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + visualDensity: VisualDensity.compact, + ), + segments: const [ + ButtonSegment( + value: ThemeMode.system, + label: Text('System', style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold)), + icon: Icon(Icons.settings_brightness, size: 16), + ), + ButtonSegment( + value: ThemeMode.light, + label: Text('Light', style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold)), + icon: Icon(Icons.light_mode, size: 16), + ), + ButtonSegment( + value: ThemeMode.dark, + label: Text('Dark', style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold)), + icon: Icon(Icons.dark_mode, size: 16), + ), + ], + selected: {appThemeMode}, + onSelectionChanged: (Set selection) { + ref + .read(appThemeModeProvider.notifier) + .setThemeMode(selection.first); + }, + ), + ); + }, + ), + ], + ), + ), + ); + } + + Widget _buildSensorsCard(BuildContext context) { + final colors = context.motorColors; + return Card( + clipBehavior: Clip.antiAlias, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14), + side: BorderSide(color: colors.outlineVariant), + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + Icons.screen_rotation, + color: colors.cyberCyan, + size: 20, + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Calibrate Mount Angle', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(height: 2), + Text( + 'Place the motorcycle upright and tap Calibrate to zero the lean angle sensor.', + style: TextStyle( + fontSize: 12, + color: Theme.of(context).colorScheme.onSurfaceVariant, + height: 1.3, + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 14), + Align( + alignment: Alignment.centerRight, + child: ElevatedButton.icon( + style: ElevatedButton.styleFrom( + backgroundColor: colors.cyberCyan, + foregroundColor: Colors.black, + minimumSize: const Size(120, 48), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + icon: const Icon(Icons.tune, size: 18), + label: const Text( + 'CALIBRATE', + style: TextStyle(fontSize: 12, fontWeight: FontWeight.w800), + ), + onPressed: () async { + final session = ref.read(rideStateMachineProvider); + if (session.state == RideState.riding) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Cannot calibrate while a ride is actively recording.', + ), + ), + ); + return; + } + ref.read(sensorFusionServiceProvider).calibrateUpright(); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Sensors Calibrated upright!'), + ), + ); + } + }, + ), + ), + ], + ), + ), + ); + } + + Widget _buildMapsCard(BuildContext context) { + final colors = context.motorColors; + final mapType = ref.watch(mapProviderTypeNotifierProvider); + + return Card( + clipBehavior: Clip.antiAlias, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14), + side: BorderSide(color: colors.outlineVariant), + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.map_outlined, color: colors.cyberCyan, size: 20), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Active Map Engine', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(height: 2), + Text( + 'Select map rendering backend for live map, route preview, and replay.', + style: TextStyle( + fontSize: 12, + color: Theme.of(context).colorScheme.onSurfaceVariant, + height: 1.3, + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 16), + SizedBox( + width: double.infinity, + child: SegmentedButton( + showSelectedIcon: false, + style: const ButtonStyle( + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + visualDensity: VisualDensity.compact, + ), + segments: const [ + ButtonSegment( + value: AppMapType.openStreetMap, + label: FittedBox( + fit: BoxFit.scaleDown, + child: Text('OpenStreetMap', style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold)), + ), + icon: Icon(Icons.public, size: 16), + ), + ButtonSegment( + value: AppMapType.googleMaps, + label: FittedBox( + fit: BoxFit.scaleDown, + child: Text('Google Maps', style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold)), + ), + icon: Icon(Icons.map_outlined, size: 16), + ), + ], + selected: {mapType}, + onSelectionChanged: (Set selection) { + ref + .read(mapProviderTypeNotifierProvider.notifier) + .setMapProviderType(selection.first); + }, + ), + ), + const SizedBox(height: 16), + const Divider(), + const SizedBox(height: 8), + Consumer( + builder: (context, ref, _) { + final osmAuth = ref.watch(osmOAuthServiceProvider); + final isAuthed = osmAuth.isAuthenticated; + final user = osmAuth.user; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Expanded( + child: Text( + 'OpenStreetMap Account', + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 13, + ), + ), + ), + const SizedBox(width: 8), + Chip( + label: Text( + isAuthed ? 'Connected' : 'Disconnected', + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.bold, + color: isAuthed ? colors.cyberCyan : colors.outline, + ), + ), + backgroundColor: isAuthed + ? colors.cyberCyan.withValues(alpha: 0.15) + : colors.outlineVariant.withValues(alpha: 0.2), + visualDensity: VisualDensity.compact, + padding: EdgeInsets.zero, + ), + ], + ), + const SizedBox(height: 6), + if (isAuthed && user != null) ...[ + ListTile( + contentPadding: EdgeInsets.zero, + leading: CircleAvatar( + backgroundImage: user.avatarUrl != null + ? NetworkImage(user.avatarUrl!) + : null, + child: user.avatarUrl == null + ? const Icon(Icons.person) + : null, + ), + title: Text( + user.displayName, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + subtitle: Text( + 'Changesets: ${user.changesetCount} • GPS Trace Sync Enabled', + style: const TextStyle(fontSize: 11), + ), + trailing: TextButton.icon( + style: TextButton.styleFrom( + foregroundColor: colors.raceRed, + ), + icon: const Icon(Icons.link_off, size: 16), + label: const Text( + 'Disconnect', + style: TextStyle(fontSize: 12), + ), + onPressed: () => ref + .read(osmOAuthServiceProvider.notifier) + .logout(), + ), + ), + ] else ...[ + Text( + 'Sign in to OpenStreetMap via OAuth 2.0 PKCE to upload ride GPX tracks and contribute traces.', + style: TextStyle( + fontSize: 11, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 10), + ElevatedButton.icon( + style: ElevatedButton.styleFrom( + backgroundColor: colors.cyberCyan, + foregroundColor: Colors.black, + minimumSize: const Size(double.infinity, 48), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + icon: const Icon(Icons.login, size: 18), + label: const Text( + 'Connect OpenStreetMap', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ), + onPressed: osmAuth.isLoading + ? null + : () => ref + .read(osmOAuthServiceProvider.notifier) + .startLoginFlow(), + ), + ], + ], + ); + }, + ), + ], + ), + ), + ); + } + + Widget _buildRoadAlertsCard(BuildContext context) { + final colors = context.motorColors; + final roadAlertsEnabled = ref.watch(roadAlertsEnabledProvider); + final audioAlertsEnabled = ref.watch(audioAlertsEnabledProvider); + final alertRadiusMeters = ref.watch(alertRadiusMetersProvider); + final enabledCategories = ref.watch(enabledCategoriesProvider); + + return Card( + clipBehavior: Clip.antiAlias, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14), + side: BorderSide(color: colors.outlineVariant), + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SwitchListTile( + activeThumbColor: colors.cyberCyan, + contentPadding: EdgeInsets.zero, + title: const Text( + 'Road Alerts', + style: TextStyle(fontWeight: FontWeight.w700, fontSize: 14), + ), + subtitle: Text( + 'Display real-time police, hazards, and traffic incidents on map.', + style: TextStyle( + fontSize: 11, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + value: roadAlertsEnabled, + onChanged: (val) { + ref.read(roadAlertsEnabledProvider.notifier).setEnabled(val); + }, + ), + const Divider(), + SwitchListTile( + activeThumbColor: colors.cyberCyan, + contentPadding: EdgeInsets.zero, + title: const Text( + 'Audio Warnings (TTS)', + style: TextStyle(fontWeight: FontWeight.w700, fontSize: 14), + ), + subtitle: Text( + 'Announce approaching hazards and enforcement over helmet headset.', + style: TextStyle( + fontSize: 11, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + value: audioAlertsEnabled, + onChanged: roadAlertsEnabled + ? (val) { + ref + .read(audioAlertsEnabledProvider.notifier) + .setEnabled(val); + } + : null, + ), + const Divider(), + Padding( + padding: const EdgeInsets.symmetric(vertical: 6.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Alert Radius', + style: TextStyle( + fontWeight: FontWeight.w700, + fontSize: 14, + ), + ), + const SizedBox(height: 2), + Text( + 'Distance to detect upcoming incidents', + style: TextStyle( + fontSize: 11, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + const SizedBox(width: 12), + DropdownButton( + value: alertRadiusMeters, + items: const [ + DropdownMenuItem( + value: 152.4, + child: Text('500 ft'), + ), + DropdownMenuItem( + value: 304.8, + child: Text('1,000 ft'), + ), + DropdownMenuItem( + value: 402.3, + child: Text('1/4 Mile'), + ), + DropdownMenuItem( + value: 804.7, + child: Text('1/2 Mile'), + ), + DropdownMenuItem( + value: 1609.3, + child: Text('1 Mile'), + ), + ], + onChanged: roadAlertsEnabled + ? (val) { + if (val != null) { + ref + .read(alertRadiusMetersProvider.notifier) + .setRadius(val); + } + } + : null, + ), + ], + ), + ), + const Divider(), + const Padding( + padding: EdgeInsets.symmetric(vertical: 6.0), + child: Text( + 'Alert Categories', + style: TextStyle( + fontWeight: FontWeight.w700, + fontSize: 14, + ), + ), + ), + ...RoadAlertCategory.values.map((cat) { + final isChecked = enabledCategories.contains(cat); + return CheckboxListTile( + activeColor: colors.cyberCyan, + checkColor: Colors.black, + dense: true, + visualDensity: VisualDensity.compact, + contentPadding: EdgeInsets.zero, + title: Text( + cat.displayName, + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + ), + ), + secondary: Icon(cat.icon, color: cat.color, size: 20), + value: isChecked, + onChanged: roadAlertsEnabled + ? (val) { + ref + .read(enabledCategoriesProvider.notifier) + .toggleCategory(cat, val ?? true); + } + : null, + ); + }), + ], + ), + ), + ); + } + + Widget _buildCommunityProviderCard(BuildContext context) { + final colors = context.motorColors; + final communityStatus = ref.watch(communityStatusProvider); + + return Card( + clipBehavior: Clip.antiAlias, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14), + side: BorderSide(color: colors.outlineVariant), + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildProviderHeader( + icon: Icons.local_police, + iconColor: colors.velocityBlue, + title: 'Community Traffic Provider', + subtitle: 'Supplies Police, Speed Cameras, and Crowd Hazards.', + status: communityStatus, + ), + const SizedBox(height: 16), + TextField( + controller: _communityKeyController, + decoration: const InputDecoration( + labelText: 'Community / RapidAPI Key', + hintText: 'Enter OpenWebNinja, RapidAPI, or Community API Key', + helperText: 'Supplies Police, Speed Cameras, and Crowd Hazards.', + helperMaxLines: 2, + border: OutlineInputBorder(), + isDense: true, + ), + style: const TextStyle(fontSize: 14), + onChanged: (val) { + ref.read(communityApiKeyProvider.notifier).setApiKey(val); + }, + ), + const SizedBox(height: 10), + TextField( + controller: _communityEndpointController, + decoration: const InputDecoration( + labelText: 'Custom Endpoint URL (Optional)', + hintText: 'e.g. https://google-map-places.p.rapidapi.com or custom URL', + helperText: 'Leave blank for automatic OpenWebNinja / RapidAPI detection.', + helperMaxLines: 2, + border: OutlineInputBorder(), + isDense: true, + ), + style: const TextStyle(fontSize: 13), + onChanged: (val) { + ref.read(communityEndpointUrlProvider.notifier).setEndpoint(val); + }, + ), + const SizedBox(height: 12), + Align( + alignment: Alignment.centerRight, + child: ElevatedButton.icon( + style: ElevatedButton.styleFrom( + minimumSize: const Size(140, 48), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + onPressed: _isTestingCommunity + ? null + : () async { + setState(() => _isTestingCommunity = true); + final messenger = ScaffoldMessenger.of(context); + final provider = ref.read(communityProviderInstance); + final status = await provider.testConnection( + _communityKeyController.text, + ); + ref.read(communityStatusProvider.notifier).state = status; + setState(() => _isTestingCommunity = false); + + if (mounted) { + String msg = ''; + if (status == RoadAlertProviderStatus.connected) { + msg = 'Community / OpenWebNinja API Connected successfully!'; + } else if (status == RoadAlertProviderStatus.authError) { + msg = 'Authentication Error: Invalid or unauthorized key.'; + } else if (status == RoadAlertProviderStatus.notConfigured) { + msg = 'API Key is empty.'; + } else if (status == RoadAlertProviderStatus.networkError) { + msg = 'Network Error: Check internet connection or endpoint.'; + } else if (status == RoadAlertProviderStatus.rateLimited) { + msg = 'Rate Limited: Plan limit reached.'; + } else { + msg = 'Status: ${status.name}'; + } + messenger.showSnackBar(SnackBar(content: Text(msg))); + } + }, + icon: _isTestingCommunity + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.sync, size: 18), + label: const Text('Test Connection'), + ), + ), + ], + ), + ), + ); + } + + Widget _buildTomTomProviderCard(BuildContext context) { + final colors = context.motorColors; + final tomtomStatus = ref.watch(tomtomStatusProvider); + + return Card( + clipBehavior: Clip.antiAlias, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14), + side: BorderSide(color: colors.outlineVariant), + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildProviderHeader( + icon: Icons.security, + iconColor: colors.cyberCyan, + title: 'TomTom Traffic Provider', + subtitle: 'Supplies Accidents, Hazards, Closures & Construction data.', + status: tomtomStatus, + ), + const SizedBox(height: 16), + TextField( + controller: _tomtomKeyController, + decoration: const InputDecoration( + labelText: 'TomTom API Key', + hintText: 'Enter your free TomTom API Key', + helperText: 'Supplies Accidents, Hazards, Closures & Construction data.', + helperMaxLines: 2, + border: OutlineInputBorder(), + isDense: true, + ), + style: const TextStyle(fontSize: 14), + onChanged: (val) { + ref.read(tomtomApiKeyProvider.notifier).setApiKey(val); + }, + ), + const SizedBox(height: 12), + Align( + alignment: Alignment.centerRight, + child: ElevatedButton.icon( + style: ElevatedButton.styleFrom( + minimumSize: const Size(140, 48), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + onPressed: _isTestingTomTom + ? null + : () async { + setState(() => _isTestingTomTom = true); + final messenger = ScaffoldMessenger.of(context); + final provider = ref.read(tomtomProviderInstance); + final status = await provider.testConnection( + _tomtomKeyController.text, + ); + ref.read(tomtomStatusProvider.notifier).state = status; + setState(() => _isTestingTomTom = false); + + if (mounted) { + String msg = ''; + if (status == RoadAlertProviderStatus.connected) { + msg = 'TomTom API Connected successfully!'; + } else if (status == RoadAlertProviderStatus.authError) { + msg = 'Authentication Error: Invalid Key.'; + } else if (status == RoadAlertProviderStatus.notConfigured) { + msg = 'Key is empty.'; + } else if (status == RoadAlertProviderStatus.networkError) { + msg = 'Network Error: Failed to connect to API.'; + } else if (status == RoadAlertProviderStatus.rateLimited) { + msg = 'Rate Limited: Too many requests.'; + } else { + msg = 'Status: ${status.name}'; + } + messenger.showSnackBar(SnackBar(content: Text(msg))); + } + }, + icon: _isTestingTomTom + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.sync, size: 18), + label: const Text('Test Connection'), + ), + ), + ], + ), + ), + ); + } + + Widget _buildDeveloperAlertsCard(BuildContext context) { + return Card( + color: Colors.orange.withValues(alpha: 0.08), + shape: RoundedRectangleBorder( + side: const BorderSide(color: Colors.orangeAccent), + borderRadius: BorderRadius.circular(14), + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Icon(Icons.bug_report, color: Colors.orangeAccent, size: 20), + const SizedBox(width: 10), + Expanded( + child: Text( + 'Road Alert Test Mode (Debug Only)', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w700, + color: Colors.orangeAccent, + ), + ), + ), + ], + ), + const SizedBox(height: 8), + Text( + 'Generate simulated test alerts for real-device testing.', + style: TextStyle( + fontSize: 12, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 14), + Wrap( + spacing: 8, + runSpacing: 8, + children: [ + ElevatedButton.icon( + style: ElevatedButton.styleFrom(minimumSize: const Size(0, 44)), + icon: const Icon(Icons.local_police, color: Colors.blueAccent, size: 18), + label: const Text('Test Police (100m)'), + onPressed: () { + final alert = RoadAlert( + id: 'sim_police', + providerId: 'mock', + category: RoadAlertCategory.police, + description: 'Simulated police alert', + latitude: 0, + longitude: 0, + distanceFromUser: 100.0, + reportedAt: DateTime.now(), + lastUpdated: DateTime.now(), + source: 'Test', + ); + ref.read(roadAlertServiceProvider).simulateAlert(alert); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Simulated Police Alert sent.')), + ); + }, + ), + ElevatedButton.icon( + style: ElevatedButton.styleFrom(minimumSize: const Size(0, 44)), + icon: const Icon(Icons.warning, color: Colors.orangeAccent, size: 18), + label: const Text('Test Hazard (250m)'), + onPressed: () { + final alert = RoadAlert( + id: 'sim_hazard', + providerId: 'mock', + category: RoadAlertCategory.hazard, + description: 'Simulated hazard alert', + latitude: 0, + longitude: 0, + distanceFromUser: 250.0, + reportedAt: DateTime.now(), + lastUpdated: DateTime.now(), + source: 'Test', + ); + ref.read(roadAlertServiceProvider).simulateAlert(alert); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Simulated Hazard Alert sent.')), + ); + }, + ), + ], + ), + ], + ), + ), + ); + } + + Widget _buildDiagnosticsLinkCard(BuildContext context) { + final colors = context.motorColors; + return Card( + clipBehavior: Clip.antiAlias, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14), + side: BorderSide(color: colors.outlineVariant), + ), + child: InkWell( + onTap: () => context.go('/settings/diagnostics'), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + children: [ + Icon(Icons.monitor_heart, color: colors.cyberCyan, size: 20), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Sensor Diagnostics', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(height: 2), + Text( + 'View raw IMU and GPS values in real time.', + style: TextStyle( + fontSize: 12, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + Icon(Icons.chevron_right, color: colors.outline, size: 20), + ], + ), + ), + ), + ); + } + + Widget _buildClearHistoryCard(BuildContext context) { + final colors = context.motorColors; + return Card( + clipBehavior: Clip.antiAlias, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14), + side: BorderSide(color: colors.outlineVariant), + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(Icons.delete_forever, color: colors.raceRed, size: 20), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Clear Ride History', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(height: 2), + Text( + 'Permanently delete all recorded rides and performance runs on this device.', + style: TextStyle( + fontSize: 12, + color: Theme.of(context).colorScheme.onSurfaceVariant, + height: 1.3, + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 14), + Align( + alignment: Alignment.centerRight, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: colors.raceRed.withValues(alpha: 0.15), + foregroundColor: colors.raceRed, + side: BorderSide(color: colors.raceRed.withValues(alpha: 0.6)), + minimumSize: const Size(120, 48), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + onPressed: () async { + final session = ref.read(rideStateMachineProvider); + if (session.state == RideState.riding) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Cannot clear history while a ride is actively recording.', + ), + ), + ); + return; + } + + final confirmed = await showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text('Clear All Ride History?'), + content: const Text( + 'This will permanently delete all saved rides, telemetry points, and events. This action cannot be undone.', + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx, false), + child: const Text('CANCEL'), + ), + TextButton( + onPressed: () => Navigator.pop(ctx, true), + child: Text( + 'CLEAR ALL', + style: TextStyle( + color: colors.raceRed, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ); + + if (confirmed == true && mounted) { + final db = ref.read(databaseProvider); + await db.transaction(() async { + await db.delete(db.telemetrySamples).go(); + await db.delete(db.rideEvents).go(); + await db.delete(db.performanceRuns).go(); + await db.delete(db.rides).go(); + }); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('All ride history cleared.'), + ), + ); + } + } + }, + child: const Text('Clear History'), + ), + ), + ], + ), + ), + ); + } + + Widget _buildFieldTestSection(BuildContext context) { + final colors = context.motorColors; + final isEnabled = ref.watch(fieldTestModeEnabledProvider); + final activeSession = ref.watch(activeFieldTestSessionProvider); + final logger = ref.watch(fieldTestLoggerProvider); + + return Card( + clipBehavior: Clip.antiAlias, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14), + side: BorderSide(color: colors.outlineVariant), + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Row( + children: [ + Icon( + Icons.biotech, + color: isEnabled ? colors.launchGreen : colors.outline, + size: 24, + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Field Test Mode', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w700, + ), + ), + Text( + 'Silent black-box diagnostic recorder', + style: TextStyle( + fontSize: 11, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ), + ), + const SizedBox(width: 8), + Switch( + value: isEnabled, + activeThumbColor: colors.launchGreen, + onChanged: (val) { + ref + .read(fieldTestModeEnabledProvider.notifier) + .setEnabled(val); + }, + ), + ], + ), + const Divider(height: 24), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Logger Status:', + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontSize: 13, + ), + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 3, + ), + decoration: BoxDecoration( + color: activeSession != null + ? colors.launchGreen.withValues(alpha: 0.15) + : (isEnabled + ? colors.cyberCyan.withValues(alpha: 0.15) + : colors.outlineVariant.withValues(alpha: 0.15)), + borderRadius: BorderRadius.circular(6), + border: Border.all( + color: activeSession != null + ? colors.launchGreen + : (isEnabled ? colors.cyberCyan : colors.outlineVariant), + ), + ), + child: Text( + activeSession != null + ? 'RECORDING (Ride #$activeSession)' + : (isEnabled ? 'ARMED (Auto-starts on Ride)' : 'OFF'), + style: TextStyle( + fontFamily: 'Inter', + fontSize: 11, + fontWeight: FontWeight.bold, + color: activeSession != null + ? colors.launchGreen + : (isEnabled ? colors.cyberCyan : colors.outline), + ), + ), + ), + ], + ), + const SizedBox(height: 16), + Wrap( + spacing: 8, + runSpacing: 8, + children: [ + ElevatedButton.icon( + style: ElevatedButton.styleFrom( + backgroundColor: colors.launchGreen.withValues(alpha: 0.2), + foregroundColor: colors.launchGreen, + side: BorderSide(color: colors.launchGreen), + minimumSize: const Size(130, 48), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + icon: const Icon(Icons.fact_check, size: 18), + label: const Text( + 'Pre-Ride Check', + style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold), + ), + onPressed: () => context.go('/settings/field-test-pre-ride-check'), + ), + ElevatedButton.icon( + style: ElevatedButton.styleFrom( + backgroundColor: colors.cyberCyan.withValues(alpha: 0.15), + foregroundColor: colors.cyberCyan, + side: BorderSide(color: colors.cyberCyan), + minimumSize: const Size(120, 48), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + icon: const Icon(Icons.assessment, size: 18), + label: const Text( + 'Reports', + style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold), + ), + onPressed: () => context.go('/settings/field-test-report'), + ), + OutlinedButton.icon( + style: OutlinedButton.styleFrom( + foregroundColor: Theme.of(context).colorScheme.onSurface, + side: BorderSide(color: colors.outlineVariant), + minimumSize: const Size(110, 48), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + icon: const Icon(Icons.receipt_long, size: 18), + label: const Text( + 'Live Log', + style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold), + ), + onPressed: () => context.go('/settings/field-test-live-log'), + ), + ], + ), + const SizedBox(height: 8), + Align( + alignment: Alignment.centerRight, + child: TextButton.icon( + style: TextButton.styleFrom( + foregroundColor: colors.raceRed, + ), + icon: const Icon(Icons.delete_sweep, size: 16), + label: const Text( + 'Clear Field Test Logs', + style: TextStyle(fontSize: 11, fontWeight: FontWeight.bold), + ), + onPressed: () async { + final session = ref.read(rideStateMachineProvider); + if (session.state == RideState.riding) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Cannot clear logs while a ride is recording.', + ), + ), + ); + return; + } + + final confirm = await showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text('Clear All Field Test Logs?'), + content: const Text( + 'This will delete all stored black-box diagnostic sessions. Standard ride history will NOT be affected.', + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx, false), + child: const Text('CANCEL'), + ), + TextButton( + onPressed: () => Navigator.pop(ctx, true), + child: Text( + 'CLEAR', + style: TextStyle(color: colors.raceRed, fontWeight: FontWeight.bold), + ), + ), + ], + ), + ); + + if (confirm == true && context.mounted) { + await logger.clearAllSessions(); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Field test logs cleared.'), + ), + ); + } + } + }, + ), + ), + ], + ), ), ); } @@ -1539,16 +1674,16 @@ class _SettingsScreenState extends ConsumerState { title, style: const TextStyle( fontSize: 14, - fontWeight: FontWeight.w600, + fontWeight: FontWeight.w700, ), softWrap: true, ), const SizedBox(height: 2), Text( subtitle, - style: const TextStyle( + style: TextStyle( fontSize: 12, - color: Colors.white70, + color: Theme.of(context).colorScheme.onSurfaceVariant, height: 1.3, ), softWrap: true, @@ -1566,33 +1701,34 @@ class _SettingsScreenState extends ConsumerState { } Widget _buildStatusBadge(RoadAlertProviderStatus status) { + final colors = context.motorColors; String label; Color color; switch (status) { case RoadAlertProviderStatus.connected: label = 'Connected'; - color = Colors.greenAccent; + color = colors.launchGreen; break; case RoadAlertProviderStatus.configured: label = 'Configured'; - color = Colors.cyanAccent; + color = colors.cyberCyan; break; case RoadAlertProviderStatus.authError: label = 'Auth Error'; - color = Colors.redAccent; + color = colors.raceRed; break; case RoadAlertProviderStatus.rateLimited: label = 'Rate Limited'; - color = Colors.orangeAccent; + color = colors.warningAmber; break; case RoadAlertProviderStatus.networkError: label = 'Network Error'; - color = Colors.amberAccent; + color = colors.warningAmber; break; case RoadAlertProviderStatus.notConfigured: label = 'Not Set'; - color = Colors.white38; + color = colors.outlineVariant; break; } @@ -1616,8 +1752,6 @@ class _SettingsScreenState extends ConsumerState { } /// Read-only footer that surfaces the app's build version inside Settings. -/// Resolves the version asynchronously via [AppInfo]; shows a placeholder -/// while loading and the literal fallback string if the platform call fails. class _AppVersionFooter extends StatefulWidget { const _AppVersionFooter(); @@ -1636,17 +1770,17 @@ class _AppVersionFooterState extends State<_AppVersionFooter> { void _showIdentityDialog() { final identity = AppInfo.identity; + final colors = context.motorColors; showDialog( context: context, builder: (ctx) => AlertDialog( - backgroundColor: const Color(0xFF1E293B), - title: const Row( + title: Row( children: [ - Icon(Icons.fingerprint, color: Colors.cyanAccent), - SizedBox(width: 8), - Text( + Icon(Icons.fingerprint, color: colors.cyberCyan), + const SizedBox(width: 8), + const Text( 'Software Identity', - style: TextStyle(color: Colors.white, fontSize: 16), + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ), ], ), @@ -1672,9 +1806,9 @@ class _AppVersionFooterState extends State<_AppVersionFooter> { actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(), - child: const Text( + child: Text( 'CLOSE', - style: TextStyle(color: Colors.cyanAccent), + style: TextStyle(color: colors.cyberCyan, fontWeight: FontWeight.bold), ), ), ], @@ -1690,12 +1824,15 @@ class _AppVersionFooterState extends State<_AppVersionFooter> { children: [ Text( label, - style: const TextStyle(color: Colors.white70, fontSize: 12), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontSize: 12, + ), ), Text( value, - style: const TextStyle( - color: Colors.cyanAccent, + style: TextStyle( + color: context.motorColors.cyberCyan, fontWeight: FontWeight.bold, fontSize: 12, fontFamily: 'monospace', @@ -1723,16 +1860,19 @@ class _AppVersionFooterState extends State<_AppVersionFooter> { Text( 'Motorcycle Dash v$text', textAlign: TextAlign.center, - style: const TextStyle( - color: Colors.white54, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, fontSize: 12, letterSpacing: 0.4, ), ), const SizedBox(height: 2), - const Text( + Text( 'Tap for forensic build identity', - style: TextStyle(color: Colors.white24, fontSize: 9), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant.withValues(alpha: 0.6), + fontSize: 9, + ), ), ], ), diff --git a/lib/features/settings/widgets/current_plan_card.dart b/lib/features/settings/widgets/current_plan_card.dart index 769edbd..4914418 100644 --- a/lib/features/settings/widgets/current_plan_card.dart +++ b/lib/features/settings/widgets/current_plan_card.dart @@ -2,17 +2,15 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:url_launcher/url_launcher.dart'; +import '../../../core/theme/theme_extensions.dart'; import '../../../services/billing/product_catalog.dart'; import '../../../services/entitlements/entitlement_providers.dart'; import '../../../services/entitlements/entitlement_state.dart'; import '../../entitlements/widgets/pro_lock_badge.dart'; -/// Reusable card displaying the current plan and subscription management options. -/// Used in both Classic Settings and Evolution Settings. +/// Reusable canonical responsive card displaying the current plan and subscription management options. class CurrentPlanCard extends ConsumerWidget { - final bool isEvolution; - - const CurrentPlanCard({super.key, this.isEvolution = false}); + const CurrentPlanCard({super.key}); Future _openGooglePlaySubscriptions() async { // Official Google Play subscription management deep link @@ -28,6 +26,7 @@ class CurrentPlanCard extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final colors = context.motorColors; final entitlement = ref.watch(entitlementProvider); final isPro = entitlement.isPro; final plan = entitlement.plan; @@ -35,17 +34,17 @@ class CurrentPlanCard extends ConsumerWidget { String planTitle = 'MotorSignal Free'; String statusSubtitle = 'Basic telemetry & local recording active'; - Color badgeColor = Colors.white54; + Color badgeColor = colors.outlineVariant; String badgeText = 'FREE'; if (plan == MotorSignalPlan.proLifetime && isPro) { planTitle = 'MotorSignal Pro'; statusSubtitle = 'Lifetime access • Permanent ownership'; - badgeColor = const Color(0xFF00E5FF); + badgeColor = colors.cyberCyan; badgeText = 'LIFETIME'; } else if (plan == MotorSignalPlan.proMonthly) { planTitle = 'MotorSignal Pro'; - badgeColor = const Color(0xFF00E5FF); + badgeColor = colors.cyberCyan; badgeText = 'MONTHLY'; switch (status) { @@ -57,28 +56,28 @@ class CurrentPlanCard extends ConsumerWidget { case MotorSignalEntitlementStatus.proMonthlyGrace: statusSubtitle = 'Payment grace period • Action needed in Google Play'; - badgeColor = Colors.amberAccent; + badgeColor = colors.warningAmber; badgeText = 'GRACE PERIOD'; break; case MotorSignalEntitlementStatus.proMonthlyAccountHold: statusSubtitle = 'Account on hold • Please update billing in Google Play'; - badgeColor = Colors.redAccent; + badgeColor = colors.raceRed; badgeText = 'ON HOLD'; break; case MotorSignalEntitlementStatus.proMonthlyPaused: statusSubtitle = 'Subscription paused by rider in Google Play'; - badgeColor = Colors.amberAccent; + badgeColor = colors.warningAmber; badgeText = 'PAUSED'; break; case MotorSignalEntitlementStatus.proMonthlyExpired: statusSubtitle = 'Subscription expired'; - badgeColor = Colors.white38; + badgeColor = colors.outlineVariant; badgeText = 'EXPIRED'; break; case MotorSignalEntitlementStatus.syncPending: statusSubtitle = 'Verification pending with Google Play...'; - badgeColor = Colors.amberAccent; + badgeColor = colors.warningAmber; badgeText = 'VERIFYING'; break; default: @@ -87,7 +86,7 @@ class CurrentPlanCard extends ConsumerWidget { } } else if (status == MotorSignalEntitlementStatus.syncPending) { statusSubtitle = 'Checking purchases with Google Play...'; - badgeColor = Colors.amberAccent; + badgeColor = colors.warningAmber; badgeText = 'PENDING'; } @@ -96,12 +95,10 @@ class CurrentPlanCard extends ConsumerWidget { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: isEvolution ? const Color(0xFF0D0D0D) : null, - borderRadius: BorderRadius.circular(isEvolution ? 16 : 12), + color: colors.surfaceContainer, + borderRadius: BorderRadius.circular(16), border: Border.all( - color: isPro - ? const Color(0xFF00E5FF).withValues(alpha: 0.6) - : (isEvolution ? const Color(0xFF1F1F1F) : Colors.white12), + color: isPro ? colors.cyberCyan.withValues(alpha: 0.6) : colors.outlineVariant, width: 1.2, ), ), @@ -119,17 +116,17 @@ class CurrentPlanCard extends ConsumerWidget { children: [ Icon( isPro ? Icons.verified : Icons.shield_outlined, - color: isPro ? const Color(0xFF00E5FF) : Colors.white70, + color: isPro ? colors.cyberCyan : colors.outline, size: 20, ), const SizedBox(width: 10), Text( planTitle.toUpperCase(), style: TextStyle( - fontFamily: isEvolution ? 'Sora' : null, + fontFamily: 'Sora', fontSize: 13, fontWeight: FontWeight.w800, - color: isPro ? const Color(0xFF00E5FF) : Colors.white, + color: isPro ? colors.cyberCyan : Theme.of(context).colorScheme.onSurface, letterSpacing: 0.5, ), ), @@ -162,71 +159,66 @@ class CurrentPlanCard extends ConsumerWidget { const SizedBox(height: 8), Text( statusSubtitle, - style: const TextStyle( + style: TextStyle( fontSize: 12, - color: Colors.white70, + color: Theme.of(context).colorScheme.onSurfaceVariant, height: 1.3, ), ), const SizedBox(height: 16), - Row( + Wrap( + spacing: 10, + runSpacing: 10, children: [ - if (!isPro) ...[ - Expanded( - child: ElevatedButton( - key: const ValueKey('settings_upgrade_to_pro_button'), - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF00E5FF), - foregroundColor: Colors.black, - padding: const EdgeInsets.symmetric(vertical: 10), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), + if (!isPro) + ElevatedButton( + key: const ValueKey('settings_upgrade_to_pro_button'), + style: ElevatedButton.styleFrom( + backgroundColor: colors.cyberCyan, + foregroundColor: Colors.black, + minimumSize: const Size(140, 48), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), ), - onPressed: () => context.push('/settings/pro'), - child: const Text( - 'UPGRADE TO PRO', - style: TextStyle( - fontSize: 11, - fontWeight: FontWeight.w800, - ), + ), + onPressed: () => context.push('/settings/pro'), + child: const Text( + 'UPGRADE TO PRO', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w800, + letterSpacing: 0.5, ), ), ), - const SizedBox(width: 10), - ], - if (isMonthlySub) ...[ - Expanded( - child: OutlinedButton( - style: OutlinedButton.styleFrom( - foregroundColor: Colors.white70, - side: const BorderSide(color: Colors.white24), - padding: const EdgeInsets.symmetric(vertical: 10), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), + if (isMonthlySub) + OutlinedButton( + style: OutlinedButton.styleFrom( + foregroundColor: Theme.of(context).colorScheme.onSurface, + side: BorderSide(color: colors.outlineVariant), + minimumSize: const Size(160, 48), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), ), - onPressed: _openGooglePlaySubscriptions, - child: const Text( - 'MANAGE SUBSCRIPTION', - style: TextStyle( - fontSize: 11, - fontWeight: FontWeight.bold, - ), + ), + onPressed: _openGooglePlaySubscriptions, + child: const Text( + 'MANAGE SUBSCRIPTION', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.bold, ), ), ), - const SizedBox(width: 10), - ], OutlinedButton( key: const ValueKey('settings_pro_details_button'), style: OutlinedButton.styleFrom( - foregroundColor: Colors.white70, - side: const BorderSide(color: Colors.white24), - padding: const EdgeInsets.symmetric( - vertical: 10, - horizontal: 12, - ), + foregroundColor: Theme.of(context).colorScheme.onSurface, + side: BorderSide(color: colors.outlineVariant), + minimumSize: const Size(100, 48), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), @@ -234,7 +226,7 @@ class CurrentPlanCard extends ConsumerWidget { onPressed: () => context.push('/settings/pro'), child: Text( isPro ? 'VIEW PLANS' : 'DETAILS', - style: const TextStyle(fontSize: 11), + style: const TextStyle(fontSize: 11, fontWeight: FontWeight.bold), ), ), ], diff --git a/lib/features/settings/widgets/evolution_settings_view.dart b/lib/features/settings/widgets/evolution_settings_view.dart deleted file mode 100644 index 7f9b734..0000000 --- a/lib/features/settings/widgets/evolution_settings_view.dart +++ /dev/null @@ -1,2187 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:go_router/go_router.dart'; -import '../../../services/map/map_provider_interface.dart'; -import '../../../services/map/map_providers.dart'; -import '../../../services/alerts/road_alert_models.dart'; -import '../../../services/alerts/road_alert_providers.dart'; -import '../../../services/alerts/road_alert_service.dart'; -import '../../../services/osm_oauth_service.dart'; -import '../../../services/github/github_auth_service.dart'; -import '../../../services/sensor_fusion_service.dart'; -import '../../../services/field_test/field_test_logger.dart'; -import '../../../database/database.dart'; -import '../../rides/ride_state_machine.dart'; -import '../../../services/auth/auth_providers.dart'; -import '../../../core/theme/evolution_theme_config.dart'; -import '../../../core/theme/theme_mode_provider.dart'; -import '../../../core/app_info.dart'; -import 'current_plan_card.dart'; - -class EvolutionSettingsView extends ConsumerStatefulWidget { - const EvolutionSettingsView({super.key}); - - @override - ConsumerState createState() => - _EvolutionSettingsViewState(); -} - -class _EvolutionSettingsViewState extends ConsumerState { - late TextEditingController _tomtomKeyController; - late TextEditingController _communityKeyController; - late TextEditingController _communityEndpointController; - bool _isTestingTomTom = false; - bool _isTestingCommunity = false; - late TextEditingController _githubTokenController; - bool _isTestingGitHub = false; - bool _obscureGitHubToken = true; - - @override - void initState() { - super.initState(); - _tomtomKeyController = TextEditingController( - text: ref.read(tomtomApiKeyProvider), - ); - _communityKeyController = TextEditingController( - text: ref.read(communityApiKeyProvider), - ); - _communityEndpointController = TextEditingController( - text: ref.read(communityEndpointUrlProvider), - ); - _githubTokenController = TextEditingController( - text: ref.read(githubAuthServiceProvider).getToken() ?? '', - ); - } - - @override - void dispose() { - _tomtomKeyController.dispose(); - _communityKeyController.dispose(); - _communityEndpointController.dispose(); - _githubTokenController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final mapType = ref.watch(mapProviderTypeNotifierProvider); - final themeMode = ref.watch(dashboardThemeModeProvider); - final roadAlertsEnabled = ref.watch(roadAlertsEnabledProvider); - final audioAlertsEnabled = ref.watch(audioAlertsEnabledProvider); - final alertRadiusMeters = ref.watch(alertRadiusMetersProvider); - final enabledCategories = ref.watch(enabledCategoriesProvider); - final osmAuth = ref.watch(osmOAuthServiceProvider); - - final radiusMiles = (alertRadiusMeters / 1609.34).toStringAsFixed(1); - - final isLandscape = - MediaQuery.of(context).orientation == Orientation.landscape; - - return Scaffold( - backgroundColor: Colors.black, - appBar: AppBar( - toolbarHeight: isLandscape ? 36 : 52, - backgroundColor: Colors.transparent, - elevation: 0, - centerTitle: true, - title: Text( - 'SETTINGS & SYSTEM CONFIG', - style: TextStyle( - fontFamily: 'Sora', - fontSize: isLandscape ? 13 : 16, - fontWeight: FontWeight.w900, - color: Colors.white, - letterSpacing: 0.12, - ), - ), - ), - body: LayoutBuilder( - builder: (context, constraints) { - final isLandscape = constraints.maxWidth > constraints.maxHeight; - - final appThemeMode = ref.watch(appThemeModeProvider); - - final appearanceToggleBar = Container( - margin: EdgeInsets.only(bottom: isLandscape ? 6 : 12), - padding: EdgeInsets.symmetric( - horizontal: 12, - vertical: isLandscape ? 6 : 12, - ), - decoration: BoxDecoration( - color: const Color(0xFF0D0D0D), - borderRadius: BorderRadius.circular(14), - border: Border.all(color: const Color(0xFF1F1F1F), width: 1.2), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Expanded( - child: Row( - children: [ - Icon( - Icons.brightness_medium_outlined, - color: Color(0xFF00E5FF), - size: 18, - ), - SizedBox(width: 8), - Flexible( - child: Text( - 'APPEARANCE:', - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontFamily: 'Inter', - fontSize: 11, - fontWeight: FontWeight.w800, - color: Colors.white70, - ), - ), - ), - ], - ), - ), - const SizedBox(width: 8), - SegmentedButton( - showSelectedIcon: false, - style: const ButtonStyle( - visualDensity: VisualDensity.compact, - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - ), - segments: const [ - ButtonSegment( - value: ThemeMode.system, - label: Text('System', style: TextStyle(fontSize: 11)), - ), - ButtonSegment( - value: ThemeMode.light, - label: Text('Light', style: TextStyle(fontSize: 11)), - ), - ButtonSegment( - value: ThemeMode.dark, - label: Text('Dark', style: TextStyle(fontSize: 11)), - ), - ], - selected: {appThemeMode}, - onSelectionChanged: (Set selection) { - ref - .read(appThemeModeProvider.notifier) - .setThemeMode(selection.first); - }, - ), - ], - ), - ); - - final themeToggleBar = Container( - margin: EdgeInsets.only(bottom: isLandscape ? 6 : 12), - padding: EdgeInsets.symmetric( - horizontal: 12, - vertical: isLandscape ? 6 : 12, - ), - decoration: BoxDecoration( - color: const Color(0xFF0D0D0D), - borderRadius: BorderRadius.circular(14), - border: Border.all(color: const Color(0xFF1F1F1F), width: 1.2), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Expanded( - child: Row( - children: [ - Icon( - Icons.dashboard_customize_outlined, - color: Color(0xFF00E5FF), - size: 18, - ), - SizedBox(width: 8), - Flexible( - child: Text( - 'COCKPIT LAYOUT:', - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontFamily: 'Inter', - fontSize: 11, - fontWeight: FontWeight.w800, - color: Colors.white70, - ), - ), - ), - ], - ), - ), - const SizedBox(width: 8), - SegmentedButton( - showSelectedIcon: false, - style: const ButtonStyle( - visualDensity: VisualDensity.compact, - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - ), - segments: const [ - ButtonSegment( - value: DashboardThemeMode.classic, - label: Text('Classic', style: TextStyle(fontSize: 11)), - ), - ButtonSegment( - value: DashboardThemeMode.evolution, - label: Text('Evolution', style: TextStyle(fontSize: 11)), - ), - ], - selected: {themeMode}, - onSelectionChanged: (Set selection) { - ref - .read(dashboardThemeModeProvider.notifier) - .setThemeMode(selection.first); - }, - ), - ], - ), - ); - - final currentPlanCard = const CurrentPlanCard(isEvolution: true); - final accountCard = _buildAccountCard(context, ref); - final osmAccountCard = _buildOsmAccountCard(context, ref, osmAuth); - final mapEngineCard = _buildMapEngineCard(context, ref, mapType); - final alertConfigCard = _buildAlertConfigCard( - context, - ref, - roadAlertsEnabled, - audioAlertsEnabled, - enabledCategories, - alertRadiusMeters, - radiusMiles, - ); - final sensorsCard = _buildSensorsCalibrationCard(context, ref); - final tomtomCard = _buildTomTomCard(context, ref); - final communityCard = _buildCommunityCard(context, ref); - final githubCard = _buildGitHubCard(context, ref); - final dataCard = _buildDataManagementCard(context, ref); - final engineeringCard = _buildEngineeringDiagnosticsCard( - context, - ref, - ); - final versionFooter = _buildVersionFooter(context); - - if (isLandscape) { - return SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: Column( - children: [ - appearanceToggleBar, - themeToggleBar, - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Column( - children: [ - currentPlanCard, - const SizedBox(height: 16), - accountCard, - const SizedBox(height: 16), - sensorsCard, - const SizedBox(height: 16), - mapEngineCard, - const SizedBox(height: 16), - osmAccountCard, - const SizedBox(height: 16), - tomtomCard, - const SizedBox(height: 16), - githubCard, - ], - ), - ), - const SizedBox(width: 16), - Expanded( - child: Column( - children: [ - alertConfigCard, - const SizedBox(height: 16), - communityCard, - const SizedBox(height: 16), - engineeringCard, - const SizedBox(height: 16), - dataCard, - ], - ), - ), - ], - ), - const SizedBox(height: 24), - versionFooter, - ], - ), - ); - } - - return SingleChildScrollView( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - appearanceToggleBar, - themeToggleBar, - currentPlanCard, - const SizedBox(height: 16), - accountCard, - const SizedBox(height: 16), - sensorsCard, - const SizedBox(height: 16), - engineeringCard, - const SizedBox(height: 16), - dataCard, - const SizedBox(height: 16), - osmAccountCard, - const SizedBox(height: 16), - mapEngineCard, - const SizedBox(height: 16), - alertConfigCard, - const SizedBox(height: 16), - communityCard, - const SizedBox(height: 16), - tomtomCard, - const SizedBox(height: 24), - versionFooter, - const SizedBox(height: 24), - ], - ), - ); - }, - ), - ); - } - - Widget _buildAccountCard(BuildContext context, WidgetRef ref) { - final authState = ref.watch(authNotifierProvider); - final isAuthed = authState.isAuthenticated; - final user = authState.user; - - return Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: const Color(0xFF0D0D0D), - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: isAuthed - ? const Color(0xFF00E5FF).withValues(alpha: 0.6) - : const Color(0xFF1F1F1F), - width: 1.2, - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Expanded( - child: Row( - children: [ - Icon( - Icons.account_circle, - color: Color(0xFF00E5FF), - size: 18, - ), - SizedBox(width: 8), - Flexible( - child: Text( - 'MOTORCLOUD ACCOUNT', - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontFamily: 'Sora', - fontSize: 12, - fontWeight: FontWeight.w800, - color: Color(0xFF00E5FF), - letterSpacing: 0.1, - ), - ), - ), - ], - ), - ), - const SizedBox(width: 8), - Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), - decoration: BoxDecoration( - color: isAuthed - ? const Color(0xFF00E5FF).withValues(alpha: 0.15) - : Colors.white10, - borderRadius: BorderRadius.circular(8), - ), - child: Text( - isAuthed ? 'CONNECTED' : 'OFFLINE MODE', - style: TextStyle( - fontFamily: 'Inter', - fontSize: 9, - fontWeight: FontWeight.w800, - color: isAuthed ? const Color(0xFF00E5FF) : Colors.white38, - ), - ), - ), - ], - ), - const SizedBox(height: 10), - if (isAuthed && user != null) ...[ - Row( - children: [ - CircleAvatar( - radius: 18, - backgroundColor: Colors.white12, - backgroundImage: user.photoUrl != null - ? NetworkImage(user.photoUrl!) - : null, - child: user.photoUrl == null - ? const Icon( - Icons.person, - color: Color(0xFF00E5FF), - size: 20, - ) - : null, - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - user.displayLabel, - style: const TextStyle( - fontFamily: 'Inter', - fontSize: 13, - fontWeight: FontWeight.w700, - color: Colors.white, - ), - ), - Text( - user.email ?? 'Cloud Identity Active', - style: const TextStyle( - fontFamily: 'Inter', - fontSize: 11, - color: Colors.white54, - ), - ), - ], - ), - ), - ], - ), - const SizedBox(height: 12), - OutlinedButton.icon( - style: OutlinedButton.styleFrom( - foregroundColor: const Color(0xFF00E5FF), - side: const BorderSide(color: Color(0xFF00E5FF), width: 0.8), - minimumSize: const Size(double.infinity, 38), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - ), - icon: const Icon(Icons.manage_accounts, size: 16), - label: const Text( - 'Manage Account & Cloud Sync', - style: TextStyle(fontSize: 11, fontWeight: FontWeight.bold), - ), - onPressed: () => context.go('/settings/account'), - ), - ] else ...[ - const Text( - 'Sign in for optional multi-device account sync and cloud backup. All telemetry and ride logging work completely offline.', - style: TextStyle( - fontFamily: 'Inter', - fontSize: 10, - color: Colors.white60, - height: 1.3, - ), - ), - const SizedBox(height: 12), - ElevatedButton.icon( - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF00E5FF), - foregroundColor: Colors.black, - minimumSize: const Size(double.infinity, 38), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - ), - icon: const Icon(Icons.login, size: 16), - label: const Text( - 'Sign In / Create Account', - style: TextStyle( - fontFamily: 'Inter', - fontSize: 11, - fontWeight: FontWeight.w800, - ), - ), - onPressed: () => context.go('/settings/account'), - ), - ], - ], - ), - ); - } - - Widget _buildOsmAccountCard( - BuildContext context, - WidgetRef ref, - OsmAuthState osmAuth, - ) { - final isAuthed = osmAuth.isAuthenticated; - final user = osmAuth.user; - - return Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: const Color(0xFF0D0D0D), - borderRadius: BorderRadius.circular(16), - border: Border.all(color: const Color(0xFF1F1F1F), width: 1.2), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Expanded( - child: Row( - children: [ - Icon(Icons.public, color: Color(0xFF00E5FF), size: 18), - SizedBox(width: 8), - Flexible( - child: Text( - 'OPENSTREETMAP ACCOUNT', - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontFamily: 'Sora', - fontSize: 12, - fontWeight: FontWeight.w800, - color: Color(0xFF00E5FF), - letterSpacing: 0.1, - ), - ), - ), - ], - ), - ), - const SizedBox(width: 8), - Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), - decoration: BoxDecoration( - color: isAuthed - ? const Color(0xFF00E5FF).withValues(alpha: 0.15) - : Colors.white10, - borderRadius: BorderRadius.circular(8), - ), - child: Text( - isAuthed ? 'CONNECTED' : 'DISCONNECTED', - style: TextStyle( - fontFamily: 'Inter', - fontSize: 9, - fontWeight: FontWeight.w800, - color: isAuthed ? const Color(0xFF00E5FF) : Colors.white38, - ), - ), - ), - ], - ), - const SizedBox(height: 12), - if (isAuthed && user != null) ...[ - Row( - children: [ - CircleAvatar( - radius: 20, - backgroundColor: Colors.white12, - backgroundImage: user.avatarUrl != null - ? NetworkImage(user.avatarUrl!) - : null, - child: user.avatarUrl == null - ? const Icon( - Icons.person, - color: Color(0xFF00E5FF), - size: 22, - ) - : null, - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - user.displayName, - style: const TextStyle( - fontFamily: 'Inter', - fontSize: 14, - fontWeight: FontWeight.w700, - color: Colors.white, - ), - ), - Text( - 'Changesets: ${user.changesetCount} • Sync Enabled', - style: const TextStyle( - fontFamily: 'Inter', - fontSize: 11, - color: Colors.white54, - ), - ), - ], - ), - ), - ], - ), - const SizedBox(height: 12), - OutlinedButton.icon( - style: OutlinedButton.styleFrom( - foregroundColor: Colors.redAccent, - side: const BorderSide(color: Colors.redAccent, width: 0.8), - minimumSize: const Size(double.infinity, 38), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - ), - icon: const Icon(Icons.link_off, size: 16), - label: const Text( - 'Disconnect OpenStreetMap', - style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold), - ), - onPressed: () => - ref.read(osmOAuthServiceProvider.notifier).logout(), - ), - ] else ...[ - const Text( - 'Link your OpenStreetMap account via OAuth 2.0 PKCE to upload GPS traces and sync ride telemetry.', - style: TextStyle( - fontFamily: 'Inter', - fontSize: 11, - color: Colors.white60, - height: 1.4, - ), - ), - const SizedBox(height: 12), - ElevatedButton.icon( - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF00E5FF), - foregroundColor: Colors.black, - minimumSize: const Size(double.infinity, 40), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - ), - icon: const Icon(Icons.login, size: 18), - label: const Text( - 'Connect OpenStreetMap Account', - style: TextStyle( - fontFamily: 'Inter', - fontSize: 12, - fontWeight: FontWeight.w800, - ), - ), - onPressed: osmAuth.isLoading - ? null - : () => ref - .read(osmOAuthServiceProvider.notifier) - .startLoginFlow(), - ), - ], - if (osmAuth.errorMessage != null) ...[ - const SizedBox(height: 8), - Text( - osmAuth.errorMessage!, - style: const TextStyle(color: Colors.redAccent, fontSize: 10), - ), - ], - ], - ), - ); - } - - Widget _buildMapEngineCard( - BuildContext context, - WidgetRef ref, - AppMapType mapType, - ) { - return Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: const Color(0xFF0D0D0D), - borderRadius: BorderRadius.circular(16), - border: Border.all(color: const Color(0xFF1F1F1F), width: 1.2), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'MAP ENGINE & RENDERING', - style: TextStyle( - fontFamily: 'Sora', - fontSize: 12, - fontWeight: FontWeight.w800, - color: Color(0xFF00E5FF), - letterSpacing: 0.1, - ), - ), - const SizedBox(height: 12), - const Text( - 'Active Map Engine', - style: TextStyle( - fontFamily: 'Inter', - fontSize: 11, - fontWeight: FontWeight.w700, - color: Colors.white70, - ), - ), - const SizedBox(height: 8), - _buildEngineOption( - title: 'OpenStreetMap', - selected: mapType == AppMapType.openStreetMap, - onTap: () => ref - .read(mapProviderTypeNotifierProvider.notifier) - .setMapProviderType(AppMapType.openStreetMap), - ), - const SizedBox(height: 6), - _buildEngineOption( - title: 'Google Maps', - selected: mapType == AppMapType.googleMaps, - onTap: () => ref - .read(mapProviderTypeNotifierProvider.notifier) - .setMapProviderType(AppMapType.googleMaps), - ), - const SizedBox(height: 16), - const Text( - 'Rendering Preferences', - style: TextStyle( - fontFamily: 'Inter', - fontSize: 11, - fontWeight: FontWeight.w700, - color: Colors.white70, - ), - ), - const SizedBox(height: 8), - _buildSwitchRow(Icons.apartment, '3D Buildings', true, (_) {}), - _buildSwitchRow(Icons.terrain, 'Terrain Topo Data', true, (_) {}), - _buildSwitchRow(Icons.traffic, 'Live Traffic Overlay', true, (_) {}), - _buildSwitchRow( - Icons.play_circle_outline, - 'Replay Preview', - false, - (_) {}, - ), - ], - ), - ); - } - - Widget _buildAlertConfigCard( - BuildContext context, - WidgetRef ref, - bool roadAlertsEnabled, - bool audioAlertsEnabled, - Set enabledCategories, - double alertRadiusMeters, - String radiusMiles, - ) { - return Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: const Color(0xFF0D0D0D), - borderRadius: BorderRadius.circular(16), - border: Border.all(color: const Color(0xFF1F1F1F), width: 1.2), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'ROAD ALERT CONFIGURATION', - style: TextStyle( - fontFamily: 'Sora', - fontSize: 12, - fontWeight: FontWeight.w800, - color: Color(0xFF00E5FF), - letterSpacing: 0.1, - ), - ), - const SizedBox(height: 12), - const Text( - 'Alert Categories', - style: TextStyle( - fontFamily: 'Inter', - fontSize: 11, - fontWeight: FontWeight.w700, - color: Colors.white70, - ), - ), - const SizedBox(height: 8), - _buildSwitchRow( - Icons.local_police_outlined, - 'Police Presence', - enabledCategories.contains(RoadAlertCategory.police), - (v) => ref - .read(enabledCategoriesProvider.notifier) - .toggleCategory(RoadAlertCategory.police, v), - ), - _buildSwitchRow( - Icons.camera_alt_outlined, - 'Speed Cameras', - enabledCategories.contains(RoadAlertCategory.speedCamera), - (v) => ref - .read(enabledCategoriesProvider.notifier) - .toggleCategory(RoadAlertCategory.speedCamera, v), - ), - _buildSwitchRow( - Icons.warning_amber_rounded, - 'Road Hazards', - enabledCategories.contains(RoadAlertCategory.hazard), - (v) => ref - .read(enabledCategoriesProvider.notifier) - .toggleCategory(RoadAlertCategory.hazard, v), - ), - _buildSwitchRow( - Icons.car_crash_outlined, - 'Traffic Incidents', - enabledCategories.contains(RoadAlertCategory.traffic), - (v) => ref - .read(enabledCategoriesProvider.notifier) - .toggleCategory(RoadAlertCategory.traffic, v), - ), - const SizedBox(height: 16), - const Text( - 'Radius Configuration', - style: TextStyle( - fontFamily: 'Inter', - fontSize: 11, - fontWeight: FontWeight.w700, - color: Colors.white70, - ), - ), - const SizedBox(height: 8), - Center( - child: Text( - '$radiusMiles MILE', - style: const TextStyle( - fontFamily: 'JetBrains Mono', - fontSize: 22, - fontWeight: FontWeight.w900, - color: Colors.white, - ), - ), - ), - const SizedBox(height: 8), - Row( - children: [ - IconButton( - icon: const Icon( - Icons.remove_circle_outline, - color: Color(0xFF00E5FF), - ), - onPressed: alertRadiusMeters > 500 - ? () => ref - .read(alertRadiusMetersProvider.notifier) - .setRadius(alertRadiusMeters - 500.0) - : null, - ), - Expanded( - child: LinearProgressIndicator( - value: (alertRadiusMeters / 5000.0).clamp(0.1, 1.0), - backgroundColor: const Color(0xFF1F1F1F), - valueColor: const AlwaysStoppedAnimation( - Color(0xFF00E5FF), - ), - minHeight: 8, - borderRadius: BorderRadius.circular(4), - ), - ), - IconButton( - icon: const Icon( - Icons.add_circle_outline, - color: Color(0xFF00E5FF), - ), - onPressed: alertRadiusMeters < 5000 - ? () => ref - .read(alertRadiusMetersProvider.notifier) - .setRadius(alertRadiusMeters + 500.0) - : null, - ), - ], - ), - const Center( - child: Text( - 'Distance to detect upcoming incidents', - style: TextStyle( - fontFamily: 'Inter', - fontSize: 9, - color: Colors.white38, - ), - ), - ), - const SizedBox(height: 12), - SizedBox( - width: double.infinity, - child: OutlinedButton.icon( - icon: const Icon(Icons.volume_up, size: 16), - label: const Text('TRIGGER TEST ALERT (TTS + HUD)'), - style: OutlinedButton.styleFrom( - foregroundColor: const Color(0xFF00E5FF), - side: const BorderSide(color: Color(0xFF00E5FF), width: 1), - padding: const EdgeInsets.symmetric(vertical: 10), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - onPressed: () => - ref.read(roadAlertServiceProvider).triggerTestAlert(), - ), - ), - ], - ), - ); - } - - Widget _buildEngineOption({ - required String title, - required bool selected, - required VoidCallback onTap, - }) { - return InkWell( - onTap: onTap, - borderRadius: BorderRadius.circular(8), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), - decoration: BoxDecoration( - color: selected ? const Color(0xFF00252A) : const Color(0xFF141414), - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: selected ? const Color(0xFF00E5FF) : const Color(0xFF222222), - width: selected ? 1.4 : 1.0, - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - title, - style: TextStyle( - fontFamily: 'Inter', - fontSize: 12, - fontWeight: FontWeight.w700, - color: selected ? Colors.white : Colors.white60, - ), - ), - if (selected) - const Icon(Icons.check, color: Color(0xFF00E5FF), size: 16), - ], - ), - ), - ); - } - - Widget _buildSwitchRow( - IconData icon, - String title, - bool value, - ValueChanged onChanged, - ) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 2), - child: Row( - children: [ - Icon(icon, color: Colors.white60, size: 16), - const SizedBox(width: 8), - Expanded( - child: Text( - title, - style: const TextStyle( - fontFamily: 'Inter', - fontSize: 11, - fontWeight: FontWeight.w600, - color: Colors.white, - ), - ), - ), - Switch( - value: value, - activeThumbColor: const Color(0xFF00E5FF), - onChanged: onChanged, - ), - ], - ), - ); - } - - Widget _buildSensorsCalibrationCard(BuildContext context, WidgetRef ref) { - final leanAsync = ref.watch(leanAngleStreamProvider); - final currentLean = leanAsync.value ?? 0.0; - - return Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: const Color(0xFF0D0D0D), - borderRadius: BorderRadius.circular(16), - border: Border.all(color: const Color(0xFF1F1F1F), width: 1.2), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Row( - children: [ - Icon(Icons.screen_rotation, color: Color(0xFF00E5FF), size: 18), - SizedBox(width: 8), - Expanded( - child: Text( - 'SENSORS & MOUNT CALIBRATION', - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontFamily: 'Inter', - fontSize: 12, - fontWeight: FontWeight.w800, - color: Colors.white, - letterSpacing: 0.1, - ), - ), - ), - ], - ), - const SizedBox(height: 8), - const Text( - 'Place motorcycle upright on level ground. Zeroes the roll and pitch offsets for your phone mount angle.', - style: TextStyle( - fontFamily: 'Inter', - fontSize: 10, - color: Colors.white60, - height: 1.3, - ), - ), - const SizedBox(height: 12), - Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), - decoration: BoxDecoration( - color: const Color(0xFF141414), - borderRadius: BorderRadius.circular(8), - border: Border.all(color: const Color(0xFF222222)), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( - 'CURRENT LEAN ANGLE:', - style: TextStyle( - fontFamily: 'Inter', - fontSize: 10, - fontWeight: FontWeight.bold, - color: Colors.white54, - ), - ), - Text( - '${currentLean.toStringAsFixed(1)}°', - style: const TextStyle( - fontFamily: 'Inter', - fontSize: 14, - fontWeight: FontWeight.w900, - color: Color(0xFF00E5FF), - ), - ), - ], - ), - ), - const SizedBox(height: 12), - SizedBox( - width: double.infinity, - child: ElevatedButton.icon( - icon: const Icon(Icons.tune, size: 16), - label: const Text('CALIBRATE UPRIGHT ZERO'), - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF00E5FF), - foregroundColor: Colors.black, - padding: const EdgeInsets.symmetric(vertical: 12), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - textStyle: const TextStyle( - fontFamily: 'Inter', - fontSize: 11, - fontWeight: FontWeight.w900, - letterSpacing: 0.8, - ), - ), - onPressed: () { - final session = ref.read(rideStateMachineProvider); - if (session.state == RideState.riding) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text( - 'Cannot calibrate while a ride is actively recording.', - ), - ), - ); - return; - } - ref.read(sensorFusionServiceProvider).calibrateUpright(); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text( - 'Sensors Calibrated! Zero mount angle saved.', - ), - ), - ); - }, - ), - ), - ], - ), - ); - } - - Widget _buildDataManagementCard(BuildContext context, WidgetRef ref) { - return Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: const Color(0xFF0D0D0D), - borderRadius: BorderRadius.circular(16), - border: Border.all(color: const Color(0xFF1F1F1F), width: 1.2), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Row( - children: [ - Icon(Icons.storage_outlined, color: Color(0xFF00E5FF), size: 18), - SizedBox(width: 8), - Text( - 'DATA & STORAGE', - style: TextStyle( - fontFamily: 'Inter', - fontSize: 12, - fontWeight: FontWeight.w800, - color: Colors.white, - letterSpacing: 0.1, - ), - ), - ], - ), - const SizedBox(height: 12), - InkWell( - onTap: () => context.go('/settings/diagnostics'), - borderRadius: BorderRadius.circular(8), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), - decoration: BoxDecoration( - color: const Color(0xFF141414), - borderRadius: BorderRadius.circular(8), - border: Border.all(color: const Color(0xFF222222)), - ), - child: const Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Icon( - Icons.monitor_heart, - color: Color(0xFF00E5FF), - size: 16, - ), - SizedBox(width: 8), - Text( - 'Sensor Diagnostics & Raw Telemetry', - style: TextStyle( - fontFamily: 'Inter', - fontSize: 11, - fontWeight: FontWeight.w700, - color: Colors.white, - ), - ), - ], - ), - Icon(Icons.chevron_right, color: Colors.white54, size: 18), - ], - ), - ), - ), - const SizedBox(height: 10), - SizedBox( - width: double.infinity, - child: OutlinedButton.icon( - icon: const Icon( - Icons.delete_forever, - color: Colors.redAccent, - size: 16, - ), - label: const Text('CLEAR ALL RIDE HISTORY'), - style: OutlinedButton.styleFrom( - foregroundColor: Colors.redAccent, - side: const BorderSide(color: Colors.redAccent, width: 1), - padding: const EdgeInsets.symmetric(vertical: 10), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - textStyle: const TextStyle( - fontFamily: 'Inter', - fontSize: 11, - fontWeight: FontWeight.w800, - letterSpacing: 0.5, - ), - ), - onPressed: () async { - final session = ref.read(rideStateMachineProvider); - if (session.state == RideState.riding) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text( - 'Cannot clear history while a ride is actively recording.', - ), - ), - ); - return; - } - - final confirmed = await showDialog( - context: context, - builder: (ctx) => AlertDialog( - backgroundColor: const Color(0xFF141414), - title: const Text('Clear All Ride History?'), - content: const Text( - 'This will permanently delete all saved rides, telemetry points, and events. This action cannot be undone.', - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(ctx, false), - child: const Text('CANCEL'), - ), - TextButton( - onPressed: () => Navigator.pop(ctx, true), - child: const Text( - 'CLEAR ALL', - style: TextStyle( - color: Colors.redAccent, - fontWeight: FontWeight.bold, - ), - ), - ), - ], - ), - ); - - if (confirmed == true && context.mounted) { - final db = ref.read(databaseProvider); - await db.transaction(() async { - await db.delete(db.telemetrySamples).go(); - await db.delete(db.rideEvents).go(); - await db.delete(db.performanceRuns).go(); - await db.delete(db.rides).go(); - }); - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('All ride history cleared.'), - ), - ); - } - } - }, - ), - ), - ], - ), - ); - } - - Widget _buildEngineeringDiagnosticsCard(BuildContext context, WidgetRef ref) { - final isEnabled = ref.watch(fieldTestModeEnabledProvider); - final activeSession = ref.watch(activeFieldTestSessionProvider); - - return Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: const Color(0xFF0D0D0D), - borderRadius: BorderRadius.circular(16), - border: Border.all(color: const Color(0xFF1F1F1F), width: 1.2), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Row( - children: [ - Icon( - Icons.biotech, - color: isEnabled - ? Colors.greenAccent - : const Color(0xFF00E5FF), - size: 18, - ), - const SizedBox(width: 8), - const Flexible( - child: Text( - 'ENGINEERING & DIAGNOSTICS', - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontFamily: 'Inter', - fontSize: 12, - fontWeight: FontWeight.w800, - color: Colors.white, - letterSpacing: 0.1, - ), - ), - ), - ], - ), - ), - const SizedBox(width: 8), - Switch( - value: isEnabled, - activeThumbColor: Colors.greenAccent, - onChanged: (val) { - ref - .read(fieldTestModeEnabledProvider.notifier) - .setEnabled(val); - }, - ), - ], - ), - const SizedBox(height: 6), - const Text( - 'High-resolution black-box recorder for sensor fusion, GPS, and Kalman filter diagnostics.', - style: TextStyle( - fontFamily: 'Inter', - fontSize: 10, - color: Colors.white60, - height: 1.3, - ), - ), - const SizedBox(height: 12), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( - 'RECORDER STATUS:', - style: TextStyle( - fontFamily: 'Inter', - color: Colors.white54, - fontSize: 10, - fontWeight: FontWeight.bold, - ), - ), - Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), - decoration: BoxDecoration( - color: activeSession != null - ? Colors.greenAccent.withValues(alpha: 0.15) - : (isEnabled - ? const Color(0xFF00E5FF).withValues(alpha: 0.15) - : Colors.white10), - borderRadius: BorderRadius.circular(6), - border: Border.all( - color: activeSession != null - ? Colors.greenAccent - : (isEnabled - ? const Color(0xFF00E5FF) - : Colors.white24), - ), - ), - child: Text( - activeSession != null - ? 'RECORDING (Ride #$activeSession)' - : (isEnabled ? 'ARMED (Auto-starts on Ride)' : 'OFF'), - style: TextStyle( - fontFamily: 'Inter', - fontSize: 10, - fontWeight: FontWeight.bold, - color: activeSession != null - ? Colors.greenAccent - : (isEnabled - ? const Color(0xFF00E5FF) - : Colors.white54), - ), - ), - ), - ], - ), - const SizedBox(height: 12), - Row( - children: [ - Expanded( - child: ElevatedButton.icon( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.greenAccent.withValues(alpha: 0.2), - foregroundColor: Colors.greenAccent, - side: const BorderSide(color: Colors.greenAccent), - padding: const EdgeInsets.symmetric(vertical: 10), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - icon: const Icon(Icons.fact_check, size: 16), - label: const Text( - 'PRE-RIDE CHECK', - style: TextStyle( - fontFamily: 'Inter', - fontSize: 9, - fontWeight: FontWeight.w800, - ), - ), - onPressed: () => - context.go('/settings/field-test-pre-ride-check'), - ), - ), - const SizedBox(width: 6), - Expanded( - child: ElevatedButton.icon( - style: ElevatedButton.styleFrom( - backgroundColor: const Color( - 0xFF00E5FF, - ).withValues(alpha: 0.15), - foregroundColor: const Color(0xFF00E5FF), - side: const BorderSide(color: Color(0xFF00E5FF)), - padding: const EdgeInsets.symmetric(vertical: 10), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - icon: const Icon(Icons.assessment, size: 16), - label: const Text( - 'REPORTS', - style: TextStyle( - fontFamily: 'Inter', - fontSize: 9, - fontWeight: FontWeight.w800, - ), - ), - onPressed: () => context.go('/settings/field-test-report'), - ), - ), - const SizedBox(width: 6), - Expanded( - child: ElevatedButton.icon( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.white10, - foregroundColor: Colors.white, - side: const BorderSide(color: Colors.white24), - padding: const EdgeInsets.symmetric(vertical: 10), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - icon: const Icon(Icons.stream, size: 16), - label: const Text( - 'LIVE LOG', - style: TextStyle( - fontFamily: 'Inter', - fontSize: 9, - fontWeight: FontWeight.w800, - ), - ), - onPressed: () => context.go('/settings/field-test-live-log'), - ), - ), - ], - ), - ], - ), - ); - } - - Widget _buildGitHubCard(BuildContext context, WidgetRef ref) { - final authService = ref.watch(githubAuthServiceProvider); - final hasToken = - authService.getToken() != null && authService.getToken()!.isNotEmpty; - - return Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: const Color(0xFF0D0D0D), - borderRadius: BorderRadius.circular(16), - border: Border.all(color: const Color(0xFF1F1F1F), width: 1.2), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Expanded( - child: Row( - children: [ - Icon( - Icons.cloud_upload_outlined, - color: Color(0xFF00E5FF), - size: 18, - ), - SizedBox(width: 8), - Flexible( - child: Text( - 'GITHUB RIDE LOG SYNC', - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontFamily: 'Inter', - fontSize: 12, - fontWeight: FontWeight.w800, - color: Colors.white, - letterSpacing: 0.1, - ), - ), - ), - ], - ), - ), - const SizedBox(width: 8), - Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), - decoration: BoxDecoration( - color: hasToken - ? const Color(0xFF00E676).withValues(alpha: 0.15) - : Colors.white10, - borderRadius: BorderRadius.circular(8), - ), - child: Text( - hasToken ? 'CONFIGURED' : 'NO TOKEN', - style: TextStyle( - fontFamily: 'Inter', - fontSize: 9, - fontWeight: FontWeight.w800, - color: hasToken ? const Color(0xFF00E676) : Colors.white38, - ), - ), - ), - ], - ), - const SizedBox(height: 8), - const Text( - 'Target Repository: bponders12-cell/ride_logs (branch: main). Offline-first background uploads with zero telemetry disruption.', - style: TextStyle( - fontFamily: 'Inter', - fontSize: 10, - color: Colors.white60, - height: 1.3, - ), - ), - const SizedBox(height: 12), - TextField( - controller: _githubTokenController, - obscureText: _obscureGitHubToken, - decoration: InputDecoration( - labelText: 'GitHub Personal Access Token (PAT)', - labelStyle: const TextStyle( - fontFamily: 'Inter', - fontSize: 11, - color: Colors.white60, - ), - hintText: 'ghp_xxxxxxxxxxxxxxxxxxxx', - hintStyle: const TextStyle( - fontFamily: 'Inter', - fontSize: 11, - color: Colors.white30, - ), - filled: true, - fillColor: const Color(0xFF141414), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: const BorderSide(color: Color(0xFF222222)), - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: const BorderSide(color: Color(0xFF222222)), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: const BorderSide(color: Color(0xFF00E5FF)), - ), - isDense: true, - suffixIcon: IconButton( - icon: Icon( - _obscureGitHubToken ? Icons.visibility_off : Icons.visibility, - color: Colors.white38, - size: 18, - ), - onPressed: () { - setState(() => _obscureGitHubToken = !_obscureGitHubToken); - }, - ), - ), - style: const TextStyle( - fontFamily: 'JetBrains Mono', - fontSize: 12, - color: Colors.white, - ), - onChanged: (val) { - authService.saveToken(val); - }, - ), - const SizedBox(height: 12), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - if (hasToken) - TextButton( - onPressed: () async { - await authService.clearToken(); - _githubTokenController.clear(); - setState(() {}); - }, - child: const Text( - 'CLEAR', - style: TextStyle( - color: Colors.redAccent, - fontFamily: 'Inter', - fontSize: 10, - ), - ), - ), - const SizedBox(width: 8), - ElevatedButton.icon( - style: ElevatedButton.styleFrom( - backgroundColor: const Color( - 0xFF00E5FF, - ).withValues(alpha: 0.15), - foregroundColor: const Color(0xFF00E5FF), - side: const BorderSide(color: Color(0xFF00E5FF)), - padding: const EdgeInsets.symmetric( - horizontal: 14, - vertical: 8, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - onPressed: _isTestingGitHub - ? null - : () async { - setState(() => _isTestingGitHub = true); - final messenger = ScaffoldMessenger.of(context); - final result = await authService.testConnection(); - setState(() => _isTestingGitHub = false); - - messenger.showSnackBar( - SnackBar( - content: Text(result['message'] as String), - backgroundColor: result['success'] == true - ? const Color(0xFF00E676) - : Colors.redAccent, - ), - ); - }, - icon: _isTestingGitHub - ? const SizedBox( - width: 14, - height: 14, - child: CircularProgressIndicator( - strokeWidth: 2, - color: Color(0xFF00E5FF), - ), - ) - : const Icon(Icons.sync, size: 16), - label: const Text( - 'TEST CONNECTION', - style: TextStyle( - fontFamily: 'Inter', - fontSize: 10, - fontWeight: FontWeight.w800, - ), - ), - ), - ], - ), - ], - ), - ); - } - - Widget _buildTomTomCard(BuildContext context, WidgetRef ref) { - final tomtomStatus = ref.watch(tomtomStatusProvider); - - return Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: const Color(0xFF0D0D0D), - borderRadius: BorderRadius.circular(16), - border: Border.all(color: const Color(0xFF1F1F1F), width: 1.2), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Expanded( - child: Row( - children: [ - Icon(Icons.traffic, color: Color(0xFF00E5FF), size: 18), - SizedBox(width: 8), - Flexible( - child: Text( - 'TOMTOM TRAFFIC PROVIDER', - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontFamily: 'Inter', - fontSize: 12, - fontWeight: FontWeight.w800, - color: Colors.white, - letterSpacing: 0.1, - ), - ), - ), - ], - ), - ), - const SizedBox(width: 8), - _buildStatusBadge(tomtomStatus), - ], - ), - const SizedBox(height: 8), - const Text( - 'Supplies real-time accidents, hazards, road closures, and construction feeds.', - style: TextStyle( - fontFamily: 'Inter', - fontSize: 10, - color: Colors.white60, - height: 1.3, - ), - ), - const SizedBox(height: 12), - TextField( - controller: _tomtomKeyController, - decoration: InputDecoration( - labelText: 'TomTom API Key', - labelStyle: const TextStyle( - fontFamily: 'Inter', - fontSize: 11, - color: Colors.white60, - ), - hintText: 'Enter your TomTom API Key', - hintStyle: const TextStyle( - fontFamily: 'Inter', - fontSize: 11, - color: Colors.white30, - ), - filled: true, - fillColor: const Color(0xFF141414), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: const BorderSide(color: Color(0xFF222222)), - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: const BorderSide(color: Color(0xFF222222)), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: const BorderSide(color: Color(0xFF00E5FF)), - ), - isDense: true, - ), - style: const TextStyle( - fontFamily: 'Inter', - fontSize: 12, - color: Colors.white, - ), - onChanged: (val) { - ref.read(tomtomApiKeyProvider.notifier).setApiKey(val); - }, - ), - const SizedBox(height: 12), - Align( - alignment: Alignment.centerRight, - child: ElevatedButton.icon( - style: ElevatedButton.styleFrom( - backgroundColor: const Color( - 0xFF00E5FF, - ).withValues(alpha: 0.15), - foregroundColor: const Color(0xFF00E5FF), - side: const BorderSide(color: Color(0xFF00E5FF)), - padding: const EdgeInsets.symmetric( - horizontal: 14, - vertical: 8, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - onPressed: _isTestingTomTom - ? null - : () async { - setState(() => _isTestingTomTom = true); - final messenger = ScaffoldMessenger.of(context); - final provider = ref.read(tomtomProviderInstance); - final status = await provider.testConnection( - _tomtomKeyController.text, - ); - ref.read(tomtomStatusProvider.notifier).state = status; - setState(() => _isTestingTomTom = false); - - String msg = status == RoadAlertProviderStatus.connected - ? 'TomTom API Connected successfully!' - : 'Status: ${status.name}'; - messenger.showSnackBar(SnackBar(content: Text(msg))); - }, - icon: _isTestingTomTom - ? const SizedBox( - width: 14, - height: 14, - child: CircularProgressIndicator( - strokeWidth: 2, - color: Color(0xFF00E5FF), - ), - ) - : const Icon(Icons.sync, size: 16), - label: const Text( - 'TEST CONNECTION', - style: TextStyle( - fontFamily: 'Inter', - fontSize: 10, - fontWeight: FontWeight.w800, - ), - ), - ), - ), - ], - ), - ); - } - - Widget _buildCommunityCard(BuildContext context, WidgetRef ref) { - final communityStatus = ref.watch(communityStatusProvider); - - return Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: const Color(0xFF0D0D0D), - borderRadius: BorderRadius.circular(16), - border: Border.all(color: const Color(0xFF1F1F1F), width: 1.2), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Expanded( - child: Row( - children: [ - Icon( - Icons.local_police, - color: Color(0xFF00E5FF), - size: 18, - ), - SizedBox(width: 8), - Flexible( - child: Text( - 'COMMUNITY TRAFFIC PROVIDER', - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontFamily: 'Inter', - fontSize: 12, - fontWeight: FontWeight.w800, - color: Colors.white, - letterSpacing: 0.1, - ), - ), - ), - ], - ), - ), - const SizedBox(width: 8), - _buildStatusBadge(communityStatus), - ], - ), - const SizedBox(height: 8), - const Text( - 'Live crowdsourced speed traps, police radars, and hazard reports.', - style: TextStyle( - fontFamily: 'Inter', - fontSize: 10, - color: Colors.white60, - height: 1.3, - ), - ), - const SizedBox(height: 12), - TextField( - controller: _communityKeyController, - decoration: InputDecoration( - labelText: 'Community API Key', - labelStyle: const TextStyle( - fontFamily: 'Inter', - fontSize: 11, - color: Colors.white60, - ), - hintText: 'Enter API Key', - hintStyle: const TextStyle( - fontFamily: 'Inter', - fontSize: 11, - color: Colors.white30, - ), - filled: true, - fillColor: const Color(0xFF141414), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: const BorderSide(color: Color(0xFF222222)), - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: const BorderSide(color: Color(0xFF222222)), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: const BorderSide(color: Color(0xFF00E5FF)), - ), - isDense: true, - ), - style: const TextStyle( - fontFamily: 'Inter', - fontSize: 12, - color: Colors.white, - ), - onChanged: (val) { - ref.read(communityApiKeyProvider.notifier).setApiKey(val); - }, - ), - const SizedBox(height: 10), - TextField( - controller: _communityEndpointController, - decoration: InputDecoration( - labelText: 'Custom Endpoint URL (Optional)', - labelStyle: const TextStyle( - fontFamily: 'Inter', - fontSize: 11, - color: Colors.white60, - ), - hintText: 'e.g. https://google-map-places.p.rapidapi.com', - hintStyle: const TextStyle( - fontFamily: 'Inter', - fontSize: 11, - color: Colors.white30, - ), - filled: true, - fillColor: const Color(0xFF141414), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: const BorderSide(color: Color(0xFF222222)), - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: const BorderSide(color: Color(0xFF222222)), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: const BorderSide(color: Color(0xFF00E5FF)), - ), - isDense: true, - ), - style: const TextStyle( - fontFamily: 'Inter', - fontSize: 12, - color: Colors.white, - ), - onChanged: (val) { - ref.read(communityEndpointUrlProvider.notifier).setEndpoint(val); - }, - ), - const SizedBox(height: 12), - Align( - alignment: Alignment.centerRight, - child: ElevatedButton.icon( - style: ElevatedButton.styleFrom( - backgroundColor: const Color( - 0xFF00E5FF, - ).withValues(alpha: 0.15), - foregroundColor: const Color(0xFF00E5FF), - side: const BorderSide(color: Color(0xFF00E5FF)), - padding: const EdgeInsets.symmetric( - horizontal: 14, - vertical: 8, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - onPressed: _isTestingCommunity - ? null - : () async { - setState(() => _isTestingCommunity = true); - final messenger = ScaffoldMessenger.of(context); - final provider = ref.read(communityProviderInstance); - final status = await provider.testConnection( - _communityKeyController.text, - ); - ref.read(communityStatusProvider.notifier).state = status; - setState(() => _isTestingCommunity = false); - - String msg = status == RoadAlertProviderStatus.connected - ? 'Community API Connected successfully!' - : 'Status: ${status.name}'; - messenger.showSnackBar(SnackBar(content: Text(msg))); - }, - icon: _isTestingCommunity - ? const SizedBox( - width: 14, - height: 14, - child: CircularProgressIndicator( - strokeWidth: 2, - color: Color(0xFF00E5FF), - ), - ) - : const Icon(Icons.sync, size: 16), - label: const Text( - 'TEST CONNECTION', - style: TextStyle( - fontFamily: 'Inter', - fontSize: 10, - fontWeight: FontWeight.w800, - ), - ), - ), - ), - ], - ), - ); - } - - Widget _buildStatusBadge(RoadAlertProviderStatus status) { - Color color = Colors.white30; - String text = 'OFF'; - if (status == RoadAlertProviderStatus.connected) { - color = const Color(0xFF69F0AE); - text = 'CONNECTED'; - } else if (status == RoadAlertProviderStatus.authError) { - color = const Color(0xFFFF5252); - text = 'AUTH ERROR'; - } else if (status == RoadAlertProviderStatus.rateLimited) { - color = const Color(0xFFFFD740); - text = 'RATE LIMITED'; - } else if (status == RoadAlertProviderStatus.networkError) { - color = const Color(0xFFFF5252); - text = 'NETWORK ERROR'; - } - - return Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), - decoration: BoxDecoration( - color: color.withValues(alpha: 0.15), - borderRadius: BorderRadius.circular(6), - border: Border.all(color: color, width: 0.8), - ), - child: Text( - text, - style: TextStyle( - fontFamily: 'Inter', - fontSize: 9, - fontWeight: FontWeight.w800, - color: color, - ), - ), - ); - } - - Widget _buildVersionFooter(BuildContext context) { - return FutureBuilder( - future: AppInfo.versionString(), - builder: (context, snapshot) { - final ver = snapshot.data ?? AppInfo.fallbackVersion; - return Center( - child: InkWell( - onTap: () { - final identity = AppInfo.identity; - showDialog( - context: context, - builder: (ctx) => AlertDialog( - backgroundColor: const Color(0xFF1E293B), - title: const Row( - children: [ - Icon(Icons.fingerprint, color: Colors.cyanAccent), - SizedBox(width: 8), - Text( - 'Software Identity', - style: TextStyle(color: Colors.white, fontSize: 16), - ), - ], - ), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _diagRow('Application', 'MotorSignal'), - _diagRow('Version', identity.semanticVersion), - _diagRow('Build Number', '${identity.buildNumber}'), - _diagRow('Release Channel', identity.releaseChannel), - _diagRow( - 'Telemetry Schema', - '${identity.telemetrySchemaVersion}', - ), - _diagRow( - 'Export Schema', - '${identity.exportSchemaVersion}', - ), - _diagRow( - 'Git Commit', - identity.gitCommit.length > 12 - ? identity.gitCommit.substring(0, 12) - : identity.gitCommit, - ), - _diagRow('Build Timestamp', identity.buildTimestamp), - ], - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(ctx).pop(), - child: const Text( - 'CLOSE', - style: TextStyle(color: Colors.cyanAccent), - ), - ), - ], - ), - ); - }, - borderRadius: BorderRadius.circular(8), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), - child: Column( - children: [ - Text( - 'MotorSignal v$ver', - style: const TextStyle( - fontFamily: 'Inter', - fontSize: 12, - fontWeight: FontWeight.w700, - color: Colors.white54, - letterSpacing: 1.0, - ), - ), - const SizedBox(height: 2), - const Text( - 'High-Performance Motorcycle Telemetry Engine', - style: TextStyle( - fontFamily: 'Inter', - fontSize: 9, - color: Colors.white30, - ), - ), - ], - ), - ), - ), - ); - }, - ); - } - - static Widget _diagRow(String label, String value) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - label, - style: const TextStyle(color: Colors.white70, fontSize: 12), - ), - Text( - value, - style: const TextStyle( - color: Colors.cyanAccent, - fontWeight: FontWeight.bold, - fontSize: 12, - fontFamily: 'monospace', - ), - ), - ], - ), - ); - } -} diff --git a/test/canonical_settings_account_pro_responsive_test.dart b/test/canonical_settings_account_pro_responsive_test.dart new file mode 100644 index 0000000..5e1cdfc --- /dev/null +++ b/test/canonical_settings_account_pro_responsive_test.dart @@ -0,0 +1,386 @@ +import 'dart:async'; +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:motorcycle_dash/core/theme/theme.dart'; +import 'package:motorcycle_dash/core/theme/theme_mode_provider.dart'; +import 'package:motorcycle_dash/features/settings/settings_screen.dart'; +import 'package:motorcycle_dash/features/settings/account_screen.dart'; +import 'package:motorcycle_dash/features/entitlements/pro_screen.dart'; +import 'package:motorcycle_dash/services/auth/auth_providers.dart'; +import 'package:motorcycle_dash/services/auth/auth_service.dart'; +import 'package:motorcycle_dash/services/auth/auth_state.dart'; +import 'package:motorcycle_dash/services/auth/auth_user.dart'; +import 'package:motorcycle_dash/services/billing/billing_providers.dart'; +import 'package:motorcycle_dash/services/billing/fake_billing_service.dart'; +import 'package:motorcycle_dash/services/entitlements/entitlement_providers.dart'; +import 'package:motorcycle_dash/services/entitlements/entitlement_storage.dart'; +import 'package:motorcycle_dash/database/database.dart'; +import 'package:motorcycle_dash/features/records/records_service.dart'; +import 'package:drift/native.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class _FakeAuth implements AuthService { + final _states = StreamController.broadcast(); + AuthUser? user; + + _FakeAuth({this.user}); + + @override + Stream get authStateChanges => _states.stream; + + @override + AuthState get currentState => user == null + ? const AuthState.unauthenticated(isFirebaseConfigured: true) + : (user!.isAnonymous + ? AuthState.anonymous(user!, isFirebaseConfigured: true) + : AuthState.authenticated(user!, isFirebaseConfigured: true)); + + @override + AuthUser? get currentUser => user; + + @override + bool get isConfigured => true; + + @override + bool get isEmailVerified => user?.emailVerified ?? false; + + @override + Future deleteCurrentUser() async { + user = null; + _states.add(currentState); + } + + @override + void dispose() => _states.close(); + + @override + Future getIdToken([bool forceRefresh = false]) async => 'token'; + + @override + Future reauthenticate({String? password}) async {} + + @override + Future reloadCurrentUser() async {} + + @override + Future sendEmailVerification() async {} + + @override + Future sendPasswordResetEmail(String email) async {} + + @override + Future signInAnonymously() async { + user = const AuthUser( + uid: 'anon-123', + email: null, + isAnonymous: true, + providerId: 'anonymous', + ); + _states.add(currentState); + } + + @override + Future signInWithEmailAndPassword(String email, String password) async { + user = AuthUser( + uid: 'user-123', + email: email, + isAnonymous: false, + providerId: 'password', + ); + _states.add(currentState); + } + + @override + Future createUserWithEmailAndPassword(String email, String password, + {String? displayName}) async { + user = AuthUser( + uid: 'user-123', + email: email, + displayName: displayName, + isAnonymous: false, + providerId: 'password', + ); + _states.add(currentState); + } + + @override + Future signInWithGoogle() async { + user = const AuthUser( + uid: 'google-123', + email: 'test@motorcycle.app', + displayName: 'Test Rider', + isAnonymous: false, + providerId: 'google.com', + ); + _states.add(currentState); + } + + @override + Future signOut() async { + user = null; + _states.add(currentState); + } +} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + final canonicalViewports = { + '320x640 (very narrow phone)': const Size(320, 640), + '400x800 (standard phone)': const Size(400, 800), + '412x915 (tall modern phone)': const Size(412, 915), + '640x320 (short landscape)': const Size(640, 320), + '780x360 (wide landscape)': const Size(780, 360), + '800x360 (wide landscape)': const Size(800, 360), + '840x390 (ultra-wide landscape)': const Size(840, 390), + '768x1024 (portrait tablet)': const Size(768, 1024), + '1024x768 (landscape tablet)': const Size(1024, 768), + }; + + Widget buildTestHost({ + required Widget child, + required SharedPreferences prefs, + required AppDatabase db, + _FakeAuth? auth, + FakeBillingService? billing, + }) { + final fakeAuth = auth ?? _FakeAuth(); + final fakeBilling = billing ?? FakeBillingService(); + + return ProviderScope( + overrides: [ + sharedPreferencesProvider.overrideWithValue(prefs), + databaseProvider.overrideWithValue(db), + authServiceProvider.overrideWithValue(fakeAuth), + billingServiceProvider.overrideWithValue(fakeBilling), + entitlementStorageProvider.overrideWithValue(InMemoryEntitlementStorage()), + ], + child: Consumer( + builder: (context, ref, _) { + final themeMode = ref.watch(appThemeModeProvider); + return MaterialApp( + themeMode: themeMode, + theme: AppTheme.lightTheme, + darkTheme: AppTheme.darkTheme, + home: child, + ); + }, + ), + ); + } + + group('Canonical 9-Viewport SettingsScreen Responsive Tests', () { + for (final entry in canonicalViewports.entries) { + testWidgets('renders cleanly on ${entry.key}', (tester) async { + tester.view.physicalSize = entry.value; + tester.view.devicePixelRatio = 1.0; + addTearDown(() => tester.view.resetPhysicalSize()); + + SharedPreferences.setMockInitialValues({}); + final prefs = await SharedPreferences.getInstance(); + final db = AppDatabase.forTesting(NativeDatabase.memory()); + addTearDown(() => db.close()); + + await tester.pumpWidget( + buildTestHost( + child: const SettingsScreen(), + prefs: prefs, + db: db, + ), + ); + await tester.pumpAndSettle(); + + expect(find.text('Settings'), findsOneWidget); + expect(tester.takeException(), isNull); + }); + } + }); + + group('Canonical 9-Viewport AccountScreen Responsive Tests', () { + for (final entry in canonicalViewports.entries) { + testWidgets('renders signed-out view cleanly on ${entry.key}', (tester) async { + tester.view.physicalSize = entry.value; + tester.view.devicePixelRatio = 1.0; + addTearDown(() => tester.view.resetPhysicalSize()); + + SharedPreferences.setMockInitialValues({}); + final prefs = await SharedPreferences.getInstance(); + final db = AppDatabase.forTesting(NativeDatabase.memory()); + addTearDown(() => db.close()); + + await tester.pumpWidget( + buildTestHost( + child: const AccountScreen(), + prefs: prefs, + db: db, + ), + ); + await tester.pumpAndSettle(); + + expect(find.text('Account & Cloud Identity'), findsOneWidget); + expect(find.text('SIGN IN TO MOTORCLOUD'), findsOneWidget); + expect(tester.takeException(), isNull); + }); + + testWidgets('renders signed-in view cleanly on ${entry.key}', (tester) async { + tester.view.physicalSize = entry.value; + tester.view.devicePixelRatio = 1.0; + addTearDown(() => tester.view.resetPhysicalSize()); + + SharedPreferences.setMockInitialValues({}); + final prefs = await SharedPreferences.getInstance(); + final db = AppDatabase.forTesting(NativeDatabase.memory()); + addTearDown(() => db.close()); + final auth = _FakeAuth( + user: const AuthUser( + uid: 'rider-abc', + email: 'rider@motorcycle.app', + displayName: 'Rider Alpha', + isAnonymous: false, + providerId: 'password', + emailVerified: true, + ), + ); + + await tester.pumpWidget( + buildTestHost( + child: const AccountScreen(), + prefs: prefs, + db: db, + auth: auth, + ), + ); + await tester.pumpAndSettle(); + + expect(find.text('Account & Cloud Identity'), findsOneWidget); + expect(find.text('Rider Alpha'), findsOneWidget); + expect(find.text('SIGN OUT'), findsOneWidget); + expect(tester.takeException(), isNull); + }); + } + }); + + group('Canonical 9-Viewport ProScreen Responsive Tests', () { + for (final entry in canonicalViewports.entries) { + testWidgets('renders cleanly on ${entry.key}', (tester) async { + tester.view.physicalSize = entry.value; + tester.view.devicePixelRatio = 1.0; + addTearDown(() => tester.view.resetPhysicalSize()); + + SharedPreferences.setMockInitialValues({}); + final prefs = await SharedPreferences.getInstance(); + final db = AppDatabase.forTesting(NativeDatabase.memory()); + addTearDown(() => db.close()); + + await tester.pumpWidget( + buildTestHost( + child: const ProScreen(), + prefs: prefs, + db: db, + ), + ); + await tester.pumpAndSettle(); + + expect(find.text('MOTORSIGNAL'), findsOneWidget); + expect(find.text('PRO LIFETIME'), findsOneWidget); + expect(find.text('PRO MONTHLY'), findsOneWidget); + expect(tester.takeException(), isNull); + }); + } + }); + + group('ThemeMode State & Switching Verification', () { + testWidgets('toggles through system, light, dark without data loss', (tester) async { + SharedPreferences.setMockInitialValues({}); + final prefs = await SharedPreferences.getInstance(); + final db = AppDatabase.forTesting(NativeDatabase.memory()); + addTearDown(() => db.close()); + + await tester.pumpWidget( + buildTestHost( + child: const SettingsScreen(), + prefs: prefs, + db: db, + ), + ); + await tester.pumpAndSettle(); + + final container = ProviderScope.containerOf(tester.element(find.byType(SettingsScreen))); + expect(container.read(appThemeModeProvider), ThemeMode.system); + + // Switch to Dark + await container.read(appThemeModeProvider.notifier).setThemeMode(ThemeMode.dark); + await tester.pumpAndSettle(); + expect(container.read(appThemeModeProvider), ThemeMode.dark); + + // Switch to Light + await container.read(appThemeModeProvider.notifier).setThemeMode(ThemeMode.light); + await tester.pumpAndSettle(); + expect(container.read(appThemeModeProvider), ThemeMode.light); + + // Switch back to System + await container.read(appThemeModeProvider.notifier).setThemeMode(ThemeMode.system); + await tester.pumpAndSettle(); + expect(container.read(appThemeModeProvider), ThemeMode.system); + + expect(tester.takeException(), isNull); + }); + }); + + group('Short Landscape & IME Safety', () { + testWidgets('auth forms in 640x320 landscape allow scrolling and input with keyboard', (tester) async { + tester.view.physicalSize = const Size(640, 320); + tester.view.devicePixelRatio = 1.0; + addTearDown(() => tester.view.resetPhysicalSize()); + + SharedPreferences.setMockInitialValues({}); + final prefs = await SharedPreferences.getInstance(); + final db = AppDatabase.forTesting(NativeDatabase.memory()); + addTearDown(() => db.close()); + + await tester.pumpWidget( + buildTestHost( + child: const AccountScreen(), + prefs: prefs, + db: db, + ), + ); + await tester.pumpAndSettle(); + + // Enter email and password + await tester.enterText(find.byType(TextFormField).first, 'test@rider.com'); + await tester.enterText(find.byType(TextFormField).last, 'secret123'); + await tester.pump(); + + expect(find.text('test@rider.com'), findsOneWidget); + expect(tester.takeException(), isNull); + }); + }); + + group('Legacy Theme Retirement Audit Assertion', () { + test('verifies zero references to retired theme identifiers in lib/', () { + final libDir = Directory('lib'); + final files = libDir.listSync(recursive: true).whereType().where((f) => f.path.endsWith('.dart')); + + final retiredTerms = [ + 'DashboardThemeMode', + 'dashboardThemeModeProvider', + 'EvolutionSettingsView', + 'app_dashboard_theme_mode', + ]; + + for (final file in files) { + final text = file.readAsStringSync(); + for (final term in retiredTerms) { + expect( + text.contains(term), + isFalse, + reason: 'Found retired term "$term" in ${file.path}', + ); + } + } + }); + }); +} diff --git a/test/evolution_theme_test.dart b/test/evolution_theme_test.dart index e00b351..b4f06ae 100644 --- a/test/evolution_theme_test.dart +++ b/test/evolution_theme_test.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:motorcycle_dash/core/theme/evolution_theme_config.dart'; import 'package:motorcycle_dash/features/records/records_service.dart'; import 'package:motorcycle_dash/features/dashboard/widgets/evolution_portrait_dashboard.dart'; import 'package:motorcycle_dash/features/records/records_screen.dart'; @@ -11,28 +10,6 @@ import 'package:motorcycle_dash/features/rides/ride_state_machine.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - group('Evolution Theme Configuration Tests', () { - test('Default theme is classic without altering existing user experience', () async { - SharedPreferences.setMockInitialValues({}); - final prefs = await SharedPreferences.getInstance(); - - final notifier = DashboardThemeModeNotifier(prefs); - expect(notifier.state, equals(DashboardThemeMode.classic)); - }); - - test('Toggling theme switches to evolution and persists', () async { - SharedPreferences.setMockInitialValues({}); - final prefs = await SharedPreferences.getInstance(); - - final notifier = DashboardThemeModeNotifier(prefs); - notifier.setThemeMode(DashboardThemeMode.evolution); - expect(notifier.state, equals(DashboardThemeMode.evolution)); - - final reloaded = DashboardThemeModeNotifier(prefs); - expect(reloaded.state, equals(DashboardThemeMode.evolution)); - }); - }); - group('Evolution UI Widgets Rendering Tests', () { testWidgets('EvolutionPortraitDashboard renders speedometer and HUD pods', (tester) async { SharedPreferences.setMockInitialValues({}); diff --git a/test/pro_screen_test.dart b/test/pro_screen_test.dart index 6adb486..4cee823 100644 --- a/test/pro_screen_test.dart +++ b/test/pro_screen_test.dart @@ -160,7 +160,7 @@ void main() { await tester.pumpWidget( buildTestableWidget( - child: const Scaffold(body: CurrentPlanCard(isEvolution: true)), + child: const Scaffold(body: CurrentPlanCard()), overrides: [entitlementProvider.overrideWithValue(freeState)], ), ); @@ -186,7 +186,7 @@ void main() { await tester.pumpWidget( buildTestableWidget( - child: const Scaffold(body: CurrentPlanCard(isEvolution: false)), + child: const Scaffold(body: CurrentPlanCard()), overrides: [entitlementProvider.overrideWithValue(proState)], ), ); @@ -207,7 +207,7 @@ void main() { await tester.pumpWidget( buildTestableWidget( - child: const Scaffold(body: CurrentPlanCard(isEvolution: true)), + child: const Scaffold(body: CurrentPlanCard()), overrides: [entitlementProvider.overrideWithValue(lifetimeState)], ), ); @@ -235,7 +235,7 @@ void main() { await tester.pumpWidget( buildTestableWidget( - child: const Scaffold(body: CurrentPlanCard(isEvolution: true)), + child: const Scaffold(body: CurrentPlanCard()), overrides: [entitlementProvider.overrideWithValue(graceState)], ), ); diff --git a/test/theme_test.dart b/test/theme_test.dart index 93337a7..b015a93 100644 --- a/test/theme_test.dart +++ b/test/theme_test.dart @@ -1,6 +1,5 @@ -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:motorcycle_dash/core/theme/app_colors.dart'; import 'package:motorcycle_dash/core/theme/app_spacing.dart'; @@ -8,8 +7,6 @@ import 'package:motorcycle_dash/core/theme/app_typography.dart'; import 'package:motorcycle_dash/core/theme/theme.dart'; import 'package:motorcycle_dash/core/theme/theme_extensions.dart'; import 'package:motorcycle_dash/core/theme/theme_mode_provider.dart'; -import 'package:motorcycle_dash/core/theme/evolution_theme_config.dart'; -import 'package:motorcycle_dash/features/records/records_service.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -133,23 +130,5 @@ void main() { final notifier = AppThemeModeNotifier(prefs); expect(notifier.state, ThemeMode.light); }); - - test('DashboardThemeMode and ThemeMode are distinct independent concepts', () async { - SharedPreferences.setMockInitialValues({ - 'app_theme_mode': 'light', - 'app_dashboard_theme_mode': 'evolution', - }); - final prefs = await SharedPreferences.getInstance(); - - final container = ProviderContainer( - overrides: [ - sharedPreferencesProvider.overrideWithValue(prefs), - ], - ); - - expect(container.read(appThemeModeProvider), ThemeMode.light); - // Deprecated dashboard theme mode maintains legacy behavior - expect(container.read(dashboardThemeModeProvider), DashboardThemeMode.evolution); - }); }); }