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>
91 lines
3 KiB
Dart
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);
|
|
}
|
|
}
|