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 createState() => _GuardianLoginCardState(); } class _GuardianLoginCardState extends State { final _emailFormKey = GlobalKey(); final _codeFormKey = GlobalKey(); 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 _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 _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)', ), ), ], ), ], ), ); } }