打包:版本发布1.1.1

This commit is contained in:
ivan 2026-03-21 23:39:28 +08:00
parent eb72fb4979
commit cacb32c25f
13 changed files with 1114 additions and 40 deletions

View File

@ -1,7 +1,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="com.android.vending.BILLING" />
<application
android:usesCleartextTraffic="${usesCleartextTraffic}"

View File

@ -2293,6 +2293,52 @@
"fill": "#A1A1AA"
}
]
},
{
"type": "frame",
"id": "WYY7b",
"name": "menuItem3",
"width": "fill_container",
"fill": "#FFFFFF",
"cornerRadius": 12,
"effect": {
"type": "shadow",
"shadowType": "outer",
"color": "#00000008",
"offset": {
"x": 0,
"y": 2
},
"blur": 6
},
"padding": [
14,
16
],
"justifyContent": "space_between",
"alignItems": "center",
"children": [
{
"type": "text",
"id": "P6isW",
"name": "textNode",
"fill": "#DC2626",
"content": "Delete Account",
"fontFamily": "Inter",
"fontSize": 14,
"fontWeight": "500"
},
{
"type": "icon_font",
"id": "8OWH9",
"name": "iconNode",
"width": 20,
"height": 20,
"iconFontName": "trash-2",
"iconFontFamily": "lucide",
"fill": "#DC2626"
}
]
}
]
}
@ -4272,6 +4318,312 @@
]
}
]
},
{
"type": "frame",
"id": "5qKUB",
"x": 3150,
"y": 100,
"name": "Report Dialog",
"width": 342,
"fill": "#FFFFFF",
"cornerRadius": 16,
"effect": {
"type": "shadow",
"shadowType": "outer",
"color": "#00000026",
"offset": {
"x": 0,
"y": 8
},
"blur": 24
},
"layout": "vertical",
"gap": 20,
"padding": 24,
"children": [
{
"type": "frame",
"id": "6i0Kw",
"name": "dialogHeader",
"width": "fill_container",
"justifyContent": "space_between",
"alignItems": "center",
"children": [
{
"type": "text",
"id": "ISmLf",
"name": "title",
"fill": "#18181B",
"content": "Report",
"fontFamily": "Plus Jakarta Sans",
"fontSize": 20,
"fontWeight": "700"
},
{
"type": "frame",
"id": "yxaWM",
"name": "closeBtn",
"width": 40,
"height": 40,
"justifyContent": "center",
"alignItems": "center",
"children": [
{
"type": "icon_font",
"id": "g1Nfm",
"width": 24,
"height": 24,
"iconFontName": "x",
"iconFontFamily": "lucide",
"fill": "#71717A"
}
]
}
]
},
{
"type": "frame",
"id": "884m3",
"name": "descriptionInput",
"width": "fill_container",
"height": 120,
"fill": "#FAFAFA",
"cornerRadius": 12,
"stroke": {
"align": "inside",
"thickness": 1,
"fill": "#E4E4E7"
},
"layout": "vertical",
"padding": 16,
"children": [
{
"type": "text",
"id": "JDjJq",
"name": "placeholder",
"fill": "#A1A1AA",
"textGrowth": "fixed-width",
"width": "fill_container",
"content": "Describe the issue...",
"fontFamily": "Inter",
"fontSize": 14,
"fontWeight": "normal"
}
]
},
{
"type": "frame",
"id": "2ofcH",
"name": "imageUpload",
"width": "fill_container",
"height": 120,
"fill": "#F4F4F5",
"cornerRadius": 12,
"stroke": {
"align": "center",
"thickness": 2,
"fill": "#D4D4D8"
},
"layout": "vertical",
"gap": 8,
"justifyContent": "center",
"alignItems": "center",
"children": [
{
"type": "icon_font",
"id": "1NYr7",
"name": "uploadIcon",
"width": 32,
"height": 32,
"iconFontName": "image-plus",
"iconFontFamily": "lucide",
"fill": "#A1A1AA"
},
{
"type": "text",
"id": "jnJim",
"name": "uploadLabel",
"fill": "#71717A",
"content": "Tap to upload image",
"fontFamily": "Inter",
"fontSize": 14,
"fontWeight": "normal"
}
]
},
{
"type": "frame",
"id": "ZOA9y",
"name": "submitBtn",
"width": "fill_container",
"height": 52,
"fill": "#7C3AED",
"cornerRadius": 14,
"justifyContent": "center",
"alignItems": "center",
"children": [
{
"type": "text",
"id": "Jfj7q",
"fill": "#FFFFFF",
"content": "Submit",
"fontFamily": "Inter",
"fontSize": 16,
"fontWeight": "600"
}
]
}
]
},
{
"type": "frame",
"id": "Xp6Qz",
"x": 900,
"y": 0,
"name": "Delete Account Overlay",
"width": 390,
"height": 844,
"fill": "#00000080",
"layout": "vertical",
"justifyContent": "center",
"alignItems": "center",
"children": [
{
"type": "frame",
"id": "vFAbj",
"name": "Delete Account Dialog",
"width": 342,
"fill": "#FFFFFF",
"cornerRadius": 16,
"effect": {
"type": "shadow",
"shadowType": "outer",
"color": "#00000026",
"offset": {
"x": 0,
"y": 8
},
"blur": 24
},
"layout": "vertical",
"gap": 20,
"padding": 24,
"children": [
{
"type": "frame",
"id": "YR8mP",
"name": "header",
"width": "fill_container",
"justifyContent": "space_between",
"alignItems": "center",
"children": [
{
"type": "text",
"id": "Pck1A",
"name": "title",
"fill": "#18181B",
"content": "Delete Account?",
"fontFamily": "Plus Jakarta Sans",
"fontSize": 20,
"fontWeight": "700"
},
{
"type": "frame",
"id": "d8c1P",
"name": "closeBtn",
"width": 40,
"height": 40,
"layout": "vertical",
"justifyContent": "center",
"alignItems": "center",
"children": [
{
"type": "icon_font",
"id": "l4PfI",
"name": "closeIcon",
"width": 24,
"height": 24,
"iconFontName": "x",
"iconFontFamily": "lucide",
"fill": "#71717A"
}
]
}
]
},
{
"type": "text",
"id": "8K9GG",
"name": "desc",
"fill": "#71717A",
"textGrowth": "fixed-width",
"width": "fill_container",
"content": "This action cannot be undone. All your data will be permanently deleted.",
"fontFamily": "Inter",
"fontSize": 14,
"fontWeight": "normal"
},
{
"type": "frame",
"id": "0HXLh",
"name": "btnRow",
"width": "fill_container",
"gap": 12,
"children": [
{
"type": "frame",
"id": "csIsQ",
"name": "cancelBtn",
"width": "fill_container",
"height": 52,
"fill": "#F4F4F5",
"cornerRadius": 14,
"layout": "vertical",
"justifyContent": "center",
"alignItems": "center",
"children": [
{
"type": "text",
"id": "yqSEk",
"name": "cancelText",
"fill": "#18181B",
"content": "Cancel",
"fontFamily": "Inter",
"fontSize": 16,
"fontWeight": "600"
}
]
},
{
"type": "frame",
"id": "2r5VL",
"name": "deleteBtn",
"width": "fill_container",
"height": 52,
"fill": "#DC2626",
"cornerRadius": 14,
"layout": "vertical",
"justifyContent": "center",
"alignItems": "center",
"children": [
{
"type": "text",
"id": "fQlFx",
"name": "deleteText",
"fill": "#FFFFFF",
"content": "Delete",
"fontFamily": "Inter",
"fontSize": 16,
"fontWeight": "600"
}
]
}
]
}
]
}
]
}
]
}

