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
+252 -166
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: [
const Text( Row(
'Bürgerforum Oggau', children: [
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), 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(
'Bürgerforum Oggau',
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,168 +319,221 @@ class _AuthDialogState extends State<AuthDialog>
), ),
), ),
], ],
const SizedBox(height: 16), const SizedBox(height: 12),
Flexible( Expanded(
child: SingleChildScrollView( child: TabBarView(
child: SizedBox( controller: _tabController,
height: 280, children: [
child: TabBarView( // Tab 1: Anmelden
controller: _tabController, SingleChildScrollView(
children: [ padding: const EdgeInsets.only(top: 6, bottom: 12),
// Tab 1: Login child: Column(
Column( crossAxisAlignment: CrossAxisAlignment.stretch,
crossAxisAlignment: CrossAxisAlignment.stretch, children: [
children: [ TextField(
TextField( controller: _loginEmailController,
controller: _loginEmailController, keyboardType: TextInputType.emailAddress,
keyboardType: TextInputType.emailAddress, decoration: const InputDecoration(
decoration: const InputDecoration( labelText: 'E-Mail-Adresse',
labelText: 'E-Mail-Adresse', prefixIcon: Icon(Icons.email_outlined),
prefixIcon: Icon(Icons.email_outlined), border: OutlineInputBorder(),
border: OutlineInputBorder(),
),
), ),
const SizedBox(height: 12), ),
TextField( const SizedBox(height: 12),
controller: _loginPasswordController, TextField(
obscureText: true, controller: _loginPasswordController,
decoration: const InputDecoration( obscureText: _obscureLoginPassword,
labelText: 'Passwort', decoration: InputDecoration(
prefixIcon: Icon(Icons.lock_outline), labelText: 'Passwort',
border: OutlineInputBorder(), prefixIcon: const Icon(Icons.lock_outline),
), suffixIcon: IconButton(
), icon: Icon(
Align( _obscureLoginPassword
alignment: Alignment.centerRight, ? Icons.visibility_off
child: TextButton( : Icons.visibility,
onPressed: _showPasswordResetDialog,
child: const Text(
'Passwort vergessen?',
style: TextStyle(fontSize: 12),
), ),
onPressed: () {
setState(() {
_obscureLoginPassword =
!_obscureLoginPassword;
});
},
),
border: const OutlineInputBorder(),
),
),
Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: _showPasswordResetDialog,
child: const Text(
'Passwort vergessen?',
style: TextStyle(fontSize: 12),
), ),
), ),
const Spacer(), ),
ElevatedButton( const SizedBox(height: 12),
onPressed: _isLoading ? null : _handleLogin, ElevatedButton(
style: ElevatedButton.styleFrom( onPressed: _isLoading ? null : _handleLogin,
padding: style: ElevatedButton.styleFrom(
const EdgeInsets.symmetric(vertical: 12), padding: const EdgeInsets.symmetric(vertical: 13),
),
child: _isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2),
)
: const Text('Anmelden'),
), ),
], child: _isLoading
), ? const SizedBox(
width: 20,
// Tab 2: Registrierung height: 20,
Column( child: CircularProgressIndicator(
crossAxisAlignment: CrossAxisAlignment.stretch, strokeWidth: 2),
children: [ )
TextField( : const Text('Anmelden',
controller: _regNameController, style: TextStyle(fontSize: 15)),
decoration: const InputDecoration( ),
labelText: 'Name / Vorname', ],
prefixIcon: Icon(Icons.person_outline), ),
border: OutlineInputBorder(),
),
),
const SizedBox(height: 10),
TextField(
controller: _regEmailController,
keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration(
labelText: 'E-Mail-Adresse',
prefixIcon: Icon(Icons.email_outlined),
border: OutlineInputBorder(),
),
),
const SizedBox(height: 10),
TextField(
controller: _regPasswordController,
obscureText: true,
decoration: const InputDecoration(
labelText: 'Passwort (mind. 6 Zeichen)',
prefixIcon: Icon(Icons.lock_outline),
border: OutlineInputBorder(),
),
),
const SizedBox(height: 10),
TextField(
controller: _regConfirmPasswordController,
obscureText: true,
decoration: const InputDecoration(
labelText: 'Passwort bestätigen',
prefixIcon: Icon(Icons.lock_outline),
border: OutlineInputBorder(),
),
),
const Spacer(),
ElevatedButton(
onPressed: _isLoading ? null : _handleRegister,
style: ElevatedButton.styleFrom(
padding:
const EdgeInsets.symmetric(vertical: 12),
),
child: _isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2),
)
: const Text('Konto erstellen'),
),
],
),
// Tab 3: Bürger-Code
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'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.',
style:
TextStyle(fontSize: 13, color: Colors.black87),
),
const SizedBox(height: 16),
TextField(
controller: _codeController,
obscureText: true,
decoration: const InputDecoration(
labelText: 'Bürger-Zugangscode',
prefixIcon: Icon(Icons.key_outlined),
border: OutlineInputBorder(),
hintText: 'z.B. oggau',
),
),
const Spacer(),
ElevatedButton(
onPressed: _isLoading ? null : _handleCodeUnlock,
style: ElevatedButton.styleFrom(
padding:
const EdgeInsets.symmetric(vertical: 12),
),
child: _isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2),
)
: const Text('Freischalten'),
),
],
),
],
), ),
),
// Tab 2: Registrierung
SingleChildScrollView(
padding: const EdgeInsets.only(top: 6, bottom: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextField(
controller: _regNameController,
textCapitalization: TextCapitalization.words,
decoration: const InputDecoration(
labelText: 'Name / Vorname',
prefixIcon: Icon(Icons.person_outline),
border: OutlineInputBorder(),
),
),
const SizedBox(height: 10),
TextField(
controller: _regEmailController,
keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration(
labelText: 'E-Mail-Adresse',
prefixIcon: Icon(Icons.email_outlined),
border: OutlineInputBorder(),
helperText:
'Erhält einen Bestätigungslink zur Prüfung',
),
),
const SizedBox(height: 10),
TextField(
controller: _regPasswordController,
obscureText: _obscureRegPassword,
decoration: InputDecoration(
labelText: 'Passwort (mind. 6 Zeichen)',
prefixIcon: const Icon(Icons.lock_outline),
suffixIcon: IconButton(
icon: Icon(
_obscureRegPassword
? Icons.visibility_off
: Icons.visibility,
),
onPressed: () {
setState(() {
_obscureRegPassword = !_obscureRegPassword;
});
},
),
border: const OutlineInputBorder(),
),
),
const SizedBox(height: 10),
TextField(
controller: _regConfirmPasswordController,
obscureText: _obscureRegConfirmPassword,
decoration: InputDecoration(
labelText: 'Passwort bestätigen',
prefixIcon: const Icon(Icons.lock_outline),
suffixIcon: IconButton(
icon: Icon(
_obscureRegConfirmPassword
? Icons.visibility_off
: Icons.visibility,
),
onPressed: () {
setState(() {
_obscureRegConfirmPassword =
!_obscureRegConfirmPassword;
});
},
),
border: const OutlineInputBorder(),
),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _isLoading ? null : _handleRegister,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 13),
),
child: _isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2),
)
: const Text(
'Konto erstellen & E-Mail bestätigen',
style: TextStyle(fontSize: 14)),
),
],
),
),
// Tab 3: Bürger-Code
SingleChildScrollView(
padding: const EdgeInsets.only(top: 6, bottom: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
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),
TextField(
controller: _codeController,
decoration: const InputDecoration(
labelText: 'Bürger-Zugangscode',
prefixIcon: Icon(Icons.key_outlined),
border: OutlineInputBorder(),
hintText: 'z.B. oggau',
),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: _isLoading ? null : _handleCodeUnlock,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 13),
),
child: _isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2),
)
: const Text('Freischalten',
style: TextStyle(fontSize: 15)),
),
],
),
),
],
), ),
), ),
], ],
+34 -2
View File
@@ -92,8 +92,18 @@ class AuthService extends ChangeNotifier {
password: password, password: password,
); );
_user = credential.user; _user = credential.user;
if (_user != null && name.isNotEmpty) { if (_user != null) {
await _user!.updateDisplayName(name.trim()); if (name.isNotEmpty) {
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
@@ -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 {