import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:qpay/models/responsive_policy.dart'; /// Shared layout for the onboarding flow (login, register, verify, complete). /// /// Provides the consistent Velocity look & feel: /// - Soft brand gradient background /// - Top bar with the Velocity brand mark and contextual trailing action /// - Centered content card (form area) /// - Bottom "Continue as guest?" + footer /// /// All screens wrap their content with this layout so the flow feels cohesive /// across mobile and web. class OnboardingLayout extends StatelessWidget { const OnboardingLayout({ super.key, required this.child, this.trailingLabel, this.trailingRoute, this.trailingIcon, this.showBackButton = false, this.onBack, this.bottomLinkLabel, this.bottomLinkRoute, this.bottomLinkOnPressed, }); /// The body of the screen (typically a form inside an [OnboardingCard]). final Widget child; /// Optional label for the trailing action in the top bar /// (e.g. "Sign in", "Back"). Hidden if null. final String? trailingLabel; /// Route to navigate to when the trailing action is pressed. final String? trailingRoute; /// Optional icon for the trailing action (defaults to chevron/arrow). final IconData? trailingIcon; /// If true, shows a back-arrow icon button on the leading side of the top bar. final bool showBackButton; /// Optional callback for the back button. If not provided, the button pops /// the route via [GoRouter]. final VoidCallback? onBack; /// Optional label for a link rendered below the main content /// (e.g. "Continue as guest?"). final String? bottomLinkLabel; /// Route to navigate to when the bottom link is pressed. final String? bottomLinkRoute; /// Optional callback for the bottom link (overrides [bottomLinkRoute]). final VoidCallback? bottomLinkOnPressed; // Max content width on large screens (desktop / web) for the page chrome // (top bar, footer, etc.). 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; // Max width for the form card on desktop — keeps forms a comfortable // reading/interaction width on wide screens. static const double _formMaxWidth = 480; @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( // Apply the gradient to the Scaffold body itself via a Container so // it is guaranteed to cover the full viewport (including the area // under the SafeArea) regardless of how tall the form content ends // up being. The previous Stack + Positioned.fill version shrunk to // fit the non-positioned SingleChildScrollView, which left an // unstyled area at the bottom of the page. body: Container( 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), ], ), ), child: SafeArea( child: LayoutBuilder( builder: (context, viewportConstraints) { final horizontalPadding = isDesktop ? 48.0 : 20.0; final shellWidth = viewportConstraints.maxWidth < _maxContentWidth ? viewportConstraints.maxWidth : _maxContentWidth; final availableContentWidth = (shellWidth - (horizontalPadding * 2)).clamp( 0.0, double.infinity, ); final contentMaxWidth = isDesktop ? _maxContentWidth : (isTablet ? (_maxTabletWidth < availableContentWidth ? _maxTabletWidth : availableContentWidth) : availableContentWidth); return SingleChildScrollView( child: ConstrainedBox( constraints: BoxConstraints( minHeight: viewportConstraints.maxHeight, ), child: IntrinsicHeight( child: Center( child: ConstrainedBox( constraints: const BoxConstraints( maxWidth: _maxContentWidth, ), child: Padding( padding: EdgeInsets.symmetric( horizontal: horizontalPadding, vertical: isDesktop ? 32 : 16, ), child: SizedBox( width: contentMaxWidth, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const SizedBox(height: 4), _buildTopBar( context, theme, colorScheme, isDesktop, ), SizedBox(height: isDesktop ? 32 : 20), // On desktop, narrow the form to a // comfortable width and center it. On // mobile/tablet, let it expand to fill // the available space. isDesktop ? Center( child: ConstrainedBox( constraints: const BoxConstraints( maxWidth: _formMaxWidth, ), child: child, ), ) : child, if (bottomLinkLabel != null || bottomLinkOnPressed != null) ...[ const SizedBox(height: 16), _buildBottomLink(context, theme, colorScheme), ], const Spacer(), _buildFooter(theme, colorScheme), const SizedBox(height: 12), ], ), ), ), ), ), ), ), ); }, ), ), ), ); } Widget _buildTopBar( BuildContext context, ThemeData theme, ColorScheme colorScheme, bool isDesktop, ) { return Row( children: [ if (showBackButton) IconButton( onPressed: onBack ?? () { if (context.canPop()) { context.pop(); } else { context.go('/onboarding/landing'); } }, icon: Icon(Icons.arrow_back_rounded, color: colorScheme.onSurface), tooltip: 'Back', style: IconButton.styleFrom( backgroundColor: colorScheme.surface, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), side: BorderSide( color: colorScheme.onSurface.withValues(alpha: 0.08), ), ), ), ) else GestureDetector( onTap: () => context.go('/onboarding/landing'), child: Row( children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: colorScheme.primary.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), ), child: Icon( Icons.bolt_rounded, color: colorScheme.primary, size: 24, ), ), const SizedBox(width: 10), Text( 'Velocity', style: theme.textTheme.titleLarge?.copyWith( color: colorScheme.primary, fontWeight: FontWeight.w800, letterSpacing: -0.5, ), ), ], ), ), const Spacer(), if (trailingLabel != null && trailingRoute != null) TextButton( onPressed: () => context.go(trailingRoute!), style: TextButton.styleFrom( foregroundColor: colorScheme.primary, padding: EdgeInsets.symmetric(horizontal: isDesktop ? 16 : 8), textStyle: const TextStyle( fontWeight: FontWeight.w600, fontSize: 14, ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ if (trailingIcon != null) ...[ Icon(trailingIcon, size: 18), const SizedBox(width: 6), ], Text(trailingLabel!), ], ), ), ], ); } Widget _buildBottomLink( BuildContext context, ThemeData theme, ColorScheme colorScheme, ) { return Center( child: TextButton( onPressed: bottomLinkOnPressed ?? () { if (bottomLinkRoute != null) context.go(bottomLinkRoute!); }, child: Text( bottomLinkLabel ?? '', style: TextStyle( fontSize: 14, color: colorScheme.onSurface.withValues(alpha: 0.7), fontWeight: FontWeight.w500, ), ), ), ); } 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, ), ), ); } } /// A rounded card used to wrap onboarding forms. Provides the matching /// border, shadow, and inner padding used throughout the onboarding flow. class OnboardingCard extends StatelessWidget { const OnboardingCard({ super.key, required this.child, this.padding = const EdgeInsets.fromLTRB(24, 28, 24, 24), }); final Widget child; final EdgeInsetsGeometry padding; @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return Container( width: double.infinity, padding: padding, decoration: BoxDecoration( color: colorScheme.surface, borderRadius: BorderRadius.circular(24), border: Border.all( color: colorScheme.onSurface.withValues(alpha: 0.06), width: 1, ), boxShadow: [ BoxShadow( color: colorScheme.primary.withValues(alpha: 0.06), blurRadius: 24, offset: const Offset(0, 12), ), BoxShadow( color: Colors.black.withValues(alpha: 0.03), blurRadius: 6, offset: const Offset(0, 2), ), ], ), child: child, ); } } /// Hero section used at the top of onboarding cards: a tinted circular icon /// badge plus a title and optional subtitle. class OnboardingHero extends StatelessWidget { const OnboardingHero({ super.key, required this.icon, required this.title, this.subtitle, this.accent, }); final IconData icon; final String title; final String? subtitle; final Color? accent; @override Widget build(BuildContext context) { final theme = Theme.of(context); final colorScheme = theme.colorScheme; final tint = accent ?? colorScheme.primary; return Column( children: [ Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: tint.withValues(alpha: 0.12), shape: BoxShape.circle, ), child: Icon(icon, color: tint, size: 32), ), const SizedBox(height: 16), Text( title, textAlign: TextAlign.center, style: theme.textTheme.headlineSmall?.copyWith( fontWeight: FontWeight.w800, color: colorScheme.onSurface, letterSpacing: -0.5, ), ), if (subtitle != null) ...[ const SizedBox(height: 6), Text( subtitle!, textAlign: TextAlign.center, style: theme.textTheme.bodyMedium?.copyWith( color: colorScheme.onSurface.withValues(alpha: 0.65), height: 1.45, ), ), ], ], ); } } /// Modern, theme-aligned text field used by the onboarding forms. /// /// Wraps a [TextFormField] and applies the consistent border, prefix icon, /// and focus styling that matches the rest of the Velocity design system. class OnboardingTextField extends StatelessWidget { const OnboardingTextField({ super.key, required this.controller, this.hintText, this.labelText, this.prefixIcon, this.suffixIcon, this.keyboardType, this.textCapitalization = TextCapitalization.none, this.autocorrect = false, this.enableSuggestions = true, this.obscureText = false, this.validator, this.onChanged, }); final TextEditingController controller; final String? hintText; final String? labelText; final IconData? prefixIcon; final Widget? suffixIcon; final TextInputType? keyboardType; final TextCapitalization textCapitalization; final bool autocorrect; final bool enableSuggestions; final bool obscureText; final String? Function(String?)? validator; final ValueChanged? onChanged; @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; final borderRadius = BorderRadius.circular(14); final enabledBorder = OutlineInputBorder( borderRadius: borderRadius, borderSide: BorderSide( color: colorScheme.onSurface.withValues(alpha: 0.12), width: 1, ), ); final focusedBorder = OutlineInputBorder( borderRadius: borderRadius, borderSide: BorderSide(color: colorScheme.primary, width: 1.6), ); final errorBorder = OutlineInputBorder( borderRadius: borderRadius, borderSide: BorderSide(color: colorScheme.error, width: 1), ); final focusedErrorBorder = OutlineInputBorder( borderRadius: borderRadius, borderSide: BorderSide(color: colorScheme.error, width: 1.6), ); return TextFormField( controller: controller, keyboardType: keyboardType, textCapitalization: textCapitalization, autocorrect: autocorrect, enableSuggestions: enableSuggestions, obscureText: obscureText, validator: validator, onChanged: onChanged, style: TextStyle( color: colorScheme.onSurface, fontSize: 15, fontWeight: FontWeight.w500, ), decoration: InputDecoration( hintText: hintText, labelText: labelText, prefixIcon: prefixIcon != null ? Icon( prefixIcon, color: colorScheme.onSurface.withValues(alpha: 0.55), ) : null, suffixIcon: suffixIcon, filled: true, fillColor: colorScheme.surface, enabledBorder: enabledBorder, focusedBorder: focusedBorder, errorBorder: errorBorder, focusedErrorBorder: focusedErrorBorder, contentPadding: const EdgeInsets.symmetric( horizontal: 16, vertical: 16, ), ), ); } } /// Primary CTA button used across the onboarding screens. class OnboardingPrimaryButton extends StatelessWidget { const OnboardingPrimaryButton({ super.key, required this.label, this.icon, this.onPressed, this.fullWidth = true, this.height = 52, }); final String label; final IconData? icon; final VoidCallback? onPressed; final bool fullWidth; final double height; @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return SizedBox( width: fullWidth ? double.infinity : null, height: height, child: ElevatedButton( onPressed: onPressed, 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: Row( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ Text(label), if (icon != null) ...[ const SizedBox(width: 8), Icon(icon, size: 20), ], ], ), ), ); } } /// Secondary outlined button used across the onboarding screens. class OnboardingSecondaryButton extends StatelessWidget { const OnboardingSecondaryButton({ super.key, required this.label, this.icon, this.onPressed, this.fullWidth = true, this.height = 52, }); final String label; final IconData? icon; final VoidCallback? onPressed; final bool fullWidth; final double height; @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return SizedBox( width: fullWidth ? double.infinity : null, height: height, child: OutlinedButton( onPressed: onPressed, 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: Row( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ if (icon != null) ...[ Icon(icon, size: 20), const SizedBox(width: 8), ], Text(label), ], ), ), ); } }