139 lines
4.1 KiB
Dart
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,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|