fix: refactor password reset dialog to its own StatefulWidget

Fixes red screen crash ('_dependents.isEmpty is not true') caused by
auth state change rebuilding the widget tree while the dialog was
still open and sharing the parent context across an async gap.
This commit is contained in:
Lukas Müllner 2026-02-23 08:38:05 +01:00
parent 2c3c6d990b
commit 8c453e3f13

View file

@ -90,59 +90,13 @@ class _AuthGateState extends State<AuthGate> {
/// Shows a dialog so the user can type a new password after clicking
/// the "Reset Password" link from their email.
Future<void> _showResetPasswordDialog() async {
final passwordController = TextEditingController();
if (!mounted) return;
await showDialog<void>(
context: context,
barrierDismissible: false,
builder: (ctx) => AlertDialog(
title: const Text('Set New Password'),
content: TextField(
controller: passwordController,
obscureText: true,
decoration: const InputDecoration(
labelText: 'New password',
border: OutlineInputBorder(),
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(),
child: const Text('Cancel'),
),
ElevatedButton(
onPressed: () async {
final newPassword = passwordController.text.trim();
if (newPassword.length < 6) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Password must be at least 6 characters.')),
);
return;
}
try {
await supabase.auth.updateUser(
UserAttributes(password: newPassword),
);
if (!ctx.mounted) return;
Navigator.of(ctx).pop();
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Password updated successfully!')),
);
} on AuthException catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(e.message), backgroundColor: Colors.red),
);
}
},
child: const Text('Save'),
),
],
),
builder: (_) => const _ResetPasswordDialog(),
);
passwordController.dispose();
}
@override
@ -161,6 +115,86 @@ class _AuthGateState extends State<AuthGate> {
}
}
// Reset Password Dialog
class _ResetPasswordDialog extends StatefulWidget {
const _ResetPasswordDialog();
@override
State<_ResetPasswordDialog> createState() => _ResetPasswordDialogState();
}
class _ResetPasswordDialogState extends State<_ResetPasswordDialog> {
final _passwordController = TextEditingController();
bool _isSaving = false;
@override
void dispose() {
_passwordController.dispose();
super.dispose();
}
Future<void> _save() async {
final newPassword = _passwordController.text.trim();
if (newPassword.length < 6) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Password must be at least 6 characters.')),
);
return;
}
setState(() => _isSaving = true);
try {
await supabase.auth.updateUser(
UserAttributes(password: newPassword),
);
if (!mounted) return;
Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Password updated successfully!')),
);
} on AuthException catch (e) {
if (!mounted) return;
setState(() => _isSaving = false);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(e.message), backgroundColor: Colors.red),
);
}
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('Set New Password'),
content: TextField(
controller: _passwordController,
obscureText: true,
decoration: const InputDecoration(
labelText: 'New password',
border: OutlineInputBorder(),
),
onSubmitted: (_) => _save(),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Cancel'),
),
ElevatedButton(
onPressed: _isSaving ? null : _save,
child: _isSaving
? const SizedBox(
height: 18,
width: 18,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Save'),
),
],
);
}
}
// Login Screen
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});