import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:shuffle/core/l10n/l10n.dart'; import 'package:shuffle/core/routing/app_routes.dart'; import 'package:shuffle/core/theme/app_spacing.dart'; import 'package:shuffle/core/widgets/app_banner.dart'; import 'package:shuffle/core/widgets/app_form_scaffold.dart'; import 'package:shuffle/core/widgets/app_password_field.dart'; import 'package:shuffle/core/widgets/app_primary_button.dart'; import 'package:shuffle/core/widgets/app_text_field.dart'; import 'package:shuffle/features/auth/domain/auth_failure.dart'; import 'package:shuffle/features/auth/presentation/auth_failure_texts.dart'; import 'package:shuffle/features/auth/presentation/check_email/resend_controller.dart'; import 'package:shuffle/features/auth/presentation/login/login_controller.dart'; import 'package:shuffle/features/auth/presentation/login/login_state.dart'; import 'package:shuffle/features/auth/presentation/widgets/auth_screen_title.dart'; /// Sign-in (`/login`, KTO-01 Kriterien 5, 6 und 7). Only shows state and /// forwards input to the [LoginController]. [from] is the unchecked `from` /// query parameter; the controller validates it. class LoginScreen extends ConsumerStatefulWidget { const LoginScreen({this.from, super.key}); final String? from; @override ConsumerState createState() => _LoginScreenState(); } class _LoginScreenState extends ConsumerState { final _email = TextEditingController(); final _password = TextEditingController(); final _emailFocus = FocusNode(); final _passwordFocus = FocusNode(); @override void initState() { super.initState(); _emailFocus.addListener( () => _onFocusChange(_emailFocus, LoginField.email), ); _passwordFocus.addListener( () => _onFocusChange(_passwordFocus, LoginField.password), ); } void _onFocusChange(FocusNode node, LoginField field) { if (!node.hasFocus) { ref.read(loginControllerProvider.notifier).fieldBlurred(field); } } @override void dispose() { _email.dispose(); _password.dispose(); _emailFocus.dispose(); _passwordFocus.dispose(); super.dispose(); } Future _submit() async { final target = await ref .read(loginControllerProvider.notifier) .submit(from: widget.from); if (!mounted || target == null) { return; } TextInput.finishAutofillContext(); context.go(target); } void _requestFocus(LoginField? field) { switch (field) { case LoginField.email: _emailFocus.requestFocus(); case LoginField.password: _passwordFocus.requestFocus(); case null: break; } } @override Widget build(BuildContext context) { ref.listen(loginControllerProvider, (previous, next) { if (previous?.focusRequest != next.focusRequest) { _requestFocus(next.focusField); } }); final state = ref.watch(loginControllerProvider); final resend = ref.watch(resendControllerProvider); final controller = ref.read(loginControllerProvider.notifier); final l10n = context.l10n; final busy = state.isSubmitting; return AppFormScaffold( key: const Key('login_screen'), child: AutofillGroup( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ AuthScreenTitle(l10n.authLoginTitle), if (state.bannerFailure case final failure?) ...[ const SizedBox(height: AppSpacing.lg), if (state.showsNotConfirmed) AppBanner( key: const Key('login_not_confirmed_banner'), type: AppBannerType.info, typeLabel: l10n.authBannerTypeInfo, message: l10n.authErrorEmailNotConfirmed, actionLabel: resend.isSending ? null : l10n.authActionResendConfirmation, onAction: resend.isSending ? null : () => unawaited(controller.resendConfirmation()), ) else _FailureBanner( key: const Key('login_banner'), failure: failure, onRetry: busy ? null : _submit, ), ], if (state.showsNotConfirmed && resend.sent) ...[ const SizedBox(height: AppSpacing.md), AppBanner( key: const Key('login_resent_banner'), type: AppBannerType.info, typeLabel: l10n.authBannerTypeInfo, message: l10n.authCheckEmailResent, ), ], if (state.showsNotConfirmed && resend.failure != null) ...[ const SizedBox(height: AppSpacing.md), AppBanner( key: const Key('login_resend_error_banner'), type: AppBannerType.error, typeLabel: l10n.authBannerTypeError, message: authFailureMessage(l10n, resend.failure!), ), ], const SizedBox(height: AppSpacing.xl), AppTextField( key: const Key('login_email'), label: l10n.authFieldEmailLabel, controller: _email, focusNode: _emailFocus, keyboardType: TextInputType.emailAddress, textInputAction: TextInputAction.next, autofillHints: const [ AutofillHints.username, AutofillHints.email, ], autocorrect: false, enableSuggestions: false, enabled: !busy, errorText: state.shownEmailError ? l10n.authErrorEmailInvalid : null, onChanged: controller.emailChanged, ), const SizedBox(height: AppSpacing.lg), AppPasswordField( key: const Key('login_password'), label: l10n.authFieldPasswordLabel, showPasswordLabel: l10n.authPasswordShow, hidePasswordLabel: l10n.authPasswordHide, controller: _password, focusNode: _passwordFocus, textInputAction: TextInputAction.done, enabled: !busy, errorText: state.shownPasswordRequired ? l10n.authPasswordRequired : null, onChanged: controller.passwordChanged, onSubmitted: (_) { if (!busy) { unawaited(_submit()); } }, ), const SizedBox(height: AppSpacing.xl), AppPrimaryButton( key: const Key('login_submit'), label: l10n.authLoginSubmit, loadingLabel: l10n.authSubmitting, isLoading: busy, onPressed: _submit, ), const SizedBox(height: AppSpacing.sm), TextButton( key: const Key('login_forgot_link'), onPressed: busy ? null : () => context.go(AppRoutes.forgotPassword), child: Text(l10n.authLoginForgotPassword), ), TextButton( key: const Key('login_register_link'), onPressed: busy ? null : () => context.go(AppRoutes.register), child: Text(l10n.authLoginCreateAccount), ), ], ), ), ); } } class _FailureBanner extends StatelessWidget { const _FailureBanner({ required this.failure, required this.onRetry, super.key, }); final AuthFailure failure; final VoidCallback? onRetry; @override Widget build(BuildContext context) { final l10n = context.l10n; final retryable = failure is NetworkFailure || failure is Unexpected; return AppBanner( type: AppBannerType.error, typeLabel: l10n.authBannerTypeError, message: authFailureMessage(l10n, failure), actionLabel: retryable && onRetry != null ? l10n.authActionRetry : null, onAction: retryable ? onRetry : null, ); } }