Flutter统一支付接口插件upi_pay的使用

发布于 1周前 作者 songsunli 来自 Flutter

Flutter统一支付接口插件upi_pay的使用

插件简介

upi_pay 是一个用于在Flutter应用中集成UPI(统一支付接口)支付功能的插件。它允许用户找到手机上已安装的UPI支付应用程序,并通过其中任何一个进行付款。该插件实现了UPI Deep Linking And Proximity Integration Specification

版本信息

version

平台支持

平台 状态
Android Android Screen Capture
iOS IOS Screen Capture

快速开始

依赖添加

在您的 pubspec.yaml 文件中添加 upi_pay 作为依赖项:

dependencies:
  ...
  upi_pay: ^1.1.0

导入包

在需要使用的Dart文件中导入 upi_pay 包:

import 'package:upi_pay/upi_pay.dart';

iOS配置

Runner/Info.plist 文件中添加或修改 LSApplicationQueriesSchemes 键,以包含以下自定义查询方案:

<key>LSApplicationQueriesSchemes</key>
<array>
  <string>freecharge</string>
  <string>gpay</string>
  <string>in.fampay.app</string>
  <string>lotza</string>
  <string>mobikwik</string>
  <string>paytm</string>
  <string>phonepe</string>
  <string>upi</string>
  <string>upibillpay</string>
  <string>whatsapp</string>
</array>

使用方法

创建插件实例

final upiPay = UpiPay();

获取已安装的应用列表

final List<ApplicationMeta> appMetaList = await upiPay.getInstalledUpiApps();

显示应用详情

Widget appWidget(ApplicationMeta appMeta) {
  return Column(
    mainAxisAlignment: MainAxisAlignment.center,
    children: <Widget>[
      appMeta.iconImage(48), // Logo
      Container(
        margin: EdgeInsets.only(top: 4),
        alignment: Alignment.center,
        child: Text(
          appMeta.upiApplication.getAppName(),
          textAlign: TextAlign.center,
        ),
      ),
    ],
  );
}

发起UPI交易

Future doUpiTransaction(ApplicationMeta appMeta) async {
  final UpiTransactionResponse response = await upiPay.initiateTransaction(
    amount: '100.00',
    app: appMeta.application,
    receiverName: 'John Doe',
    receiverUpiAddress: 'john@doe',
    transactionRef: 'UPITXREF0001',
    transactionNote: 'A UPI Transaction',
  );
  print(response.status);
}

行为、限制和措施

Android平台行为

  • 流程:在Android平台上,UPI深度链接规范是通过Intents实现的。发起支付请求时会指定要调用的具体UPI应用。处理完UPI交易后,目标应用会按照规范格式返回响应给插件层,插件层解析此响应并生成 UpiTransactionResponse 对象。
  • 措施:建议在客户端之外实现服务器端支付验证,以防客户端上的UPI交易流程被篡改。

iOS平台行为

  • 流程:iOS上通过自定义scheme实现UPI深度链接规范。由于无法指定目标应用ID,因此不确定哪个UPI应用会被调用。此外,由于自定义scheme机制限制,无法获取交易状态。
  • 措施:需要额外实现支付验证逻辑;并且区分检测到的应用和支持但未检测到的应用。

动态变化

UPI标准和技术不断发展,导致不同UPI应用的行为和功能也在变化。具体参见 Apps 获取各应用当前的功能状态。

示例代码

下面是一个完整的示例项目结构:

import 'dart:io';
import 'dart:math';

import 'package:flutter/material.dart';
import 'package:upi_pay/upi_pay.dart';

void main() => runApp(App());

class App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('UPI Pay'),
        ),
        body: Screen(),
      ),
    );
  }
}

class Screen extends StatefulWidget {
  @override
  _ScreenState createState() => _ScreenState();
}

class _ScreenState extends State<Screen> {
  String? _upiAddrError;

