Files
Holzleitner-Lieferservice-App/lib/feature/settings/presentation/settings_page.dart
Dennis Nemec 7c362549ee feat(settings): Konto-Konsole (Keycloak) oeffnen + Logout mit Bestaetigung
- 'Konto verwalten' oeffnet die Keycloak-Account-Konsole (issuer/account/) im externen Browser (Passwort/E-Mail/2FA)
- 'Ausloggen': ganze Zeile tappbar + Bestaetigungsdialog -> LogoutRequested

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 13:08:18 +02:00

192 lines
6.8 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:hl_lieferservice/data/network/backend_config.dart';
import 'package:hl_lieferservice/feature/authentication/bloc/auth_bloc.dart';
import 'package:hl_lieferservice/feature/authentication/bloc/auth_event.dart';
import 'package:hl_lieferservice/feature/settings/bloc/settings_bloc.dart';
import 'package:hl_lieferservice/feature/settings/bloc/settings_event.dart';
import 'package:hl_lieferservice/feature/settings/bloc/settings_state.dart';
import 'package:hl_lieferservice/feature/settings/model/settings.dart';
class SettingsPage extends StatefulWidget {
const SettingsPage({super.key});
@override
State<StatefulWidget> createState() => _SettingsPage();
}
class _SettingsPage extends State<SettingsPage> {
/// Bestätigt das Abmelden und triggert dann `LogoutRequested` am
/// [AuthBloc]. Die Navigation zur LoginPage übernimmt der globale
/// `LoginEnforcer` automatisch beim Wechsel auf `Unauthenticated` —
/// gleicher Pfad wie der Abmelden-Button im Drawer
/// (`home_drawer._confirmLogout`).
///
/// Wording bewusst identisch zum Drawer-Dialog, damit es egal ist, von
/// welchem Einstiegspunkt der Fahrer kommt.
Future<void> _logout() async {
final authBloc = context.read<AuthBloc>();
final confirmed = await showDialog<bool>(
context: context,
builder: (dialogContext) => AlertDialog(
title: const Text("Abmelden"),
content: const Text(
"Möchten Sie sich wirklich abmelden? "
"Beim nächsten Start ist eine erneute Anmeldung erforderlich.",
),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(false),
child: const Text("Abbrechen"),
),
FilledButton(
style: FilledButton.styleFrom(backgroundColor: Colors.red),
onPressed: () => Navigator.of(dialogContext).pop(true),
child: const Text("Abmelden"),
),
],
),
);
if (confirmed != true) return;
authBloc.add(const LogoutRequested());
}
// ─── Konto / Keycloak ─────────────────────────────────────────────────
/// Öffnet die Keycloak-Account-Konsole im externen Browser. Dort kann
/// der Fahrer sein Passwort ändern, hinterlegte E-Mail aktualisieren
/// und (sofern aktiviert) Zwei-Faktor-Authentifizierung verwalten.
///
/// URL-Pfad `/account/` ist die Standard-Route der Keycloak-26-Account-
/// Konsole; sie hängt sich an den Realm-Issuer aus [BackendConfig].
/// Externer Browser statt In-App-WebView, damit ggf. im Browser
/// gespeicherte Credentials / Authenticator-Apps weiter funktionieren.
Future<void> _openAccountConsole() async {
final issuer = BackendConfig.fromEnvironment.keycloakIssuerUrl;
final uri = Uri.parse('$issuer/account/');
final ok = await launchUrl(uri, mode: LaunchMode.externalApplication);
if (!ok && mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Kontoeinstellungen konnten nicht geöffnet werden: $uri'),
),
);
}
}
Widget _scanSettings() {
return BlocBuilder<SettingsBloc, SettingsState>(
builder: (context, state) {
final currentState = state;
if (currentState is AppSettingsLoaded) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(20),
child: Text(
"Scaneinstellungen",
style: Theme.of(context).textTheme.headlineSmall,
),
),
ListTile(
title: const Text("Hardware-Scanner"),
subtitle: const Text(
"Schaltet die Kamera beim Scannen aus und nutzt den Hardware-Scanner",
),
trailing: Switch(
value: currentState.settings.useHardwareScanner,
onChanged: (value) {
Settings newSettings = currentState.settings.copyWith();
newSettings.useHardwareScanner = value;
context.read<SettingsBloc>().add(
UpdateSettings(settings: newSettings),
);
},
),
tileColor: Theme.of(context).colorScheme.onSecondary,
),
],
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(20),
child: Text(
"Scaneinstellungen",
style: Theme.of(context).textTheme.headlineSmall,
),
),
Card(
color: Theme.of(context).colorScheme.onSecondary,
child: Padding(
padding: const EdgeInsets.all(20),
child: Center(
child: Text("Fehler beim Lesen der Scan-Einstellungen"),
),
),
),
],
);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: ListView(
children: [
_scanSettings(),
Padding(
padding: const EdgeInsets.all(20),
child: Text(
"Kontoeinstellungen",
style: Theme.of(context).textTheme.headlineSmall,
),
),
ListTile(
title: const Text("Konto verwalten"),
subtitle: const Text(
"Passwort, E-Mail und Sicherheitseinstellungen — öffnet die Konto-Seite im Browser",
),
trailing: Padding(
padding: const EdgeInsets.all(2),
child: FilledButton.icon(
onPressed: _openAccountConsole,
icon: const Icon(Icons.open_in_new),
label: const Text("Öffnen"),
),
),
tileColor: Theme.of(context).colorScheme.onSecondary,
),
ListTile(
title: const Text("Ausloggen"),
// Ganze Zeile tappbar machen — sonst muss der Fahrer das kleine
// Icon präzise treffen.
onTap: _logout,
trailing: const Icon(Icons.logout, color: Colors.redAccent),
tileColor: Theme.of(context).colorScheme.onSecondary,
),
],
),
appBar: AppBar(
title: const Text("Einstellungen"),
backgroundColor: Theme.of(context).primaryColor,
foregroundColor: Theme.of(context).colorScheme.onSecondary,
),
);
}
}