mpay_plugin
A Flutter plugin that integrates Macau Pass (MPay) payment into Android and iOS apps. In addition to the MPay unified channel, the plugin exposes direct Alipay and WeChat Pay (including Hong Kong WeChat Wallet) as first-class payment paths.
Features
| Feature | Android | iOS |
|---|---|---|
| MPay unified channel (MPay / Alipay / WeChat) | ✅ | ✅ |
| Direct Alipay payment | ✅ | ✅ |
| Direct WeChat Pay (mainland) | ✅ | ✅ |
| Hong Kong WeChat Wallet | ✅ | ✅ |
| Scan-to-pay QR code (native rendering) | ✅ | ✅ |
| Alipay sandbox environment | ✅ | — |
| MPay SIT / UAT / Production switching | ✅ | ✅ |
Architecture
The plugin follows the Adapter pattern. On each platform, a single
PayChannelAdapter interface abstracts the differences between the vendor
SDKs (MPay OpenSDK, Alipay SDK, WeChat Open SDK), and one adapter exists per
payment channel:
| Channel | Android | iOS |
|---|---|---|
| MPay unified channel | MPayAdapter + MPayHandler |
MPayAdapter |
| Direct Alipay | AliPayAdapter |
AliPayAdapter |
| Direct WeChat Pay | WeChatPayAdapter + WXAPIEventHandler |
WeChatPayAdapter + WXAPIEventHandler |
| Hong Kong WeChat Wallet | HongKongWalletAdapter |
HongKongWalletAdapter |
| Scan-to-pay QR code | QrCodeAdapter |
QrCodeAdapter |
MpayPlugin itself is a thin dispatcher: method-channel calls are routed to
the matching adapter through PayChannelAdapterRegistry, and payment result
text mapping lives in a shared PayResultMapper. Vendor-specific code stays
isolated, so adding a new payment channel only requires one new adapter class.
The Dart public API and the method-channel protocol are unchanged.
Installation
dependencies:
mpay_plugin: ^1.3.0
Android Setup
1. AndroidManifest.xml
The plugin registers a transparent WXEntryActivity / WXPayEntryActivity automatically
via its own manifest merge. No manual Activity declaration is needed.
Add the following permissions if not already present:
<uses-permission android:name="android.permission.INTERNET" />
2. WeChat payment callback
WeChat calls back to <your.package>.wxapi.WXPayEntryActivity. The plugin ships an
activity-alias that routes this to its internal WechatCallbackActivity, so no
extra Kotlin/Java file is required. Ensure applicationId is set correctly in
android/app/build.gradle.
3. ProGuard / R8
If you use code shrinking, add the following rules:
-keep class com.tencent.mm.opensdk.** { *; }
-keep class com.alipay.sdk.** { *; }
-keep class com.macau.pay.sdk.** { *; }
iOS Setup
1. URL Schemes
In Xcode → Target → Info → URL Types, register:
| Identifier | Schemes |
|---|---|
wx<your_wechat_appid> |
|
| Alipay | <your_app_bundle_id> (or custom scheme passed to aliPay) |
| MPay | <your_mpay_scheme> (provided by Macau Pass) |
2. LSApplicationQueriesSchemes
Add the following to Info.plist so the app can query whether WeChat and Alipay are
installed:
<key>LSApplicationQueriesSchemes</key>
<array>
<string>weixin</string>
<string>weixinULAPI</string>
<string>alipay</string>
<string>alipays</string>
</array>
3. Universal Link (WeChat only)
WeChat requires a Universal Link for iOS 9+ deep-linking. Pass it when registering:
await _mpay.registerApi(
appId: 'wx1234567890abcdef',
universalLink: 'https://your.domain/app/', // must start with https
);
Configure the corresponding apple-app-site-association file on your web server.
4. AppDelegate
In your AppDelegate.swift (or AppDelegate.m), make sure GeneratedPluginRegistrant
is called before anything else — Flutter handles this automatically for new projects.
Quick Start
import 'package:mpay_plugin/mpay_plugin.dart';
final _mpay = MpayPlugin();
// 1. Initialise environments (call once, e.g. in initState or main)
await _mpay.init(
aliPayEnv: AliPayEnv.ONLINE, // ONLINE | SANDBOX (Android only)
mPayEnv: MPayEnv.PRODUCTION, // PRODUCTION | SIT | UAT
);
// 2. Register WeChat (required before any WeChat payment)
final registered = await _mpay.registerApi(
appId: 'wx1234567890abcdef',
universalLink: 'https://your.domain/app/', // iOS only
);
API Reference
init
Configure the Alipay and MPay runtime environments. Call once at app startup.
await _mpay.init(
aliPayEnv: AliPayEnv.ONLINE, // default
mPayEnv: MPayEnv.PRODUCTION, // default
);
| Parameter | Type | Description |
|---|---|---|
aliPayEnv |
AliPayEnv |
ONLINE (production) or SANDBOX (Android only) |
mPayEnv |
MPayEnv |
PRODUCTION, SIT, or UAT |
registerApi
Register your WeChat AppID. Must be called before any WeChat payment.
final bool ok = await _mpay.registerApi(
appId: 'wx1234567890abcdef',
universalLink: 'https://your.domain/app/',
doOnIOS: true, // default
doOnAndroid: true, // default
);
| Parameter | Type | Description |
|---|---|---|
appId |
String |
WeChat Open Platform AppID |
universalLink |
String? |
Required on iOS; must start with https |
doOnIOS |
bool |
Whether to register on iOS (default true) |
doOnAndroid |
bool |
Whether to register on Android (default true) |
mPay — MPay unified channel
Routes payment through the MPay SDK, which internally handles MPay, Alipay, or WeChat
Pay based on the channel argument.
final ResultModel result = await _mpay.mPay(
signedOrderData, // server-signed order JSON string
PayChannel.mPay, // mPay | aliPay | wechatPay
withScheme: 'myapp', // iOS URL scheme registered for your app
);
if (result.resultStatus == '9000') {
// success
}
| Parameter | Type | Description |
|---|---|---|
data |
String? |
Server-signed order data from your back-end |
channel |
PayChannel |
mPay, aliPay, or wechatPay |
withScheme |
String? |
iOS URL scheme (required on iOS) |
aliPay — Direct Alipay
Calls the Alipay SDK directly, bypassing the MPay channel.
final ResultModel result = await _mpay.aliPay(
payInfo, // server-signed order string (Alipay format)
'myapp', // iOS URL scheme
);
| Parameter | Type | Description |
|---|---|---|
payInfo |
String |
Alipay order string from your server |
setIosUrlSchema |
String |
iOS URL scheme for Alipay callback |
wechatPay — Direct WeChat Pay
Call registerApi first. Uses a sealed PayType class to distinguish payment modes.
Mainland WeChat Pay
final ResultModel result = await _mpay.wechatPay(
Payment(
appId: 'wx1234567890abcdef',
partnerId: '1234567890',
prepayId: 'wx20240101000000xxxxxxxxxxxxxxxx',
packageValue: 'Sign=WXPay',
nonceStr: 'random32chars',
timestamp: 1717000000, // Unix timestamp (UInt32)
sign: 'ABCDEF...',
signType: 'RSA', // optional, default MD5
extData: null, // optional
),
);
Hong Kong WeChat Wallet
final ResultModel result = await _mpay.wechatPay(
HongKongWallet(prepayId: 'hk_token_from_server'),
);
ResultModel
All payment methods return a ResultModel:
class ResultModel {
final String? resultStatus; // '9000' = success; other = error/cancel
final String? result; // human-readable description
final String? memo; // extra info / error detail
final String? type; // 'mPay' | 'aliPay' | 'WeChatPay' | 'HKWallet' | ...
}
Common resultStatus values:
| Value | Meaning |
|---|---|
9000 |
Payment successful |
6001 |
User cancelled |
5000 |
Result unknown (query your server) |
-1 |
Payment error / exception |
-2 |
WeChat user cancelled |
Enums
enum AliPayEnv { ONLINE, SANDBOX } // SANDBOX is Android-only
enum MPayEnv { PRODUCTION, SIT, UAT }
enum PayChannel { mPay, aliPay, wechatPay }
Scan-to-Pay (QR code)
For scan-to-pay (the customer scans a QR shown in your app with another device), the QR payload is issued by your merchant server — the client SDKs provide no merchant-QR generation API:
- Alipay: call
alipay.trade.precreatewithproduct_code=QR_CODE_OFFLINE; the response contains theqr_codestring (e.g.https://qr.alipay.com/bax...). - WeChat Pay: call the unified order API (
trade_type=NATIVE); the response contains thecode_urlstring (e.g.weixin://wxpay/bizpayurl?pr=...).
The plugin renders that string as a QR image natively (ZXing on Android,
CoreImage CIQRCodeGenerator on iOS — no Dart-side QR library):
import 'package:mpay_plugin/qr_pay.dart';
// One-liner: renders the QR with loading/error states built in.
QrPayView(code: qrCodeFromServer, size: 240)
// Or fetch raw PNG bytes and render them yourself:
final Uint8List? png = await _mpay.generateQrCode(qrCodeFromServer, size: 240);
size is clamped to 64..1024 on the native side; the QR is black-on-white with
error-correction level M. No init() call is required — generation is purely local.
Important: confirming the payment result (polling
alipay.trade.query/ WeChat order query, or handling async notifications) is the merchant server's responsibility. The client only displays the QR code and never receives the payment receipt.
Error Handling
try {
final result = await _mpay.wechatPay(Payment(...));
switch (result.resultStatus) {
case '9000': /* success */ break;
case '6001': /* cancelled by user */ break;
default: /* failure — check result.memo */ break;
}
} on PlatformException catch (e) {
// Plugin-level error (e.g. no Activity, WeChat not registered)
debugPrint('Payment error: ${e.code} — ${e.message}');
}
Notes
- WeChat registration must happen before the first
wechatPaycall; callingwechatPaywithout registering will return aPlatformExceptionwith codeWXAPI_NOT_REGISTERED. - Alipay sandbox is only supported on Android. iOS always uses the production
Alipay SDK regardless of
aliPayEnv. - For the Hong Kong WeChat Wallet flow, the initial call returns once WeChat has confirmed receipt of the payment request. The actual payment result (success/failure) is handled inside the WeChat app; your server should query the order status afterwards.
- The plugin requires iOS 12.0+ and Android API 21+.