feat: unify settings account and pro experience

This commit is contained in:
Developer 2026-09-05 04:29:43 -04:00
parent 4beacfde03
commit e6e870c991
10 changed files with 3540 additions and 5159 deletions

View File

@ -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<DashboardThemeMode> {
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<DashboardThemeModeNotifier, DashboardThemeMode>((ref) {
final prefs = ref.watch(sharedPreferencesProvider);
return DashboardThemeModeNotifier(prefs);
});

View File

@ -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<ProScreen> {
});
}
} else {
// Flow handed over to PlayBillingService purchaseStream -> EntitlementCoordinator
if (mounted) {
setState(() {
_currentStep = ProPurchaseStep.idle;
@ -90,7 +91,6 @@ class _ProScreenState extends ConsumerState<ProScreen> {
try {
final billing = ref.read(billingServiceProvider);
await billing.restorePurchases();
// Allow brief moment for store emission
await Future<void>.delayed(const Duration(seconds: 1));
if (mounted) {
@ -126,6 +126,10 @@ class _ProScreenState extends ConsumerState<ProScreen> {
@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<ProScreen> {
),
),
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<ProScreen> {
),
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<ProScreen> {
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<ProScreen> {
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<ProScreen> {
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(

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -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<void> _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),
),
),
],

File diff suppressed because it is too large Load Diff

View File

@ -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<AuthState>.broadcast();
AuthUser? user;
_FakeAuth({this.user});
@override
Stream<AuthState> 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<void> deleteCurrentUser() async {
user = null;
_states.add(currentState);
}
@override
void dispose() => _states.close();
@override
Future<String?> getIdToken([bool forceRefresh = false]) async => 'token';
@override
Future<void> reauthenticate({String? password}) async {}
@override
Future<void> reloadCurrentUser() async {}
@override
Future<void> sendEmailVerification() async {}
@override
Future<void> sendPasswordResetEmail(String email) async {}
@override
Future<void> signInAnonymously() async {
user = const AuthUser(
uid: 'anon-123',
email: null,
isAnonymous: true,
providerId: 'anonymous',
);
_states.add(currentState);
}
@override
Future<void> signInWithEmailAndPassword(String email, String password) async {
user = AuthUser(
uid: 'user-123',
email: email,
isAnonymous: false,
providerId: 'password',
);
_states.add(currentState);
}
@override
Future<void> 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<void> 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<void> signOut() async {
user = null;
_states.add(currentState);
}
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final canonicalViewports = <String, Size>{
'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<File>().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}',
);
}
}
});
});
}

View File

@ -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({});

View File

@ -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)],
),
);

View File

@ -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);
});
});
}