226 lines
7.0 KiB
Dart
226 lines
7.0 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
|
|
import '../guardian_login_controller.dart';
|
|
import 'login_error_banner.dart';
|
|
import 'login_form_parts.dart';
|
|
|
|
/// Passwordless guardian login: e-mail step, then the mailed six-digit code.
|
|
/// A tapped mail link completes the second step without typing (handled by
|
|
/// the login screen).
|
|
class GuardianLoginCard extends StatefulWidget {
|
|
final GuardianLoginController controller;
|
|
final VoidCallback onSuccess;
|
|
|
|
const GuardianLoginCard({
|
|
required this.controller,
|
|
required this.onSuccess,
|
|
super.key,
|
|
});
|
|
|
|
@override
|
|
State<GuardianLoginCard> createState() => _GuardianLoginCardState();
|
|
}
|
|
|
|
class _GuardianLoginCardState extends State<GuardianLoginCard> {
|
|
final _emailFormKey = GlobalKey<FormState>();
|
|
final _codeFormKey = GlobalKey<FormState>();
|
|
final _emailController = TextEditingController();
|
|
final _codeController = TextEditingController();
|
|
Timer? _resendTicker;
|
|
|
|
GuardianLoginController get _controller => widget.controller;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_controller.addListener(_onControllerChange);
|
|
_syncResendTicker();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.removeListener(_onControllerChange);
|
|
_resendTicker?.cancel();
|
|
_emailController.dispose();
|
|
_codeController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
void _onControllerChange() {
|
|
if (!mounted) return;
|
|
_syncResendTicker();
|
|
setState(() {});
|
|
}
|
|
|
|
// Rebuilds once per second while the resend cooldown runs so the countdown
|
|
// stays current.
|
|
void _syncResendTicker() {
|
|
final waiting =
|
|
_controller.step == GuardianLoginStep.enterCode &&
|
|
!_controller.canResend();
|
|
if (waiting && _resendTicker == null) {
|
|
_resendTicker = Timer.periodic(const Duration(seconds: 1), (_) {
|
|
if (!mounted) return;
|
|
setState(() {});
|
|
if (_controller.canResend()) {
|
|
_resendTicker?.cancel();
|
|
_resendTicker = null;
|
|
}
|
|
});
|
|
} else if (!waiting) {
|
|
_resendTicker?.cancel();
|
|
_resendTicker = null;
|
|
}
|
|
}
|
|
|
|
String? _validateEmail(String? value) {
|
|
final email = (value ?? '').trim();
|
|
if (email.isEmpty) return 'Eingabe erforderlich';
|
|
if (!RegExp(r'^[^@\s]+@[^@\s]+\.[^@\s]+$').hasMatch(email)) {
|
|
return 'Bitte eine gültige E-Mail-Adresse eingeben';
|
|
}
|
|
return null;
|
|
}
|
|
|
|
String? _validateCode(String? value) {
|
|
final length = _controller.pending!.codeLength;
|
|
final code = GuardianLoginController.normalizeCode(value ?? '');
|
|
return code.length == length
|
|
? null
|
|
: 'Bitte den $length-stelligen Code eingeben';
|
|
}
|
|
|
|
Future<void> _requestCode() async {
|
|
if (_controller.loading) return;
|
|
if (!(_emailFormKey.currentState?.validate() ?? false)) return;
|
|
final signedIn = await _controller.requestCode(_emailController.text);
|
|
if (signedIn && mounted) widget.onSuccess();
|
|
}
|
|
|
|
Future<void> _submitCode() async {
|
|
if (_controller.loading) return;
|
|
if (!(_codeFormKey.currentState?.validate() ?? false)) return;
|
|
final signedIn = await _controller.submitCode(_codeController.text);
|
|
if (signedIn && mounted) widget.onSuccess();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) => switch (_controller.step) {
|
|
GuardianLoginStep.enterEmail => _buildEmailStep(context),
|
|
GuardianLoginStep.enterCode => _buildCodeStep(context),
|
|
};
|
|
|
|
Widget _buildEmailStep(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
return Form(
|
|
key: _emailFormKey,
|
|
child: LoginCardFrame(
|
|
title: 'Anmeldung für Eltern',
|
|
hint:
|
|
'Gib die E-Mail-Adresse ein, die bei der Schule hinterlegt ist. '
|
|
'Du erhältst einen Anmeldecode per E-Mail.',
|
|
children: [
|
|
TextFormField(
|
|
key: const Key('guardian-email-field'),
|
|
controller: _emailController,
|
|
enabled: !_controller.loading,
|
|
validator: _validateEmail,
|
|
autocorrect: false,
|
|
keyboardType: TextInputType.emailAddress,
|
|
autofillHints: const [AutofillHints.email],
|
|
textInputAction: TextInputAction.done,
|
|
onFieldSubmitted: (_) => _requestCode(),
|
|
decoration: loginInputDecoration(
|
|
theme,
|
|
'E-Mail-Adresse',
|
|
Icons.alternate_email,
|
|
),
|
|
),
|
|
LoginErrorBanner(
|
|
message: _controller.errorMessage,
|
|
details: _controller.errorDetails,
|
|
),
|
|
const SizedBox(height: 20),
|
|
LoginSubmitButton(
|
|
key: const Key('guardian-request-button'),
|
|
label: 'Code anfordern',
|
|
loading: _controller.loading,
|
|
onPressed: _requestCode,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildCodeStep(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
final pending = _controller.pending!;
|
|
final remaining = pending.resendAvailableAt.difference(DateTime.now());
|
|
return Form(
|
|
key: _codeFormKey,
|
|
child: LoginCardFrame(
|
|
title: 'Code eingeben',
|
|
hint:
|
|
'Wir haben eine E-Mail an ${pending.email} gesendet. Gib den Code '
|
|
'ein oder tippe auf den Link in der E-Mail.',
|
|
children: [
|
|
TextFormField(
|
|
key: const Key('guardian-code-field'),
|
|
controller: _codeController,
|
|
enabled: !_controller.loading,
|
|
validator: _validateCode,
|
|
autofocus: true,
|
|
keyboardType: TextInputType.number,
|
|
autofillHints: const [AutofillHints.oneTimeCode],
|
|
inputFormatters: [
|
|
FilteringTextInputFormatter.digitsOnly,
|
|
LengthLimitingTextInputFormatter(pending.codeLength),
|
|
],
|
|
textInputAction: TextInputAction.done,
|
|
onFieldSubmitted: (_) => _submitCode(),
|
|
decoration: loginInputDecoration(
|
|
theme,
|
|
'Anmeldecode',
|
|
Icons.pin_outlined,
|
|
),
|
|
),
|
|
LoginErrorBanner(
|
|
message: _controller.errorMessage,
|
|
details: _controller.errorDetails,
|
|
),
|
|
const SizedBox(height: 20),
|
|
LoginSubmitButton(
|
|
key: const Key('guardian-verify-button'),
|
|
label: 'Anmelden',
|
|
loading: _controller.loading,
|
|
onPressed: _submitCode,
|
|
),
|
|
const SizedBox(height: 8),
|
|
Row(
|
|
children: [
|
|
TextButton(
|
|
onPressed: _controller.loading ? null : _controller.changeEmail,
|
|
child: const Text('E-Mail ändern'),
|
|
),
|
|
const Spacer(),
|
|
TextButton(
|
|
onPressed: _controller.loading || !_controller.canResend()
|
|
? null
|
|
: _controller.resend,
|
|
child: Text(
|
|
_controller.canResend()
|
|
? 'Erneut senden'
|
|
: 'Erneut senden (${remaining.inSeconds + 1} s)',
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|