FunyMeeAI/lib/features/profile/delete_account_flow.dart

599 lines
19 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import '../../design/pencil_theme.dart';
/// 设计:`desgin/funymee_home.pen`「注销账户 · 步骤1」「注销账户 · 步骤2 二次验证」。
/// 返回 `true` 表示用户在第二步确认注销(可继续调接口)。
Future<bool> showDeleteAccountConfirmationFlow(BuildContext context) async {
while (true) {
final step1Ok = await showDialog<bool>(
context: context,
barrierDismissible: false,
barrierColor: _kScrim,
builder: (ctx) => const _DeleteAccountStep1Dialog(),
);
if (!context.mounted) return false;
if (step1Ok != true) return false;
final step2 = await showDialog<_DeleteStep2Result>(
context: context,
barrierDismissible: false,
barrierColor: _kScrim,
builder: (ctx) => const _DeleteAccountStep2Dialog(),
);
if (!context.mounted) return false;
if (step2 == _DeleteStep2Result.confirmed) return true;
if (step2 == _DeleteStep2Result.backToStep1) continue;
return false;
}
}
enum _DeleteStep2Result { backToStep1, confirmed }
// --- 设计 token与 .pen 一致)---
const _kModalWidth = 350.0;
const _kScrim = Color(0xB31C1917);
const _kModalBorder = Color(0xFFFECACA);
const _kDanger = Color(0xFFDC2626);
const _kDangerIconBg = Color(0xFFFEE2E2);
const _kTitle = PencilTheme.stone900;
const _kBody = Color(0xFF57534E);
const _kChkLabel = PencilTheme.stone700;
const _kCancelFill = Color(0xFFF5F5F4);
const _kCancelStroke = Color(0xFFE7E5E4);
const _kPillBg = Color(0xFFFEF3C7);
const _kPillInk = Color(0xFFB45309);
const _kShieldBg = Color(0xFFFFEDD5);
const _kShieldIcon = Color(0xFFC2410C);
const _kInputFill = Color(0xFFFAFAF9);
const _kInputStroke = Color(0xFFD6D3D1);
const _kHintInfo = Color(0xFF78716C);
class _DeleteAccountStep1Dialog extends StatefulWidget {
const _DeleteAccountStep1Dialog();
@override
State<_DeleteAccountStep1Dialog> createState() =>
_DeleteAccountStep1DialogState();
}
class _DeleteAccountStep1DialogState extends State<_DeleteAccountStep1Dialog> {
bool _ack = false;
@override
Widget build(BuildContext context) {
return Dialog(
backgroundColor: Colors.transparent,
insetPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: _kModalWidth),
child: Container(
padding: const EdgeInsets.all(22),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: _kModalBorder),
boxShadow: const [
BoxShadow(
color: Color(0x18000000),
blurRadius: 28,
offset: Offset(0, 12),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Center(
child: Container(
width: 52,
height: 52,
decoration: BoxDecoration(
color: _kDangerIconBg,
borderRadius: BorderRadius.circular(999),
),
child: const Icon(
Icons.warning_amber_rounded,
size: 28,
color: _kDanger,
),
),
),
const SizedBox(height: 16),
Text(
'Delete account',
textAlign: TextAlign.center,
style: GoogleFonts.inter(
fontSize: 20,
fontWeight: FontWeight.w700,
color: _kTitle,
),
),
const SizedBox(height: 16),
Text(
'This cannot be undone. Your account, creations, credits, and all related data will be permanently removed.',
textAlign: TextAlign.center,
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w500,
height: 1.45,
color: _kBody,
),
),
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 22,
height: 22,
child: Checkbox(
value: _ack,
onChanged: (v) =>
setState(() => _ack = v ?? false),
activeColor: _kDanger,
side: const BorderSide(color: _kDanger, width: 2),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(6),
),
materialTapTargetSize:
MaterialTapTargetSize.shrinkWrap,
),
),
const SizedBox(width: 10),
Expanded(
child: Text(
'I understand the consequences and accept the risk of losing my data.',
style: GoogleFonts.inter(
fontSize: 13,
fontWeight: FontWeight.w600,
height: 1.4,
color: _kChkLabel,
),
),
),
],
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: _DialogSecondaryButton(
label: 'Cancel',
onTap: () => Navigator.of(context).pop(false),
),
),
const SizedBox(width: 12),
Expanded(
child: _DialogPrimaryButton(
label: 'Continue',
enabled: _ack,
onTap: _ack
? () => Navigator.of(context).pop(true)
: null,
),
),
],
),
],
),
),
),
);
}
}
class _DeleteAccountStep2Dialog extends StatefulWidget {
const _DeleteAccountStep2Dialog();
@override
State<_DeleteAccountStep2Dialog> createState() =>
_DeleteAccountStep2DialogState();
}
class _DeleteAccountStep2DialogState extends State<_DeleteAccountStep2Dialog> {
static const _phrase = 'PERMANENTLY DELETE';
final _controller = TextEditingController();
@override
void initState() {
super.initState();
_controller.addListener(() => setState(() {}));
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
bool get _canConfirm => _controller.text == _phrase;
@override
Widget build(BuildContext context) {
return Dialog(
backgroundColor: Colors.transparent,
insetPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: _kModalWidth),
child: Container(
padding: const EdgeInsets.all(22),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: _kModalBorder),
boxShadow: const [
BoxShadow(
color: Color(0x18000000),
blurRadius: 28,
offset: Offset(0, 12),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Center(
child: Container(
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: _kPillBg,
borderRadius: BorderRadius.circular(999),
),
child: Text(
'Verification',
style: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.w700,
color: _kPillInk,
),
),
),
),
const SizedBox(height: 14),
Center(
child: Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: _kShieldBg,
borderRadius: BorderRadius.circular(14),
),
child: const Icon(
Icons.gpp_maybe_rounded,
size: 26,
color: _kShieldIcon,
),
),
),
const SizedBox(height: 14),
Text(
'Final confirmation',
textAlign: TextAlign.center,
style: GoogleFonts.inter(
fontSize: 19,
fontWeight: FontWeight.w700,
color: _kTitle,
),
),
const SizedBox(height: 14),
Text(
'Type PERMANENTLY DELETE below exactly as shown. Wrong text cannot be submitted.',
textAlign: TextAlign.center,
style: GoogleFonts.inter(
fontSize: 13,
fontWeight: FontWeight.w500,
height: 1.45,
color: _kBody,
),
),
const SizedBox(height: 14),
SizedBox(
height: 48,
child: TextField(
controller: _controller,
textAlignVertical: TextAlignVertical.center,
style: GoogleFonts.inter(
fontSize: 15,
fontWeight: FontWeight.w600,
color: _kTitle,
),
decoration: InputDecoration(
isDense: true,
contentPadding: const EdgeInsets.symmetric(
horizontal: 14,
vertical: 14,
),
filled: true,
fillColor: _kInputFill,
hintText: _phrase,
hintStyle: GoogleFonts.inter(
fontSize: 15,
fontWeight: FontWeight.w600,
color: _kBody.withValues(alpha: 0.45),
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: _kInputStroke),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: _kInputStroke),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: _kDanger, width: 1.5),
),
),
),
),
const SizedBox(height: 14),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Padding(
padding: EdgeInsets.only(top: 2),
child: Icon(
Icons.info_outline_rounded,
size: 16,
color: PencilTheme.underlineGold,
),
),
const SizedBox(width: 8),
Expanded(
child: Text(
'The button stays disabled until the phrase matches exactly.',
style: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.w500,
height: 1.35,
color: _kHintInfo,
),
),
),
],
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: _DialogSecondaryButton(
label: 'Back',
onTap: () => Navigator.of(context)
.pop(_DeleteStep2Result.backToStep1),
),
),
const SizedBox(width: 12),
Expanded(
child: _DialogPrimaryButton(
label: 'Delete account',
enabled: _canConfirm,
onTap: _canConfirm
? () => Navigator.of(context)
.pop(_DeleteStep2Result.confirmed)
: null,
),
),
],
),
],
),
),
),
);
}
}
class _DialogSecondaryButton extends StatelessWidget {
const _DialogSecondaryButton({
required this.label,
required this.onTap,
});
final String label;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return Material(
color: _kCancelFill,
borderRadius: BorderRadius.circular(14),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(14),
child: Container(
width: double.infinity,
height: 48,
alignment: Alignment.center,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(14),
border: Border.all(color: _kCancelStroke),
),
child: Text(
label,
style: GoogleFonts.inter(
fontSize: 15,
fontWeight: FontWeight.w600,
color: _kChkLabel,
),
),
),
),
);
}
}
class _DialogPrimaryButton extends StatelessWidget {
const _DialogPrimaryButton({
required this.label,
required this.enabled,
required this.onTap,
});
final String label;
final bool enabled;
final VoidCallback? onTap;
@override
Widget build(BuildContext context) {
return Material(
color: enabled ? _kDanger : _kDanger.withValues(alpha: 0.38),
borderRadius: BorderRadius.circular(14),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(14),
child: SizedBox(
width: double.infinity,
height: 48,
child: Center(
child: Text(
label,
style: GoogleFonts.inter(
fontSize: 15,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
),
),
);
}
}
/// 白底危险确认弹窗(与 [_DeleteAccountStep1Dialog] 同一套圆角、描边、阴影与底部双按钮)。
///
/// 返回 `true` 表示用户点击主按钮(默认「确定」)。
Future<bool> showPencilWhiteDangerConfirmDialog(
BuildContext context, {
required String title,
required String body,
String cancelLabel = 'Cancel',
String confirmLabel = 'Confirm',
IconData icon = Icons.delete_outline_rounded,
bool barrierDismissible = true,
}) async {
final r = await showDialog<bool>(
context: context,
barrierDismissible: barrierDismissible,
barrierColor: _kScrim,
builder: (ctx) => _PencilWhiteDangerConfirmBody(
title: title,
body: body,
cancelLabel: cancelLabel,
confirmLabel: confirmLabel,
icon: icon,
),
);
return r ?? false;
}
class _PencilWhiteDangerConfirmBody extends StatelessWidget {
const _PencilWhiteDangerConfirmBody({
required this.title,
required this.body,
required this.cancelLabel,
required this.confirmLabel,
required this.icon,
});
final String title;
final String body;
final String cancelLabel;
final String confirmLabel;
final IconData icon;
@override
Widget build(BuildContext context) {
return Dialog(
backgroundColor: Colors.transparent,
insetPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: _kModalWidth),
child: Container(
padding: const EdgeInsets.all(22),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: _kModalBorder),
boxShadow: const [
BoxShadow(
color: Color(0x18000000),
blurRadius: 28,
offset: Offset(0, 12),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Center(
child: Container(
width: 52,
height: 52,
decoration: BoxDecoration(
color: _kDangerIconBg,
borderRadius: BorderRadius.circular(999),
),
child: Icon(
icon,
size: 26,
color: _kDanger,
),
),
),
const SizedBox(height: 16),
Text(
title,
textAlign: TextAlign.center,
style: GoogleFonts.inter(
fontSize: 20,
fontWeight: FontWeight.w700,
color: _kTitle,
),
),
const SizedBox(height: 12),
Text(
body,
textAlign: TextAlign.center,
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w500,
height: 1.45,
color: _kBody,
),
),
const SizedBox(height: 22),
Row(
children: [
Expanded(
child: _DialogSecondaryButton(
label: cancelLabel,
onTap: () => Navigator.of(context).pop(false),
),
),
const SizedBox(width: 12),
Expanded(
child: _DialogPrimaryButton(
label: confirmLabel,
enabled: true,
onTap: () => Navigator.of(context).pop(true),
),
),
],
),
],
),
),
),
);
}
}