forgot design flip

This commit is contained in:
Surendiran 2026-07-15 12:42:41 +05:30
parent 9856ff08b2
commit f871af752b

View File

@ -1,3 +1,5 @@
import 'dart:math' as math;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.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/app_toast.dart';
import '../../../../shared/widgets/sidebar_logo.dart'; import '../../../../shared/widgets/sidebar_logo.dart';
import '../../../settings/presentation/providers/settings_provider.dart'; import '../../../settings/presentation/providers/settings_provider.dart';
import '../../data/repositories/auth_repository_impl.dart';
import '../widgets/login_colors.dart'; import '../widgets/login_colors.dart';
import '../widgets/login_hero_panel.dart'; import '../widgets/login_hero_panel.dart';
@ -27,17 +30,40 @@ class LoginScreen extends ConsumerStatefulWidget {
ConsumerState<LoginScreen> createState() => _LoginScreenState(); ConsumerState<LoginScreen> createState() => _LoginScreenState();
} }
class _LoginScreenState extends ConsumerState<LoginScreen> { class _LoginScreenState extends ConsumerState<LoginScreen>
final _formKey = GlobalKey<FormState>(); with SingleTickerProviderStateMixin {
final _loginFormKey = GlobalKey<FormState>();
final _forgotFormKey = GlobalKey<FormState>();
final _cardMeasureKey = GlobalKey();
final _emailController = TextEditingController(); final _emailController = TextEditingController();
final _passwordController = TextEditingController(); final _passwordController = TextEditingController();
final _forgotEmailController = TextEditingController();
late final AnimationController _flipController;
late final Animation<double> _flipAnimation;
bool _isLoading = false; bool _isLoading = false;
bool _isForgotLoading = false;
bool _obscurePassword = true; bool _obscurePassword = true;
bool _rememberMe = false; bool _rememberMe = false;
String? _forgotSuccessMessage;
String? _forgotErrorMessage;
double? _cardHeight;
bool get _showingForgot => _flipAnimation.value > 0.5;
@override @override
void initState() { void initState() {
super.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()); WidgetsBinding.instance.addPostFrameCallback((_) => _loadRememberedEmail());
} }
@ -70,13 +96,15 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
@override @override
void dispose() { void dispose() {
_flipController.dispose();
_emailController.dispose(); _emailController.dispose();
_passwordController.dispose(); _passwordController.dispose();
_forgotEmailController.dispose();
super.dispose(); super.dispose();
} }
Future<void> _login() async { Future<void> _login() async {
if (!_formKey.currentState!.validate()) return; if (!_loginFormKey.currentState!.validate()) return;
setState(() => _isLoading = true); setState(() => _isLoading = true);
await _persistRememberMe(); 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({ InputDecoration _fieldDecoration({
required LoginColors colors, required LoginColors colors,
required String label, required String label,
@ -149,16 +235,11 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
); );
} }
Widget _buildLoginCard(LoginColors colors) { Widget _cardShell({
final companyProfile = ref.watch(appSettingsProvider).companyProfile; required LoginColors colors,
final branding = ref.watch(brandingProvider); required Widget child,
final logoUrl = resolveSidebarLogoUrl( }) {
companyProfileLogo: companyProfile.logoUrl,
brandingLogo: branding.logoUrl,
);
return Container( return Container(
constraints: const BoxConstraints(maxWidth: 420),
decoration: BoxDecoration( decoration: BoxDecoration(
color: colors.surface.withValues(alpha: 0.92), color: colors.surface.withValues(alpha: 0.92),
borderRadius: BorderRadius.circular(28), borderRadius: BorderRadius.circular(28),
@ -172,240 +253,492 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
], ],
), ),
padding: const EdgeInsets.fromLTRB(36, 40, 36, 32), padding: const EdgeInsets.fromLTRB(36, 40, 36, 32),
child: Form( child: child,
key: _formKey, );
child: Column( }
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch, Widget _buildLogo(String logoUrl) {
children: [ return Center(
Center( child: SidebarLogo(
child: SidebarLogo( logoUrl: logoUrl,
logoUrl: logoUrl, height: 64,
height: 64, width: 220,
width: 220, fit: BoxFit.contain,
fit: BoxFit.contain, showBackground: false,
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), const SizedBox(height: 20),
Divider(color: colors.outlineSoft),
const SizedBox(height: 14),
Text( Text(
'Welcome back', 'Login API unavailable? Browse all screens without signing in:',
style: GoogleFonts.manrope( textAlign: TextAlign.center,
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( style: GoogleFonts.inter(
fontSize: 13.5, fontSize: 12,
color: colors.subtitleColor, color: colors.subtitleColor,
), ),
), ),
const SizedBox(height: 26), const SizedBox(height: 12),
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),
Theme( Theme(
data: Theme.of(context).copyWith( data: Theme.of(context).copyWith(
elevatedButtonTheme: ElevatedButtonThemeData( outlinedButtonTheme: OutlinedButtonThemeData(
style: ElevatedButton.styleFrom( style: OutlinedButton.styleFrom(
backgroundColor: colors.primary, foregroundColor: colors.primary,
foregroundColor: colors.onPrimary, side: BorderSide(color: colors.outlineSoft),
disabledBackgroundColor: minimumSize: const Size(double.infinity, 46),
colors.primary.withValues(alpha: 0.75),
minimumSize: const Size(double.infinity, 52),
elevation: 0,
shadowColor: colors.primary.withValues(alpha: 0.35),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
textStyle: GoogleFonts.inter(
fontSize: 15,
fontWeight: FontWeight.w700,
letterSpacing: 0.2,
),
), ),
), ),
), ),
child: AppButton( child: AppButton(
label: _isLoading ? 'Signing in…' : 'Sign in', label: 'Explore All Screens',
isLoading: _isLoading, isOutlined: true,
onPressed: _login, 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( Widget _buildForgotFace({
'SECURE ACCESS', required LoginColors colors,
style: GoogleFonts.inter( required String logoUrl,
fontSize: 11.5, }) {
letterSpacing: 0.6, final fillHeight = _cardHeight != null;
color: colors.onSurfaceVariant, return Form(
), key: _forgotFormKey,
), child: Column(
), mainAxisSize: fillHeight ? MainAxisSize.max : MainAxisSize.min,
Expanded(child: Divider(color: colors.outlineSoft, height: 1)), 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( const SizedBox(height: 5),
mainAxisAlignment: MainAxisAlignment.center, Text(
children: [ 'Enter your registered email address to receive password reset instructions.',
Icon( style: GoogleFonts.inter(
Icons.verified_user_outlined, fontSize: 13.5,
size: 14, height: 1.45,
color: colors.success, 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( style: GoogleFonts.inter(
fontSize: 12, fontSize: 11.5,
letterSpacing: 0.6,
color: colors.onSurfaceVariant, 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,
), ),
), Expanded(child: Divider(color: colors.outlineSoft, height: 1)),
if (DevConfig.screenPreviewEnabled) ...[ ],
const SizedBox(height: 20), ),
Divider(color: colors.outlineSoft), const SizedBox(height: 14),
const SizedBox(height: 14), Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.verified_user_outlined,
size: 14,
color: colors.success,
),
const SizedBox(width: 6),
Text( Text(
'Login API unavailable? Browse all screens without signing in:', 'Your data is protected and encrypted',
textAlign: TextAlign.center,
style: GoogleFonts.inter( style: GoogleFonts.inter(
fontSize: 12, fontSize: 12,
color: colors.subtitleColor, color: colors.onSurfaceVariant,
),
),
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);
},
), ),
), ),
], ],
], ),
), 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, horizontal: 32,
vertical: 40, vertical: 40,
), ),
child: _buildLoginCard(colors), child: _buildAuthCard(colors),
), ),
), ),
], ],
@ -524,7 +857,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
), ),
Padding( Padding(
padding: const EdgeInsets.fromLTRB(16, 24, 16, 40), padding: const EdgeInsets.fromLTRB(16, 24, 16, 40),
child: Center(child: _buildLoginCard(colors)), child: Center(child: _buildAuthCard(colors)),
), ),
], ],
), ),