Files
velocity-pay-flutter/lib/screens/onboarding/landing/landing_screen.dart

657 lines
21 KiB
Dart

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import 'package:qpay/models/responsive_policy.dart';
import 'package:qpay/screens/onboarding/onboarding_controller.dart';
class LandingScreen extends StatefulWidget {
const LandingScreen({super.key});
@override
State<LandingScreen> createState() => _LandingScreenState();
}
class _LandingScreenState extends State<LandingScreen> {
late OnboardingController onboardingController;
@override
void initState() {
super.initState();
onboardingController = Provider.of<OnboardingController>(
context,
listen: false,
);
}
void _goToRegister() {
// Reset the registration state so a fresh sign-up starts cleanly.
onboardingController.resetState();
context.go('/onboarding/phone');
}
// Max content width on large screens (desktop / web).
static const double _maxContentWidth = 1200;
// Max content width on tablet — the current mobile cap lifted slightly
// so the page breathes a bit on landscape phones / small tablets.
static const double _maxTabletWidth = 760;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
final width = MediaQuery.of(context).size.width;
final isTablet = width >= ResponsivePolicy.md;
final isDesktop = width >= ResponsivePolicy.lg;
return Scaffold(
// Use a Stack so the gradient background is guaranteed to cover the
// full viewport (including the area under the SafeArea) regardless
// of how tall the content ends up being.
body: Stack(
children: [
Positioned.fill(
child: DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
colorScheme.primary.withValues(alpha: 0.08),
colorScheme.surface,
colorScheme.secondary.withValues(alpha: 0.05),
],
),
),
),
),
SafeArea(
child: SingleChildScrollView(
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: _maxContentWidth),
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: isDesktop ? 48 : 20,
vertical: isDesktop ? 32 : 16,
),
child: LayoutBuilder(
builder: (context, constraints) {
final contentMaxWidth = isDesktop
? _maxContentWidth
: (isTablet
? _maxTabletWidth
: constraints.maxWidth);
return SizedBox(
width: contentMaxWidth,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 12),
_buildTopBar(theme, colorScheme),
SizedBox(height: isDesktop ? 40 : 24),
_buildHero(theme, colorScheme, isDesktop),
SizedBox(height: isDesktop ? 72 : 36),
_buildValuePropsHeadline(theme, colorScheme),
const SizedBox(height: 28),
_buildValueProps(isTablet),
SizedBox(height: isDesktop ? 64 : 32),
_buildCallToActions(
theme,
colorScheme,
isDesktop,
),
const SizedBox(height: 8),
TextButton(
onPressed: () {
context.go('/');
},
child: Text(
'Continue as guest?',
style: TextStyle(
fontSize: 14,
color: colorScheme.onSurface.withValues(
alpha: 0.7,
),
fontWeight: FontWeight.w500,
),
),
),
const SizedBox(height: 24),
_buildFooter(theme, colorScheme),
const SizedBox(height: 12),
],
),
);
},
),
),
),
),
),
),
],
),
);
}
Widget _buildTopBar(ThemeData theme, ColorScheme colorScheme) {
return Row(
children: [
Image(image: AssetImage('assets/velocity.png'), width: 150),
const Spacer(),
TextButton(
onPressed: () {
context.go('/onboarding/login');
},
style: TextButton.styleFrom(
foregroundColor: colorScheme.primary,
textStyle: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
child: const Text('Sign in'),
),
],
);
}
Widget _buildHero(ThemeData theme, ColorScheme colorScheme, bool isDesktop) {
final textBlock = Column(
crossAxisAlignment: isDesktop
? CrossAxisAlignment.start
: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: isDesktop
? Colors.white.withValues(alpha: 0.18)
: colorScheme.primary.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(20),
border: isDesktop
? Border.all(color: Colors.white.withValues(alpha: 0.25))
: null,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.flash_on_rounded,
size: 14,
color: isDesktop ? Colors.white : colorScheme.primary,
),
const SizedBox(width: 6),
Text(
'Fast. Secure. Local.',
style: theme.textTheme.labelMedium?.copyWith(
color: isDesktop ? Colors.white : colorScheme.primary,
fontWeight: FontWeight.w700,
letterSpacing: 0.4,
),
),
],
),
),
SizedBox(height: isDesktop ? 20 : 16),
Text(
'Welcome to Velocity',
textAlign: isDesktop ? TextAlign.start : TextAlign.center,
style:
(isDesktop
? theme.textTheme.displaySmall
: theme.textTheme.headlineMedium)
?.copyWith(
color: isDesktop ? null : colorScheme.primary,
fontWeight: FontWeight.w800,
letterSpacing: -0.5,
height: 1.1,
),
),
SizedBox(height: isDesktop ? 16 : 8),
Text(
'The fastest way to pay for local goods and services.',
textAlign: isDesktop ? TextAlign.start : TextAlign.center,
style: theme.textTheme.titleMedium?.copyWith(
color: isDesktop
? colorScheme.onSurface.withValues(alpha: 0.75)
: colorScheme.onSurface.withValues(alpha: 0.8),
height: 1.4,
fontWeight: FontWeight.w400,
),
),
if (isDesktop) ...[
const SizedBox(height: 28),
Wrap(
spacing: 12,
runSpacing: 12,
children: [
_buildHeroCta(context, colorScheme),
_buildHeroSecondaryCta(context, colorScheme),
],
),
],
],
);
final imageBlock = ClipRRect(
borderRadius: BorderRadius.circular(24),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: colorScheme.primary.withValues(alpha: 0.25),
blurRadius: 32,
offset: const Offset(0, 18),
),
],
),
child: Image.asset(
'assets/signup.png',
width: isDesktop ? 360 : 260,
fit: BoxFit.contain,
),
),
);
if (isDesktop) {
return Container(
padding: const EdgeInsets.all(40),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(32),
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
colorScheme.primary.withValues(alpha: 0.06),
colorScheme.secondary.withValues(alpha: 0.04),
],
),
border: Border.all(
color: colorScheme.onSurface.withValues(alpha: 0.06),
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(flex: 5, child: textBlock),
const SizedBox(width: 32),
Expanded(flex: 4, child: Center(child: imageBlock)),
],
),
);
}
// Mobile / tablet — original vertical hero with gradient background.
return Container(
padding: const EdgeInsets.symmetric(vertical: 32, horizontal: 20),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(28),
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [colorScheme.primary, colorScheme.secondary],
),
boxShadow: [
BoxShadow(
color: colorScheme.primary.withValues(alpha: 0.25),
blurRadius: 24,
offset: const Offset(0, 12),
),
],
),
child: Column(
children: [
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.15),
shape: BoxShape.circle,
),
child: const Icon(
Icons.bolt_rounded,
color: Colors.white,
size: 44,
),
),
const SizedBox(height: 16),
Text(
'Welcome to Velocity',
textAlign: TextAlign.center,
style: theme.textTheme.headlineMedium?.copyWith(
color: Colors.white,
fontWeight: FontWeight.w800,
letterSpacing: -0.5,
),
),
const SizedBox(height: 8),
Text(
'The fastest way to pay for local goods\nand services',
textAlign: TextAlign.center,
style: theme.textTheme.bodyLarge?.copyWith(
color: Colors.white.withValues(alpha: 0.92),
height: 1.4,
),
),
const SizedBox(height: 20),
imageBlock,
],
),
);
}
Widget _buildHeroCta(BuildContext context, ColorScheme colorScheme) {
return SizedBox(
height: 52,
child: ElevatedButton(
onPressed: _goToRegister,
style: ElevatedButton.styleFrom(
backgroundColor: colorScheme.primary,
foregroundColor: Colors.white,
elevation: 0,
padding: const EdgeInsets.symmetric(horizontal: 28),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
textStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
letterSpacing: 0.3,
),
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Text('Create Free Account'),
SizedBox(width: 8),
Icon(Icons.arrow_forward_rounded, size: 20),
],
),
),
);
}
Widget _buildHeroSecondaryCta(BuildContext context, ColorScheme colorScheme) {
return SizedBox(
height: 52,
child: OutlinedButton(
onPressed: () {
context.go('/onboarding/login');
},
style: OutlinedButton.styleFrom(
side: BorderSide(color: colorScheme.primary, width: 1.5),
foregroundColor: colorScheme.primary,
padding: const EdgeInsets.symmetric(horizontal: 28),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
textStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
letterSpacing: 0.3,
),
),
child: const Text('Sign in'),
),
);
}
Widget _buildValuePropsHeadline(ThemeData theme, ColorScheme colorScheme) {
return Column(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
decoration: BoxDecoration(
color: colorScheme.primary.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(20),
),
child: Text(
'Why register?',
style: theme.textTheme.labelLarge?.copyWith(
color: colorScheme.primary,
fontWeight: FontWeight.w700,
letterSpacing: 0.5,
),
),
),
const SizedBox(height: 14),
Text(
'Unlock the full Velocity experience',
textAlign: TextAlign.center,
style: theme.textTheme.headlineSmall?.copyWith(
color: colorScheme.onSurface,
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 8),
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 600),
child: Text(
'Create a free account to access powerful features designed to make your payments effortless.',
textAlign: TextAlign.center,
style: theme.textTheme.bodyLarge?.copyWith(
color: colorScheme.onSurface.withValues(alpha: 0.65),
height: 1.45,
),
),
),
],
);
}
Widget _buildValueProps(bool isWide) {
final props = const [
_ValueProp(
icon: Icons.groups_2_rounded,
title: 'Batch Pay Groups',
description:
'Send payments to many recipients at once. Perfect for splitting bills, paying teams, or settling group purchases in a single tap.',
accent: Color(0xFF9D6711),
),
_ValueProp(
icon: Icons.devices_rounded,
title: 'Access Anywhere',
description:
'Your recipients and full transaction history sync seamlessly across all your devices — pick up right where you left off.',
accent: Color(0xFF8B0000),
),
_ValueProp(
icon: Icons.account_balance_wallet_rounded,
title: 'Prefund Your Wallet',
description:
'Top up your wallet today and use the balance whenever you need it. No rushing, no last-minute transfers.',
accent: Color(0xFFB7791F),
),
];
if (isWide) {
return IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: props
.map(
(p) => Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: p,
),
),
)
.toList(),
),
);
}
return Column(
children: props
.map(
(p) =>
Padding(padding: const EdgeInsets.only(bottom: 12), child: p),
)
.toList(),
);
}
Widget _buildCallToActions(
ThemeData theme,
ColorScheme colorScheme,
bool isDesktop,
) {
final registerButton = SizedBox(
width: double.infinity,
height: 54,
child: ElevatedButton(
onPressed: _goToRegister,
style: ElevatedButton.styleFrom(
backgroundColor: colorScheme.primary,
foregroundColor: Colors.white,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
textStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
letterSpacing: 0.3,
),
),
child: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Create Free Account'),
SizedBox(width: 8),
Icon(Icons.arrow_forward_rounded, size: 20),
],
),
),
);
final loginButton = SizedBox(
width: double.infinity,
height: 54,
child: OutlinedButton(
onPressed: () {
context.go('/onboarding/login');
},
style: OutlinedButton.styleFrom(
side: BorderSide(color: colorScheme.primary, width: 1.5),
foregroundColor: colorScheme.primary,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
textStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
letterSpacing: 0.3,
),
),
child: const Text('I already have an account'),
),
);
if (isDesktop) {
return Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 520),
child: Column(
children: [registerButton, const SizedBox(height: 12), loginButton],
),
),
);
}
return Column(
children: [registerButton, const SizedBox(height: 12), loginButton],
);
}
Widget _buildFooter(ThemeData theme, ColorScheme colorScheme) {
return Center(
child: Text(
'© Velocity · Fast, secure local payments',
style: theme.textTheme.bodySmall?.copyWith(
color: colorScheme.onSurface.withValues(alpha: 0.45),
letterSpacing: 0.2,
),
),
);
}
}
class _ValueProp extends StatelessWidget {
const _ValueProp({
required this.icon,
required this.title,
required this.description,
required this.accent,
});
final IconData icon;
final String title;
final String description;
final Color accent;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: colorScheme.surface,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: colorScheme.onSurface.withValues(alpha: 0.06),
width: 1,
),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.04),
blurRadius: 16,
offset: const Offset(0, 4),
),
],
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: accent.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(14),
),
child: Icon(icon, color: accent, size: 26),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w700,
color: colorScheme.onSurface,
),
),
const SizedBox(height: 6),
Text(
description,
style: theme.textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurface.withValues(alpha: 0.68),
height: 1.5,
),
),
],
),
),
],
),
);
}
}