470 lines
15 KiB
Dart
470 lines
15 KiB
Dart
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,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|