added guardian login with views for their assigned childs
This commit is contained in:
@@ -0,0 +1,225 @@
|
||||
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)',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user