82
docs/feedback_flow.md Normal file
View File

@ -0,0 +1,82 @@
# 举报 / 反馈流程
本文档说明举报弹窗的提交流程及接口调用顺序。字段映射详见 **petsHeroAI_client_guide.md**
---
## 一、前置校验
点击 Submit 时需校验:
| 校验项 | 要求 | 不满足时 |
|--------|------|----------|
| 文字输入 | 必填 | 提示用户填写描述 |
| 图片选择 | 必选 | 提示用户上传图片 |
---
## 二、接口调用顺序
### 2.1 获取上传 URL
| 接口 | 方法 | 说明 |
|------|------|------|
| `/v1/feedback/upload-presigned-url` | POST | 获取图片上传地址 |
**请求体(映射后)**
- `layer` (fileName):文件名
**响应 data映射后**
- `shed` (uploadUrl):上传 URL用于 PUT 请求
- `hunt` (filePath):文件路径,用于 submit 的 fileUrls
---
### 2.2 上传图片
使用 **PUT** 方式将所选图片上传到上一步返回的 `uploadUrl`
- 不经过代理,直接请求返回的 URL
- 请求体为图片二进制数据
- Content-Type 按 HTTP 规范填写(如 `image/jpeg`
---
### 2.3 提交反馈
| 接口 | 方法 | 说明 |
|------|------|------|
| `/v1/feedback/submit` | POST | 提交举报内容 |
**请求体(映射后)**
- `inventory` (fileUrls):文件路径列表,填入 2.1 返回的 `filePath`
- `cloak` (content):用户输入的文字描述
- `pauldron` (contentType):内容类型,按 HTTP 格式填写(如 `text/plain`
---
## 三、流程概览
```
用户填写描述 + 选择图片
校验:文字 + 图片均必填
POST /v1/feedback/upload-presigned-url
body: { layer: fileName }
获取 uploadUrl、filePath
PUT 图片到 uploadUrl
POST /v1/feedback/submit
body: { inventory: [filePath], cloak: content, pauldron: contentType }
提交成功,关闭弹窗
```

View File

@ -4,8 +4,8 @@ FLUTTER_APPLICATION_PATH=/Users/sven/Project/Workplace/app_client
COCOAPODS_PARALLEL_CODE_SIGN=true
FLUTTER_TARGET=lib/main.dart
FLUTTER_BUILD_DIR=build
FLUTTER_BUILD_NAME=1.0.2
FLUTTER_BUILD_NUMBER=3
FLUTTER_BUILD_NAME=1.0.9
FLUTTER_BUILD_NUMBER=10
EXCLUDED_ARCHS[sdk=iphonesimulator*]=i386
EXCLUDED_ARCHS[sdk=iphoneos*]=armv7
DART_OBFUSCATION=false

View File

@ -5,8 +5,8 @@ export "FLUTTER_APPLICATION_PATH=/Users/sven/Project/Workplace/app_client"
export "COCOAPODS_PARALLEL_CODE_SIGN=true"
export "FLUTTER_TARGET=lib/main.dart"
export "FLUTTER_BUILD_DIR=build"
export "FLUTTER_BUILD_NAME=1.0.2"
export "FLUTTER_BUILD_NUMBER=3"
export "FLUTTER_BUILD_NAME=1.0.9"
export "FLUTTER_BUILD_NUMBER=10"
export "DART_OBFUSCATION=false"
export "TRACK_WIDGET_CREATION=true"
export "TREE_SHAKE_ICONS=false"

View File

@ -12,8 +12,8 @@ abstract final class ApiConfig {
static const String packageName = 'com.petsheroai.app';
///
static const String preBaseUrl =
'https://ai.petsheroai.xyz'; //'https://pre-ai.petsheroai.xyz';
static const String preBaseUrl = 'https://pre-ai.petsheroai.xyz';
//'https://ai.petsheroai.xyz'; //'https://pre-ai.petsheroai.xyz';
///
static const String prodBaseUrl = 'https://ai.petsheroai.xyz';

View File

@ -0,0 +1,38 @@
import '../api_client.dart';
import '../proxy_client.dart';
/// / API
abstract final class FeedbackApi {
static final _client = ApiClient.instance.proxy;
/// URL
/// body: layer (fileName)
/// data: shed (uploadUrl), hunt (filePath)
static Future<ApiResponse> getUploadPresignedUrl({
required String fileName,
}) async {
return _client.request(
path: '/v1/feedback/upload-presigned-url',
method: 'POST',
body: {'layer': fileName},
);
}
///
/// body: inventory (fileUrls), cloak (content), pauldron (contentType)
static Future<ApiResponse> submit({
required List<String> fileUrls,
required String content,
required String contentType,
}) async {
return _client.request(
path: '/v1/feedback/submit',
method: 'POST',
body: {
'inventory': fileUrls,
'cloak': content,
'pauldron': contentType,
},
);
}
}

View File

@ -97,4 +97,19 @@ abstract final class UserApi {
},
);
}
///
static Future<ApiResponse> deleteAccount({
required String sentinel,
String? asset,
}) async {
return _client.request(
path: '/v1/user/delete',
method: 'GET',
queryParams: {
'sentinel': sentinel,
if (asset != null) 'asset': asset,
},
);
}
}

