311 lines
10 KiB
Dart
311 lines
10 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import '../main.dart' show supabase;
|
|
import '../privacy_gate_prefs.dart';
|
|
import '../terms_gate_prefs.dart';
|
|
import '../widgets/entrance.dart';
|
|
import '../widgets/liquid_waves_background.dart';
|
|
import '../widgets/tap_bounce.dart';
|
|
import '../widgets/privacy_content.dart';
|
|
|
|
/// Ecrã de bloqueio mostrado logo após o cadastro (primeira vez), depois
|
|
/// de aceites os Termos ([TermsGateScreen]) e antes de entrar na app. Só
|
|
/// avança para a Home se o utilizador consentir os itens obrigatórios de
|
|
/// privacidade (o terceiro item é opcional); se recusar (botão ou seta de
|
|
/// voltar), a conta acabada de criar é removida (perfil apagado + sessão
|
|
/// terminada) e volta-se ao login.
|
|
class PrivacyGateScreen extends StatefulWidget {
|
|
const PrivacyGateScreen({
|
|
super.key,
|
|
required this.userId,
|
|
required this.onAccepted,
|
|
});
|
|
|
|
final String userId;
|
|
final VoidCallback onAccepted;
|
|
|
|
@override
|
|
State<PrivacyGateScreen> createState() => _PrivacyGateScreenState();
|
|
}
|
|
|
|
class _PrivacyGateScreenState extends State<PrivacyGateScreen> {
|
|
final Set<String> _accepted = {};
|
|
bool _declining = false;
|
|
|
|
bool get _allAccepted => kPrivacyConsentItems
|
|
.where((item) => item.required)
|
|
.every((item) => _accepted.contains(item.id));
|
|
|
|
void _toggle(String id, bool value) {
|
|
setState(() {
|
|
if (value) {
|
|
_accepted.add(id);
|
|
} else {
|
|
_accepted.remove(id);
|
|
}
|
|
});
|
|
}
|
|
|
|
void _acceptAll() {
|
|
setState(() {
|
|
_accepted.addAll(kPrivacyConsentItems.map((e) => e.id));
|
|
});
|
|
}
|
|
|
|
Future<void> _decline() async {
|
|
if (_declining) return;
|
|
setState(() => _declining = true);
|
|
try {
|
|
await supabase
|
|
.from('profiles')
|
|
.delete()
|
|
.eq('id', widget.userId)
|
|
.timeout(const Duration(seconds: 20));
|
|
} catch (_) {
|
|
// Mesmo que a limpeza do perfil falhe, termina a sessão de qualquer
|
|
// forma — o AuthGate trata contas sem perfil como inexistentes.
|
|
}
|
|
await PrivacyGatePrefs.clearPendingUid();
|
|
// A conta vai ser apagada — não faz sentido deixar o utilizador cair
|
|
// no ecrã de Termos logo a seguir com uma sessão já sem perfil.
|
|
await TermsGatePrefs.clearPendingUid();
|
|
await supabase.auth.signOut();
|
|
// Não faz setState depois disto: o widget é removido da árvore assim
|
|
// que o AuthGate reage à sessão terminada.
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return PopScope(
|
|
canPop: false,
|
|
onPopInvokedWithResult: (didPop, _) {
|
|
if (!didPop) _decline();
|
|
},
|
|
child: Scaffold(
|
|
body: Stack(
|
|
clipBehavior: Clip.none,
|
|
children: [
|
|
Positioned.fill(child: Container(color: const Color(0xFFFAFAF7))),
|
|
const LiquidWavesBackground(),
|
|
SafeArea(
|
|
child: Column(
|
|
children: [
|
|
Align(
|
|
alignment: Alignment.topLeft,
|
|
child: IconButton(
|
|
onPressed: _declining ? null : _decline,
|
|
icon: const Icon(Icons.close_rounded),
|
|
color: Colors.black54,
|
|
),
|
|
),
|
|
// Só o cabeçalho e a lista de itens rolam — os botões
|
|
// ficam fixos, fora do SingleChildScrollView, tal como
|
|
// no TermsGateScreen.
|
|
Expanded(
|
|
child: SingleChildScrollView(
|
|
padding: const EdgeInsets.fromLTRB(24, 4, 24, 4),
|
|
child: Column(
|
|
children: [
|
|
const FadeSlideIn(child: PrivacyHeader()),
|
|
const SizedBox(height: 22),
|
|
FadeSlideIn(
|
|
delay: const Duration(milliseconds: 80),
|
|
child: _PrivacyConsentList(
|
|
accepted: _accepted,
|
|
onChanged: _declining ? null : _toggle,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.fromLTRB(24, 12, 24, 20),
|
|
child: Column(
|
|
children: [
|
|
TapBounce(
|
|
child: TextButton(
|
|
onPressed: _declining ? null : _acceptAll,
|
|
child: const Text(
|
|
'Aceitar tudo',
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.w800,
|
|
fontSize: 15,
|
|
color: kPrivacyTeal,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
_AdvanceButton(
|
|
enabled: _allAccepted && !_declining,
|
|
onPressed: widget.onAccepted,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
if (_declining)
|
|
Positioned.fill(
|
|
child: ColoredBox(
|
|
color: Colors.black.withValues(alpha: 0.12),
|
|
child: const Center(
|
|
child: CircularProgressIndicator(color: kPrivacyTeal),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _PrivacyConsentList extends StatelessWidget {
|
|
const _PrivacyConsentList({required this.accepted, required this.onChanged});
|
|
|
|
final Set<String> accepted;
|
|
final void Function(String id, bool value)? onChanged;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Column(
|
|
children: [
|
|
for (var i = 0; i < kPrivacyConsentItems.length; i++) ...[
|
|
if (i > 0) const SizedBox(height: 14),
|
|
_PrivacyConsentRow(
|
|
item: kPrivacyConsentItems[i],
|
|
checked: accepted.contains(kPrivacyConsentItems[i].id),
|
|
onChanged: onChanged == null
|
|
? null
|
|
: (v) => onChanged!(kPrivacyConsentItems[i].id, v),
|
|
),
|
|
],
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class _PrivacyConsentRow extends StatelessWidget {
|
|
const _PrivacyConsentRow({
|
|
required this.item,
|
|
required this.checked,
|
|
required this.onChanged,
|
|
});
|
|
|
|
final PrivacyConsentItem item;
|
|
final bool checked;
|
|
final ValueChanged<bool>? onChanged;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return TapBounce(
|
|
scale: 0.98,
|
|
child: InkWell(
|
|
borderRadius: BorderRadius.circular(14),
|
|
onTap: onChanged == null ? null : () => onChanged!(!checked),
|
|
child: Container(
|
|
padding: const EdgeInsets.all(14),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white.withValues(alpha: 0.9),
|
|
borderRadius: BorderRadius.circular(16),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withValues(alpha: 0.05),
|
|
blurRadius: 14,
|
|
offset: const Offset(0, 6),
|
|
),
|
|
],
|
|
),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
SizedBox(
|
|
width: 24,
|
|
height: 24,
|
|
// IgnorePointer: o Checkbox tem um tap target mínimo de
|
|
// 48x48 que competia na arena de gestos com o InkWell da
|
|
// linha toda — o mesmo problema já resolvido no
|
|
// TermsGateScreen. Aqui serve só para o visual; quem trata
|
|
// o toque é sempre o InkWell à volta de toda a linha.
|
|
child: IgnorePointer(
|
|
child: Checkbox(
|
|
value: checked,
|
|
onChanged: onChanged == null ? null : (_) {},
|
|
activeColor: kPrivacyTeal,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(6),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Text.rich(
|
|
TextSpan(
|
|
children: [
|
|
TextSpan(
|
|
text: item.required
|
|
? '(Obrigatório) '
|
|
: '(Opcional) ',
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.w900,
|
|
color: item.required
|
|
? kPrivacyTeal
|
|
: Colors.black.withValues(alpha: 0.45),
|
|
),
|
|
),
|
|
TextSpan(text: item.text),
|
|
],
|
|
),
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w700,
|
|
height: 1.45,
|
|
color: Colors.black.withValues(alpha: 0.8),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _AdvanceButton extends StatelessWidget {
|
|
const _AdvanceButton({required this.enabled, required this.onPressed});
|
|
|
|
final bool enabled;
|
|
final VoidCallback onPressed;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return TapBounce(
|
|
child: SizedBox(
|
|
width: double.infinity,
|
|
height: 52,
|
|
child: FilledButton(
|
|
style: FilledButton.styleFrom(
|
|
backgroundColor: kPrivacyTeal,
|
|
disabledBackgroundColor: kPrivacyTeal.withValues(alpha: 0.35),
|
|
foregroundColor: Colors.white,
|
|
shape: const StadiumBorder(),
|
|
textStyle: const TextStyle(
|
|
fontWeight: FontWeight.w800,
|
|
fontSize: 16,
|
|
),
|
|
),
|
|
onPressed: enabled ? onPressed : null,
|
|
child: const Text('Avançar'),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|