  final _upiAddressController = TextEditingController();
  final _amountController = TextEditingController();
  final _upiPayPlugin = UpiPay();

  bool _isUpiEditable = false;
  List<ApplicationMeta>? _apps;

  @override
  void initState() {
    super.initState();

    _amountController.text =
        (Random.secure().nextDouble() * 10).toStringAsFixed(2);

    Future.delayed(Duration(milliseconds: 0), () async {
      _apps = await _upiPayPlugin.getInstalledUpiApplications(
          statusType: UpiApplicationDiscoveryAppStatusType.all);
      setState(() {});
    });
  }

  @override
  void dispose() {
    _amountController.dispose();
    _upiAddressController.dispose();
    super.dispose();
  }

  void _generateAmount() {
    setState(() {
      _amountController.text = '10';
    });
  }

  Future<void> _onTap(ApplicationMeta app) async {
    final err = _validateUpiAddress(_upiAddressController.text);
    if (err != null) {
      setState(() {
        _upiAddrError = err;
      });
      return;
    }
    setState(() {
      _upiAddrError = null;
    });

    final transactionRef = Random.secure().nextInt(1 << 32).toString();
    print("Starting transaction with id $transactionRef");

    final a = await _upiPayPlugin.initiateTransaction(
      amount: _amountController.text,
      app: app.upiApplication,
      receiverName: 'Sharad',
      receiverUpiAddress: _upiAddressController.text,
      transactionRef: transactionRef,
      transactionNote: 'UPI Payment',
      // merchantCode: '7372',
    );

    print(a);
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.symmetric(horizontal: 16),
      child: ListView(
        children: <Widget>[
          _vpa(),
          if (_upiAddrError != null) _vpaError(),
          _amount(),
          if (Platform.isIOS) _submitButton(),
          Platform.isAndroid ? _androidApps() : _iosApps(),
        ],
      ),
    );
  }

  Widget _vpa() {
    return Container(
      margin: EdgeInsets.only(top: 32),
      child: Row(
        children: <Widget>[
          Expanded(
            child: TextFormField(
              controller: _upiAddressController,
              enabled: _isUpiEditable,
              decoration: InputDecoration(
                border: OutlineInputBorder(),
                hintText: 'address@upi',
                labelText: 'Receiving UPI Address',
              ),
            ),
          ),
          Container(
            margin: EdgeInsets.only(left: 8),
            child: IconButton(
              icon: Icon(
                _isUpiEditable ? Icons.check : Icons.edit,
              ),
              onPressed: () {
                setState(() {
                  _isUpiEditable = !_isUpiEditable;
                });
              },
            ),
          ),
        ],
      ),
    );
  }

  Widget _vpaError() {
    return Container(
      margin: EdgeInsets.only(top: 4, left: 12),
      child: Text(
        _upiAddrError!,
        style: TextStyle(color: Colors.red),
      ),
    );
  }

  Widget _amount() {
    return Container(
      margin: EdgeInsets.only(top: 32),
      child: Row(
        children: <Widget>[
          Expanded(
            child: TextField(
              controller: _amountController,
              readOnly: true,
              enabled: false,
              decoration: InputDecoration(
                border: OutlineInputBorder(),
                labelText: 'Amount',
              ),
            ),
          ),
          Container(
            margin: EdgeInsets.only(left: 8),
            child: IconButton(
              icon: Icon(Icons.loop),
              onPressed: _generateAmount,
            ),
          ),
        ],
      ),
    );
  }

  Widget _submitButton() {
    return Container(
      margin: EdgeInsets.only(top: 32),
      child: Row(
        children: <Widget>[
          Expanded(
            child: MaterialButton(
              onPressed: () async => await _onTap(_apps![0]),
              child: Text('Initiate Transaction',
                  style: Theme.of(context)
                      .textTheme
                      .bodyMedium!
                      .copyWith(color: Colors.white)),
              color: Theme.of(context).primaryColor,
              height: 48,
              shape: RoundedRectangleBorder(
                  borderRadius: BorderRadius.circular(6)),
            ),
          ),
        ],
      ),
    );
  }

  Widget _androidApps() {
    return Container(
      margin: EdgeInsets.only(top: 32, bottom: 32),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          Container(
            margin: EdgeInsets.only(bottom: 12),
            child: Text(
              'Pay Using',
              style: Theme.of(context).textTheme.bodyMedium,
            ),
          ),
          if (_apps != null) _appsGrid(_apps!.map((e) => e).toList()),
        ],
      ),
    );
  }

  Widget _iosApps() {
    return Container(
      margin: EdgeInsets.only(top: 32, bottom: 32),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          Container(
            margin: EdgeInsets.only(bottom: 24),
            child: Text(
              'One of these will be invoked automatically by your phone to '
              'make a payment',
              style: Theme.of(context).textTheme.bodySmall,
            ),
          ),
          Container(
            margin: EdgeInsets.only(bottom: 12),
            child: Text(
              'Detected Installed Apps',
              style: Theme.of(context).textTheme.bodyMedium,
            ),
          ),
          if (_apps != null) _discoverableAppsGrid(),
          Container(
            margin: EdgeInsets.only(top: 12, bottom: 12),
            child: Text(
              'Other Supported Apps (Cannot detect)',
              style: Theme.of(context).textTheme.bodyMedium,
            ),
          ),
          if (_apps != null) _nonDiscoverableAppsGrid(),
        ],
      ),
    );
  }

  GridView _discoverableAppsGrid() {
    List<ApplicationMeta> metaList = [];
    _apps!.forEach((e) {
      if (e.upiApplication.discoveryCustomScheme != null) {
        metaList.add(e);
      }
    });
    return _appsGrid(metaList);
  }

  GridView _nonDiscoverableAppsGrid() {
    List<ApplicationMeta> metaList = [];
    _apps!.forEach((e) {
      if (e.upiApplication.discoveryCustomScheme == null) {
        metaList.add(e);
      }
    });
    return _appsGrid(metaList);
  }

  GridView _appsGrid(List<ApplicationMeta> apps) {
    apps.sort((a, b) => a.upiApplication
        .getAppName()
        .toLowerCase()
        .compareTo(b.upiApplication.getAppName().toLowerCase()));
    return GridView.count(
      crossAxisCount: 4,
      shrinkWrap: true,
      mainAxisSpacing: 4,
      crossAxisSpacing: 4,
      physics: NeverScrollableScrollPhysics(),
      children: apps
          .map(
            (it) => Material(
              key: ObjectKey(it.upiApplication),
              child: InkWell(
                onTap: Platform.isAndroid ? () async => await _onTap(it) : null,
                child: Column(
                  mainAxisSize: MainAxisSize.min,
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: <Widget>[
                    it.iconImage(48),
                    Container(
                      margin: EdgeInsets.only(top: 4),
                      alignment: Alignment.center,
                      child: Text(
                        it.upiApplication.getAppName(),
                        textAlign: TextAlign.center,
                      ),
                    ),
                  ],
                ),
              ),
            ),
          )
          .toList(),
    );
  }
}

