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/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.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_models.dart';
import '../../services/billing/billing_providers.dart'; import '../../services/billing/billing_providers.dart';
import '../../services/billing/product_catalog.dart'; import '../../services/billing/product_catalog.dart';
@ -72,7 +74,6 @@ class _ProScreenState extends ConsumerState<ProScreen> {
}); });
} }
} else { } else {
// Flow handed over to PlayBillingService purchaseStream -> EntitlementCoordinator
if (mounted) { if (mounted) {
setState(() { setState(() {
_currentStep = ProPurchaseStep.idle; _currentStep = ProPurchaseStep.idle;
@ -90,7 +91,6 @@ class _ProScreenState extends ConsumerState<ProScreen> {
try { try {
final billing = ref.read(billingServiceProvider); final billing = ref.read(billingServiceProvider);
await billing.restorePurchases(); await billing.restorePurchases();
// Allow brief moment for store emission
await Future<void>.delayed(const Duration(seconds: 1)); await Future<void>.delayed(const Duration(seconds: 1));
if (mounted) { if (mounted) {
@ -126,6 +126,10 @@ class _ProScreenState extends ConsumerState<ProScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final entitlement = ref.watch(entitlementProvider); final entitlement = ref.watch(entitlementProvider);
final colors = context.motorColors;
final snapshot = context.responsive;
final isWide = snapshot.isMedium || snapshot.isExpanded;
final monthlyProd = _products final monthlyProd = _products
.where((p) => p.id == ProductCatalog.proMonthly) .where((p) => p.id == ProductCatalog.proMonthly)
.firstOrNull; .firstOrNull;
@ -158,39 +162,135 @@ class _ProScreenState extends ConsumerState<ProScreen> {
), ),
), ),
actions: [ actions: [
TextButton( 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 onPressed: _currentStep != ProPurchaseStep.idle
? null ? null
: _handleRestore, : _handleRestore,
child: const Text( child: Text(
'RESTORE', 'RESTORE',
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Color(0xFF00E5FF), color: colors.cyberCyan,
),
), ),
), ),
), ),
], ],
), ),
body: SafeArea( body: SafeArea(
child: ListView( child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 12.0), 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),
),
),
),
),
);
}
Widget _buildWideLayout(
EntitlementState entitlement,
StoreProduct? monthlyProd,
StoreProduct? lifetimeProd,
) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
flex: 5,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
// Status Banner if present
if (_statusBanner != null) ...[ if (_statusBanner != null) ...[
Container( _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), padding: const EdgeInsets.all(12),
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: _isBannerError color: _isBannerError
? Colors.redAccent.withValues(alpha: 0.15) ? colors.raceRed.withValues(alpha: 0.15)
: Colors.greenAccent.withValues(alpha: 0.15), : colors.launchGreen.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
border: Border.all( border: Border.all(
color: _isBannerError color: _isBannerError ? colors.raceRed : colors.launchGreen,
? Colors.redAccent
: Colors.greenAccent,
width: 0.8, width: 0.8,
), ),
), ),
@ -200,9 +300,7 @@ class _ProScreenState extends ConsumerState<ProScreen> {
_isBannerError _isBannerError
? Icons.error_outline ? Icons.error_outline
: Icons.check_circle_outline, : Icons.check_circle_outline,
color: _isBannerError color: _isBannerError ? colors.raceRed : colors.launchGreen,
? Colors.redAccent
: Colors.greenAccent,
size: 20, size: 20,
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
@ -211,33 +309,30 @@ class _ProScreenState extends ConsumerState<ProScreen> {
_statusBanner!, _statusBanner!,
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: _isBannerError color: _isBannerError ? colors.raceRed : colors.launchGreen,
? Colors.redAccent
: Colors.greenAccent,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
), ),
), ),
], ],
), ),
), );
], }
// Active Pro Banner if user is already verified Pro Widget _buildActiveProBanner(EntitlementState entitlement) {
if (entitlement.isPro) ...[ final colors = context.motorColors;
Container( return Container(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
margin: const EdgeInsets.only(bottom: 20),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF00E5FF).withValues(alpha: 0.1), color: colors.cyberCyan.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFF00E5FF)), border: Border.all(color: colors.cyberCyan),
), ),
child: Row( child: Row(
children: [ children: [
const Icon( Icon(
Icons.verified, Icons.verified,
color: Color(0xFF00E5FF), color: colors.cyberCyan,
size: 28, size: 28,
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
@ -249,11 +344,11 @@ class _ProScreenState extends ConsumerState<ProScreen> {
entitlement.plan == MotorSignalPlan.proLifetime entitlement.plan == MotorSignalPlan.proLifetime
? 'MOTORSIGNAL PRO LIFETIME ACTIVE' ? 'MOTORSIGNAL PRO LIFETIME ACTIVE'
: 'MOTORSIGNAL PRO ACTIVE', : 'MOTORSIGNAL PRO ACTIVE',
style: const TextStyle( style: TextStyle(
fontFamily: 'Sora', fontFamily: 'Sora',
fontSize: 13, fontSize: 13,
fontWeight: FontWeight.w900, fontWeight: FontWeight.w900,
color: Color(0xFF00E5FF), color: colors.cyberCyan,
letterSpacing: 0.5, letterSpacing: 0.5,
), ),
), ),
@ -274,11 +369,14 @@ class _ProScreenState extends ConsumerState<ProScreen> {
), ),
], ],
), ),
), );
], }
// Header Pitch Widget _buildPitchHeader() {
const Text( return const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'PRECISION TELEMETRY.\nUNLIMITED RIDE DATA.', 'PRECISION TELEMETRY.\nUNLIMITED RIDE DATA.',
style: TextStyle( style: TextStyle(
fontFamily: 'Sora', fontFamily: 'Sora',
@ -289,8 +387,8 @@ class _ProScreenState extends ConsumerState<ProScreen> {
height: 1.2, height: 1.2,
), ),
), ),
const SizedBox(height: 8), SizedBox(height: 8),
const Text( Text(
'Unlock 20Hz deep telemetry analysis, multiple motorcycle profiles, unlimited cloud sync, and forensic data exports.', 'Unlock 20Hz deep telemetry analysis, multiple motorcycle profiles, unlimited cloud sync, and forensic data exports.',
style: TextStyle( style: TextStyle(
fontSize: 13, fontSize: 13,
@ -298,19 +396,31 @@ class _ProScreenState extends ConsumerState<ProScreen> {
height: 1.4, height: 1.4,
), ),
), ),
const SizedBox(height: 24), ],
);
}
// Plan Cards Widget _buildPlanSection(
if (_isLoadingProducts) ...[ EntitlementState entitlement,
const Center( StoreProduct? monthlyProd,
StoreProduct? lifetimeProd,
) {
if (_isLoadingProducts) {
return Center(
child: Padding( child: Padding(
padding: EdgeInsets.all(32.0), padding: const EdgeInsets.all(32.0),
child: CircularProgressIndicator(color: Color(0xFF00E5FF)), child: CircularProgressIndicator(color: context.motorColors.cyberCyan),
), ),
), );
] else if (!_storeAvailable) ...[ }
_buildStoreUnavailableCard(),
] else ...[ if (!_storeAvailable) {
return _buildStoreUnavailableCard();
}
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Lifetime Plan Card // Lifetime Plan Card
_buildPlanCard( _buildPlanCard(
title: 'PRO LIFETIME', title: 'PRO LIFETIME',
@ -346,24 +456,25 @@ class _ProScreenState extends ConsumerState<ProScreen> {
: null, : null,
), ),
], ],
);
}
const SizedBox(height: 28), Widget _buildBenefitsSection() {
const Divider(color: Colors.white12), final colors = context.motorColors;
const SizedBox(height: 16), return Column(
crossAxisAlignment: CrossAxisAlignment.start,
// Benefits Checklist children: [
const Text( Text(
'WHAT IS INCLUDED IN PRO', 'WHAT IS INCLUDED IN PRO',
style: TextStyle( style: TextStyle(
fontFamily: 'Sora', fontFamily: 'Sora',
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
color: Color(0xFF00E5FF), color: colors.cyberCyan,
letterSpacing: 1.0, letterSpacing: 1.0,
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
_buildBenefitRow( _buildBenefitRow(
icon: Icons.speed, icon: Icons.speed,
title: 'Advanced Acceleration & Braking', title: 'Advanced Acceleration & Braking',
@ -412,13 +523,12 @@ class _ProScreenState extends ConsumerState<ProScreen> {
desc: desc:
'Multi-waypoint scenic routing and group pack coordination features.', 'Multi-waypoint scenic routing and group pack coordination features.',
), ),
],
);
}
const SizedBox(height: 24), Widget _buildDisclaimer() {
const Divider(color: Colors.white12), return const Text(
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.', '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, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
@ -426,25 +536,21 @@ class _ProScreenState extends ConsumerState<ProScreen> {
color: Colors.white38, color: Colors.white38,
height: 1.4, height: 1.4,
), ),
),
const SizedBox(height: 24),
],
),
),
); );
} }
Widget _buildStoreUnavailableCard() { Widget _buildStoreUnavailableCard() {
final colors = context.motorColors;
return Container( return Container(
padding: const EdgeInsets.all(18), padding: const EdgeInsets.all(18),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF141414), color: colors.surfaceContainer,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.white12), border: Border.all(color: colors.outlineVariant),
), ),
child: Column( child: Column(
children: [ 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 SizedBox(height: 12),
const Text( const Text(
'Google Play Billing Unavailable', 'Google Play Billing Unavailable',
@ -458,6 +564,7 @@ class _ProScreenState extends ConsumerState<ProScreen> {
), ),
const SizedBox(height: 14), const SizedBox(height: 14),
OutlinedButton( OutlinedButton(
style: OutlinedButton.styleFrom(minimumSize: const Size(120, 48)),
onPressed: _loadStoreProducts, onPressed: _loadStoreProducts,
child: const Text('RETRY CONNECTION'), child: const Text('RETRY CONNECTION'),
), ),
@ -476,27 +583,28 @@ class _ProScreenState extends ConsumerState<ProScreen> {
required bool isCurrentPlan, required bool isCurrentPlan,
required VoidCallback? onTap, required VoidCallback? onTap,
}) { }) {
return Container( final colors = context.motorColors;
decoration: BoxDecoration( return Material(
color: isHighlighted
? const Color(0xFF001A1E)
: const Color(0xFF0F0F0F),
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, color: Colors.transparent,
child: InkWell( child: InkWell(
borderRadius: BorderRadius.circular(14), borderRadius: BorderRadius.circular(14),
onTap: isCurrentPlan ? null : onTap, onTap: isCurrentPlan ? null : onTap,
child: Padding( 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), padding: const EdgeInsets.all(16.0),
child: Row( child: Row(
children: [ children: [
@ -526,12 +634,12 @@ class _ProScreenState extends ConsumerState<ProScreen> {
vertical: 2, vertical: 2,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF00E5FF), color: colors.cyberCyan,
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
), ),
child: Text( child: const Text(
badgeText, 'BEST VALUE',
style: const TextStyle( style: TextStyle(
fontSize: 9, fontSize: 9,
fontWeight: FontWeight.w900, fontWeight: FontWeight.w900,
color: Colors.black, color: Colors.black,
@ -546,18 +654,18 @@ class _ProScreenState extends ConsumerState<ProScreen> {
vertical: 2, vertical: 2,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.greenAccent.withValues( color: colors.launchGreen.withValues(
alpha: 0.2, alpha: 0.2,
), ),
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
border: Border.all(color: Colors.greenAccent), border: Border.all(color: colors.launchGreen),
), ),
child: const Text( child: Text(
'CURRENT PLAN', 'CURRENT PLAN',
style: TextStyle( style: TextStyle(
fontSize: 9, fontSize: 9,
fontWeight: FontWeight.w900, fontWeight: FontWeight.w900,
color: Colors.greenAccent, color: colors.launchGreen,
letterSpacing: 0.5, letterSpacing: 0.5,
), ),
), ),
@ -591,7 +699,7 @@ class _ProScreenState extends ConsumerState<ProScreen> {
fontSize: 17, fontSize: 17,
fontWeight: FontWeight.w900, fontWeight: FontWeight.w900,
color: isHighlighted color: isHighlighted
? const Color(0xFF00E5FF) ? colors.cyberCyan
: Colors.white, : Colors.white,
), ),
), ),
@ -602,7 +710,7 @@ class _ProScreenState extends ConsumerState<ProScreen> {
style: TextStyle( style: TextStyle(
fontSize: 11, fontSize: 11,
color: isHighlighted color: isHighlighted
? const Color(0xFF00E5FF) ? colors.cyberCyan
: Colors.white54, : Colors.white54,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
@ -614,7 +722,6 @@ class _ProScreenState extends ConsumerState<ProScreen> {
), ),
), ),
), ),
),
); );
} }
@ -623,6 +730,7 @@ class _ProScreenState extends ConsumerState<ProScreen> {
required String title, required String title,
required String desc, required String desc,
}) { }) {
final colors = context.motorColors;
return Padding( return Padding(
padding: const EdgeInsets.only(bottom: 16.0), padding: const EdgeInsets.only(bottom: 16.0),
child: Row( child: Row(
@ -631,10 +739,10 @@ class _ProScreenState extends ConsumerState<ProScreen> {
Container( Container(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF00E5FF).withValues(alpha: 0.1), color: colors.cyberCyan.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8), 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), const SizedBox(width: 14),
Expanded( 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:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import '../../../core/theme/theme_extensions.dart';
import '../../../services/billing/product_catalog.dart'; import '../../../services/billing/product_catalog.dart';
import '../../../services/entitlements/entitlement_providers.dart'; import '../../../services/entitlements/entitlement_providers.dart';
import '../../../services/entitlements/entitlement_state.dart'; import '../../../services/entitlements/entitlement_state.dart';
import '../../entitlements/widgets/pro_lock_badge.dart'; import '../../entitlements/widgets/pro_lock_badge.dart';
/// Reusable card displaying the current plan and subscription management options. /// Reusable canonical responsive card displaying the current plan and subscription management options.
/// Used in both Classic Settings and Evolution Settings.
class CurrentPlanCard extends ConsumerWidget { class CurrentPlanCard extends ConsumerWidget {
final bool isEvolution; const CurrentPlanCard({super.key});
const CurrentPlanCard({super.key, this.isEvolution = false});
Future<void> _openGooglePlaySubscriptions() async { Future<void> _openGooglePlaySubscriptions() async {
// Official Google Play subscription management deep link // Official Google Play subscription management deep link
@ -28,6 +26,7 @@ class CurrentPlanCard extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final colors = context.motorColors;
final entitlement = ref.watch(entitlementProvider); final entitlement = ref.watch(entitlementProvider);
final isPro = entitlement.isPro; final isPro = entitlement.isPro;
final plan = entitlement.plan; final plan = entitlement.plan;
@ -35,17 +34,17 @@ class CurrentPlanCard extends ConsumerWidget {
String planTitle = 'MotorSignal Free'; String planTitle = 'MotorSignal Free';
String statusSubtitle = 'Basic telemetry & local recording active'; String statusSubtitle = 'Basic telemetry & local recording active';
Color badgeColor = Colors.white54; Color badgeColor = colors.outlineVariant;
String badgeText = 'FREE'; String badgeText = 'FREE';
if (plan == MotorSignalPlan.proLifetime && isPro) { if (plan == MotorSignalPlan.proLifetime && isPro) {
planTitle = 'MotorSignal Pro'; planTitle = 'MotorSignal Pro';
statusSubtitle = 'Lifetime access • Permanent ownership'; statusSubtitle = 'Lifetime access • Permanent ownership';
badgeColor = const Color(0xFF00E5FF); badgeColor = colors.cyberCyan;
badgeText = 'LIFETIME'; badgeText = 'LIFETIME';
} else if (plan == MotorSignalPlan.proMonthly) { } else if (plan == MotorSignalPlan.proMonthly) {
planTitle = 'MotorSignal Pro'; planTitle = 'MotorSignal Pro';
badgeColor = const Color(0xFF00E5FF); badgeColor = colors.cyberCyan;
badgeText = 'MONTHLY'; badgeText = 'MONTHLY';
switch (status) { switch (status) {
@ -57,28 +56,28 @@ class CurrentPlanCard extends ConsumerWidget {
case MotorSignalEntitlementStatus.proMonthlyGrace: case MotorSignalEntitlementStatus.proMonthlyGrace:
statusSubtitle = statusSubtitle =
'Payment grace period • Action needed in Google Play'; 'Payment grace period • Action needed in Google Play';
badgeColor = Colors.amberAccent; badgeColor = colors.warningAmber;
badgeText = 'GRACE PERIOD'; badgeText = 'GRACE PERIOD';
break; break;
case MotorSignalEntitlementStatus.proMonthlyAccountHold: case MotorSignalEntitlementStatus.proMonthlyAccountHold:
statusSubtitle = statusSubtitle =
'Account on hold • Please update billing in Google Play'; 'Account on hold • Please update billing in Google Play';
badgeColor = Colors.redAccent; badgeColor = colors.raceRed;
badgeText = 'ON HOLD'; badgeText = 'ON HOLD';
break; break;
case MotorSignalEntitlementStatus.proMonthlyPaused: case MotorSignalEntitlementStatus.proMonthlyPaused:
statusSubtitle = 'Subscription paused by rider in Google Play'; statusSubtitle = 'Subscription paused by rider in Google Play';
badgeColor = Colors.amberAccent; badgeColor = colors.warningAmber;
badgeText = 'PAUSED'; badgeText = 'PAUSED';
break; break;
case MotorSignalEntitlementStatus.proMonthlyExpired: case MotorSignalEntitlementStatus.proMonthlyExpired:
statusSubtitle = 'Subscription expired'; statusSubtitle = 'Subscription expired';
badgeColor = Colors.white38; badgeColor = colors.outlineVariant;
badgeText = 'EXPIRED'; badgeText = 'EXPIRED';
break; break;
case MotorSignalEntitlementStatus.syncPending: case MotorSignalEntitlementStatus.syncPending:
statusSubtitle = 'Verification pending with Google Play...'; statusSubtitle = 'Verification pending with Google Play...';
badgeColor = Colors.amberAccent; badgeColor = colors.warningAmber;
badgeText = 'VERIFYING'; badgeText = 'VERIFYING';
break; break;
default: default:
@ -87,7 +86,7 @@ class CurrentPlanCard extends ConsumerWidget {
} }
} else if (status == MotorSignalEntitlementStatus.syncPending) { } else if (status == MotorSignalEntitlementStatus.syncPending) {
statusSubtitle = 'Checking purchases with Google Play...'; statusSubtitle = 'Checking purchases with Google Play...';
badgeColor = Colors.amberAccent; badgeColor = colors.warningAmber;
badgeText = 'PENDING'; badgeText = 'PENDING';
} }
@ -96,12 +95,10 @@ class CurrentPlanCard extends ConsumerWidget {
return Container( return Container(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isEvolution ? const Color(0xFF0D0D0D) : null, color: colors.surfaceContainer,
borderRadius: BorderRadius.circular(isEvolution ? 16 : 12), borderRadius: BorderRadius.circular(16),
border: Border.all( border: Border.all(
color: isPro color: isPro ? colors.cyberCyan.withValues(alpha: 0.6) : colors.outlineVariant,
? const Color(0xFF00E5FF).withValues(alpha: 0.6)
: (isEvolution ? const Color(0xFF1F1F1F) : Colors.white12),
width: 1.2, width: 1.2,
), ),
), ),
@ -119,17 +116,17 @@ class CurrentPlanCard extends ConsumerWidget {
children: [ children: [
Icon( Icon(
isPro ? Icons.verified : Icons.shield_outlined, isPro ? Icons.verified : Icons.shield_outlined,
color: isPro ? const Color(0xFF00E5FF) : Colors.white70, color: isPro ? colors.cyberCyan : colors.outline,
size: 20, size: 20,
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
Text( Text(
planTitle.toUpperCase(), planTitle.toUpperCase(),
style: TextStyle( style: TextStyle(
fontFamily: isEvolution ? 'Sora' : null, fontFamily: 'Sora',
fontSize: 13, fontSize: 13,
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
color: isPro ? const Color(0xFF00E5FF) : Colors.white, color: isPro ? colors.cyberCyan : Theme.of(context).colorScheme.onSurface,
letterSpacing: 0.5, letterSpacing: 0.5,
), ),
), ),
@ -162,23 +159,25 @@ class CurrentPlanCard extends ConsumerWidget {
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
statusSubtitle, statusSubtitle,
style: const TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: Colors.white70, color: Theme.of(context).colorScheme.onSurfaceVariant,
height: 1.3, height: 1.3,
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Row( Wrap(
spacing: 10,
runSpacing: 10,
children: [ children: [
if (!isPro) ...[ if (!isPro)
Expanded( ElevatedButton(
child: ElevatedButton(
key: const ValueKey('settings_upgrade_to_pro_button'), key: const ValueKey('settings_upgrade_to_pro_button'),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF00E5FF), backgroundColor: colors.cyberCyan,
foregroundColor: Colors.black, foregroundColor: Colors.black,
padding: const EdgeInsets.symmetric(vertical: 10), minimumSize: const Size(140, 48),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
@ -187,21 +186,19 @@ class CurrentPlanCard extends ConsumerWidget {
child: const Text( child: const Text(
'UPGRADE TO PRO', 'UPGRADE TO PRO',
style: TextStyle( style: TextStyle(
fontSize: 11, fontSize: 12,
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
letterSpacing: 0.5,
), ),
), ),
), ),
), if (isMonthlySub)
const SizedBox(width: 10), OutlinedButton(
],
if (isMonthlySub) ...[
Expanded(
child: OutlinedButton(
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
foregroundColor: Colors.white70, foregroundColor: Theme.of(context).colorScheme.onSurface,
side: const BorderSide(color: Colors.white24), side: BorderSide(color: colors.outlineVariant),
padding: const EdgeInsets.symmetric(vertical: 10), minimumSize: const Size(160, 48),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
@ -215,18 +212,13 @@ class CurrentPlanCard extends ConsumerWidget {
), ),
), ),
), ),
),
const SizedBox(width: 10),
],
OutlinedButton( OutlinedButton(
key: const ValueKey('settings_pro_details_button'), key: const ValueKey('settings_pro_details_button'),
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
foregroundColor: Colors.white70, foregroundColor: Theme.of(context).colorScheme.onSurface,
side: const BorderSide(color: Colors.white24), side: BorderSide(color: colors.outlineVariant),
padding: const EdgeInsets.symmetric( minimumSize: const Size(100, 48),
vertical: 10, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
horizontal: 12,
),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
@ -234,7 +226,7 @@ class CurrentPlanCard extends ConsumerWidget {
onPressed: () => context.push('/settings/pro'), onPressed: () => context.push('/settings/pro'),
child: Text( child: Text(
isPro ? 'VIEW PLANS' : 'DETAILS', 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_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.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/records/records_service.dart';
import 'package:motorcycle_dash/features/dashboard/widgets/evolution_portrait_dashboard.dart'; import 'package:motorcycle_dash/features/dashboard/widgets/evolution_portrait_dashboard.dart';
import 'package:motorcycle_dash/features/records/records_screen.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() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); 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', () { group('Evolution UI Widgets Rendering Tests', () {
testWidgets('EvolutionPortraitDashboard renders speedometer and HUD pods', (tester) async { testWidgets('EvolutionPortraitDashboard renders speedometer and HUD pods', (tester) async {
SharedPreferences.setMockInitialValues({}); SharedPreferences.setMockInitialValues({});

View File

@ -160,7 +160,7 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
buildTestableWidget( buildTestableWidget(
child: const Scaffold(body: CurrentPlanCard(isEvolution: true)), child: const Scaffold(body: CurrentPlanCard()),
overrides: [entitlementProvider.overrideWithValue(freeState)], overrides: [entitlementProvider.overrideWithValue(freeState)],
), ),
); );
@ -186,7 +186,7 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
buildTestableWidget( buildTestableWidget(
child: const Scaffold(body: CurrentPlanCard(isEvolution: false)), child: const Scaffold(body: CurrentPlanCard()),
overrides: [entitlementProvider.overrideWithValue(proState)], overrides: [entitlementProvider.overrideWithValue(proState)],
), ),
); );
@ -207,7 +207,7 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
buildTestableWidget( buildTestableWidget(
child: const Scaffold(body: CurrentPlanCard(isEvolution: true)), child: const Scaffold(body: CurrentPlanCard()),
overrides: [entitlementProvider.overrideWithValue(lifetimeState)], overrides: [entitlementProvider.overrideWithValue(lifetimeState)],
), ),
); );
@ -235,7 +235,7 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
buildTestableWidget( buildTestableWidget(
child: const Scaffold(body: CurrentPlanCard(isEvolution: true)), child: const Scaffold(body: CurrentPlanCard()),
overrides: [entitlementProvider.overrideWithValue(graceState)], 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_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:motorcycle_dash/core/theme/app_colors.dart'; import 'package:motorcycle_dash/core/theme/app_colors.dart';
import 'package:motorcycle_dash/core/theme/app_spacing.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.dart';
import 'package:motorcycle_dash/core/theme/theme_extensions.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/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() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
@ -133,23 +130,5 @@ void main() {
final notifier = AppThemeModeNotifier(prefs); final notifier = AppThemeModeNotifier(prefs);
expect(notifier.state, ThemeMode.light); 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);
});
}); });
} }