payment page

This commit is contained in:
Phuoc Nguyen
2025-11-03 16:16:15 +07:00
parent c689f967d5
commit 988216b151
3 changed files with 649 additions and 9 deletions

View File

@@ -5,7 +5,9 @@ library;
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:worker/core/constants/ui_constants.dart';
import 'package:worker/core/router/app_router.dart';
import 'package:worker/core/theme/colors.dart';
/// Checkout Submit Button
@@ -103,22 +105,38 @@ class CheckoutSubmitButton extends StatelessWidget {
duration: Duration(seconds: 2),
),
);
// Navigate back after a short delay
Future.delayed(const Duration(milliseconds: 500), () {
if (context.mounted) {
context.pop();
}
});
} else {
// Generate order ID (mock - replace with actual from backend)
final orderId = 'DH${DateTime.now().millisecondsSinceEpoch.toString().substring(7)}';
// Show order success message
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Đặt hàng thành công!'),
content: Text('Đặt hàng thành công! Chuyển đến thanh toán...'),
backgroundColor: AppColors.success,
duration: Duration(seconds: 2),
duration: Duration(seconds: 1),
),
);
}
// Navigate back after a short delay
Future.delayed(const Duration(milliseconds: 500), () {
if (context.mounted) {
context.pop();
}
});
// Navigate to payment QR page after a short delay
Future.delayed(const Duration(milliseconds: 500), () {
if (context.mounted) {
context.pushNamed(
RouteNames.paymentQr,
queryParameters: {
'orderId': orderId,
'amount': total.toString(),
},
);
}
});
}
}
}

View File

