fixed guardian login edge cases and misleading placeholders on failed loads

This commit is contained in:
2026-09-20 19:03:45 +02:00
parent 2423c1a75e
commit 630497abdd
23 changed files with 299 additions and 72 deletions
+43 -8
View File
@@ -54,9 +54,14 @@ class GuardianLoginController extends ChangeNotifier {
String? get errorMessage => _errorMessage;
String? get errorDetails => _errorDetails;
bool canResend() {
bool canResend() => resendCooldown() == Duration.zero;
/// Restzeit, bis [resend] wieder erlaubt ist.
Duration resendCooldown() {
final pending = _pending;
return pending != null && !_now().isBefore(pending.resendAvailableAt);
if (pending == null) return Duration.zero;
final remaining = pending.resendAvailableAt.difference(_now());
return remaining.isNegative ? Duration.zero : remaining;
}
/// Picks up a request started before the app was closed.
@@ -77,8 +82,12 @@ class GuardianLoginController extends ChangeNotifier {
Future<bool> requestCode(String email) async {
final normalized = email.trim().toLowerCase();
if (DemoMode.matchesGuardian(normalized)) {
await _signIn(GuardianSession(email: normalized, isDemo: true));
return true;
var signedIn = false;
await _run(() async {
await _signIn(GuardianSession(email: normalized, isDemo: true));
signedIn = true;
});
return signedIn;
}
await _run(() async {
final secret = DeviceBinding.generateSecret();
@@ -128,6 +137,17 @@ class GuardianLoginController extends ChangeNotifier {
return _complete(linkToken: link.linkToken);
}
/// A mail link that does not belong to the server the app talks to (a live
/// link while the app points at beta). Without this the tap would do nothing
/// at all.
void rejectForeignLink() {
_errorMessage =
'Dieser Anmeldelink gehört zu einem anderen Server als dem, mit dem '
'die App gerade verbunden ist. Bitte gib den Code aus der E-Mail ein.';
_errorDetails = null;
notifyListeners();
}
/// Abandons the running request, e.g. to correct a mistyped address.
Future<void> changeEmail() async {
await _store.clear();
@@ -140,7 +160,15 @@ class GuardianLoginController extends ChangeNotifier {
Future<bool> _complete({String? code, String? linkToken}) async {
final pending = _pending;
if (pending == null) return false;
if (pending == null) {
_errorMessage = GuardianLoginException.messageFor(
GuardianLoginError.requestExpired,
);
_errorDetails = null;
_step = GuardianLoginStep.enterEmail;
notifyListeners();
return false;
}
var signedIn = false;
await _run(() async {
await _verify.run(
@@ -188,9 +216,16 @@ class GuardianLoginController extends ChangeNotifier {
}
static Future<void> _defaultSignIn(Session session) async {
// Drop any widget snapshot of a previous account before the new one loads.
await WidgetSync.clear();
await WidgetSync.triggerUpdate();
// Sign in first: the one-time code is already spent at this point, so a
// failing widget reset must not cost the session (the user would have to
// request a fresh mail for a login that actually succeeded).
await SessionManager().signIn(session);
// Drop any widget snapshot of a previous account.
try {
await WidgetSync.clear();
await WidgetSync.triggerUpdate();
} on Object catch (e) {
log('Guardian login: widget reset failed: $e');
}
}
}
+16 -1
View File
@@ -7,6 +7,7 @@ import '../../api/marianumconnect/marianumconnect_endpoint.dart' as mc;
import '../../auth_link/guardian_link_listener.dart';
import '../../auth_link/guardian_login_link.dart';
import '../../background/widget_background_task.dart';
import '../../session/session_lifecycle.dart';
import '../../state/app/modules/account/bloc/account_bloc.dart';
import '../../state/app/modules/account/bloc/account_state.dart';
import '../../state/app/modules/settings/bloc/settings_cubit.dart';
@@ -14,6 +15,7 @@ import '../../storage/dev_tools_settings.dart';
import '../../storage/settings.dart' as model;
import '../../theming/light_app_theme.dart';
import '../../utils/haptics.dart';
import '../../widget/info_dialog.dart';
import '../pages/settings/widgets/endpoint_picker.dart';
import 'guardian_login_controller.dart';
import 'login_controller.dart';
@@ -54,6 +56,16 @@ class _LoginState extends State<Login> with SingleTickerProviderStateMixin {
});
GuardianLinkListener.pending.addListener(_consumeGuardianLink);
_consumeGuardianLink();
WidgetsBinding.instance.addPostFrameCallback((_) => _showSignOutNotice());
}
/// An involuntary sign-out (expired token, rotated password) otherwise just
/// drops the user here without a word.
void _showSignOutNotice() {
final notice = SessionLifecycle.signOutNotice.value;
if (notice == null || !mounted) return;
SessionLifecycle.signOutNotice.value = null;
InfoDialog.show(context, notice, title: 'Erneut anmelden');
}
/// A tapped mail link finishes the guardian login without typing the code.
@@ -65,10 +77,13 @@ class _LoginState extends State<Login> with SingleTickerProviderStateMixin {
uri,
apiBase: Uri.parse(mc.MarianumConnectEndpoint.current()),
);
if (link == null) return;
await _guardianRestored;
if (!mounted) return;
setState(() => _audience = LoginAudience.guardian);
if (link == null) {
_guardianController.rejectForeignLink();
return;
}
final signedIn = await _guardianController.submitLink(link);
if (signedIn && mounted) _onLoginSuccess();
}
@@ -158,7 +158,7 @@ class _GuardianLoginCardState extends State<GuardianLoginCard> {
Widget _buildCodeStep(BuildContext context) {
final theme = Theme.of(context);
final pending = _controller.pending!;
final remaining = pending.resendAvailableAt.difference(DateTime.now());
final remaining = _controller.resendCooldown();
return Form(
key: _codeFormKey,
child: LoginCardFrame(
@@ -14,7 +14,7 @@ class LoginAudienceCard extends StatelessWidget {
@override
Widget build(BuildContext context) => LoginCardFrame(
title: 'Anmelden',
hint: 'Bite wähle deine Anmeldemethode',
hint: 'Bitte wähle deine Anmeldemethode',
children: [
_AudienceButton(
key: const Key('login-audience-school'),