View File

@ -32,26 +32,49 @@ class GalleryTaskItem {
factory GalleryTaskItem.fromJson(Map<String, dynamic> json) {
final downsample = json['downsample'] as List<dynamic>? ?? [];
final items = <GalleryMediaItem>[];
for (final item in downsample) {
if (item is String) {
items.add(GalleryMediaItem(imageUrl: item));
} else if (item is Map<String, dynamic>) {
final reconfigure = item['reconfigure'] as String?;
if (reconfigure == null || reconfigure.isEmpty) continue;
// reconnect(imgType): 0=1=
final reconnect = item['reconnect'];
final imgType = reconnect is int
? reconnect
: reconnect is num
? reconnect.toInt()
: 1;
if (imgType == 2) {
items.add(GalleryMediaItem(videoUrl: reconfigure));
} else {
items.add(GalleryMediaItem(imageUrl: reconfigure));
// downsample的array[0]
if (downsample.isNotEmpty) {
final first = downsample[0];
if (first is String) {
items.add(GalleryMediaItem(imageUrl: first));
} else if (first is Map<String, dynamic>) {
final reconfigure = first['reconfigure'] as String?;
if (reconfigure != null && reconfigure.isNotEmpty) {
final reconnect = first['reconnect'];
final imgType = reconnect is int
? reconnect
: reconnect is num
? reconnect.toInt()
: 1;
if (imgType == 2) {
items.add(GalleryMediaItem(videoUrl: reconfigure));
} else {
items.add(GalleryMediaItem(imageUrl: reconfigure));
}
}
}
//
}
// for (final item in downsample) {
// if (item is String) {
// items.add(GalleryMediaItem(imageUrl: item));
// } else if (item is Map<String, dynamic>) {
// final reconfigure = item['reconfigure'] as String?;
// if (reconfigure == null || reconfigure.isEmpty) continue;
// // reconnect(imgType): 0=1=
// final reconnect = item['reconnect'];
// final imgType = reconnect is int
// ? reconnect
// : reconnect is num
// ? reconnect.toInt()
// : 1;
// if (imgType == 2) {
// items.add(GalleryMediaItem(videoUrl: reconfigure));
// } else {
// items.add(GalleryMediaItem(imageUrl: reconfigure));
// }
// }
// }
return GalleryTaskItem(
taskId: (json['tree'] as num?)?.toInt() ?? 0,
state: json['listing']?.toString() ?? '',

View File

@ -6,9 +6,11 @@ import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:flutter_lucide/flutter_lucide.dart';
import 'package:gal/gal.dart';
import 'package:http/http.dart' as http;
import 'package:image_picker/image_picker.dart';
import 'package:path_provider/path_provider.dart';
import 'package:video_player/video_player.dart';
import '../../core/api/services/feedback_api.dart';
import '../../core/theme/app_colors.dart';
import '../../core/theme/app_spacing.dart';
import '../../core/theme/app_typography.dart';
@ -228,9 +230,7 @@ class _MediaDisplay extends StatelessWidget {
top: 16,
right: 20,
child: GestureDetector(
onTap: () {
// TODO: Report action
},
onTap: () => _ReportDialog.show(context),
child: Container(
width: 44,
height: 44,
@ -399,6 +399,292 @@ class _DownloadButton extends StatelessWidget {
}
}
/// Report dialog - matches Pencil 5qKUB
class _ReportDialog extends StatefulWidget {
const _ReportDialog({required this.parentContext});
final BuildContext parentContext;
static void show(BuildContext context) {
showDialog<void>(
context: context,
barrierColor: Colors.black54,
builder: (_) => _ReportDialog(parentContext: context),
);
}
@override
State<_ReportDialog> createState() => _ReportDialogState();
}
class _ReportDialogState extends State<_ReportDialog> {
final _controller = TextEditingController();
File? _pickedImage;
final _picker = ImagePicker();
bool _submitting = false;
String? _errorText;
@override
void dispose() {
_controller.dispose();
super.dispose();
}
Future<void> _pickImage() async {
final x = await _picker.pickImage(source: ImageSource.gallery);
if (x != null && mounted) {
setState(() => _pickedImage = File(x.path));
}
}
Future<void> _submit() async {
final content = _controller.text.trim();
if (content.isEmpty) {
setState(() => _errorText = 'Please describe the issue');
return;
}
if (_pickedImage == null) {
setState(() => _errorText = 'Please upload an image');
return;
}
setState(() {
_errorText = null;
_submitting = true;
});
try {
final file = _pickedImage!;
final ext = file.path.split('.').last.toLowerCase();
final contentType = ext == 'png'
? 'image/png'
: ext == 'gif'
? 'image/gif'
: 'image/jpeg';
final fileName =
'feedback_${DateTime.now().millisecondsSinceEpoch}.$ext';
final presignedRes =
await FeedbackApi.getUploadPresignedUrl(fileName: fileName);
if (!presignedRes.isSuccess || presignedRes.data == null) {
throw Exception(
presignedRes.msg.isNotEmpty ? presignedRes.msg : 'Failed to get upload URL');
}
final data = presignedRes.data as Map<String, dynamic>;
final uploadUrl = data['shed'] as String?;
final filePath = data['hunt'] as String?;
if (uploadUrl == null ||
uploadUrl.isEmpty ||
filePath == null ||
filePath.isEmpty) {
throw Exception('Invalid presigned URL response');
}
final bytes = await file.readAsBytes();
final uploadResponse = await http.put(
Uri.parse(uploadUrl),
headers: {'Content-Type': contentType},
body: bytes,
);
if (uploadResponse.statusCode < 200 ||
uploadResponse.statusCode >= 300) {
throw Exception('Upload failed: ${uploadResponse.statusCode}');
}
final submitRes = await FeedbackApi.submit(
fileUrls: [filePath],
content: content,
contentType: 'text/plain',
);
if (!submitRes.isSuccess) {
throw Exception(
submitRes.msg.isNotEmpty ? submitRes.msg : 'Failed to submit report');
}
if (!mounted) return;
Navigator.of(context).pop();
if (widget.parentContext.mounted) {
ScaffoldMessenger.of(widget.parentContext).showSnackBar(
const SnackBar(
content: Text('Report submitted'),
behavior: SnackBarBehavior.floating,
),
);
}
} catch (e) {
if (mounted) {
setState(() => _errorText = e.toString().replaceAll('Exception: ', ''));
}
} finally {
if (mounted) {
setState(() => _submitting = false);
}
}
}
@override
Widget build(BuildContext context) {
return Dialog(
backgroundColor: Colors.transparent,
insetPadding: const EdgeInsets.symmetric(horizontal: 24),
child: Container(
width: 342,
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.15),
blurRadius: 24,
offset: const Offset(0, 8),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Report',
style: AppTypography.bodyMedium.copyWith(
fontSize: 20,
fontWeight: FontWeight.w700,
color: AppColors.textPrimary,
),
),
GestureDetector(
onTap: () => Navigator.of(context).pop(),
child: const SizedBox(
width: 40,
height: 40,
child: Icon(
LucideIcons.x,
size: 24,
color: AppColors.textSecondary,
),
),
),
],
),
const SizedBox(height: 20),
Container(
height: 120,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.background,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.border),
),
child: TextField(
controller: _controller,
maxLines: null,
decoration: const InputDecoration(
hintText: 'Describe the issue...',
hintStyle: TextStyle(color: AppColors.textMuted),
border: InputBorder.none,
contentPadding: EdgeInsets.zero,
isDense: true,
),
),
),
const SizedBox(height: 20),
GestureDetector(
onTap: _pickImage,
child: Container(
height: 120,
decoration: BoxDecoration(
color: AppColors.surfaceAlt,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: const Color(0xFFD4D4D8),
width: 2,
),
),
child: _pickedImage != null
? ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Image.file(
_pickedImage!,
fit: BoxFit.cover,
width: double.infinity,
height: double.infinity,
),
)
: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
LucideIcons.image_plus,
size: 32,
color: AppColors.textMuted,
),
const SizedBox(height: 8),
Text(
'Tap to upload image',
style: TextStyle(
color: AppColors.textSecondary,
fontSize: 14,
),
),
],
),
),
),
if (_errorText != null) ...[
const SizedBox(height: 12),
Text(
_errorText!,
style: TextStyle(
color: AppColors.accentOrange,
fontSize: 14,
),
),
],
const SizedBox(height: 20),
GestureDetector(
onTap: _submitting ? null : _submit,
child: Container(
height: 52,
decoration: BoxDecoration(
color: _submitting
? AppColors.textMuted
: const Color(0xFF7C3AED),
borderRadius: BorderRadius.circular(14),
),
alignment: Alignment.center,
child: _submitting
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: AppColors.surface,
),
)
: Text(
'Submit',
style: AppTypography.bodyMedium.copyWith(
color: AppColors.surface,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
),
],
),
),
);
}
}
class _ShareButton extends StatelessWidget {
const _ShareButton({required this.onShare});

View File

@ -2,6 +2,9 @@ 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/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';
@ -81,7 +84,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
onTap: () => Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => const PaymentWebViewScreen(
paymentUrl: 'http://www.petsheroai.xyz/privacy.html',
paymentUrl: 'https://www.petsheroai.xyz/privacy.html',
title: 'Privacy Policy',
),
),
@ -93,12 +96,18 @@ class _ProfileScreenState extends State<ProfileScreen> {
onTap: () => Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => const PaymentWebViewScreen(
paymentUrl: 'http://www.petsheroai.xyz/terms.html',
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),
),
],
),
],
@ -296,11 +305,13 @@ class _MenuItem extends StatelessWidget {
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) {
@ -328,13 +339,257 @@ class _MenuItem extends StatelessWidget {
Text(
title,
style: AppTypography.bodyRegular.copyWith(
color: AppColors.textPrimary,
color: iconColor ?? AppColors.textPrimary,
),
),
Icon(icon, size: 20, color: AppColors.textMuted),
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);
ApiClient.instance.setUserToken(null);
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,
),
),
),
),
),
],
),
],
),
),
),
);
}
}

