Atulização do quiz | Porcentagem na base de dados | Telas de Contrato com o usuario
This commit is contained in:
244
lib/screens/terms_gate_screen.dart
Normal file
244
lib/screens/terms_gate_screen.dart
Normal file
@@ -0,0 +1,244 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lottie/lottie.dart';
|
||||
|
||||
import '../main.dart' show supabase;
|
||||
import '../terms_gate_prefs.dart';
|
||||
import '../widgets/entrance.dart';
|
||||
import '../widgets/tap_bounce.dart';
|
||||
import '../widgets/terms_content.dart';
|
||||
|
||||
/// Ecrã de bloqueio mostrado logo após o cadastro (primeira vez), antes de
|
||||
/// entrar na app. Só avança para a Home se o utilizador aceitar os Termos;
|
||||
/// 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 TermsGateScreen extends StatefulWidget {
|
||||
const TermsGateScreen({
|
||||
super.key,
|
||||
required this.userId,
|
||||
required this.onAccepted,
|
||||
});
|
||||
|
||||
final String userId;
|
||||
final VoidCallback onAccepted;
|
||||
|
||||
@override
|
||||
State<TermsGateScreen> createState() => _TermsGateScreenState();
|
||||
}
|
||||
|
||||
class _TermsGateScreenState extends State<TermsGateScreen> {
|
||||
bool _accepted = false;
|
||||
bool _declining = false;
|
||||
|
||||
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 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) {
|
||||
final size = MediaQuery.sizeOf(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)),
|
||||
),
|
||||
Positioned(
|
||||
left: -size.width * 0.38,
|
||||
bottom: -size.width * 0.38,
|
||||
child: IgnorePointer(
|
||||
child: SizedBox(
|
||||
width: size.width * 1.05,
|
||||
height: size.width * 1.05,
|
||||
child: Transform.rotate(
|
||||
angle: 35 * math.pi / 180,
|
||||
child: Opacity(
|
||||
opacity: 0.95,
|
||||
child: Lottie.asset(
|
||||
'lottie/Liquid waves.json',
|
||||
fit: BoxFit.cover,
|
||||
repeat: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: IconButton(
|
||||
onPressed: _declining ? null : _decline,
|
||||
icon: const Icon(Icons.close_rounded),
|
||||
color: Colors.black54,
|
||||
),
|
||||
),
|
||||
// O checkbox e o botão ficam fixos, fora do
|
||||
// SingleChildScrollView — só o texto dos termos rola. Além
|
||||
// de ser o padrão comum em ecrãs de Termos (ação sempre
|
||||
// visível, sem precisar de chegar ao fundo do texto), evita
|
||||
// que a ação de aceitar dependa de o scroll estar
|
||||
// completamente parado.
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.fromLTRB(24, 4, 24, 4),
|
||||
child: Column(
|
||||
children: [
|
||||
const FadeSlideIn(child: TermsHeader()),
|
||||
const SizedBox(height: 22),
|
||||
FadeSlideIn(
|
||||
delay: const Duration(milliseconds: 80),
|
||||
child: const TermsBody(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 12, 24, 20),
|
||||
child: Column(
|
||||
children: [
|
||||
_AcceptCheckboxRow(
|
||||
accepted: _accepted,
|
||||
onChanged: _declining
|
||||
? null
|
||||
: (v) => setState(() => _accepted = v),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
_AdvanceButton(
|
||||
enabled: _accepted && !_declining,
|
||||
onPressed: widget.onAccepted,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_declining)
|
||||
Positioned.fill(
|
||||
child: ColoredBox(
|
||||
color: Colors.black.withValues(alpha: 0.12),
|
||||
child: const Center(
|
||||
child: CircularProgressIndicator(color: kTermsPink),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AcceptCheckboxRow extends StatelessWidget {
|
||||
const _AcceptCheckboxRow({required this.accepted, required this.onChanged});
|
||||
|
||||
final bool accepted;
|
||||
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!(!accepted),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 4),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
// IgnorePointer: o Checkbox tem um tap target mínimo de
|
||||
// 48x48 que, espremido neste SizedBox de 24x24 dentro do
|
||||
// InkWell da linha toda, competia na arena de gestos e
|
||||
// acabava por engolir o toque sem disparar nada. Aqui serve
|
||||
// só para o visual (marcado/desmarcado); quem trata o toque
|
||||
// é sempre o InkWell à volta de toda a linha.
|
||||
child: IgnorePointer(
|
||||
child: Checkbox(
|
||||
value: accepted,
|
||||
onChanged: onChanged == null ? null : (_) {},
|
||||
activeColor: kTermsPink,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
const Text(
|
||||
'Aceitar tudo',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 15,
|
||||
color: kTermsPink,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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: kTermsPink,
|
||||
disabledBackgroundColor: kTermsPink.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'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user