Files
Client/lib/view/login/widgets/login_card.dart
T

139 lines
4.1 KiB
Dart

import 'package:flutter/material.dart';
import '../../../routing/app_routes.dart';
import '../login_controller.dart';
import 'login_error_banner.dart';
import 'login_form_parts.dart';
/// White Card hosting the login form (heading, two text fields, error
/// banner, submit button). Submitting calls [controller.submit] and signals
/// success via [onSuccess].
class LoginCard extends StatefulWidget {
final LoginController controller;
final VoidCallback onSuccess;
const LoginCard({
required this.controller,
required this.onSuccess,
super.key,
});
@override
State<LoginCard> createState() => _LoginCardState();
}
class _LoginCardState extends State<LoginCard> {
final _formKey = GlobalKey<FormState>();
final _usernameController = TextEditingController();
final _passwordController = TextEditingController();
final _passwordFocus = FocusNode();
@override
void initState() {
super.initState();
widget.controller.addListener(_onControllerChange);
}
@override
void dispose() {
widget.controller.removeListener(_onControllerChange);
_usernameController.dispose();
_passwordController.dispose();
_passwordFocus.dispose();
super.dispose();
}
void _onControllerChange() {
if (mounted) setState(() {});
}
String? _required(String? value) =>
(value ?? '').trim().isEmpty ? 'Eingabe erforderlich' : null;
Future<void> _submit() async {
if (widget.controller.loading) return;
if (!(_formKey.currentState?.validate() ?? false)) return;
final result = await widget.controller.submit(
_usernameController.text,
_passwordController.text,
);
if (!mounted) return;
switch (result) {
case LoginResult.success:
widget.onSuccess();
case LoginResult.nextcloudLoginRequired:
// 2FA (oder abweichendes NC-Passwort): Anmeldung im Browser über den
// Login Flow v2 abschließen; ohne Erfolg wird der Login zurückgerollt.
final ok = await AppRoutes.openNextcloudLoginFlow(context);
if (!mounted) return;
if (ok) {
widget.onSuccess();
} else {
await widget.controller.abortNextcloudLogin();
}
case LoginResult.failure:
break;
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final loading = widget.controller.loading;
return Form(
key: _formKey,
child: LoginCardFrame(
title: 'Anmelden',
hint: 'Melde dich mit deinen Marianum-Zugangsdaten an.',
children: [
TextFormField(
key: const Key('login-username-field'),
controller: _usernameController,
enabled: !loading,
validator: _required,
autocorrect: false,
textInputAction: TextInputAction.next,
onFieldSubmitted: (_) => _passwordFocus.requestFocus(),
decoration: loginInputDecoration(
theme,
'Nutzername',
Icons.person_outline,
),
),
const SizedBox(height: 12),
TextFormField(
key: const Key('login-password-field'),
controller: _passwordController,
focusNode: _passwordFocus,
enabled: !loading,
validator: _required,
obscureText: true,
obscuringCharacter: '•',
autocorrect: false,
enableSuggestions: false,
keyboardType: TextInputType.visiblePassword,
textInputAction: TextInputAction.done,
onFieldSubmitted: (_) => _submit(),
decoration: loginInputDecoration(
theme,
'Passwort',
Icons.lock_outline,
),
),
LoginErrorBanner(
message: widget.controller.errorMessage,
details: widget.controller.errorDetails,
),
const SizedBox(height: 20),
LoginSubmitButton(
key: const Key('login-submit-button'),
label: 'Anmelden',
loading: loading,
onPressed: _submit,
),
],
),
);
}
}