vnpay_payment_flutter 1.0.0 copy "vnpay_payment_flutter: ^1.0.0" to clipboard
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).

example/lib/main.dart

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
            ],
          ),
        ),
      ),
    );
  }
}
6
likes
140
points
17
downloads

Publisher

verified publisherbuiphukhuyen.io.vn

Weekly Downloads

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).

Repository (GitHub)
View/report issues

Documentation

API reference

License

MIT (license)

Dependencies

crypto, flutter, intl, url_launcher

More

Packages that depend on vnpay_payment_flutter