View File

@ -23,6 +23,7 @@ class PaymentWebViewScreen extends StatefulWidget {
class _PaymentWebViewScreenState extends State<PaymentWebViewScreen> {
late final WebViewController _controller;
int _loadingProgress = 0;
@override
void initState() {
@ -31,8 +32,15 @@ class _PaymentWebViewScreenState extends State<PaymentWebViewScreen> {
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(
NavigationDelegate(
onPageStarted: (_) {},
onPageFinished: (_) {},
onProgress: (progress) {
if (mounted) setState(() => _loadingProgress = progress);
},
onPageStarted: (_) {
if (mounted) setState(() => _loadingProgress = 0);
},
onPageFinished: (_) {
if (mounted) setState(() => _loadingProgress = 100);
},
onWebResourceError: (e) {},
),
)
@ -44,11 +52,27 @@ class _PaymentWebViewScreenState extends State<PaymentWebViewScreen> {
return Scaffold(
backgroundColor: AppColors.surface,
appBar: PreferredSize(
preferredSize: const Size.fromHeight(56),
child: TopNavBar(
title: widget.title,
showBackButton: true,
onBack: () => Navigator.of(context).pop(),
preferredSize: const Size.fromHeight(59),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TopNavBar(
title: widget.title,
showBackButton: true,
onBack: () => Navigator.of(context).pop(),
),
if (_loadingProgress < 100)
SizedBox(
height: 3,
child: LinearProgressIndicator(
value: _loadingProgress / 100,
backgroundColor: AppColors.surfaceAlt,
valueColor:
const AlwaysStoppedAnimation<Color>(AppColors.primary),
),
),
],
),
),
body: SafeArea(

View File

@ -1,7 +1,7 @@
name: pets_hero_ai
description: PetsHero AI Application.
publish_to: 'none'
version: 1.0.5+6
version: 1.1.1+12
environment:
sdk: '>=3.0.0 <4.0.0'
@ -31,13 +31,13 @@ dependencies:
in_app_purchase: ^3.2.0
webview_flutter: ^4.10.0
screen_secure: ^1.0.3
flutter_native_splash: ^2.4.7
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^3.0.0
flutter_launcher_icons: ^0.14.4
flutter_native_splash: ^2.4.7
flutter_launcher_icons:
android: true