MotorSignal-App/lib/features/entitlements/widgets/pro_lock_badge.dart

329 lines
11 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../../services/entitlements/entitlement_providers.dart';
import '../../../services/entitlements/feature_access_service.dart';
import '../../rides/ride_state_machine.dart';
/// Small inline badge indicating a Pro-only feature.
/// Follows MotorSignal cockpit aesthetic (dark surface, subtle cyan accent, no neon glow).
class ProBadge extends StatelessWidget {
final double fontSize;
final EdgeInsetsGeometry padding;
const ProBadge({
super.key,
this.fontSize = 9,
this.padding = const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
});
@override
Widget build(BuildContext context) {
return Container(
padding: padding,
decoration: BoxDecoration(
color: const Color(0xFF00E5FF).withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: const Color(0xFF00E5FF).withValues(alpha: 0.6),
width: 0.8,
),
),
child: Text(
'PRO',
style: TextStyle(
fontFamily: 'Inter',
fontSize: fontSize,
fontWeight: FontWeight.w900,
color: const Color(0xFF00E5FF),
letterSpacing: 0.8,
),
),
);
}
}
/// Metadata description helper for MotorSignal features.
class FeatureBenefitInfo {
final String title;
final String description;
final IconData icon;
const FeatureBenefitInfo({
required this.title,
required this.description,
required this.icon,
});
static FeatureBenefitInfo forFeature(MotorSignalFeature feature) {
switch (feature) {
case MotorSignalFeature.cloudBackup:
case MotorSignalFeature.unlimitedCloudHistory:
return const FeatureBenefitInfo(
title: 'Unlimited MotorCloud Sync',
description:
'Automatically preserve your entire ride catalog, telemetry, and motorcycle configurations to MotorCloud.',
icon: Icons.cloud_done_outlined,
);
case MotorSignalFeature.multiDeviceRestore:
return const FeatureBenefitInfo(
title: 'Multi-Device Cloud Restore',
description:
'Seamlessly sync your complete ride records and garage to new phones or secondary devices.',
icon: Icons.devices_outlined,
);
case MotorSignalFeature.advancedTelemetry:
case MotorSignalFeature.advancedAnalytics:
return const FeatureBenefitInfo(
title: 'Advanced Telemetry & Analytics',
description:
'Deep sensor analysis including 20Hz telemetry graphs, elevation profiles, and cornering metrics.',
icon: Icons.show_chart,
);
case MotorSignalFeature.advancedAcceleration:
return const FeatureBenefitInfo(
title: 'Advanced Acceleration Analysis',
description:
'Analyze longitudinal acceleration forces, sprint splits, and throttle roll-on dynamics.',
icon: Icons.speed,
);
case MotorSignalFeature.advancedBraking:
return const FeatureBenefitInfo(
title: 'Advanced Braking Analysis',
description:
'Detailed braking G-force thresholds, trail-braking telemetry, and stopping deceleration.',
icon: Icons.disc_full_outlined,
);
case MotorSignalFeature.advancedLean:
return const FeatureBenefitInfo(
title: 'Advanced Lean Angle Analysis',
description:
'Comprehensive left/right apex lean angle distribution, transition rate, and corner stability.',
icon: Icons.screen_rotation,
);
case MotorSignalFeature.unlimitedHistory:
return const FeatureBenefitInfo(
title: 'Unlimited Ride History',
description:
'Keep a permanent, searchable ledger of every ride you record without duration caps.',
icon: Icons.history,
);
case MotorSignalFeature.csvExport:
case MotorSignalFeature.pdfExport:
return const FeatureBenefitInfo(
title: 'Forensic CSV & PDF Export',
description:
'Export sub-second 20Hz forensic data tables and printable performance reports.',
icon: Icons.table_chart_outlined,
);
case MotorSignalFeature.useMultipleMotorcycles:
return const FeatureBenefitInfo(
title: 'Multiple Motorcycle Garage',
description:
'Manage your full fleet with motorcycle-specific geometry, specs, and ride logs.',
icon: Icons.two_wheeler,
);
case MotorSignalFeature.planTrips:
return const FeatureBenefitInfo(
title: 'Advanced Route & Trip Planning',
description:
'Plan multi-waypoint tours, fuel stops, and twisty scenic routing.',
icon: Icons.alt_route,
);
case MotorSignalFeature.createGroupRides:
case MotorSignalFeature.useAdvancedGroupRideFeatures:
return const FeatureBenefitInfo(
title: 'Advanced Group Riding',
description:
'Coordinate organized pack rides with real-time route progress and rendezvous points.',
icon: Icons.group_work_outlined,
);
}
}
}
/// Helper to trigger a contextual Pro upgrade sheet.
/// STRICT RIDER SAFETY GUARANTEE:
/// If the rider is currently riding (active ride recording or moving), this modal is SUPPRESSED
/// and replaced with an unobtrusive passive toast to prevent screen distraction.
Future<void> showProUpgradeSheet(
BuildContext context,
WidgetRef ref, {
required MotorSignalFeature feature,
}) async {
final session = ref.read(rideStateMachineProvider);
final isRiding = session.state == RideState.riding;
if (isRiding) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Pro feature locked. Upgrade available when stopped.'),
duration: Duration(seconds: 2),
),
);
return;
}
final info = FeatureBenefitInfo.forFeature(feature);
await showModalBottomSheet<void>(
context: context,
backgroundColor: const Color(0xFF0F0F0F),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder: (ctx) {
return SafeArea(
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: const Color(0xFF00E5FF).withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(10),
),
child: Icon(
info.icon,
color: const Color(0xFF00E5FF),
size: 24,
),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Flexible(
child: Text(
info.title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
const SizedBox(width: 8),
const ProBadge(),
],
),
const SizedBox(height: 2),
const Text(
'MotorSignal Pro Feature',
style: TextStyle(fontSize: 11, color: Colors.white54),
),
],
),
),
],
),
const SizedBox(height: 16),
Text(
info.description,
style: const TextStyle(
fontSize: 13,
color: Colors.white70,
height: 1.4,
),
),
const SizedBox(height: 24),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF00E5FF),
foregroundColor: Colors.black,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
onPressed: () {
Navigator.of(ctx).pop();
context.push('/settings/pro');
},
child: const Text(
'VIEW MOTORSIGNAL PRO',
style: TextStyle(
fontWeight: FontWeight.bold,
letterSpacing: 1.0,
),
),
),
const SizedBox(height: 10),
TextButton(
style: TextButton.styleFrom(foregroundColor: Colors.white60),
onPressed: () => Navigator.of(ctx).pop(),
child: const Text('NOT NOW'),
),
],
),
),
);
},
);
}
/// Reusable inline locked feature row.
class ProLockedRow extends ConsumerWidget {
final MotorSignalFeature feature;
final Widget title;
final Widget? subtitle;
final Widget? leading;
const ProLockedRow({
super.key,
required this.feature,
required this.title,
this.subtitle,
this.leading,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final featureAccess = ref.watch(featureAccessProvider);
final hasAccess = featureAccess.canAccess(feature);
if (hasAccess) {
return const SizedBox.shrink();
}
return InkWell(
onTap: () => showProUpgradeSheet(context, ref, feature: feature),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 12.0),
child: Row(
children: [
if (leading != null) ...[leading!, const SizedBox(width: 12)],
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Flexible(child: title),
const SizedBox(width: 6),
const ProBadge(),
],
),
if (subtitle != null) ...[
const SizedBox(height: 2),
subtitle!,
],
],
),
),
const Icon(Icons.lock_outline, size: 18, color: Colors.white38),
],
),
),
);
}
}