fix: Fehlerbehandlung beim Laden und Meldungen überarbeiten

Reklamationen der Fahrer: Meldungen verdeckten Buttons, und nach dem
Abschluss stand „Lieferung nicht in der Tour gefunden" ohne Ausweg da.

Meldungen:
- Erfolgs-SnackBars entfernt, bei Erfolg bleibt der Spinner
- einheitliche, schwebende Meldungen statt Stau über den Buttons
- Server-Rohtexte durch verständliche Texte ersetzt (api_failure_text)

Laden:
- fehlt eine geöffnete Lieferung, wird die Tour einmal neu geholt; die
  Meldung kommt nur nach erfolgreichem Abruf ohne die Lieferung
- Endlos-Spinner auf Lieferdetail, Filial-Abholung und Beladen ersetzt
  durch Kopfzeile, Fehlergrund und „Erneut versuchen"
- Tour wird nach einem Tageswechsel nachgeladen, offene Seiten bleiben
  dabei unberührt
- Abschluss und Zuweisung überschreiben kein zwischenzeitliches Neuladen,
  alte Antworten drehen einen Abschluss nicht mehr zurück
- leere Server-Antworten gelten als Fehler statt als „keine Daten"

Anmeldung:
- Token-Erneuerung wartet höchstens 12 s, der native Aufruf läuft weiter
- App-Start ohne Netz zeigt einen Hinweis mit „Erneut versuchen"
- Fehlerseite der Fahrzeugauswahl bietet „Abmelden"

Außerdem Podfile.lock an die Plugins angeglichen. Tests für die neuen
Fehlerpfade ergänzt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Dennis Nemec
2026-10-30 20:28:24 +01:00
parent 2ff331294e
commit bc95fe9577
40 changed files with 2063 additions and 365 deletions

View File

@ -105,6 +105,8 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
RestoreSessionRequested event,
Emitter<AuthState> emit,
) async {
// Auch für „Erneut versuchen" von der LoginPage: wieder der Splash.
emit(AuthBootstrapping());
try {
// Timeout-Schutz: hängt der Restore (z. B. nativer flutter_appauth-
// Token-Call nach Hot-Restart, nicht erreichbarer Issuer), darf der
@ -112,18 +114,21 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
// sauber auf die LoginPage zurück. Läuft der Restore später doch noch
// erfolgreich durch, kommt der Login via Stream-Event (AuthLoggedIn)
// nachträglich an und der State wird zu Authenticated.
final restored = await tokenProvider.restoreSession().timeout(
final result = await tokenProvider.restoreSession().timeout(
const Duration(seconds: 15),
onTimeout: () => false,
onTimeout: () => SessionRestoreResult.temporarilyUnavailable,
);
if (!restored) {
// Kein gespeicherter Refresh-Token oder Refresh fehlgeschlagen:
// Vom Splash zur LoginPage übergehen. Kein Snackbar — das ist
// der normale Cold-Start-Pfad.
emit(Unauthenticated());
switch (result) {
case SessionRestoreResult.restored:
// Erfolg landet via Stream als ProviderSessionChanged.loggedIn,
// der Handler emittiert Authenticated.
break;
case SessionRestoreResult.noSession:
// Normaler Cold-Start-Pfad, daher kein Hinweis.
emit(Unauthenticated());
case SessionRestoreResult.temporarilyUnavailable:
emit(Unauthenticated(restoreUnavailable: true));
}
// Erfolg landet via Stream als ProviderSessionChanged.loggedIn,
// der Handler emittiert Authenticated.
} catch (err, st) {
debugPrint('Restore-Session fehlgeschlagen: $err\n$st');
emit(Unauthenticated());

View File

@ -17,7 +17,16 @@ class Unauthenticated extends AuthState {
/// blendet daraufhin einen Hinweis ein.
final bool loginTimedOut;
Unauthenticated({this.sessionExpired = false, this.loginTimedOut = false});
/// `true`, wenn beim App-Start eine Session gespeichert ist, sie sich wegen
/// Netz/VPN aber nicht erneuern ließ. Die [LoginPage] bietet dann „Erneut
/// versuchen" an, statt nach einem Login auszusehen, den es nicht braucht.
final bool restoreUnavailable;
Unauthenticated({
this.sessionExpired = false,
this.loginTimedOut = false,
this.restoreUnavailable = false,
});
}
/// Transient state während dem PKCE-Flow (Browser-Tab offen,