feat(auth): Login-Timeout (10s) mit Hinweisbanner
Haengt der interaktive Login (Browser-Tab/Token-Exchange) bei Verbindungsabbruch/Issuer-Hang, bricht er nach 10s ab; LoginPage zeigt 'Einloggen nicht moeglich. Spaeter erneut versuchen.' (Unauthenticated.loginTimedOut). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -48,15 +48,32 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
|||||||
|
|
||||||
late final StreamSubscription<AuthSessionEvent> _subscription;
|
late final StreamSubscription<AuthSessionEvent> _subscription;
|
||||||
|
|
||||||
|
/// Timeout für den interaktiven Login. Schützt den Fahrer vor dem
|
||||||
|
/// „Spinner dreht ewig"-Fall, wenn der native `flutter_appauth`-Aufruf
|
||||||
|
/// (Browser-Tab + Token-Exchange) im Verbindungsabbruch / Issuer-Hang
|
||||||
|
/// stecken bleibt. Bewusst etwas knapper als der Restore-Timeout
|
||||||
|
/// (15 s im `_handleRestore`) — beim manuellen Login wartet der User
|
||||||
|
/// aktiv vor dem Bildschirm.
|
||||||
|
static const Duration _loginTimeout = Duration(seconds: 10);
|
||||||
|
|
||||||
Future<void> _handleLogin(
|
Future<void> _handleLogin(
|
||||||
LoginRequested event,
|
LoginRequested event,
|
||||||
Emitter<AuthState> emit,
|
Emitter<AuthState> emit,
|
||||||
) async {
|
) async {
|
||||||
try {
|
try {
|
||||||
emit(Authenticating());
|
emit(Authenticating());
|
||||||
await tokenProvider.login();
|
await tokenProvider.login().timeout(_loginTimeout);
|
||||||
// Erfolg landet via Stream-Subscription als
|
// Erfolg landet via Stream-Subscription als
|
||||||
// ProviderSessionChanged.loggedIn → _handleProviderEvent.
|
// ProviderSessionChanged.loggedIn → _handleProviderEvent.
|
||||||
|
} on TimeoutException {
|
||||||
|
// Native Login-Routine läuft im Hintergrund ggf. weiter. Sollte sie
|
||||||
|
// doch noch erfolgreich durchlaufen, feuert der Provider-Stream
|
||||||
|
// später ein `AuthLoggedIn` — der ProviderEvent-Handler hebt den
|
||||||
|
// State dann auf `Authenticated`. Kein Schaden, nur „nachträgliche
|
||||||
|
// Anmeldung". Bei späterer Exception passiert nichts; das Future
|
||||||
|
// ist hier nicht mehr awaited.
|
||||||
|
debugPrint('Login-Timeout nach ${_loginTimeout.inSeconds}s.');
|
||||||
|
emit(Unauthenticated(loginTimedOut: true));
|
||||||
} catch (err, st) {
|
} catch (err, st) {
|
||||||
debugPrint('Login fehlgeschlagen: $err\n$st');
|
debugPrint('Login fehlgeschlagen: $err\n$st');
|
||||||
emit(Unauthenticated());
|
emit(Unauthenticated());
|
||||||
|
|||||||
@ -10,7 +10,14 @@ class AuthBootstrapping extends AuthState {}
|
|||||||
|
|
||||||
class Unauthenticated extends AuthState {
|
class Unauthenticated extends AuthState {
|
||||||
final bool sessionExpired;
|
final bool sessionExpired;
|
||||||
Unauthenticated({this.sessionExpired = false});
|
|
||||||
|
/// `true`, wenn der letzte Login-Versuch in das 10-s-Timeout im
|
||||||
|
/// [AuthBloc] gelaufen ist (z. B. Verbindungsabbruch während
|
||||||
|
/// `tokenProvider.login()` oder hängender Issuer). Die [LoginPage]
|
||||||
|
/// blendet daraufhin einen Hinweis ein.
|
||||||
|
final bool loginTimedOut;
|
||||||
|
|
||||||
|
Unauthenticated({this.sessionExpired = false, this.loginTimedOut = false});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Transient state während dem PKCE-Flow (Browser-Tab offen,
|
/// Transient state während dem PKCE-Flow (Browser-Tab offen,
|
||||||
|
|||||||
@ -28,8 +28,11 @@ class LoginEnforcer extends StatelessWidget {
|
|||||||
if (state is AuthBootstrapping) {
|
if (state is AuthBootstrapping) {
|
||||||
return const _AuthBootstrapSplash();
|
return const _AuthBootstrapSplash();
|
||||||
}
|
}
|
||||||
final expired = state is Unauthenticated && state.sessionExpired;
|
final unauth = state is Unauthenticated ? state : null;
|
||||||
return LoginPage(sessionExpired: expired);
|
return LoginPage(
|
||||||
|
sessionExpired: unauth?.sessionExpired ?? false,
|
||||||
|
loginTimedOut: unauth?.loginTimedOut ?? false,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,10 +12,19 @@ import 'package:hl_lieferservice/feature/authentication/bloc/auth_state.dart';
|
|||||||
/// Browser-Tab, der RedirectURI `holzleitner://oauth2redirect` kommt
|
/// Browser-Tab, der RedirectURI `holzleitner://oauth2redirect` kommt
|
||||||
/// zurück, der Code wird gegen Tokens getauscht.
|
/// zurück, der Code wird gegen Tokens getauscht.
|
||||||
class LoginPage extends StatelessWidget {
|
class LoginPage extends StatelessWidget {
|
||||||
const LoginPage({super.key, this.sessionExpired = false});
|
const LoginPage({
|
||||||
|
super.key,
|
||||||
|
this.sessionExpired = false,
|
||||||
|
this.loginTimedOut = false,
|
||||||
|
});
|
||||||
|
|
||||||
final bool sessionExpired;
|
final bool sessionExpired;
|
||||||
|
|
||||||
|
/// Hinweis-Banner, dass der vorherige Login-Versuch ins 10-s-Timeout
|
||||||
|
/// gelaufen ist (typisch: Verbindungsabbruch oder hängender Issuer).
|
||||||
|
/// Wird vom `LoginEnforcer` aus `Unauthenticated.loginTimedOut` gefüttert.
|
||||||
|
final bool loginTimedOut;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@ -36,6 +45,20 @@ class LoginPage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
actions: const [SizedBox.shrink()],
|
actions: const [SizedBox.shrink()],
|
||||||
),
|
),
|
||||||
|
if (loginTimedOut)
|
||||||
|
MaterialBanner(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
content: const Text(
|
||||||
|
'Einloggen nicht möglich. Später erneut versuchen.',
|
||||||
|
style: TextStyle(color: Colors.white),
|
||||||
|
),
|
||||||
|
backgroundColor: Colors.red.shade700,
|
||||||
|
leading: const Icon(
|
||||||
|
Icons.cloud_off,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
actions: const [SizedBox.shrink()],
|
||||||
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
|
|||||||
Reference in New Issue
Block a user