vnpay_payment_flutter 1.0.0
vnpay_payment_flutter: ^1.0.0 copied to clipboard
Plugin Flutter giúp tích hợp Cổng thanh toán VNPAY (Việt Nam) vào ứng dụng di động. Hỗ trợ tạo URL thanh toán và kiểm tra chữ ký bảo mật (HMAC-SHA512).
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:app_links/app_links.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:vnpay_payment_flutter/vnpay_payment_flutter.dart';
void main() {
runApp(const VNPayApp());
}
/// Widget gốc của ứng dụng
/// Cấu hình Material3 theme và AppLinks listener cho deeplink xử lý
class VNPayApp extends StatelessWidget {
const VNPayApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'VNPAY Payment Example',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
useMaterial3: true,
inputDecorationTheme: InputDecorationTheme(
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
filled: true,
fillColor: Colors.grey[50],
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
),
home: const PaymentPage(),
);
}
}
class PaymentPage extends StatefulWidget {
const PaymentPage({super.key});
@override
State<PaymentPage> createState() => _PaymentPageState();
}
class _PaymentPageState extends State<PaymentPage> with WidgetsBindingObserver {
final _formKey = GlobalKey<FormState>();
// ===== CONTROLLER (Đầu vào từ người dùng) =====
final _tmnCodeController = TextEditingController(text: '');
final _hashSecretController = TextEditingController(text: '');
final _amountController = TextEditingController(text: '100000');
final _orderInfoController = TextEditingController(
text: 'Thanh toan don hang',
);
// ===== CẤU HÌNH THANH TOÁN =====
bool _isSandbox = true;
bool _isLoading = false;
String _status = '';
late VNPAYPayment _vnpayPayment;
late AppLinks _appLinks;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_updateVNPayPayment();
_initAppLinks();
}
void _initAppLinks() {
_appLinks = AppLinks();
// Lắng nghe deeplink từ VNPAY khi ứng dụng đang chạy
// Format deeplink: vnpaypayment://return?vnp_ResponseCode=00&...
_appLinks.uriLinkStream.listen(
(uri) {
debugPrint('[VNPAY] Deeplink nhận được: $uri');
if (uri.scheme == 'vnpaypayment' && uri.host == 'return') {
_handlePaymentReturn(uri.toString());
}
},
onError: (err) {
debugPrint('[VNPAY] Lỗi deeplink: $err');
},
);
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
debugPrint('[VNPAY] Ứng dụng quay trở lại active');
// Note: AppLinks listener (ở _initAppLinks) đã xử lý deeplink
// Lifecycle observer được dùng chỉ để log trạng thái app
}
}
void _handlePaymentReturn(String deepLink) {
try {
final uri = Uri.parse(deepLink);
final params = uri.queryParameters;
debugPrint('[VNPAY] Tham số phản hồi: $params');
// ⚠️ CRITICAL: Xác minh chữ ký để đảm bảo phản hồi từ VNPAY
// Nếu signature không hợp lệ => dữ liệu bị giả mạo hoặc bị tấn công
final isValid = _vnpayPayment.verifyResponse(params);
if (!isValid) {
setState(() {
_status = '❌ Lỗi: Chữ ký không hợp lệ!';
});
return;
}
final responseCodeStr = params['vnp_ResponseCode'] ?? '99';
final amount = params['vnp_Amount'] ?? '';
final txnRef = params['vnp_TxnRef'] ?? '';
// Lấy chi tiết từ response code (tương ứng với status code từ VNPAY API)
final responseCode = VNPayResponseCode.getByCode(responseCodeStr);
final amountVnd = int.tryParse(amount) != null
? int.parse(amount) ~/ 100
: 0;
if (responseCode.isSuccess) {
setState(() {
_status =
'✅ ${responseCode.message}\n'
'Số tiền: $amountVnd VND\n'
'Mã đơn: $txnRef\n'
'Chi tiết: ${responseCode.description}';
});
} else {
setState(() {
_status =
'❌ ${responseCode.message}\n'
'Mã lỗi: $responseCodeStr\n'
'Mã đơn: $txnRef\n'
'Chi tiết: ${responseCode.description}';
});
}
} catch (e) {
debugPrint('[VNPAY] Lỗi xử lý kết quả: $e');
setState(() {
_status = '❌ Lỗi xử lý kết quả: $e';
});
}
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_tmnCodeController.dispose();
_hashSecretController.dispose();
_amountController.dispose();
_orderInfoController.dispose();
super.dispose();
}
// Cập nhật cấu hình VNPAYPayment từ input của người dùng
// Phải gọi hàm này trước khi tạo URL thanh toán
void _updateVNPayPayment() {
_vnpayPayment = VNPAYPayment(
tmnCode: _tmnCodeController.text,
hashSecret: _hashSecretController.text,
isSandbox: _isSandbox,
hashType: VNPayHashType.sha512,
);
}
Future<void> _handlePayment() async {
if (!_formKey.currentState!.validate()) return;
// Cập nhật cấu hình với thông tin từ form trước khi thanh toán
_updateVNPayPayment();
setState(() {
_isLoading = true;
_status = 'Đang khởi tạo thanh toán...';
});
try {
final now = DateTime.now();
final txnRef = 'ORD_${now.millisecondsSinceEpoch}';
// Tạo URL thanh toán
// Phương thức này sẽ sinh HMAC-SHA512 signature tự động
final paymentUrl = _vnpayPayment.generatePaymentUrl(
txnRef: txnRef,
amount: double.parse(_amountController.text),
orderInfo: _orderInfoController.text,
returnUrl: 'vnpaypayment://return', // Deeplink để quay về app
expireDate: now.add(const Duration(minutes: 15)),
);
debugPrint('[VNPAY] Payment URL: $paymentUrl');
setState(() {
_status = 'Đã mở trang thanh toán...';
});
// Mở URL thanh toán trong trình duyệt
if (await canLaunchUrl(Uri.parse(paymentUrl))) {
await launchUrl(
Uri.parse(paymentUrl),
mode: LaunchMode.externalApplication,
);
} else {
setState(() {
_status = 'Lỗi: Không thể mở URL thanh toán';
});
}
} catch (e) {
setState(() {
_status = 'Lỗi: $e';
});
} finally {
setState(() {
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('VNPAY Payment Demo'),
centerTitle: true,
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Cấu hình kết nối
Card(
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Cấu hình kết nối',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
TextFormField(
controller: _tmnCodeController,
decoration: const InputDecoration(
labelText: 'TMN Code',
hintText: 'Nhập TMN Code',
prefixIcon: Icon(Icons.business),
),
validator: (value) => value?.isEmpty ?? true
? 'Vui lòng nhập TMN Code'
: null,
),
const SizedBox(height: 12),
TextFormField(
controller: _hashSecretController,
decoration: const InputDecoration(
labelText: 'Hash Secret',
hintText: 'Nhập Hash Secret',
prefixIcon: Icon(Icons.security),
),
obscureText: true,
validator: (value) => value?.isEmpty ?? true
? 'Vui lòng nhập Hash Secret'
: null,
),
const SizedBox(height: 12),
SwitchListTile(
title: const Text('Sandbox Mode'),
subtitle: const Text(
'Bật để sử dụng sandbox.vnpayment.vn',
),
value: _isSandbox,
onChanged: (value) {
setState(() {
_isSandbox = value;
});
},
),
],
),
),
),
const SizedBox(height: 16),
// Thông tin giao dịch
Card(
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Thông tin giao dịch',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
TextFormField(
controller: _amountController,
decoration: const InputDecoration(
labelText: 'Số tiền (VND)',
hintText: 'Nhập số tiền cần thanh toán',
prefixIcon: Icon(Icons.attach_money),
suffixText: 'VND',
),
keyboardType: TextInputType.number,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Vui lòng nhập số tiền';
}
if (double.tryParse(value) == null) {
return 'Số tiền không hợp lệ';
}
return null;
},
),
const SizedBox(height: 12),
TextFormField(
controller: _orderInfoController,
decoration: const InputDecoration(
labelText: 'Nội dung đơn hàng',
hintText: 'Nhập nội dung thanh toán',
prefixIcon: Icon(Icons.description),
),
validator: (value) => value?.isEmpty ?? true
? 'Vui lòng nhập nội dung'
: null,
),
],
),
),
),
const SizedBox(height: 24),
// Nút thanh toán
if (_isLoading)
const Center(child: CircularProgressIndicator())
else
ElevatedButton.icon(
onPressed: _handlePayment,
icon: const Icon(Icons.payment),
label: const Text(
'THANH TOÁN QUA VNPAY',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
),
),
const SizedBox(height: 16),
// Trạng thái
if (_status.isNotEmpty)
Card(
color: Colors.grey[50],
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(color: Colors.grey[300]!),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
const Icon(Icons.info_outline, color: Colors.blue),
const SizedBox(width: 12),
Expanded(
child: Text(
_status,
style: const TextStyle(fontSize: 14, height: 1.5),
),
),
],
),
),
),
// Thẻ test
],
),
),
),
);
}
}