CKT 1.0.2 | otorrinolaringologista

This commit is contained in:
Carlos Correia
2026-07-16 12:58:46 +01:00
parent a8af2c6845
commit f5884d8c36
12 changed files with 611 additions and 53 deletions

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import '../colors/app_colors.dart';
import '../main.dart' show supabase;
import '../strings/common_strings.dart';
import '../strings/settings_strings.dart';
import '../widgets/app_dialogs.dart';
import '../widgets/entrance.dart';
@@ -41,9 +42,29 @@ class _SettingsBodyState extends State<SettingsBody> {
);
if (confirmed != true) return;
if (!mounted) return;
final password = await _promptPassword(context);
if (password == null || password.isEmpty) return;
if (!mounted) return;
final email = (supabase.auth.currentUser?.email ?? '').trim();
if (email.isEmpty) return;
setState(() => _deletingAccount = true);
try {
// Reautentica com a palavra-passe introduzida antes de apagar nada —
// sem esta verificação, qualquer pessoa com o telemóvel desbloqueado
// (sessão já iniciada) conseguia apagar a conta sem confirmar que é
// mesmo o dono.
try {
await supabase.auth.signInWithPassword(email: email, password: password);
} catch (_) {
if (mounted) showPillSnackBar(context, SettingsStrings.wrongPassword);
return;
}
if (!mounted) return;
// A remoção de `auth.users` exige a service role key, que o app nunca
// deve carregar — por isso corre numa Edge Function (server-side); ver
// supabase/functions/delete-account. Sem isto, apagar só as linhas de
@@ -146,7 +167,7 @@ class _SettingsBodyState extends State<SettingsBody> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_SectionLabel(SettingsStrings.dangerZone),
_SectionLabel(SettingsStrings.userData),
_SettingsCard(
children: [
_ActionTile(
@@ -261,3 +282,91 @@ class _ActionTile extends StatelessWidget {
);
}
}
/// Pede a palavra-passe atual antes de uma ação irreversível (apagar dados
/// da conta) — devolve a palavra-passe introduzida, ou `null` se cancelado.
/// Só valida que o campo não está vazio; a palavra-passe em si é validada
/// depois via `signInWithPassword`, pelo chamador.
Future<String?> _promptPassword(BuildContext context) {
final controller = TextEditingController();
return showDialog<String>(
context: context,
builder: (ctx) {
var obscure = true;
String? errorText;
return StatefulBuilder(
builder: (ctx, setState) {
void submit() {
if (controller.text.isEmpty) {
setState(() => errorText = SettingsStrings.passwordRequired);
return;
}
Navigator.of(ctx).pop(controller.text);
}
return AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24),
),
title: const Text(
SettingsStrings.confirmPasswordTitle,
textAlign: TextAlign.center,
style: TextStyle(fontWeight: FontWeight.w900, color: _accentPink),
),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
SettingsStrings.confirmPasswordMessage,
textAlign: TextAlign.center,
style: TextStyle(color: Colors.black.withValues(alpha: 0.72)),
),
const SizedBox(height: 14),
TextField(
controller: controller,
obscureText: obscure,
autofocus: true,
onSubmitted: (_) => submit(),
decoration: InputDecoration(
labelText: SettingsStrings.password,
errorText: errorText,
suffixIcon: IconButton(
icon: Icon(
obscure
? Icons.visibility_off_rounded
: Icons.visibility_rounded,
),
onPressed: () => setState(() => obscure = !obscure),
),
),
),
],
),
actionsAlignment: MainAxisAlignment.center,
actions: [
TapBounce(
child: TextButton(
style: TextButton.styleFrom(foregroundColor: _teal),
onPressed: () => Navigator.of(ctx).pop(null),
child: const Text(CommonStrings.cancel),
),
),
TapBounce(
child: FilledButton(
style: FilledButton.styleFrom(
backgroundColor: _accentPink,
foregroundColor: Colors.white,
shape: const StadiumBorder(),
textStyle: const TextStyle(fontWeight: FontWeight.w800),
),
onPressed: submit,
child: const Text(SettingsStrings.confirm),
),
),
],
);
},
);
},
);
}