fix(auth): make auth dialog responsive and scrollable without overflow, add email verification on signup, and add password toggles
Build Android APK / Build Release APKs (push) Successful in 13m30s

This commit is contained in:
2026-09-14 10:26:44 +02:00
parent 66246c9b85
commit a50456032c
2 changed files with 286 additions and 168 deletions
+134 -48
View File
@@ -36,6 +36,10 @@ class _AuthDialogState extends State<AuthDialog>
bool _isLoading = false; bool _isLoading = false;
String? _errorMessage; String? _errorMessage;
bool _obscureLoginPassword = true;
bool _obscureRegPassword = true;
bool _obscureRegConfirmPassword = true;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@@ -128,9 +132,12 @@ class _AuthDialogState extends State<AuthDialog>
if (error == null) { if (error == null) {
Navigator.pop(context); Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( SnackBar(
content: Text('Konto erfolgreich erstellt! Willkommen im Bürgerforum.'), content: Text(
'Konto erfolgreich erstellt! Wir haben einen Bestätigungslink an $email gesendet. Bitte prüfe dein Postfach.',
),
backgroundColor: Colors.green, backgroundColor: Colors.green,
duration: const Duration(seconds: 6),
), ),
); );
} else { } else {
@@ -228,22 +235,48 @@ class _AuthDialogState extends State<AuthDialog>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final screenHeight = MediaQuery.of(context).size.height;
return Dialog( return Dialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
insetPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24), insetPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
clipBehavior: Clip.antiAlias,
child: ConstrainedBox( child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 420), constraints: BoxConstraints(
maxWidth: 440,
maxHeight: screenHeight * 0.82,
),
child: Padding( child: Padding(
padding: const EdgeInsets.all(20.0), padding: const EdgeInsets.fromLTRB(20, 16, 20, 16),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Row(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(6),
child: Image.asset(
'assets/images/logo.png',
height: 28,
errorBuilder: (_, __, ___) => const Icon(
Icons.campaign,
color: Colors.blue,
),
),
),
const SizedBox(width: 10),
const Text( const Text(
'Bürgerforum Oggau', 'Bürgerforum Oggau',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
), ),
IconButton( IconButton(
icon: const Icon(Icons.close), icon: const Icon(Icons.close),
@@ -258,11 +291,11 @@ class _AuthDialogState extends State<AuthDialog>
tabs: const [ tabs: const [
Tab(text: 'Anmelden'), Tab(text: 'Anmelden'),
Tab(text: 'Registrieren'), Tab(text: 'Registrieren'),
Tab(text: 'Code-Login'), Tab(text: 'Bürger-Code'),
], ],
), ),
if (_errorMessage != null) ...[ if (_errorMessage != null) ...[
const SizedBox(height: 12), const SizedBox(height: 10),
Container( Container(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -286,16 +319,15 @@ class _AuthDialogState extends State<AuthDialog>
), ),
), ),
], ],
const SizedBox(height: 16), const SizedBox(height: 12),
Flexible( Expanded(
child: SingleChildScrollView(
child: SizedBox(
height: 280,
child: TabBarView( child: TabBarView(
controller: _tabController, controller: _tabController,
children: [ children: [
// Tab 1: Login // Tab 1: Anmelden
Column( SingleChildScrollView(
padding: const EdgeInsets.only(top: 6, bottom: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
TextField( TextField(
@@ -310,11 +342,24 @@ class _AuthDialogState extends State<AuthDialog>
const SizedBox(height: 12), const SizedBox(height: 12),
TextField( TextField(
controller: _loginPasswordController, controller: _loginPasswordController,
obscureText: true, obscureText: _obscureLoginPassword,
decoration: const InputDecoration( decoration: InputDecoration(
labelText: 'Passwort', labelText: 'Passwort',
prefixIcon: Icon(Icons.lock_outline), prefixIcon: const Icon(Icons.lock_outline),
border: OutlineInputBorder(), suffixIcon: IconButton(
icon: Icon(
_obscureLoginPassword
? Icons.visibility_off
: Icons.visibility,
),
onPressed: () {
setState(() {
_obscureLoginPassword =
!_obscureLoginPassword;
});
},
),
border: const OutlineInputBorder(),
), ),
), ),
Align( Align(
@@ -327,12 +372,11 @@ class _AuthDialogState extends State<AuthDialog>
), ),
), ),
), ),
const Spacer(), const SizedBox(height: 12),
ElevatedButton( ElevatedButton(
onPressed: _isLoading ? null : _handleLogin, onPressed: _isLoading ? null : _handleLogin,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
padding: padding: const EdgeInsets.symmetric(vertical: 13),
const EdgeInsets.symmetric(vertical: 12),
), ),
child: _isLoading child: _isLoading
? const SizedBox( ? const SizedBox(
@@ -341,17 +385,22 @@ class _AuthDialogState extends State<AuthDialog>
child: CircularProgressIndicator( child: CircularProgressIndicator(
strokeWidth: 2), strokeWidth: 2),
) )
: const Text('Anmelden'), : const Text('Anmelden',
style: TextStyle(fontSize: 15)),
), ),
], ],
), ),
),
// Tab 2: Registrierung // Tab 2: Registrierung
Column( SingleChildScrollView(
padding: const EdgeInsets.only(top: 6, bottom: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
TextField( TextField(
controller: _regNameController, controller: _regNameController,
textCapitalization: TextCapitalization.words,
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'Name / Vorname', labelText: 'Name / Vorname',
prefixIcon: Icon(Icons.person_outline), prefixIcon: Icon(Icons.person_outline),
@@ -366,34 +415,60 @@ class _AuthDialogState extends State<AuthDialog>
labelText: 'E-Mail-Adresse', labelText: 'E-Mail-Adresse',
prefixIcon: Icon(Icons.email_outlined), prefixIcon: Icon(Icons.email_outlined),
border: OutlineInputBorder(), border: OutlineInputBorder(),
helperText:
'Erhält einen Bestätigungslink zur Prüfung',
), ),
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
TextField( TextField(
controller: _regPasswordController, controller: _regPasswordController,
obscureText: true, obscureText: _obscureRegPassword,
decoration: const InputDecoration( decoration: InputDecoration(
labelText: 'Passwort (mind. 6 Zeichen)', labelText: 'Passwort (mind. 6 Zeichen)',
prefixIcon: Icon(Icons.lock_outline), prefixIcon: const Icon(Icons.lock_outline),
border: OutlineInputBorder(), suffixIcon: IconButton(
icon: Icon(
_obscureRegPassword
? Icons.visibility_off
: Icons.visibility,
),
onPressed: () {
setState(() {
_obscureRegPassword = !_obscureRegPassword;
});
},
),
border: const OutlineInputBorder(),
), ),
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
TextField( TextField(
controller: _regConfirmPasswordController, controller: _regConfirmPasswordController,
obscureText: true, obscureText: _obscureRegConfirmPassword,
decoration: const InputDecoration( decoration: InputDecoration(
labelText: 'Passwort bestätigen', labelText: 'Passwort bestätigen',
prefixIcon: Icon(Icons.lock_outline), prefixIcon: const Icon(Icons.lock_outline),
border: OutlineInputBorder(), suffixIcon: IconButton(
icon: Icon(
_obscureRegConfirmPassword
? Icons.visibility_off
: Icons.visibility,
),
onPressed: () {
setState(() {
_obscureRegConfirmPassword =
!_obscureRegConfirmPassword;
});
},
),
border: const OutlineInputBorder(),
), ),
), ),
const Spacer(), const SizedBox(height: 16),
ElevatedButton( ElevatedButton(
onPressed: _isLoading ? null : _handleRegister, onPressed: _isLoading ? null : _handleRegister,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
padding: padding: const EdgeInsets.symmetric(vertical: 13),
const EdgeInsets.symmetric(vertical: 12),
), ),
child: _isLoading child: _isLoading
? const SizedBox( ? const SizedBox(
@@ -402,24 +477,36 @@ class _AuthDialogState extends State<AuthDialog>
child: CircularProgressIndicator( child: CircularProgressIndicator(
strokeWidth: 2), strokeWidth: 2),
) )
: const Text('Konto erstellen'), : const Text(
'Konto erstellen & E-Mail bestätigen',
style: TextStyle(fontSize: 14)),
), ),
], ],
), ),
),
// Tab 3: Bürger-Code // Tab 3: Bürger-Code
Column( SingleChildScrollView(
padding: const EdgeInsets.only(top: 6, bottom: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
const Text( Container(
'Du möchtest kein persönliches Konto anlegen? Gib einfach den Zugangscode des Bürgerforums ein, um sofort Mängel und Ideen melden zu können.', padding: const EdgeInsets.all(12),
style: decoration: BoxDecoration(
TextStyle(fontSize: 13, color: Colors.black87), color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.blue.shade100),
),
child: const Text(
'Du möchtest kein persönliches E-Mail-Konto anlegen? Gib einfach den Zugangscode des Bürgerforums ein (z.B. oggau), um sofort Mängel und Ideen melden zu können.',
style: TextStyle(
fontSize: 13, color: Colors.black87),
),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
TextField( TextField(
controller: _codeController, controller: _codeController,
obscureText: true,
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'Bürger-Zugangscode', labelText: 'Bürger-Zugangscode',
prefixIcon: Icon(Icons.key_outlined), prefixIcon: Icon(Icons.key_outlined),
@@ -427,12 +514,11 @@ class _AuthDialogState extends State<AuthDialog>
hintText: 'z.B. oggau', hintText: 'z.B. oggau',
), ),
), ),
const Spacer(), const SizedBox(height: 20),
ElevatedButton( ElevatedButton(
onPressed: _isLoading ? null : _handleCodeUnlock, onPressed: _isLoading ? null : _handleCodeUnlock,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
padding: padding: const EdgeInsets.symmetric(vertical: 13),
const EdgeInsets.symmetric(vertical: 12),
), ),
child: _isLoading child: _isLoading
? const SizedBox( ? const SizedBox(
@@ -441,15 +527,15 @@ class _AuthDialogState extends State<AuthDialog>
child: CircularProgressIndicator( child: CircularProgressIndicator(
strokeWidth: 2), strokeWidth: 2),
) )
: const Text('Freischalten'), : const Text('Freischalten',
style: TextStyle(fontSize: 15)),
), ),
], ],
), ),
),
], ],
), ),
), ),
),
),
], ],
), ),
), ),
+33 -1
View File
@@ -92,9 +92,19 @@ class AuthService extends ChangeNotifier {
password: password, password: password,
); );
_user = credential.user; _user = credential.user;
if (_user != null && name.isNotEmpty) { if (_user != null) {
if (name.isNotEmpty) {
await _user!.updateDisplayName(name.trim()); await _user!.updateDisplayName(name.trim());
} }
// Send email verification so user verifies they own the email address
try {
await _user!.sendEmailVerification();
} catch (e) {
if (kDebugMode) {
debugPrint('Error sending verification email: $e');
}
}
}
notifyListeners(); notifyListeners();
return null; // success return null; // success
} on FirebaseAuthException catch (e) { } on FirebaseAuthException catch (e) {
@@ -104,6 +114,28 @@ class AuthService extends ChangeNotifier {
} }
} }
/// Check if user's email is verified
bool get isEmailVerified => _user?.emailVerified ?? false;
/// Reload current user from Firebase to refresh emailVerified status
Future<void> reloadUser() async {
await _user?.reload();
_user = _auth.currentUser;
notifyListeners();
}
/// Re-send verification email
Future<String?> sendEmailVerification() async {
try {
await _user?.sendEmailVerification();
return null;
} on FirebaseAuthException catch (e) {
return _getLocalizedErrorMessage(e.code);
} catch (e) {
return 'Fehler beim Senden der Bestätigungs-E-Mail: $e';
}
}
/// Send password reset email /// Send password reset email
Future<String?> sendPasswordReset(String email) async { Future<String?> sendPasswordReset(String email) async {
try { try {