String? _validateUpiAddress(String value) {
  if (value.isEmpty) {
    return 'UPI VPA is required.';
  }
  if (value.split('@').length != 2) {
    return 'Invalid UPI VPA';
  }
  return null;
}

以上代码展示了如何创建一个简单的Flutter应用来演示 upi_pay 插件的基本功能。希望这些信息能帮助您快速上手并成功集成UPI支付功能到您的Flutter项目中。


更多关于Flutter统一支付接口插件upi_pay的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter统一支付接口插件upi_pay的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


当然,下面是一个关于如何在Flutter项目中使用upi_pay插件来实现统一支付接口的示例代码。这个插件假设已经支持了多种支付渠道,并且已经集成到你的Flutter项目中。

首先,确保你已经在pubspec.yaml文件中添加了upi_pay依赖:

dependencies:
  flutter:
    sdk: flutter
  upi_pay: ^x.y.z  # 请替换为实际的版本号

然后运行flutter pub get来安装依赖。

接下来,你需要在你的Flutter项目中初始化并使用upi_pay插件。以下是一个简单的示例,展示如何调用支付接口:

import 'package:flutter/material.dart';
import 'package:upi_pay/upi_pay.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('UPI Pay Example'),
        ),
        body: Center(
          child: ElevatedButton(
            onPressed: () => _initiatePayment(),
            child: Text('Initiate Payment'),
          ),
        ),
      ),
    );
  }

  Future<void> _initiatePayment() async {
    try {
      // 初始化 UPI Pay 配置
      final UpiPayConfig config = UpiPayConfig(
        environment: UpiPayEnvironment.sandbox, // 使用沙盒环境,生产环境请替换为 UpiPayEnvironment.production
        merchantId: 'your_merchant_id',         // 替换为你的商户ID
        key: 'your_key',                        // 替换为你的密钥
        returnUrl: 'your_return_url',           // 替换为你的回调URL
      );

      // 设置支付参数
      final UpiPayParams params = UpiPayParams(
        amount: 100.0,          // 支付金额,单位通常是元
        currency: 'CNY',        // 货币类型
        orderId: 'order_12345', // 订单ID
        description: 'Test Order', // 订单描述
      );

      // 调用支付接口
      final UpiPayResult result = await UpiPay.pay(config: config, params: params);

      // 处理支付结果
      if (result.status == UpiPayStatus.success) {
        // 支付成功
        showDialog(
          context: context,
          builder: (BuildContext context) {
            return AlertDialog(
              title: Text('Payment Successful'),
              content: Text('Transaction ID: ${result.transactionId}'),
              actions: <Widget>[
                TextButton(
                  onPressed: () => Navigator.of(context).pop(),
                  child: Text('OK'),
                ),
              ],
            );
          },
        );
      } else if (result.status == UpiPayStatus.failed) {
        // 支付失败
        showDialog(
          context: context,
          builder: (BuildContext context) {
            return AlertDialog(
              title: Text('Payment Failed'),
              content: Text('Error: ${result.errorMessage}'),
              actions: <Widget>[
                TextButton(
                  onPressed: () => Navigator.of(context).pop(),
                  child: Text('OK'),
                ),
              ],
            );
          },
        );
      } else if (result.status == UpiPayStatus.canceled) {
        // 支付取消
        showDialog(
          context: context,
          builder: (BuildContext context) {
            return AlertDialog(
              title: Text('Payment Canceled'),
              content: Text('Payment was canceled by the user.'),
              actions: <Widget>[
                TextButton(
                  onPressed: () => Navigator.of(context).pop(),
                  child: Text('OK'),
                ),
              ],
            );
          },
        );
      }
    } catch (e) {
      // 处理异常
      showDialog(
        context: context,
        builder: (BuildContext context) {
          return AlertDialog(
            title: Text('Error'),
            content: Text('An error occurred: $e'),
            actions: <Widget>[
              TextButton(
                onPressed: () => Navigator.of(context).pop(),
                child: Text('OK'),
              ),
            ],
          );
        },
      );
    }
  }
}

在这个示例中,我们创建了一个简单的Flutter应用,其中包含一个按钮,用于触发支付流程。我们定义了支付配置和支付参数,并调用UpiPay.pay方法来启动支付。支付完成后,我们根据返回的状态显示相应的对话框。

请注意,你需要将your_merchant_idyour_keyyour_return_url替换为你自己的实际值。同时,确保你已经正确配置了支付渠道和相关的密钥。

希望这个示例能够帮助你理解如何在Flutter项目中使用upi_pay插件来实现统一支付接口。

回到顶部