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'; import '../widgets/pill_snackbar.dart'; import '../widgets/tap_bounce.dart'; import 'credits_screen.dart'; import 'terms_screen.dart'; const Color _teal = AppColors.teal; const Color _accentPink = AppColors.pink; /// Conteúdo da aba de Configurações, para ser embutido na bottom navigation /// do LoggedHomeScreen (sem Scaffold/AppBar próprios). class SettingsBody extends StatefulWidget { const SettingsBody({super.key, required this.onReplayTutorial}); /// Corre de novo o tutorial guiado da Home (ver [showCoachMarkTour]). final VoidCallback onReplayTutorial; @override State createState() => _SettingsBodyState(); } class _SettingsBodyState extends State { bool _deletingAccount = false; Future _signOut() async { await supabase.auth.signOut(); if (!mounted) return; Navigator.of(context).popUntil((route) => route.isFirst); } Future _confirmDeleteAccountData() async { final confirmed = await showConfirmDialog( context, title: SettingsStrings.deleteAccountData, message: SettingsStrings.deleteAccountMessage, confirmLabel: SettingsStrings.delete, confirmColor: _accentPink, ); 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 // `profiles`/`children` deixava o e-mail "ocupado" no Supabase Auth, // impedindo criar uma nova conta com o mesmo e-mail. final response = await supabase.functions.invoke('delete-account'); final data = response.data; final errorMessage = (data is Map) ? data['error']?.toString() : null; if (response.status != 200 || errorMessage != null) { throw StateError( errorMessage ?? SettingsStrings.errorDeletingAccount(response.status), ); } await supabase.auth.signOut(); if (!mounted) return; Navigator.of(context).popUntil((route) => route.isFirst); } catch (e) { if (mounted) showPillSnackBar(context, SettingsStrings.errorDeleting(e)); } finally { if (mounted) setState(() => _deletingAccount = false); } } @override Widget build(BuildContext context) { final user = supabase.auth.currentUser; final name = (user?.userMetadata?['name'] ?? '').toString().trim(); final email = (user?.email ?? '').trim(); return ListView( padding: const EdgeInsets.all(16), children: [ FadeSlideIn( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _SectionLabel(SettingsStrings.account), _SettingsCard( children: [ _InfoTile( icon: Icons.person_outline_rounded, title: name.isEmpty ? SettingsStrings.noName : name, subtitle: email, ), const Divider(height: 1), _ActionTile( icon: Icons.logout_rounded, title: SettingsStrings.signOut, onTap: _signOut, ), ], ), ], ), ), const SizedBox(height: 20), FadeSlideIn( delay: const Duration(milliseconds: 80), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _SectionLabel(SettingsStrings.about), _SettingsCard( children: [ _ActionTile( icon: Icons.school_outlined, title: SettingsStrings.replayTutorial, onTap: widget.onReplayTutorial, ), const Divider(height: 1), _ActionTile( icon: Icons.description_outlined, title: SettingsStrings.termsOfService, onTap: () => Navigator.of(context).push( MaterialPageRoute( builder: (_) => const TermsScreen(), ), ), ), const Divider(height: 1), _ActionTile( icon: Icons.diversity_3_outlined, title: SettingsStrings.creatorsAndContributors, onTap: () => Navigator.of(context).push( MaterialPageRoute( builder: (_) => const CreditsScreen(), ), ), ), const Divider(height: 1), const _InfoTile( icon: Icons.info_outline_rounded, title: SettingsStrings.appVersion, subtitle: '1.2.1', ), ], ), ], ), ), const SizedBox(height: 20), FadeSlideIn( delay: const Duration(milliseconds: 160), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _SectionLabel(SettingsStrings.userData), _SettingsCard( children: [ _ActionTile( icon: Icons.delete_forever_rounded, title: SettingsStrings.deleteAccountData, titleColor: _accentPink, loading: _deletingAccount, onTap: _deletingAccount ? null : _confirmDeleteAccountData, ), ], ), ], ), ), const SizedBox(height: 12), ], ); } } class _SectionLabel extends StatelessWidget { const _SectionLabel(this.text); final String text; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(left: 4, bottom: 8), child: Text( text, style: const TextStyle( color: _teal, fontWeight: FontWeight.w900, fontSize: 14, ), ), ); } } class _SettingsCard extends StatelessWidget { const _SettingsCard({required this.children}); final List children; @override Widget build(BuildContext context) { return Material( color: Colors.white, borderRadius: BorderRadius.circular(18), elevation: 6, shadowColor: Colors.black.withValues(alpha: 0.12), child: Column(children: children), ); } } class _InfoTile extends StatelessWidget { const _InfoTile({required this.icon, required this.title, this.subtitle}); final IconData icon; final String title; final String? subtitle; @override Widget build(BuildContext context) { return ListTile( leading: Icon(icon, color: _teal), title: Text(title, style: const TextStyle(fontWeight: FontWeight.w800)), subtitle: (subtitle == null || subtitle!.isEmpty) ? null : Text(subtitle!), ); } } class _ActionTile extends StatelessWidget { const _ActionTile({ required this.icon, required this.title, required this.onTap, this.titleColor, this.loading = false, }); final IconData icon; final String title; final VoidCallback? onTap; final Color? titleColor; final bool loading; @override Widget build(BuildContext context) { return TapBounce( scale: 0.98, child: ListTile( leading: Icon(icon, color: titleColor ?? _teal), title: Text( title, style: TextStyle(fontWeight: FontWeight.w800, color: titleColor), ), trailing: loading ? const SizedBox( width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.chevron_right_rounded), onTap: onTap, ), ); } } /// 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 _promptPassword(BuildContext context) { final controller = TextEditingController(); return showDialog( 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), ), ), ], ); }, ); }, ); }