feat: unify settings account and pro experience
This commit is contained in:
parent
4beacfde03
commit
e6e870c991
|
|
@ -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);
|
|
||||||
});
|
|
||||||
|
|
@ -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
|
|
@ -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
|
|
@ -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}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -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({});
|
||||||
|
|
|
||||||
|
|
@ -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)],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue