Files
Holzleitner-Lieferservice-App/lib/feature/authentication/bloc/auth_state.dart
Dennis Nemec bc95fe9577 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>
2026-10-30 20:28:24 +01:00

114 lines
3.7 KiB
Dart

import 'package:hl_lieferservice/feature/authentication/model/user.dart';
abstract class AuthState {}
/// Initialer State beim App-Start: der Refresh-Token aus der Secure
/// Storage wird gerade gegen das Issuer-Endpoint geprüft. Die UI zeigt
/// in dieser Zeit einen Splash statt LoginPage, damit Nutzer mit
/// gespeicherter Session keinen sichtbaren Login-Flash sehen.
class AuthBootstrapping extends AuthState {}
class Unauthenticated extends AuthState {
final bool sessionExpired;
/// `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;
/// `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,
/// Token-Tausch läuft).
class Authenticating extends AuthState {}
/// Erfolgreiche Authentifizierung. Trägt sowohl die "neuen"
/// Token-/Claim-Felder als auch das Legacy-User-Objekt — Letzteres
/// wird aus den Claims befüllt und mit Phase D komplett entfernt.
class Authenticated extends AuthState {
Authenticated({
required this.personalnummer,
required this.displayName,
required this.email,
required this.user,
required this.sessionId,
});
/// Personalnummer aus dem `personalnummer`-Custom-Claim. Das
/// Backend nutzt diese als Account-Identifier.
final int personalnummer;
/// Beste verfügbare Beschriftung für den Fahrer:
/// `name` > `given_name + family_name` > `preferred_username`.
final String displayName;
/// E-Mail aus dem `email`-Claim, falls vorhanden.
final String? email;
/// Legacy: das alte `User`-Domänenobjekt, aus den Claims befüllt.
/// Wird in Phase D entfernt, sobald keine Aufrufer mehr da sind.
final User user;
/// Legacy: füllt für die alten `getSessionOrThrow()`-Aufrufer
/// einen Wert — die laufen sowieso gegen ERPframe und werden in
/// Phase D abgelöst.
final String sessionId;
/// Konstruiert den State aus dem dekodierten ID-Token-Payload.
factory Authenticated.fromClaims({
required Map<String, dynamic> claims,
required String accessToken,
}) {
final personalnummerRaw = claims['personalnummer'];
final personalnummer = switch (personalnummerRaw) {
int v => v,
String v => int.parse(v),
_ => throw FormatException(
'personalnummer-Claim fehlt oder hat unerwarteten Typ '
'(${personalnummerRaw.runtimeType})',
),
};
final given = (claims['given_name'] as String?) ?? '';
final family = (claims['family_name'] as String?) ?? '';
final preferredUsername =
(claims['preferred_username'] as String?) ?? 'fahrer';
final fullName = (claims['name'] as String?)?.trim();
final displayName =
fullName != null && fullName.isNotEmpty
? fullName
: (given.isNotEmpty || family.isNotEmpty
? '$given $family'.trim()
: preferredUsername);
final email = claims['email'] as String?;
final user = User(
number: personalnummer.toString(),
firstName: given,
lastName: family,
mail: email ?? '',
);
return Authenticated(
personalnummer: personalnummer,
displayName: displayName,
email: email,
user: user,
sessionId: accessToken,
);
}
}