Files
DriverVault/Phone App/lib/screens/settings_screen.dart
T
2026-07-06 08:50:52 +02:00

916 lines
30 KiB
Dart

import "dart:typed_data";
import "package:flutter/material.dart";
import "package:image_picker/image_picker.dart";
import "../biometric.dart";
import "../main.dart";
import "../models.dart";
import "../format.dart";
import "../theme.dart";
/// Full settings panel, mirroring the web Settings.vue (minus data export/import):
/// account (name/email/password), appearance (theme/locale/date/font), profile
/// (avatar/bio), privacy (sessions), and account deletion.
class SettingsScreen extends StatefulWidget {
const SettingsScreen({super.key});
@override
State<SettingsScreen> createState() => _SettingsScreenState();
}
class _SettingsScreenState extends State<SettingsScreen> {
UserProfile? _profile;
List<Session> _sessions = [];
Uint8List? _avatar;
bool _loading = true;
String? _loadError;
final _name = TextEditingController();
final _bio = TextEditingController();
@override
void initState() {
super.initState();
_load();
}
@override
void dispose() {
_name.dispose();
_bio.dispose();
super.dispose();
}
Future<void> _load() async {
setState(() {
_loading = true;
_loadError = null;
});
try {
final p = await apiClient.getMe();
appSettings.applyFromProfile(p);
final sessions = await apiClient.listSessions();
Uint8List? avatar;
if (p.hasAvatar) {
final bytes = await apiClient.getAvatarBytes();
if (bytes != null) avatar = Uint8List.fromList(bytes);
}
_name.text = p.name;
_bio.text = p.bio;
setState(() {
_profile = p;
_sessions = sessions;
_avatar = avatar;
});
} catch (e) {
setState(() => _loadError = e.toString());
} finally {
setState(() => _loading = false);
}
}
void _snack(String msg) =>
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg)));
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Settings")),
body: _loading
? const Center(child: CircularProgressIndicator())
: _loadError != null
? Center(child: Text(_loadError!))
: ListView(
padding: const EdgeInsets.all(12),
children: [
_AccountSection(
profile: _profile!,
nameController: _name,
onChanged: _load,
snack: _snack,
),
const SizedBox(height: 12),
_AppearanceSection(onError: _snack),
const SizedBox(height: 12),
_ProfileSection(
profile: _profile!,
bioController: _bio,
avatar: _avatar,
onChanged: _load,
snack: _snack,
),
const SizedBox(height: 12),
_SecuritySection(email: _profile!.email, snack: _snack),
const SizedBox(height: 12),
_SessionsSection(sessions: _sessions, onChanged: _load, snack: _snack),
const SizedBox(height: 12),
_DangerSection(profile: _profile!, onChanged: _load, snack: _snack),
const SizedBox(height: 24),
],
),
);
}
}
/// A titled card wrapper matching the web's sectioned layout.
class _Card extends StatelessWidget {
final String title;
final Color? titleColor;
final List<Widget> children;
const _Card({required this.title, this.titleColor, required this.children});
@override
Widget build(BuildContext context) {
return Card(
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(color: Theme.of(context).dividerColor),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title,
style: TextStyle(
fontSize: 16, fontWeight: FontWeight.w600, color: titleColor)),
const SizedBox(height: 12),
...children,
],
),
),
);
}
}
// --- Account ---------------------------------------------------------------
class _AccountSection extends StatefulWidget {
final UserProfile profile;
final TextEditingController nameController;
final Future<void> Function() onChanged;
final void Function(String) snack;
const _AccountSection({
required this.profile,
required this.nameController,
required this.onChanged,
required this.snack,
});
@override
State<_AccountSection> createState() => _AccountSectionState();
}
class _AccountSectionState extends State<_AccountSection> {
bool _savingName = false;
bool _verifying = false;
final _oldPw = TextEditingController();
final _newPw = TextEditingController();
final _confirmPw = TextEditingController();
bool _savingPw = false;
String? _pwError;
@override
void dispose() {
_oldPw.dispose();
_newPw.dispose();
_confirmPw.dispose();
super.dispose();
}
Future<void> _saveName() async {
setState(() => _savingName = true);
try {
final updated = await apiClient.updateMe({"name": widget.nameController.text.trim()});
authService.user = AuthUser(
id: updated.id,
email: updated.email,
name: updated.name,
role: updated.role,
);
widget.snack("Name saved.");
} catch (e) {
widget.snack("$e");
} finally {
if (mounted) setState(() => _savingName = false);
}
}
Future<void> _sendVerification() async {
setState(() => _verifying = true);
try {
await apiClient.requestVerification();
widget.snack("Verification email requested.");
} catch (e) {
widget.snack("$e");
} finally {
if (mounted) setState(() => _verifying = false);
}
}
Future<void> _savePassword() async {
setState(() => _pwError = null);
if (_newPw.text.length < 8) {
setState(() => _pwError = "New password must be at least 8 characters.");
return;
}
if (_newPw.text != _confirmPw.text) {
setState(() => _pwError = "New password and confirmation don't match.");
return;
}
setState(() => _savingPw = true);
try {
await apiClient.changePassword(_oldPw.text, _newPw.text);
_oldPw.clear();
_newPw.clear();
_confirmPw.clear();
widget.snack("Password updated.");
} catch (e) {
setState(() => _pwError = e.toString());
} finally {
if (mounted) setState(() => _savingPw = false);
}
}
@override
Widget build(BuildContext context) {
final p = widget.profile;
return _Card(
title: "Account",
children: [
const Text("Name", style: TextStyle(fontWeight: FontWeight.w500)),
const SizedBox(height: 4),
Row(children: [
Expanded(
child: TextField(
controller: widget.nameController,
decoration: const InputDecoration(border: OutlineInputBorder(), isDense: true),
),
),
const SizedBox(width: 8),
FilledButton(
onPressed: _savingName ? null : _saveName,
child: Text(_savingName ? "Saving…" : "Save"),
),
]),
const SizedBox(height: 16),
const Text("Email", style: TextStyle(fontWeight: FontWeight.w500)),
const SizedBox(height: 4),
Row(children: [
Expanded(child: Text(p.email)),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: p.verified
? (DriverVault.isDark(context) ? DriverVault.successSoftDark : DriverVault.successSoft)
: (DriverVault.isDark(context) ? DriverVault.warningSoftDark : DriverVault.warningSoft),
borderRadius: BorderRadius.circular(999),
),
child: Text(p.verified ? "Verified" : "Not verified",
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: p.verified ? DriverVault.success : DriverVault.warning)),
),
]),
if (!p.verified)
Align(
alignment: Alignment.centerLeft,
child: TextButton(
onPressed: _verifying ? null : _sendVerification,
child: Text(_verifying ? "Sending…" : "Resend verification email"),
),
),
const Divider(height: 24),
const Text("Change password", style: TextStyle(fontWeight: FontWeight.w500)),
const SizedBox(height: 8),
TextField(
controller: _oldPw,
obscureText: true,
decoration: const InputDecoration(
labelText: "Current password", border: OutlineInputBorder(), isDense: true),
),
const SizedBox(height: 8),
TextField(
controller: _newPw,
obscureText: true,
decoration: const InputDecoration(
labelText: "New password (min 8)", border: OutlineInputBorder(), isDense: true),
),
const SizedBox(height: 8),
TextField(
controller: _confirmPw,
obscureText: true,
decoration: const InputDecoration(
labelText: "Confirm new password", border: OutlineInputBorder(), isDense: true),
),
if (_pwError != null)
Padding(
padding: const EdgeInsets.only(top: 6),
child: Text(_pwError!, style: const TextStyle(color: DriverVault.danger, fontSize: 13)),
),
const SizedBox(height: 8),
Align(
alignment: Alignment.centerLeft,
child: OutlinedButton(
onPressed: _savingPw ? null : _savePassword,
child: Text(_savingPw ? "Updating…" : "Update password"),
),
),
],
);
}
}
// --- Appearance ------------------------------------------------------------
class _AppearanceSection extends StatefulWidget {
final void Function(String) onError;
const _AppearanceSection({required this.onError});
@override
State<_AppearanceSection> createState() => _AppearanceSectionState();
}
class _AppearanceSectionState extends State<_AppearanceSection> {
Future<void> _save(Map<String, dynamic> patch) async {
// Optimistic: apply locally first, roll back on failure.
final prev = {
"theme": appSettings.theme,
"locale": appSettings.locale,
"dateFormat": appSettings.dateFormat,
"fontSize": appSettings.fontSize,
};
appSettings.patch(
theme: patch["theme"],
locale: patch["locale"],
dateFormat: patch["dateFormat"],
fontSize: patch["fontSize"],
);
setState(() {});
try {
await apiClient.updateMe(patch);
} catch (e) {
appSettings.patch(
theme: prev["theme"],
locale: prev["locale"],
dateFormat: prev["dateFormat"],
fontSize: prev["fontSize"],
);
setState(() {});
widget.onError("$e");
}
}
@override
Widget build(BuildContext context) {
return _Card(
title: "Appearance",
children: [
const Text("Theme", style: TextStyle(fontWeight: FontWeight.w500)),
const SizedBox(height: 6),
SegmentedButton<String>(
segments: const [
ButtonSegment(value: "light", label: Text("Light")),
ButtonSegment(value: "dark", label: Text("Dark")),
ButtonSegment(value: "system", label: Text("System")),
],
selected: {appSettings.theme},
onSelectionChanged: (s) => _save({"theme": s.first}),
),
const SizedBox(height: 16),
const Text("Language & region", style: TextStyle(fontWeight: FontWeight.w500)),
const SizedBox(height: 6),
DropdownButtonFormField<String>(
initialValue: appSettings.locale,
decoration: const InputDecoration(border: OutlineInputBorder(), isDense: true),
items: const [
DropdownMenuItem(value: "en-US", child: Text("English (US)")),
DropdownMenuItem(value: "en-GB", child: Text("English (UK)")),
DropdownMenuItem(value: "pl-PL", child: Text("Polski")),
DropdownMenuItem(value: "de-DE", child: Text("Deutsch")),
DropdownMenuItem(value: "fr-FR", child: Text("Français")),
DropdownMenuItem(value: "es-ES", child: Text("Español")),
],
onChanged: (v) => v == null ? null : _save({"locale": v}),
),
const SizedBox(height: 16),
const Text("Date format", style: TextStyle(fontWeight: FontWeight.w500)),
const SizedBox(height: 6),
DropdownButtonFormField<String>(
initialValue: appSettings.dateFormat,
decoration: const InputDecoration(border: OutlineInputBorder(), isDense: true),
items: const [
DropdownMenuItem(value: "YMD", child: Text("YYYY-MM-DD")),
DropdownMenuItem(value: "DMY_NUM", child: Text("DD-MM-YYYY")),
DropdownMenuItem(value: "DMY", child: Text("DD Mon YYYY")),
DropdownMenuItem(value: "MDY", child: Text("Mon DD, YYYY")),
],
onChanged: (v) => v == null ? null : _save({"dateFormat": v}),
),
Padding(
padding: const EdgeInsets.only(top: 4),
child: Text("Example: ${formatDate(DateTime.now())}",
style: const TextStyle(color: Colors.grey, fontSize: 12)),
),
const SizedBox(height: 16),
const Text("Font size", style: TextStyle(fontWeight: FontWeight.w500)),
const SizedBox(height: 6),
SegmentedButton<String>(
segments: const [
ButtonSegment(value: "small", label: Text("Small")),
ButtonSegment(value: "medium", label: Text("Medium")),
ButtonSegment(value: "large", label: Text("Large")),
],
selected: {appSettings.fontSize},
onSelectionChanged: (s) => _save({"fontSize": s.first}),
),
],
);
}
}
// --- Profile (avatar + bio) ------------------------------------------------
class _ProfileSection extends StatefulWidget {
final UserProfile profile;
final TextEditingController bioController;
final Uint8List? avatar;
final Future<void> Function() onChanged;
final void Function(String) snack;
const _ProfileSection({
required this.profile,
required this.bioController,
required this.avatar,
required this.onChanged,
required this.snack,
});
@override
State<_ProfileSection> createState() => _ProfileSectionState();
}
class _ProfileSectionState extends State<_ProfileSection> {
bool _avatarBusy = false;
bool _savingBio = false;
Future<void> _pickAvatar() async {
final picker = ImagePicker();
final file = await picker.pickImage(source: ImageSource.gallery, maxWidth: 1024);
if (file == null) return;
setState(() => _avatarBusy = true);
try {
final bytes = await file.readAsBytes();
await apiClient.uploadAvatar(bytes, file.name);
await widget.onChanged();
} catch (e) {
widget.snack("$e");
} finally {
if (mounted) setState(() => _avatarBusy = false);
}
}
Future<void> _removeAvatar() async {
setState(() => _avatarBusy = true);
try {
await apiClient.deleteAvatar();
await widget.onChanged();
} catch (e) {
widget.snack("$e");
} finally {
if (mounted) setState(() => _avatarBusy = false);
}
}
Future<void> _saveBio() async {
setState(() => _savingBio = true);
try {
await apiClient.updateMe({"bio": widget.bioController.text});
widget.snack("Bio saved.");
} catch (e) {
widget.snack("$e");
} finally {
if (mounted) setState(() => _savingBio = false);
}
}
@override
Widget build(BuildContext context) {
final p = widget.profile;
final initial = (p.name.isNotEmpty ? p.name : p.email).characters.first.toUpperCase();
return _Card(
title: "Profile",
children: [
Row(children: [
CircleAvatar(
radius: 32,
backgroundColor: DriverVault.brandTint(context),
backgroundImage: widget.avatar != null ? MemoryImage(widget.avatar!) : null,
child: widget.avatar == null
? Text(initial,
style: TextStyle(
fontSize: 22, fontWeight: FontWeight.w700, color: DriverVault.brandOnTint(context)))
: null,
),
const SizedBox(width: 16),
Wrap(spacing: 8, children: [
OutlinedButton(
onPressed: _avatarBusy ? null : _pickAvatar,
child: Text(_avatarBusy ? "Working…" : "Upload photo"),
),
if (p.hasAvatar)
OutlinedButton(
onPressed: _avatarBusy ? null : _removeAvatar,
child: const Text("Remove"),
),
]),
]),
const SizedBox(height: 16),
const Text("Bio", style: TextStyle(fontWeight: FontWeight.w500)),
const SizedBox(height: 4),
TextField(
controller: widget.bioController,
maxLines: 3,
decoration: const InputDecoration(
border: OutlineInputBorder(),
hintText: "A short note visible to other people in your household.",
),
),
const SizedBox(height: 8),
Align(
alignment: Alignment.centerLeft,
child: OutlinedButton(
onPressed: _savingBio ? null : _saveBio,
child: Text(_savingBio ? "Saving…" : "Save bio"),
),
),
],
);
}
}
// --- Security (biometric sign-in) ------------------------------------------
class _SecuritySection extends StatefulWidget {
final String email;
final void Function(String) snack;
const _SecuritySection({required this.email, required this.snack});
@override
State<_SecuritySection> createState() => _SecuritySectionState();
}
class _SecuritySectionState extends State<_SecuritySection> {
bool _available = false;
bool _enabled = false;
bool _hasFace = false;
bool _hasFingerprint = false;
bool _busy = false;
bool _loaded = false;
@override
void initState() {
super.initState();
_refresh();
}
Future<void> _refresh() async {
final available = await biometricAuth.isAvailable();
final enabled = await biometricAuth.isEnabled();
final face = available && await biometricAuth.hasFace();
final finger = available && await biometricAuth.hasFingerprint();
if (!mounted) return;
setState(() {
_available = available;
_enabled = enabled;
_hasFace = face;
_hasFingerprint = finger;
_loaded = true;
});
}
String get _methodLabel {
if (_hasFace && _hasFingerprint) return "face or fingerprint";
if (_hasFace) return "face recognition";
if (_hasFingerprint) return "fingerprint";
return "biometrics";
}
Future<void> _toggle(bool value) async {
if (_busy) return;
if (!value) {
setState(() => _busy = true);
await biometricAuth.disable();
if (mounted) setState(() { _enabled = false; _busy = false; });
widget.snack("Biometric sign-in turned off");
return;
}
// Enabling requires re-confirming the password so we store known-good creds.
final pw = await _promptPassword();
if (pw == null || pw.isEmpty) return;
setState(() => _busy = true);
try {
// Verify the password (and refresh the session) before storing it.
await authService.login(widget.email, pw);
await biometricAuth.enable(widget.email, pw);
if (mounted) setState(() { _enabled = true; _busy = false; });
widget.snack("Biometric sign-in enabled");
} catch (e) {
if (mounted) setState(() => _busy = false);
widget.snack("Could not enable: ${e.toString()}");
}
}
Future<String?> _promptPassword() {
final ctrl = TextEditingController();
return showDialog<String>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text("Confirm your password"),
content: TextField(
controller: ctrl,
obscureText: true,
autofocus: true,
decoration: const InputDecoration(
labelText: "Password",
border: OutlineInputBorder(),
),
onSubmitted: (v) => Navigator.pop(ctx, v),
),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx), child: const Text("Cancel")),
FilledButton(onPressed: () => Navigator.pop(ctx, ctrl.text), child: const Text("Confirm")),
],
),
);
}
@override
Widget build(BuildContext context) {
return _Card(
title: "Security",
children: [
if (!_loaded)
const Padding(
padding: EdgeInsets.symmetric(vertical: 8),
child: Text("Checking device…", style: TextStyle(color: Colors.grey)),
)
else ...[
SwitchListTile(
contentPadding: EdgeInsets.zero,
title: const Text("Biometric sign-in"),
subtitle: Text(_available
? "Sign in with $_methodLabel instead of your password."
: "No biometrics are enrolled on this device."),
value: _enabled,
onChanged: (!_available || _busy) ? null : _toggle,
),
],
],
);
}
}
// --- Privacy & security (sessions) -----------------------------------------
class _SessionsSection extends StatefulWidget {
final List<Session> sessions;
final Future<void> Function() onChanged;
final void Function(String) snack;
const _SessionsSection({required this.sessions, required this.onChanged, required this.snack});
@override
State<_SessionsSection> createState() => _SessionsSectionState();
}
class _SessionsSectionState extends State<_SessionsSection> {
Future<void> _revoke(Session s) async {
try {
await apiClient.revokeSession(s.id);
if (s.current) {
await authService.logout();
return;
}
await widget.onChanged();
} catch (e) {
widget.snack("$e");
}
}
Future<void> _revokeOthers() async {
try {
await apiClient.revokeOtherSessions();
await widget.onChanged();
} catch (e) {
widget.snack("$e");
}
}
@override
Widget build(BuildContext context) {
return _Card(
title: "Privacy & security",
children: [
const Text(
"Two-factor authentication isn't available yet. Active sessions below reflect every device currently signed in.",
style: TextStyle(color: Colors.grey, fontSize: 13),
),
if (widget.sessions.length > 1)
Align(
alignment: Alignment.centerLeft,
child: TextButton(
onPressed: _revokeOthers,
child: const Text("Log out all other devices"),
),
),
...widget.sessions.map((s) => Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(children: [
Flexible(
child: Text(s.deviceLabel,
style: const TextStyle(fontWeight: FontWeight.w500))),
if (s.current)
Container(
margin: const EdgeInsets.only(left: 6),
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: DriverVault.brandTint(context),
borderRadius: BorderRadius.circular(999),
),
child: Text("This device",
style: TextStyle(fontSize: 11, color: DriverVault.brandOnTint(context))),
),
]),
Text("${s.ip} · signed in ${formatDate(s.created)}",
style: const TextStyle(color: Colors.grey, fontSize: 12)),
],
),
),
TextButton(
onPressed: () => _revoke(s),
child: const Text("Log out", style: TextStyle(color: DriverVault.danger)),
),
],
),
)),
],
);
}
}
// --- Danger zone (account deletion) ----------------------------------------
class _DangerSection extends StatefulWidget {
final UserProfile profile;
final Future<void> Function() onChanged;
final void Function(String) snack;
const _DangerSection({required this.profile, required this.onChanged, required this.snack});
@override
State<_DangerSection> createState() => _DangerSectionState();
}
class _DangerSectionState extends State<_DangerSection> {
final _confirmEmail = TextEditingController();
bool _showConfirm = false;
bool _busy = false;
DateTime? _eligibleAt;
@override
void dispose() {
_confirmEmail.dispose();
super.dispose();
}
bool get _pending => widget.profile.deletionPending;
DateTime? get _eligible {
if (!_pending) return null;
return _eligibleAt ??
widget.profile.deletionRequestedAt!.add(const Duration(days: 3));
}
bool get _cooldownElapsed => _eligible != null && DateTime.now().isAfter(_eligible!);
Future<void> _request() async {
if (_confirmEmail.text.trim().toLowerCase() != widget.profile.email.toLowerCase()) {
widget.snack("Type your email to confirm.");
return;
}
setState(() => _busy = true);
try {
_eligibleAt = await apiClient.requestAccountDeletion(_confirmEmail.text.trim());
_showConfirm = false;
_confirmEmail.clear();
await widget.onChanged();
} catch (e) {
widget.snack("$e");
} finally {
if (mounted) setState(() => _busy = false);
}
}
Future<void> _cancel() async {
try {
await apiClient.cancelAccountDeletion();
_eligibleAt = null;
await widget.onChanged();
} catch (e) {
widget.snack("$e");
}
}
Future<void> _finalize() async {
final ok = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text("Delete account?"),
content: const Text("This permanently deletes your account. This cannot be undone."),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text("Cancel")),
FilledButton(
style: FilledButton.styleFrom(backgroundColor: DriverVault.danger),
onPressed: () => Navigator.pop(ctx, true),
child: const Text("Delete"),
),
],
),
);
if (ok != true) return;
try {
await apiClient.finalizeAccountDeletion();
await authService.logout();
} catch (e) {
widget.snack("$e");
}
}
@override
Widget build(BuildContext context) {
final p = widget.profile;
return _Card(
title: "Danger zone",
titleColor: DriverVault.danger,
children: [
if (!_pending) ...[
const Text(
"Deleting your account removes your login and profile. It does not delete your household's shared cars or service history. There's a 3-day cooldown before deletion is final, and you can cancel any time before then.",
style: TextStyle(fontSize: 13),
),
const SizedBox(height: 8),
if (!_showConfirm)
OutlinedButton(
onPressed: () => setState(() => _showConfirm = true),
style: OutlinedButton.styleFrom(foregroundColor: DriverVault.danger),
child: const Text("Delete my account"),
)
else ...[
Text("Type ${p.email} to confirm",
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500)),
const SizedBox(height: 6),
TextField(
controller: _confirmEmail,
decoration: InputDecoration(
hintText: p.email, border: const OutlineInputBorder(), isDense: true),
),
const SizedBox(height: 8),
Row(children: [
TextButton(
onPressed: () => setState(() {
_showConfirm = false;
_confirmEmail.clear();
}),
child: const Text("Cancel"),
),
const SizedBox(width: 8),
FilledButton(
style: FilledButton.styleFrom(backgroundColor: DriverVault.danger),
onPressed: _busy ? null : _request,
child: Text(_busy ? "Requesting…" : "Request deletion"),
),
]),
],
] else ...[
Text(
"Account deletion requested on ${formatDate(p.deletionRequestedAt)}. "
"${_cooldownElapsed ? 'The cooldown has passed. You can now finalize the deletion.' : 'You can still cancel — it becomes permanent after the 3-day cooldown.'}",
style: const TextStyle(fontSize: 13),
),
const SizedBox(height: 8),
Row(children: [
OutlinedButton(onPressed: _cancel, child: const Text("Cancel deletion request")),
const SizedBox(width: 8),
if (_cooldownElapsed)
FilledButton(
style: FilledButton.styleFrom(backgroundColor: DriverVault.danger),
onPressed: _finalize,
child: const Text("Permanently delete"),
),
]),
],
],
);
}
}