forgot design flip
This commit is contained in:
parent
9856ff08b2
commit
f871af752b
@ -1,3 +1,5 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
@ -17,6 +19,7 @@ import '../../../../shared/widgets/app_button.dart';
|
||||
import '../../../../shared/widgets/app_toast.dart';
|
||||
import '../../../../shared/widgets/sidebar_logo.dart';
|
||||
import '../../../settings/presentation/providers/settings_provider.dart';
|
||||
import '../../data/repositories/auth_repository_impl.dart';
|
||||
import '../widgets/login_colors.dart';
|
||||
import '../widgets/login_hero_panel.dart';
|
||||
|
||||
@ -27,17 +30,40 @@ class LoginScreen extends ConsumerStatefulWidget {
|
||||
ConsumerState<LoginScreen> createState() => _LoginScreenState();
|
||||
}
|
||||
|
||||
class _LoginScreenState extends ConsumerState<LoginScreen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
with SingleTickerProviderStateMixin {
|
||||
final _loginFormKey = GlobalKey<FormState>();
|
||||
final _forgotFormKey = GlobalKey<FormState>();
|
||||
final _cardMeasureKey = GlobalKey();
|
||||
final _emailController = TextEditingController();
|
||||
final _passwordController = TextEditingController();
|
||||
final _forgotEmailController = TextEditingController();
|
||||
|
||||
late final AnimationController _flipController;
|
||||
late final Animation<double> _flipAnimation;
|
||||
|
||||
bool _isLoading = false;
|
||||
bool _isForgotLoading = false;
|
||||
bool _obscurePassword = true;
|
||||
bool _rememberMe = false;
|
||||
String? _forgotSuccessMessage;
|
||||
String? _forgotErrorMessage;
|
||||
double? _cardHeight;
|
||||
|
||||
bool get _showingForgot => _flipAnimation.value > 0.5;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_flipController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 700),
|
||||
);
|
||||
_flipAnimation = CurvedAnimation(
|
||||
parent: _flipController,
|
||||
curve: Curves.easeInOutCubic,
|
||||
reverseCurve: Curves.easeInOutCubic,
|
||||
);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _loadRememberedEmail());
|
||||
}
|
||||
|
||||
@ -70,13 +96,15 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_flipController.dispose();
|
||||
_emailController.dispose();
|
||||
_passwordController.dispose();
|
||||
_forgotEmailController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _login() async {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
if (!_loginFormKey.currentState!.validate()) return;
|
||||
|
||||
setState(() => _isLoading = true);
|
||||
await _persistRememberMe();
|
||||
@ -102,6 +130,64 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _submitForgotPassword() async {
|
||||
if (!_forgotFormKey.currentState!.validate()) return;
|
||||
|
||||
setState(() {
|
||||
_isForgotLoading = true;
|
||||
_forgotSuccessMessage = null;
|
||||
_forgotErrorMessage = null;
|
||||
});
|
||||
|
||||
final repository = ref.read(authRepositoryProvider);
|
||||
final result = await repository.forgotPassword(
|
||||
ForgotPasswordRequest(email: _forgotEmailController.text.trim()),
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
setState(() => _isForgotLoading = false);
|
||||
|
||||
if (result.failure != null) {
|
||||
setState(() => _forgotErrorMessage = result.failure.toString());
|
||||
return;
|
||||
}
|
||||
|
||||
setState(
|
||||
() => _forgotSuccessMessage =
|
||||
'Password reset instructions have been sent to your email.',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _flipToForgot() async {
|
||||
if (_flipController.isAnimating || _isLoading || _showingForgot) return;
|
||||
if (_forgotEmailController.text.isEmpty &&
|
||||
_emailController.text.trim().isNotEmpty) {
|
||||
_forgotEmailController.text = _emailController.text.trim();
|
||||
}
|
||||
setState(() {
|
||||
_forgotSuccessMessage = null;
|
||||
_forgotErrorMessage = null;
|
||||
});
|
||||
// Lock card height from the sign-in face before flipping.
|
||||
if (_cardHeight == null) {
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
if (!mounted) return;
|
||||
final box =
|
||||
_cardMeasureKey.currentContext?.findRenderObject() as RenderBox?;
|
||||
if (box != null && box.hasSize) {
|
||||
setState(() => _cardHeight = box.size.height);
|
||||
}
|
||||
}
|
||||
await _flipController.forward();
|
||||
}
|
||||
|
||||
void _flipToSignIn() {
|
||||
if (_flipController.isAnimating || _isForgotLoading || !_showingForgot) {
|
||||
return;
|
||||
}
|
||||
_flipController.reverse();
|
||||
}
|
||||
|
||||
InputDecoration _fieldDecoration({
|
||||
required LoginColors colors,
|
||||
required String label,
|
||||
@ -149,16 +235,11 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoginCard(LoginColors colors) {
|
||||
final companyProfile = ref.watch(appSettingsProvider).companyProfile;
|
||||
final branding = ref.watch(brandingProvider);
|
||||
final logoUrl = resolveSidebarLogoUrl(
|
||||
companyProfileLogo: companyProfile.logoUrl,
|
||||
brandingLogo: branding.logoUrl,
|
||||
);
|
||||
|
||||
Widget _cardShell({
|
||||
required LoginColors colors,
|
||||
required Widget child,
|
||||
}) {
|
||||
return Container(
|
||||
constraints: const BoxConstraints(maxWidth: 420),
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surface.withValues(alpha: 0.92),
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
@ -172,240 +253,492 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
|
||||
],
|
||||
),
|
||||
padding: const EdgeInsets.fromLTRB(36, 40, 36, 32),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Center(
|
||||
child: SidebarLogo(
|
||||
logoUrl: logoUrl,
|
||||
height: 64,
|
||||
width: 220,
|
||||
fit: BoxFit.contain,
|
||||
showBackground: false,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLogo(String logoUrl) {
|
||||
return Center(
|
||||
child: SidebarLogo(
|
||||
logoUrl: logoUrl,
|
||||
height: 64,
|
||||
width: 220,
|
||||
fit: BoxFit.contain,
|
||||
showBackground: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _primaryButtonTheme({
|
||||
required LoginColors colors,
|
||||
required Widget child,
|
||||
}) {
|
||||
return Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: colors.primary,
|
||||
foregroundColor: colors.onPrimary,
|
||||
disabledBackgroundColor: colors.primary.withValues(alpha: 0.75),
|
||||
minimumSize: const Size(double.infinity, 52),
|
||||
elevation: 0,
|
||||
shadowColor: colors.primary.withValues(alpha: 0.35),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
textStyle: GoogleFonts.inter(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.2,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSignInFace({
|
||||
required LoginColors colors,
|
||||
required String logoUrl,
|
||||
}) {
|
||||
return Form(
|
||||
key: _loginFormKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildLogo(logoUrl),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
'Welcome back',
|
||||
style: GoogleFonts.manrope(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w800,
|
||||
letterSpacing: -0.3,
|
||||
color: colors.headingColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
Text(
|
||||
'Sign in to continue to your BCPL workspace.',
|
||||
style: GoogleFonts.inter(
|
||||
fontSize: 13.5,
|
||||
color: colors.subtitleColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 26),
|
||||
TextFormField(
|
||||
controller: _emailController,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
autofillHints: const [AutofillHints.email],
|
||||
validator: Validators.email,
|
||||
style: GoogleFonts.inter(
|
||||
fontSize: 14.5,
|
||||
color: colors.headingColor,
|
||||
),
|
||||
decoration: _fieldDecoration(
|
||||
colors: colors,
|
||||
label: 'Email address',
|
||||
icon: Icons.mail_outline_rounded,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
TextFormField(
|
||||
controller: _passwordController,
|
||||
obscureText: _obscurePassword,
|
||||
autofillHints: const [AutofillHints.password],
|
||||
validator: (v) => Validators.required(v, fieldName: 'Password'),
|
||||
style: GoogleFonts.inter(
|
||||
fontSize: 14.5,
|
||||
color: colors.headingColor,
|
||||
),
|
||||
decoration: _fieldDecoration(
|
||||
colors: colors,
|
||||
label: 'Password',
|
||||
icon: Icons.lock_outline_rounded,
|
||||
suffix: IconButton(
|
||||
tooltip: _obscurePassword ? 'Show password' : 'Hide password',
|
||||
icon: Icon(
|
||||
_obscurePassword
|
||||
? Icons.visibility_outlined
|
||||
: Icons.visibility_off_outlined,
|
||||
color: colors.iconMuted,
|
||||
size: 18,
|
||||
),
|
||||
onPressed: () =>
|
||||
setState(() => _obscurePassword = !_obscurePassword),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 34,
|
||||
width: 34,
|
||||
child: Checkbox(
|
||||
value: _rememberMe,
|
||||
activeColor: colors.primary,
|
||||
checkColor: colors.onPrimary,
|
||||
side: BorderSide(color: colors.outline, width: 1.5),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
onChanged: (v) => setState(() => _rememberMe = v ?? false),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Remember me',
|
||||
style: GoogleFonts.inter(
|
||||
fontSize: 13,
|
||||
color: colors.labelColor,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
TextButton(
|
||||
onPressed: _flipToForgot,
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: colors.linkColor,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
minimumSize: Size.zero,
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
child: Text(
|
||||
'Forgot password?',
|
||||
style: GoogleFonts.inter(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
_primaryButtonTheme(
|
||||
colors: colors,
|
||||
child: AppButton(
|
||||
label: _isLoading ? 'Signing in…' : 'Sign in',
|
||||
isLoading: _isLoading,
|
||||
onPressed: _login,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 26),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: Divider(color: colors.outlineSoft, height: 1)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
child: Text(
|
||||
'SECURE ACCESS',
|
||||
style: GoogleFonts.inter(
|
||||
fontSize: 11.5,
|
||||
letterSpacing: 0.6,
|
||||
color: colors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(child: Divider(color: colors.outlineSoft, height: 1)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.verified_user_outlined,
|
||||
size: 14,
|
||||
color: colors.success,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'Your data is protected and encrypted',
|
||||
style: GoogleFonts.inter(
|
||||
fontSize: 12,
|
||||
color: colors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 22),
|
||||
Text(
|
||||
'${AppConstants.appName} · v${AppConstants.appVersion}',
|
||||
textAlign: TextAlign.center,
|
||||
style: GoogleFonts.inter(
|
||||
fontSize: 11,
|
||||
letterSpacing: 0.3,
|
||||
color: colors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
if (DevConfig.screenPreviewEnabled) ...[
|
||||
const SizedBox(height: 20),
|
||||
Divider(color: colors.outlineSoft),
|
||||
const SizedBox(height: 14),
|
||||
Text(
|
||||
'Welcome back',
|
||||
style: GoogleFonts.manrope(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w800,
|
||||
letterSpacing: -0.3,
|
||||
color: colors.headingColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
Text(
|
||||
'Sign in to continue to your BCPL workspace.',
|
||||
'Login API unavailable? Browse all screens without signing in:',
|
||||
textAlign: TextAlign.center,
|
||||
style: GoogleFonts.inter(
|
||||
fontSize: 13.5,
|
||||
fontSize: 12,
|
||||
color: colors.subtitleColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 26),
|
||||
TextFormField(
|
||||
controller: _emailController,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
autofillHints: const [AutofillHints.email],
|
||||
validator: Validators.email,
|
||||
style: GoogleFonts.inter(
|
||||
fontSize: 14.5,
|
||||
color: colors.headingColor,
|
||||
),
|
||||
decoration: _fieldDecoration(
|
||||
colors: colors,
|
||||
label: 'Email address',
|
||||
icon: Icons.mail_outline_rounded,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
TextFormField(
|
||||
controller: _passwordController,
|
||||
obscureText: _obscurePassword,
|
||||
autofillHints: const [AutofillHints.password],
|
||||
validator: (v) => Validators.required(v, fieldName: 'Password'),
|
||||
style: GoogleFonts.inter(
|
||||
fontSize: 14.5,
|
||||
color: colors.headingColor,
|
||||
),
|
||||
decoration: _fieldDecoration(
|
||||
colors: colors,
|
||||
label: 'Password',
|
||||
icon: Icons.lock_outline_rounded,
|
||||
suffix: IconButton(
|
||||
tooltip: _obscurePassword ? 'Show password' : 'Hide password',
|
||||
icon: Icon(
|
||||
_obscurePassword
|
||||
? Icons.visibility_outlined
|
||||
: Icons.visibility_off_outlined,
|
||||
color: colors.iconMuted,
|
||||
size: 18,
|
||||
),
|
||||
onPressed: () =>
|
||||
setState(() => _obscurePassword = !_obscurePassword),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 34,
|
||||
width: 34,
|
||||
child: Checkbox(
|
||||
value: _rememberMe,
|
||||
activeColor: colors.primary,
|
||||
checkColor: colors.onPrimary,
|
||||
side: BorderSide(color: colors.outline, width: 1.5),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
onChanged: (v) => setState(() => _rememberMe = v ?? false),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Remember me',
|
||||
style: GoogleFonts.inter(
|
||||
fontSize: 13,
|
||||
color: colors.labelColor,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
TextButton(
|
||||
onPressed: () => context.push(RouteConstants.forgotPassword),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: colors.linkColor,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
minimumSize: Size.zero,
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
child: Text(
|
||||
'Forgot password?',
|
||||
style: GoogleFonts.inter(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
const SizedBox(height: 12),
|
||||
Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: colors.primary,
|
||||
foregroundColor: colors.onPrimary,
|
||||
disabledBackgroundColor:
|
||||
colors.primary.withValues(alpha: 0.75),
|
||||
minimumSize: const Size(double.infinity, 52),
|
||||
elevation: 0,
|
||||
shadowColor: colors.primary.withValues(alpha: 0.35),
|
||||
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: colors.primary,
|
||||
side: BorderSide(color: colors.outlineSoft),
|
||||
minimumSize: const Size(double.infinity, 46),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
textStyle: GoogleFonts.inter(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.2,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: AppButton(
|
||||
label: _isLoading ? 'Signing in…' : 'Sign in',
|
||||
isLoading: _isLoading,
|
||||
onPressed: _login,
|
||||
label: 'Explore All Screens',
|
||||
isOutlined: true,
|
||||
onPressed: () {
|
||||
ref.read(authStateProvider.notifier).loginAsDemo();
|
||||
context.go(RouteConstants.screenGallery);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 26),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: Divider(color: colors.outlineSoft, height: 1)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
child: Text(
|
||||
'SECURE ACCESS',
|
||||
style: GoogleFonts.inter(
|
||||
fontSize: 11.5,
|
||||
letterSpacing: 0.6,
|
||||
color: colors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(child: Divider(color: colors.outlineSoft, height: 1)),
|
||||
],
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildForgotFace({
|
||||
required LoginColors colors,
|
||||
required String logoUrl,
|
||||
}) {
|
||||
final fillHeight = _cardHeight != null;
|
||||
return Form(
|
||||
key: _forgotFormKey,
|
||||
child: Column(
|
||||
mainAxisSize: fillHeight ? MainAxisSize.max : MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildLogo(logoUrl),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
'Forgot Password',
|
||||
style: GoogleFonts.manrope(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w800,
|
||||
letterSpacing: -0.3,
|
||||
color: colors.headingColor,
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.verified_user_outlined,
|
||||
size: 14,
|
||||
color: colors.success,
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
Text(
|
||||
'Enter your registered email address to receive password reset instructions.',
|
||||
style: GoogleFonts.inter(
|
||||
fontSize: 13.5,
|
||||
height: 1.45,
|
||||
color: colors.subtitleColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 26),
|
||||
TextFormField(
|
||||
controller: _forgotEmailController,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
autofillHints: const [AutofillHints.email],
|
||||
validator: Validators.email,
|
||||
style: GoogleFonts.inter(
|
||||
fontSize: 14.5,
|
||||
color: colors.headingColor,
|
||||
),
|
||||
decoration: _fieldDecoration(
|
||||
colors: colors,
|
||||
label: 'Email address',
|
||||
icon: Icons.mail_outline_rounded,
|
||||
),
|
||||
),
|
||||
if (_forgotErrorMessage != null) ...[
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
_forgotErrorMessage!,
|
||||
style: GoogleFonts.inter(
|
||||
fontSize: 13,
|
||||
color: colors.colorScheme.error,
|
||||
),
|
||||
),
|
||||
],
|
||||
if (_forgotSuccessMessage != null) ...[
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
_forgotSuccessMessage!,
|
||||
style: GoogleFonts.inter(
|
||||
fontSize: 13,
|
||||
color: colors.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 18),
|
||||
_primaryButtonTheme(
|
||||
colors: colors,
|
||||
child: AppButton(
|
||||
label: _isForgotLoading ? 'Sending…' : 'Send Reset Link',
|
||||
isLoading: _isForgotLoading,
|
||||
onPressed: _submitForgotPassword,
|
||||
),
|
||||
),
|
||||
if (fillHeight)
|
||||
const Spacer()
|
||||
else
|
||||
const SizedBox(height: 22),
|
||||
Center(
|
||||
child: TextButton(
|
||||
onPressed: _flipToSignIn,
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: colors.linkColor,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||
),
|
||||
child: Text(
|
||||
'Back to Sign In',
|
||||
style: GoogleFonts.inter(
|
||||
fontSize: 13.5,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'Your data is protected and encrypted',
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: Divider(color: colors.outlineSoft, height: 1)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
child: Text(
|
||||
'SECURE ACCESS',
|
||||
style: GoogleFonts.inter(
|
||||
fontSize: 12,
|
||||
fontSize: 11.5,
|
||||
letterSpacing: 0.6,
|
||||
color: colors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 22),
|
||||
Text(
|
||||
'${AppConstants.appName} · v${AppConstants.appVersion}',
|
||||
textAlign: TextAlign.center,
|
||||
style: GoogleFonts.inter(
|
||||
fontSize: 11,
|
||||
letterSpacing: 0.3,
|
||||
color: colors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
if (DevConfig.screenPreviewEnabled) ...[
|
||||
const SizedBox(height: 20),
|
||||
Divider(color: colors.outlineSoft),
|
||||
const SizedBox(height: 14),
|
||||
Expanded(child: Divider(color: colors.outlineSoft, height: 1)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.verified_user_outlined,
|
||||
size: 14,
|
||||
color: colors.success,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'Login API unavailable? Browse all screens without signing in:',
|
||||
textAlign: TextAlign.center,
|
||||
'Your data is protected and encrypted',
|
||||
style: GoogleFonts.inter(
|
||||
fontSize: 12,
|
||||
color: colors.subtitleColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: colors.primary,
|
||||
side: BorderSide(color: colors.outlineSoft),
|
||||
minimumSize: const Size(double.infinity, 46),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: AppButton(
|
||||
label: 'Explore All Screens',
|
||||
isOutlined: true,
|
||||
onPressed: () {
|
||||
ref.read(authStateProvider.notifier).loginAsDemo();
|
||||
context.go(RouteConstants.screenGallery);
|
||||
},
|
||||
color: colors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 22),
|
||||
Text(
|
||||
'${AppConstants.appName} · v${AppConstants.appVersion}',
|
||||
textAlign: TextAlign.center,
|
||||
style: GoogleFonts.inter(
|
||||
fontSize: 11,
|
||||
letterSpacing: 0.3,
|
||||
color: colors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _scheduleCardHeightCapture() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted || _flipController.value > 0) return;
|
||||
final box =
|
||||
_cardMeasureKey.currentContext?.findRenderObject() as RenderBox?;
|
||||
if (box == null || !box.hasSize) return;
|
||||
final height = box.size.height;
|
||||
if (_cardHeight == null || (_cardHeight! - height).abs() > 1) {
|
||||
setState(() => _cardHeight = height);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildAuthCard(LoginColors colors) {
|
||||
final companyProfile = ref.watch(appSettingsProvider).companyProfile;
|
||||
final branding = ref.watch(brandingProvider);
|
||||
final logoUrl = resolveSidebarLogoUrl(
|
||||
companyProfileLogo: companyProfile.logoUrl,
|
||||
brandingLogo: branding.logoUrl,
|
||||
);
|
||||
|
||||
if (_flipController.isDismissed) {
|
||||
_scheduleCardHeightCapture();
|
||||
}
|
||||
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 420),
|
||||
child: AnimatedBuilder(
|
||||
animation: _flipAnimation,
|
||||
builder: (context, _) {
|
||||
final t = _flipAnimation.value;
|
||||
final angle = t * math.pi;
|
||||
final showFront = angle <= math.pi / 2;
|
||||
// Soft mid-flip fade for a premium feel.
|
||||
final fade = 0.82 + 0.18 * math.cos(angle * 2).abs();
|
||||
|
||||
final front = KeyedSubtree(
|
||||
key: _cardMeasureKey,
|
||||
child: _cardShell(
|
||||
colors: colors,
|
||||
child: _buildSignInFace(colors: colors, logoUrl: logoUrl),
|
||||
),
|
||||
);
|
||||
final backChild = _buildForgotFace(
|
||||
colors: colors,
|
||||
logoUrl: logoUrl,
|
||||
);
|
||||
final back = _cardShell(
|
||||
colors: colors,
|
||||
child: _cardHeight != null
|
||||
? SizedBox.expand(child: backChild)
|
||||
: backChild,
|
||||
);
|
||||
|
||||
final face = showFront
|
||||
? front
|
||||
: Transform(
|
||||
alignment: Alignment.center,
|
||||
transform: Matrix4.identity()..rotateY(math.pi),
|
||||
child: back,
|
||||
);
|
||||
|
||||
final flipped = Opacity(
|
||||
opacity: fade.clamp(0.82, 1.0),
|
||||
child: Transform(
|
||||
alignment: Alignment.center,
|
||||
transform: Matrix4.identity()
|
||||
..setEntry(3, 2, 0.00115)
|
||||
..rotateY(angle),
|
||||
child: face,
|
||||
),
|
||||
);
|
||||
|
||||
if (_cardHeight == null) return flipped;
|
||||
return SizedBox(
|
||||
height: _cardHeight,
|
||||
width: double.infinity,
|
||||
child: flipped,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -502,7 +835,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
|
||||
horizontal: 32,
|
||||
vertical: 40,
|
||||
),
|
||||
child: _buildLoginCard(colors),
|
||||
child: _buildAuthCard(colors),
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -524,7 +857,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 24, 16, 40),
|
||||
child: Center(child: _buildLoginCard(colors)),
|
||||
child: Center(child: _buildAuthCard(colors)),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user