783 lines
25 KiB
Dart
783 lines
25 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import '../../core/responsive/viewport_size.dart';
|
|
import '../../core/theme/theme_extensions.dart';
|
|
import '../../services/billing/billing_models.dart';
|
|
import '../../services/billing/billing_providers.dart';
|
|
import '../../services/billing/product_catalog.dart';
|
|
import '../../services/entitlements/entitlement_providers.dart';
|
|
import '../../services/entitlements/entitlement_state.dart';
|
|
import 'widgets/pro_lock_badge.dart';
|
|
|
|
enum ProPurchaseStep { idle, purchasing, verifying, restoring }
|
|
|
|
class ProScreen extends ConsumerStatefulWidget {
|
|
const ProScreen({super.key});
|
|
|
|
@override
|
|
ConsumerState<ProScreen> createState() => _ProScreenState();
|
|
}
|
|
|
|
class _ProScreenState extends ConsumerState<ProScreen> {
|
|
ProPurchaseStep _currentStep = ProPurchaseStep.idle;
|
|
String? _statusBanner;
|
|
bool _isBannerError = false;
|
|
List<StoreProduct> _products = [];
|
|
bool _isLoadingProducts = true;
|
|
bool _storeAvailable = true;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_loadStoreProducts();
|
|
}
|
|
|
|
Future<void> _loadStoreProducts() async {
|
|
setState(() => _isLoadingProducts = true);
|
|
final billing = ref.read(billingServiceProvider);
|
|
final isAvail = await billing.isAvailable();
|
|
if (!isAvail) {
|
|
if (mounted) {
|
|
setState(() {
|
|
_storeAvailable = false;
|
|
_isLoadingProducts = false;
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
|
|
final prods = await billing.queryProducts();
|
|
if (mounted) {
|
|
setState(() {
|
|
_storeAvailable = true;
|
|
_products = prods;
|
|
_isLoadingProducts = false;
|
|
});
|
|
}
|
|
}
|
|
|
|
Future<void> _handleBuy(String productId) async {
|
|
final billing = ref.read(billingServiceProvider);
|
|
setState(() {
|
|
_currentStep = ProPurchaseStep.purchasing;
|
|
_statusBanner = null;
|
|
});
|
|
|
|
final launched = await billing.launchPurchaseFlow(productId);
|
|
if (!launched) {
|
|
if (mounted) {
|
|
setState(() {
|
|
_currentStep = ProPurchaseStep.idle;
|
|
_statusBanner =
|
|
'Unable to launch Google Play purchase. Please try again.';
|
|
_isBannerError = true;
|
|
});
|
|
}
|
|
} else {
|
|
if (mounted) {
|
|
setState(() {
|
|
_currentStep = ProPurchaseStep.idle;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _handleRestore() async {
|
|
setState(() {
|
|
_currentStep = ProPurchaseStep.restoring;
|
|
_statusBanner = null;
|
|
});
|
|
|
|
try {
|
|
final billing = ref.read(billingServiceProvider);
|
|
await billing.restorePurchases();
|
|
await Future<void>.delayed(const Duration(seconds: 1));
|
|
|
|
if (mounted) {
|
|
final entitlement = ref.read(entitlementProvider);
|
|
setState(() {
|
|
_currentStep = ProPurchaseStep.idle;
|
|
if (entitlement.isPro) {
|
|
_statusBanner = 'MotorSignal Pro restored successfully.';
|
|
_isBannerError = false;
|
|
} else if (entitlement.status ==
|
|
MotorSignalEntitlementStatus.syncPending) {
|
|
_statusBanner = 'Purchase found. Verifying entitlement...';
|
|
_isBannerError = false;
|
|
} else {
|
|
_statusBanner =
|
|
'No active purchases found for this Google Play account.';
|
|
_isBannerError = false;
|
|
}
|
|
});
|
|
}
|
|
} catch (e) {
|
|
if (mounted) {
|
|
setState(() {
|
|
_currentStep = ProPurchaseStep.idle;
|
|
_statusBanner =
|
|
'Restore request failed. Please check your network connection.';
|
|
_isBannerError = true;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final entitlement = ref.watch(entitlementProvider);
|
|
final colors = context.motorColors;
|
|
final snapshot = context.responsive;
|
|
final isWide = snapshot.isMedium || snapshot.isExpanded;
|
|
|
|
final monthlyProd = _products
|
|
.where((p) => p.id == ProductCatalog.proMonthly)
|
|
.firstOrNull;
|
|
final lifetimeProd = _products
|
|
.where((p) => p.id == ProductCatalog.proLifetime)
|
|
.firstOrNull;
|
|
|
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
|
return Scaffold(
|
|
backgroundColor: isDark ? const Color(0xFF0B0D0F) : Theme.of(context).colorScheme.surface,
|
|
appBar: AppBar(
|
|
backgroundColor: Colors.transparent,
|
|
elevation: 0,
|
|
title: const FittedBox(
|
|
fit: BoxFit.scaleDown,
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text(
|
|
'MOTORSIGNAL',
|
|
style: TextStyle(
|
|
fontFamily: 'Sora',
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w900,
|
|
letterSpacing: 1.5,
|
|
),
|
|
),
|
|
SizedBox(width: 6),
|
|
ProBadge(),
|
|
],
|
|
),
|
|
),
|
|
actions: [
|
|
Padding(
|
|
padding: const EdgeInsets.only(right: 8.0),
|
|
child: TextButton(
|
|
style: TextButton.styleFrom(
|
|
minimumSize: const Size(48, 48),
|
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
|
),
|
|
onPressed: _currentStep != ProPurchaseStep.idle
|
|
? null
|
|
: _handleRestore,
|
|
child: Text(
|
|
'RESTORE',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.bold,
|
|
color: colors.cyberCyan,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
body: SafeArea(
|
|
child: SingleChildScrollView(
|
|
padding: EdgeInsets.symmetric(
|
|
horizontal: isWide ? 24.0 : 20.0,
|
|
vertical: 12.0,
|
|
),
|
|
child: Center(
|
|
child: ConstrainedBox(
|
|
constraints: BoxConstraints(
|
|
maxWidth: isWide ? 1100 : 640,
|
|
),
|
|
child: isWide
|
|
? _buildWideLayout(entitlement, monthlyProd, lifetimeProd)
|
|
: _buildCompactLayout(entitlement, monthlyProd, lifetimeProd),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildWideLayout(
|
|
EntitlementState entitlement,
|
|
StoreProduct? monthlyProd,
|
|
StoreProduct? lifetimeProd,
|
|
) {
|
|
return Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Expanded(
|
|
flex: 5,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
if (_statusBanner != null) ...[
|
|
_buildStatusBanner(),
|
|
const SizedBox(height: 16),
|
|
],
|
|
if (entitlement.isPro) ...[
|
|
_buildActiveProBanner(entitlement),
|
|
const SizedBox(height: 20),
|
|
],
|
|
_buildPitchHeader(),
|
|
const SizedBox(height: 24),
|
|
_buildPlanSection(entitlement, monthlyProd, lifetimeProd),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(width: 32),
|
|
Expanded(
|
|
flex: 6,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
_buildBenefitsSection(),
|
|
const SizedBox(height: 20),
|
|
_buildDisclaimer(),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildCompactLayout(
|
|
EntitlementState entitlement,
|
|
StoreProduct? monthlyProd,
|
|
StoreProduct? lifetimeProd,
|
|
) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
if (_statusBanner != null) ...[
|
|
_buildStatusBanner(),
|
|
const SizedBox(height: 16),
|
|
],
|
|
if (entitlement.isPro) ...[
|
|
_buildActiveProBanner(entitlement),
|
|
const SizedBox(height: 20),
|
|
],
|
|
_buildPitchHeader(),
|
|
const SizedBox(height: 24),
|
|
_buildPlanSection(entitlement, monthlyProd, lifetimeProd),
|
|
const SizedBox(height: 28),
|
|
const Divider(color: Colors.white12),
|
|
const SizedBox(height: 16),
|
|
_buildBenefitsSection(),
|
|
const SizedBox(height: 24),
|
|
const Divider(color: Colors.white12),
|
|
const SizedBox(height: 12),
|
|
_buildDisclaimer(),
|
|
const SizedBox(height: 24),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildStatusBanner() {
|
|
final colors = context.motorColors;
|
|
return Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: _isBannerError
|
|
? colors.raceRed.withValues(alpha: 0.15)
|
|
: colors.launchGreen.withValues(alpha: 0.15),
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(
|
|
color: _isBannerError ? colors.raceRed : colors.launchGreen,
|
|
width: 0.8,
|
|
),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Icon(
|
|
_isBannerError
|
|
? Icons.error_outline
|
|
: Icons.check_circle_outline,
|
|
color: _isBannerError ? colors.raceRed : colors.launchGreen,
|
|
size: 20,
|
|
),
|
|
const SizedBox(width: 10),
|
|
Expanded(
|
|
child: Text(
|
|
_statusBanner!,
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: _isBannerError ? colors.raceRed : colors.launchGreen,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildActiveProBanner(EntitlementState entitlement) {
|
|
final colors = context.motorColors;
|
|
return Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: colors.cyberCyan.withValues(alpha: 0.1),
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: colors.cyberCyan),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Icon(
|
|
Icons.verified,
|
|
color: colors.cyberCyan,
|
|
size: 28,
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
entitlement.plan == MotorSignalPlan.proLifetime
|
|
? 'MOTORSIGNAL PRO LIFETIME ACTIVE'
|
|
: 'MOTORSIGNAL PRO ACTIVE',
|
|
style: TextStyle(
|
|
fontFamily: 'Sora',
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w900,
|
|
color: colors.cyberCyan,
|
|
letterSpacing: 0.5,
|
|
),
|
|
),
|
|
const SizedBox(height: 2),
|
|
Text(
|
|
entitlement.plan == MotorSignalPlan.proLifetime
|
|
? 'Permanent access enabled. Thank you for supporting MotorSignal.'
|
|
: (entitlement.expiresAt != null
|
|
? 'Subscription active. Renews ${entitlement.expiresAt!.toLocal().toString().split(' ')[0]}.'
|
|
: 'Subscription active.'),
|
|
style: TextStyle(
|
|
fontSize: 11,
|
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildPitchHeader() {
|
|
final theme = Theme.of(context);
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'PRECISION TELEMETRY.\nUNLIMITED RIDE DATA.',
|
|
style: TextStyle(
|
|
fontFamily: 'Sora',
|
|
fontSize: 22,
|
|
fontWeight: FontWeight.w900,
|
|
color: theme.colorScheme.onSurface,
|
|
letterSpacing: 0.2,
|
|
height: 1.2,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'Unlock 20Hz deep telemetry analysis, multiple motorcycle profiles, unlimited cloud sync, and forensic data exports.',
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
color: theme.colorScheme.onSurfaceVariant,
|
|
height: 1.4,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildPlanSection(
|
|
EntitlementState entitlement,
|
|
StoreProduct? monthlyProd,
|
|
StoreProduct? lifetimeProd,
|
|
) {
|
|
if (_isLoadingProducts) {
|
|
return Center(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(32.0),
|
|
child: CircularProgressIndicator(color: context.motorColors.cyberCyan),
|
|
),
|
|
);
|
|
}
|
|
|
|
if (!_storeAvailable) {
|
|
return _buildStoreUnavailableCard();
|
|
}
|
|
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
// Lifetime Plan Card
|
|
_buildPlanCard(
|
|
title: 'PRO LIFETIME',
|
|
subtitle: 'One-time purchase • Permanent access',
|
|
priceDisplay: lifetimeProd?.price ?? 'Unavailable',
|
|
badgeText: lifetimeProd != null ? 'BEST VALUE' : null,
|
|
isHighlighted: true,
|
|
isLoading: _currentStep == ProPurchaseStep.purchasing,
|
|
isCurrentPlan:
|
|
entitlement.plan == MotorSignalPlan.proLifetime &&
|
|
entitlement.isPro,
|
|
onTap: lifetimeProd != null
|
|
? () => _handleBuy(ProductCatalog.proLifetime)
|
|
: null,
|
|
),
|
|
const SizedBox(height: 14),
|
|
|
|
// Monthly Plan Card
|
|
_buildPlanCard(
|
|
title: 'PRO MONTHLY',
|
|
subtitle: 'Recurring subscription • Cancel anytime',
|
|
priceDisplay: monthlyProd != null
|
|
? '${monthlyProd.price}/month'
|
|
: 'Unavailable',
|
|
badgeText: null,
|
|
isHighlighted: false,
|
|
isLoading: _currentStep == ProPurchaseStep.purchasing,
|
|
isCurrentPlan:
|
|
entitlement.plan == MotorSignalPlan.proMonthly &&
|
|
entitlement.isPro,
|
|
onTap: monthlyProd != null
|
|
? () => _handleBuy(ProductCatalog.proMonthly)
|
|
: null,
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildBenefitsSection() {
|
|
final colors = context.motorColors;
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'WHAT IS INCLUDED IN PRO',
|
|
style: TextStyle(
|
|
fontFamily: 'Sora',
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w800,
|
|
color: colors.cyberCyan,
|
|
letterSpacing: 1.0,
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
_buildBenefitRow(
|
|
icon: Icons.speed,
|
|
title: 'Advanced Acceleration & Braking',
|
|
desc:
|
|
'G-force timeline, sprint intervals, roll-on speed, and deceleration thresholds.',
|
|
),
|
|
_buildBenefitRow(
|
|
icon: Icons.screen_rotation,
|
|
title: 'Advanced Lean Angle Analysis',
|
|
desc:
|
|
'Left and right apex lean distributions, transition dynamics, and turn stability.',
|
|
),
|
|
_buildBenefitRow(
|
|
icon: Icons.show_chart,
|
|
title: '20Hz Sub-Second Telemetry',
|
|
desc:
|
|
'High-frequency forensic sensor fusion playback with interactive scrub control.',
|
|
),
|
|
_buildBenefitRow(
|
|
icon: Icons.history,
|
|
title: 'Unlimited Ride History',
|
|
desc:
|
|
'Complete persistent ledger of every ride without duration or count limitations.',
|
|
),
|
|
_buildBenefitRow(
|
|
icon: Icons.table_chart_outlined,
|
|
title: 'Forensic CSV & PDF Export',
|
|
desc:
|
|
'Export structured engineering data and printable performance run sheets.',
|
|
),
|
|
_buildBenefitRow(
|
|
icon: Icons.two_wheeler,
|
|
title: 'Multiple Motorcycle Garage',
|
|
desc:
|
|
'Log and compare telemetry across your entire personal motorcycle fleet.',
|
|
),
|
|
_buildBenefitRow(
|
|
icon: Icons.cloud_done_outlined,
|
|
title: 'Unlimited MotorCloud Sync',
|
|
desc:
|
|
'Automatic encrypted cloud backup with multi-device restore capability.',
|
|
),
|
|
_buildBenefitRow(
|
|
icon: Icons.alt_route,
|
|
title: 'Route Planning & Group Rides',
|
|
desc:
|
|
'Multi-waypoint scenic routing and group pack coordination features.',
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildDisclaimer() {
|
|
return Text(
|
|
'Basic ride recording, cockpit instruments, offline history, local SQLite storage, and crash detection remain 100% free forever. MotorSignal Pro subscriptions can be managed or canceled anytime in Google Play Store settings.',
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(
|
|
fontSize: 11,
|
|
color: Theme.of(context).colorScheme.onSurfaceVariant.withValues(alpha: 0.6),
|
|
height: 1.4,
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildStoreUnavailableCard() {
|
|
final colors = context.motorColors;
|
|
return Container(
|
|
padding: const EdgeInsets.all(18),
|
|
decoration: BoxDecoration(
|
|
color: colors.surfaceContainer,
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: colors.outlineVariant),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
Icon(Icons.cloud_off, color: colors.warningAmber, size: 32),
|
|
const SizedBox(height: 12),
|
|
const Text(
|
|
'Google Play Billing Unavailable',
|
|
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
|
),
|
|
const SizedBox(height: 6),
|
|
Text(
|
|
'Cannot connect to Google Play. Please check your internet connection or Google Play Store service status.',
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant),
|
|
),
|
|
const SizedBox(height: 14),
|
|
OutlinedButton(
|
|
style: OutlinedButton.styleFrom(minimumSize: const Size(120, 48)),
|
|
onPressed: _loadStoreProducts,
|
|
child: const Text('RETRY CONNECTION'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildPlanCard({
|
|
required String title,
|
|
required String subtitle,
|
|
required String priceDisplay,
|
|
required String? badgeText,
|
|
required bool isHighlighted,
|
|
required bool isLoading,
|
|
required bool isCurrentPlan,
|
|
required VoidCallback? onTap,
|
|
}) {
|
|
final colors = context.motorColors;
|
|
return Material(
|
|
color: Colors.transparent,
|
|
child: InkWell(
|
|
borderRadius: BorderRadius.circular(14),
|
|
onTap: isCurrentPlan ? null : onTap,
|
|
child: Container(
|
|
constraints: const BoxConstraints(minHeight: 64),
|
|
decoration: BoxDecoration(
|
|
color: isHighlighted
|
|
? colors.cyberCyan.withValues(alpha: 0.08)
|
|
: colors.surfaceContainer,
|
|
borderRadius: BorderRadius.circular(14),
|
|
border: Border.all(
|
|
color: isCurrentPlan
|
|
? colors.launchGreen
|
|
: (isHighlighted
|
|
? colors.cyberCyan
|
|
: colors.outlineVariant),
|
|
width: isHighlighted || isCurrentPlan ? 1.6 : 1.0,
|
|
),
|
|
),
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Wrap(
|
|
crossAxisAlignment: WrapCrossAlignment.center,
|
|
spacing: 8,
|
|
runSpacing: 4,
|
|
children: [
|
|
Text(
|
|
title,
|
|
style: TextStyle(
|
|
fontFamily: 'Sora',
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.w900,
|
|
color: Theme.of(context).colorScheme.onSurface,
|
|
letterSpacing: 0.5,
|
|
),
|
|
),
|
|
if (badgeText != null)
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 6,
|
|
vertical: 2,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: colors.cyberCyan,
|
|
borderRadius: BorderRadius.circular(4),
|
|
),
|
|
child: const Text(
|
|
'BEST VALUE',
|
|
style: TextStyle(
|
|
fontSize: 9,
|
|
fontWeight: FontWeight.w900,
|
|
color: Colors.black,
|
|
letterSpacing: 0.5,
|
|
),
|
|
),
|
|
),
|
|
if (isCurrentPlan)
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 6,
|
|
vertical: 2,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: colors.launchGreen.withValues(
|
|
alpha: 0.2,
|
|
),
|
|
borderRadius: BorderRadius.circular(4),
|
|
border: Border.all(color: colors.launchGreen),
|
|
),
|
|
child: Text(
|
|
'CURRENT PLAN',
|
|
style: TextStyle(
|
|
fontSize: 9,
|
|
fontWeight: FontWeight.w900,
|
|
color: colors.launchGreen,
|
|
letterSpacing: 0.5,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
subtitle,
|
|
style: TextStyle(
|
|
fontSize: 11,
|
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
ConstrainedBox(
|
|
constraints: const BoxConstraints(maxWidth: 130),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.end,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
FittedBox(
|
|
fit: BoxFit.scaleDown,
|
|
child: Text(
|
|
priceDisplay,
|
|
style: TextStyle(
|
|
fontFamily: 'JetBrains Mono',
|
|
fontSize: 17,
|
|
fontWeight: FontWeight.w900,
|
|
color: isHighlighted
|
|
? colors.cyberCyan
|
|
: Theme.of(context).colorScheme.onSurface,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 2),
|
|
Text(
|
|
isCurrentPlan
|
|
? 'Active'
|
|
: (onTap == null ? 'Unavailable' : 'Select'),
|
|
style: TextStyle(
|
|
fontSize: 11,
|
|
color: isHighlighted
|
|
? colors.cyberCyan
|
|
: (onTap == null
|
|
? Theme.of(context).colorScheme.onSurfaceVariant.withValues(alpha: 0.5)
|
|
: Theme.of(context).colorScheme.onSurfaceVariant),
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildBenefitRow({
|
|
required IconData icon,
|
|
required String title,
|
|
required String desc,
|
|
}) {
|
|
final colors = context.motorColors;
|
|
return Padding(
|
|
padding: const EdgeInsets.only(bottom: 16.0),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(8),
|
|
decoration: BoxDecoration(
|
|
color: colors.cyberCyan.withValues(alpha: 0.1),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Icon(icon, color: colors.cyberCyan, size: 20),
|
|
),
|
|
const SizedBox(width: 14),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
title,
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.bold,
|
|
color: Theme.of(context).colorScheme.onSurface,
|
|
),
|
|
),
|
|
const SizedBox(height: 2),
|
|
Text(
|
|
desc,
|
|
style: TextStyle(
|
|
fontSize: 11,
|
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
height: 1.3,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|