Shuffle/app/lib/features/auth/presentation/login/login_controller.dart
cschlaefke dd91c078e0 feat(auth): add login screen with unconfirmed-account resend and safe redirect
KTO-01 Kriterien 5, 6 und 7 (01b): LoginController/State/Screen replace the
/login placeholder. Generic failure text for wrong email and password,
resend via ResendController (ResendOrigin.login), throttle and offline
banners, autofill hints, double-submit guard, validated from redirect.

Refs #1

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 09:00:53 +01:00

91 lines
3 KiB
Dart

import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shuffle/core/routing/app_routes.dart';
import 'package:shuffle/features/auth/domain/auth_failure.dart';
import 'package:shuffle/features/auth/domain/resend_origin.dart';
import 'package:shuffle/features/auth/domain/validation/email_format.dart';
import 'package:shuffle/features/auth/presentation/check_email/resend_controller.dart';
import 'package:shuffle/features/auth/presentation/login/login_state.dart';
import 'package:shuffle/features/auth/presentation/providers/auth_providers.dart';
import 'package:shuffle/features/auth/presentation/providers/auth_redirect.dart';
part 'login_controller.g.dart';
/// Logic of the sign-in form (KTO-01 Kriterien 5, 6 und 7).
@riverpod
class LoginController extends _$LoginController {
@override
LoginState build() => const LoginState();
void emailChanged(String value) {
state = state.copyWith(email: value);
}
void passwordChanged(String value) {
state = state.copyWith(password: value);
}
/// Field left: show its error.
void fieldBlurred(LoginField field) {
state = state.copyWith(visibleFields: {...state.visibleFields, field});
}
/// Validates and signs in. Returns the path to continue to on success
/// (the validated `from` target, otherwise the start page), `null` if the
/// user stays on the screen. A second call while one runs does nothing.
Future<String?> submit({String? from}) async {
if (state.isSubmitting) {
return null;
}
state = state.copyWith(
status: LoginStatus.submitting,
visibleFields: LoginField.values.toSet(),
bannerFailure: null,
unconfirmedEmail: null,
focusField: null,
);
// A new attempt starts without the result of an earlier resend.
ref.invalidate(resendControllerProvider);
final invalid = state.firstInvalidField;
if (invalid != null) {
state = state.copyWith(
status: LoginStatus.editing,
focusField: invalid,
focusRequest: state.focusRequest + 1,
);
return null;
}
final email = EmailFormat.normalize(state.email);
final result = await ref
.read(authRepositoryProvider)
.signIn(email, state.password);
if (!ref.mounted) {
return null;
}
final failure = result.errorOrNull;
if (failure == null) {
state = state.copyWith(status: LoginStatus.editing);
return safeFromPath(from) ?? AppRoutes.home;
}
state = state.copyWith(
status: LoginStatus.editing,
bannerFailure: failure,
unconfirmedEmail: failure is EmailNotConfirmed ? email : null,
);
return null;
}
/// "Resend confirmation email" after `emailNotConfirmed` (Kriterium 6).
/// The password was right, so a mail-limit answer is shown (F12).
Future<void> resendConfirmation() async {
final email = state.unconfirmedEmail;
if (email == null) {
return;
}
await ref
.read(resendControllerProvider.notifier)
.resend(email, origin: ResendOrigin.login);
}
}