@@ -0,0 +1,605 @@
/// Payment QR Page
///
/// QR code payment screen with bank transfer information.
/// Features:
/// - Payment amount display with minimum payment warning
/// - QR code for quick payment
/// - Bank transfer information with copy buttons
/// - Payment confirmation and proof upload buttons
/// - Countdown timer for payment
/// - Payment instructions modal
library;
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:worker/core/constants/ui_constants.dart';
import 'package:worker/core/theme/colors.dart';
/// Payment QR Page
///
/// Displays QR code and bank transfer information for payment.
class PaymentQrPage extends HookConsumerWidget {
final String orderId;
final double amount;
const PaymentQrPage({
super.key,
required this.orderId,
required this.amount,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
// Countdown timer (15 minutes = 900 seconds)
final remainingSeconds = useState<int>(900);
final timer = useRef<Timer?>(null);
// Start countdown timer
useEffect(() {
timer.value = Timer.periodic(const Duration(seconds: 1), (t) {
if (remainingSeconds.value > 0) {
remainingSeconds.value--;
} else {
t.cancel();
}
});
return () {
timer.value?.cancel();
};
}, []);
// Format timer display
final minutes = remainingSeconds.value ~/ 60;
final seconds = remainingSeconds.value % 60;
final timerDisplay =
'${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
return Scaffold(
backgroundColor: const Color(0xFFF4F6F8),
appBar: AppBar(
backgroundColor: Colors.white,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.black),
onPressed: () => context.pop(),
),
title: const Text(
'Thanh toán',
style: TextStyle(
color: Colors.black,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
centerTitle: false,
actions: [
IconButton(
icon: const Icon(Icons.info_outline, color: Colors.black),
onPressed: () => _showInfoDialog(context),
),
const SizedBox(width: AppSpacing.sm),
],
),
body: SingleChildScrollView(
child: Column(
children: [
const SizedBox(height: AppSpacing.md),
// Payment Amount Card
_buildAmountCard(amount),
const SizedBox(height: AppSpacing.md),
// QR Code Card
_buildQrCodeCard(amount, orderId),
const SizedBox(height: AppSpacing.md),
// Bank Transfer Info Card
_buildBankInfoCard(context, orderId),
const SizedBox(height: AppSpacing.md),
// Action Buttons
_buildActionButtons(context),
const SizedBox(height: AppSpacing.md),
// Timer
_buildTimer(timerDisplay, remainingSeconds.value),
const SizedBox(height: AppSpacing.lg),
],
),
),
);
}
/// Build payment amount card
Widget _buildAmountCard(double amount) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
padding: const EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(AppRadius.card),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Column(
children: [
Text(
_formatCurrency(amount),
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: AppColors.primaryBlue,
),
),
const SizedBox(height: 8),
const Text(
'Số tiền cần thanh toán',
style: TextStyle(fontSize: 14, color: AppColors.grey500),
),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFFFF8E1),
borderRadius: BorderRadius.circular(AppRadius.card),
border: Border.all(color: const Color(0xFFFFD54F)),
),
child: Row(
children: [
const Icon(Icons.info, color: AppColors.warning, size: 20),
const SizedBox(width: 8),
Expanded(
child: Text(
'Thanh toán không dưới 20%',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.orange[900],
),
),
),
],
),
),
],
),
);
}
/// Build QR code card
Widget _buildQrCodeCard(double amount, String orderId) {
// Generate QR code data URL
final qrData = Uri.encodeComponent(
'https://eurotile.com/payment/$orderId?amount=$amount');
final qrUrl =
'https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=$qrData';
return Container(
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
padding: const EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(AppRadius.card),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Column(
children: [
const Text(
'Quét mã QR để thanh toán',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Color(0xFF212121),
),
),
const SizedBox(height: AppSpacing.md),
Container(
width: 220,
height: 220,
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(AppRadius.card),
border: Border.all(color: const Color(0xFFE2E8F0)),
),
child: Image.network(
qrUrl,
fit: BoxFit.contain,
errorBuilder: (context, error, stackTrace) {
return const Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.qr_code, size: 80, color: AppColors.grey500),
SizedBox(height: 8),
Text(
'Không thể tải mã QR',
style: TextStyle(fontSize: 12, color: AppColors.grey500),
),
],
);
},
),
),
const SizedBox(height: AppSpacing.md),
const Text(
'Quét mã QR bằng ứng dụng ngân hàng để thanh toán nhanh chóng',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 14, color: AppColors.grey500),
),
],
),
);
}
/// Build bank transfer info card
Widget _buildBankInfoCard(BuildContext context, String orderId) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
padding: const EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(AppRadius.card),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Thông tin chuyển khoản',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Color(0xFF212121),
),
),
const SizedBox(height: AppSpacing.md),
// Bank Name
_buildInfoRow(
context: context,
label: 'Ngân hàng:',
value: 'BIDV',
),
const Divider(height: 24),
// Account Number
_buildInfoRow(
context: context,
label: 'Số tài khoản:',
value: '19036810704016',
),
const Divider(height: 24),
// Account Holder
_buildInfoRow(
context: context,
label: 'Chủ tài khoản:',
value: 'CÔNG TY EUROTILE',
),
const Divider(height: 24),
// Transfer Content
_buildInfoRow(
context: context,
label: 'Nội dung:',
value: '$orderId La Nguyen Quynh',
),
const SizedBox(height: AppSpacing.md),
// Note
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFE3F2FD),
borderRadius: BorderRadius.circular(AppRadius.card),
border: Border.all(color: const Color(0xFF90CAF9)),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(Icons.lightbulb_outline,
color: AppColors.primaryBlue, size: 20),
const SizedBox(width: 8),
Expanded(
child: RichText(
text: TextSpan(
style: const TextStyle(
fontSize: 13,
color: Color(0xFF1565C0),
height: 1.4,
),
children: [
const TextSpan(
text: 'Lưu ý: ',
style: TextStyle(fontWeight: FontWeight.bold),
),
const TextSpan(
text:
'Vui lòng ghi đúng nội dung chuyển khoản để đơn hàng được xử lý nhanh chóng.',
),
],
),
),
),
],
),
),
],
),
);
}
/// Build info row with copy button
Widget _buildInfoRow({
required BuildContext context,
required String label,
required String value,
}) {
return Row(
children: [
Expanded(
flex: 2,
child: Text(
label,
style: const TextStyle(fontSize: 14, color: AppColors.grey500),
),
),
Expanded(
flex: 3,
child: Text(
value,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Color(0xFF212121),
),
),
),
IconButton(
icon: const Icon(Icons.copy, size: 20, color: AppColors.primaryBlue),
onPressed: () => _copyToClipboard(context, value),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
],
);
}
/// Build action buttons
Widget _buildActionButtons(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
child: Row(
children: [
// Confirmed Payment Button
Expanded(
child: OutlinedButton.icon(
onPressed: () => _confirmPayment(context),
icon: const Icon(Icons.check, size: 20),
label: const Text(
'Đã thanh toán',
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600),
),
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.primaryBlue,
side: const BorderSide(color: AppColors.primaryBlue, width: 1.5),
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppRadius.button),
),
),
),
),
const SizedBox(width: AppSpacing.sm),
// Upload Proof Button
Expanded(
child: ElevatedButton.icon(
onPressed: () => _uploadProof(context),
icon: const Icon(Icons.camera_alt, size: 20),
label: const Text(
'Upload bill',
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600),
),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryBlue,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 14),
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppRadius.button),
),
),
),
),
],
),
);
}
/// Build countdown timer
Widget _buildTimer(String timerDisplay, int remainingSeconds) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(AppRadius.card),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.schedule, size: 18, color: AppColors.grey500),
const SizedBox(width: 8),
const Text(
'Thời gian thanh toán: ',
style: TextStyle(fontSize: 14, color: AppColors.grey500),
),
Text(
timerDisplay,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: remainingSeconds < 300
? AppColors.danger
: AppColors.grey900,
),
),
],
),
);
}
/// Copy text to clipboard
void _copyToClipboard(BuildContext context, String text) {
Clipboard.setData(ClipboardData(text: text));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Đã sao chép: $text'),
duration: const Duration(seconds: 1),
behavior: SnackBarBehavior.floating,
),
);
}
/// Show payment info dialog
void _showInfoDialog(BuildContext context) {
showDialog<void>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Hướng dẫn thanh toán'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Đây là nội dung hướng dẫn sử dụng cho tính năng Thanh toán:',
style: TextStyle(fontSize: 14),
),
const SizedBox(height: 12),
_buildInfoItem('Quét mã QR bằng app ngân hàng hoặc ví điện tử'),
_buildInfoItem('Chuyển khoản theo thông tin được cung cấp'),
_buildInfoItem('Ghi đúng nội dung chuyển khoản'),
_buildInfoItem('Upload hóa đơn sau khi chuyển khoản'),
_buildInfoItem('Thanh toán tối thiểu 20% giá trị đơn hàng'),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Đóng'),
),
],
),
);
}
/// Build info item for dialog
Widget _buildInfoItem(String text) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('', style: TextStyle(fontSize: 14)),
Expanded(
child: Text(text, style: const TextStyle(fontSize: 14)),
),
],
),
);
}
/// Confirm payment
void _confirmPayment(BuildContext context) {
showDialog<void>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Xác nhận thanh toán'),
content: const Text(
'Bạn đã hoàn tất thanh toán cho đơn hàng này?',
style: TextStyle(fontSize: 14),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Chưa'),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Đã xác nhận thanh toán!'),
backgroundColor: AppColors.success,
duration: Duration(seconds: 2),
),
);
// Navigate back after delay
Future.delayed(const Duration(milliseconds: 500), () {
if (context.mounted) {
context.pop();
}
});
},
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryBlue,
),
child: const Text('Đã thanh toán'),
),
],
),
);
}
/// Upload payment proof
void _uploadProof(BuildContext context) {
// TODO: Implement image picker and upload
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Tính năng upload bill đang được phát triển'),
duration: Duration(seconds: 2),
),
);
}
/// Format currency
String _formatCurrency(double amount) {
return '${amount.toStringAsFixed(0).replaceAllMapped(
RegExp(r'(\d)(?=(\d{3})+(?!\d))'),
(Match m) => '${m[1]}.',
)}';
}
}