petsHero-AI/lib/features/profile/profile_screen.dart

599 lines
18 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_lucide/flutter_lucide.dart';
import 'package:cached_network_image/cached_network_image.dart';
import '../../core/api/api_client.dart';
import '../../core/auth/auth_token_store.dart';
import '../../core/api/api_config.dart';
import '../../core/api/services/user_api.dart';
import '../../core/user/account_refresh.dart';
import '../recharge/payment_webview_screen.dart';
import '../../core/theme/app_colors.dart';
import '../../core/user/user_state.dart';
import '../../core/theme/app_spacing.dart';
import '../../core/theme/app_typography.dart';
/// Profile screen - matches Pencil KXeow
class ProfileScreen extends StatefulWidget {
const ProfileScreen({super.key, required this.isActive});
final bool isActive;
@override
State<ProfileScreen> createState() => _ProfileScreenState();
}
class _ProfileScreenState extends State<ProfileScreen> {
@override
void initState() {
super.initState();
if (widget.isActive) refreshAccount(updateProfile: true);
}
@override
void didUpdateWidget(covariant ProfileScreen oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.isActive && !oldWidget.isActive) {
refreshAccount(updateProfile: true);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.background,
body: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(
AppSpacing.screenPadding,
AppSpacing.xxl,
AppSpacing.screenPadding,
AppSpacing.screenPaddingLarge,
),
child: Column(
children: [
ValueListenableBuilder<String?>(
valueListenable: UserState.avatar,
builder: (_, avatarUrl, __) {
return ValueListenableBuilder<String?>(
valueListenable: UserState.userName,
builder: (_, userName, __) {
return ValueListenableBuilder<String?>(
valueListenable: UserState.userId,
builder: (_, userId, __) {
return _ProfileHeader(
avatarUrl: avatarUrl,
userName: userName,
uid: userId,
);
},
);
},
);
},
),
const SizedBox(height: AppSpacing.xl),
_BalanceCard(
balance: UserCreditsData.of(context)?.creditsDisplay ?? '--',
onRecharge: () => Navigator.of(context).pushNamed('/recharge'),
),
const SizedBox(height: AppSpacing.xxl),
_MenuSection(
items: [
_MenuItem(
title: 'Privacy Policy',
icon: LucideIcons.chevron_right,
onTap: () => Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => const PaymentWebViewScreen(
paymentUrl: 'https://www.petsheroai.xyz/privacy.html',
title: 'Privacy Policy',
),
),
),
),
_MenuItem(
title: 'User Agreement',
icon: LucideIcons.chevron_right,
onTap: () => Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => const PaymentWebViewScreen(
paymentUrl: 'https://www.petsheroai.xyz/terms.html',
title: 'User Agreement',
),
),
),
),
_MenuItem(
title: 'Delete Account',
icon: LucideIcons.trash_2,
iconColor: const Color(0xFFDC2626),
onTap: () => _DeleteAccountDialog.show(context),
),
],
),
],
),
),
);
}
}
class _ProfileHeader extends StatelessWidget {
const _ProfileHeader({
this.avatarUrl,
this.userName,
this.uid,
});
final String? avatarUrl;
final String? userName;
final String? uid;
@override
Widget build(BuildContext context) {
return Container(
height: 220,
alignment: Alignment.center,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: AppColors.border,
borderRadius: BorderRadius.circular(40),
border: Border.all(
color: AppColors.primaryLight,
width: 2,
),
),
clipBehavior: Clip.antiAlias,
child: avatarUrl != null && avatarUrl!.isNotEmpty
? CachedNetworkImage(
imageUrl: avatarUrl!,
fit: BoxFit.cover,
placeholder: (_, __) => const Icon(
LucideIcons.user,
size: 40,
color: AppColors.textSecondary,
),
errorWidget: (_, __, ___) => const Icon(
LucideIcons.user,
size: 40,
color: AppColors.textSecondary,
),
)
: const Icon(
LucideIcons.user,
size: 40,
color: AppColors.textSecondary,
),
),
const SizedBox(height: AppSpacing.lg),
Text(
userName ?? 'VIP',
style: AppTypography.bodyLarge.copyWith(
color: AppColors.textPrimary,
),
),
const SizedBox(height: AppSpacing.lg),
Container(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.lg,
vertical: AppSpacing.sm,
),
decoration: BoxDecoration(
color: AppColors.surfaceAlt,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: AppColors.border),
),
child: Text(
uid != null && uid!.isNotEmpty ? 'UID $uid' : 'UID --',
style: AppTypography.caption.copyWith(
color: AppColors.textSecondary,
),
),
),
],
),
);
}
}
class _BalanceCard extends StatelessWidget {
const _BalanceCard({
required this.balance,
required this.onRecharge,
});
final String balance;
final VoidCallback onRecharge;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.xxl,
vertical: AppSpacing.xl,
),
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: BorderRadius.circular(16),
boxShadow: const [
BoxShadow(
color: AppColors.shadowLight,
blurRadius: 8,
offset: Offset(0, 2),
),
],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'AVAILABLE BALANCE',
style: AppTypography.label.copyWith(
color: AppColors.textSecondary,
),
),
const SizedBox(height: AppSpacing.sm),
Text(
balance,
style: AppTypography.bodyLarge.copyWith(
fontSize: 24,
fontWeight: FontWeight.w700,
color: AppColors.primary,
),
),
],
),
GestureDetector(
onTap: onRecharge,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 14,
vertical: AppSpacing.md,
),
decoration: BoxDecoration(
color: AppColors.primary,
borderRadius: BorderRadius.circular(12),
),
child: Text(
'Recharge',
style: AppTypography.bodyRegular.copyWith(
color: AppColors.surface,
fontWeight: FontWeight.w600,
),
),
),
),
],
),
);
}
}
class _MenuSection extends StatelessWidget {
const _MenuSection({required this.items});
final List<_MenuItem> items;
@override
Widget build(BuildContext context) {
return Column(
children: items
.map(
(item) => Padding(
padding: const EdgeInsets.only(bottom: AppSpacing.md),
child: _MenuItem(
title: item.title,
icon: item.icon,
onTap: item.onTap,
),
),
)
.toList(),
);
}
}
class _MenuItem extends StatelessWidget {
const _MenuItem({
required this.title,
required this.icon,
required this.onTap,
this.iconColor,
});
final String title;
final IconData icon;
final VoidCallback onTap;
final Color? iconColor;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.xl,
vertical: 14,
),
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: BorderRadius.circular(12),
boxShadow: const [
BoxShadow(
color: AppColors.shadowLight,
blurRadius: 6,
offset: Offset(0, 2),
),
],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
title,
style: AppTypography.bodyRegular.copyWith(
color: iconColor ?? AppColors.textPrimary,
),
),
Icon(icon, size: 20, color: iconColor ?? AppColors.textMuted),
],
),
),
);
}
}
/// Delete Account confirmation dialog - matches Pencil Xp6Qz
class _DeleteAccountDialog extends StatefulWidget {
const _DeleteAccountDialog({required this.parentContext});
final BuildContext parentContext;
static void show(BuildContext context) {
showDialog<void>(
context: context,
barrierColor: const Color(0x80000000),
builder: (_) => _DeleteAccountDialog(parentContext: context),
);
}
@override
State<_DeleteAccountDialog> createState() => _DeleteAccountDialogState();
}
class _DeleteAccountDialogState extends State<_DeleteAccountDialog> {
bool _deleting = false;
String? _errorText;
final _verifyController = TextEditingController();
static const _verifyCode = 'DELETE';
bool get _isVerifyMatch =>
_verifyController.text.trim().toUpperCase() == _verifyCode;
@override
void initState() {
super.initState();
_verifyController.addListener(() => setState(() {}));
}
@override
void dispose() {
_verifyController.dispose();
super.dispose();
}
Future<void> _onDelete() async {
setState(() {
_errorText = null;
_deleting = true;
});
try {
final res = await UserApi.deleteAccount(
sentinel: ApiConfig.appId,
asset: UserState.userId.value,
);
if (!mounted) return;
if (res.isSuccess) {
// Clear user state and token
UserState.setCredits(null);
UserState.setUserId(null);
UserState.setAvatar(null);
UserState.setUserName(null);
UserState.setNavigate(null);
await AuthTokenStore.clear();
ApiClient.instance.setUserToken(null);
if (!mounted) return;
Navigator.of(context).pop();
if (widget.parentContext.mounted) {
ScaffoldMessenger.of(widget.parentContext).showSnackBar(
const SnackBar(
content: Text('Account deleted'),
behavior: SnackBarBehavior.floating,
),
);
}
} else {
setState(() => _errorText = res.msg.isNotEmpty ? res.msg : 'Delete failed');
}
} catch (e) {
if (mounted) {
setState(() => _errorText = e.toString().replaceAll('Exception: ', ''));
}
} finally {
if (mounted) {
setState(() => _deleting = false);
}
}
}
@override
Widget build(BuildContext context) {
return Center(
child: Material(
color: Colors.transparent,
child: Container(
width: 342,
margin: const EdgeInsets.symmetric(horizontal: 24),
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: const [
BoxShadow(
color: Color(0x26000000),
blurRadius: 24,
offset: Offset(0, 8),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Delete Account?',
style: AppTypography.bodyLarge.copyWith(
fontWeight: FontWeight.w700,
color: AppColors.textPrimary,
),
),
GestureDetector(
onTap: _deleting ? null : () => Navigator.of(context).pop(),
child: const SizedBox(
width: 40,
height: 40,
child: Icon(LucideIcons.x, size: 24, color: AppColors.textMuted),
),
),
],
),
const SizedBox(height: 20),
Text(
'This action cannot be undone. All your data will be permanently deleted.',
style: AppTypography.bodyRegular.copyWith(
color: AppColors.textMuted,
fontSize: 14,
),
),
const SizedBox(height: 20),
Text(
'Type $_verifyCode to confirm',
style: AppTypography.label.copyWith(
color: AppColors.textMuted,
fontSize: 12,
),
),
const SizedBox(height: 8),
TextField(
controller: _verifyController,
decoration: InputDecoration(
hintText: 'Type $_verifyCode here',
hintStyle: AppTypography.bodyRegular.copyWith(
color: AppColors.textMuted,
fontSize: 14,
),
filled: true,
fillColor: const Color(0xFFFAFAFA),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: Color(0xFFE4E4E7)),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: Color(0xFFE4E4E7)),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 14,
),
),
style: AppTypography.bodyRegular.copyWith(
color: AppColors.textPrimary,
fontSize: 14,
),
textCapitalization: TextCapitalization.characters,
),
if (_errorText != null) ...[
const SizedBox(height: 12),
Text(
_errorText!,
style: AppTypography.caption.copyWith(color: Colors.red),
),
],
const SizedBox(height: 20),
Row(
children: [
Expanded(
child: GestureDetector(
onTap: _deleting ? null : () => Navigator.of(context).pop(),
child: Container(
height: 52,
alignment: Alignment.center,
decoration: BoxDecoration(
color: const Color(0xFFF4F4F5),
borderRadius: BorderRadius.circular(14),
),
child: Text(
'Cancel',
style: AppTypography.bodyMedium.copyWith(
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
),
),
),
),
),
const SizedBox(width: 12),
Expanded(
child: GestureDetector(
onTap: (_deleting || !_isVerifyMatch) ? null : _onDelete,
child: Container(
height: 52,
alignment: Alignment.center,
decoration: BoxDecoration(
color: _isVerifyMatch
? const Color(0xFFDC2626)
: Color(0xFFDC2626).withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(14),
),
child: _deleting
? const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: Text(
'Delete',
style: AppTypography.bodyMedium.copyWith(
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
),
),
],
),
],
),
),
),
);
}
}