From 4d439827dd831a4bdd7a9a87ff1fbbb459ae5073 Mon Sep 17 00:00:00 2001 From: lokesh-univest Date: Mon, 5 Jan 2026 13:45:26 +0530 Subject: [PATCH 1/5] Add support for UPI mandate transactions and enhance app discovery --- README.md | 27 +++++++++++++++++-- .../trident/flutter_upi_india/UpiPayPlugin.kt | 17 ++++++++---- lib/src/api.dart | 9 +++++++ lib/src/discovery.dart | 16 ++++++++++- lib/src/method_channel.dart | 16 +++++++---- lib/src/transaction_details.dart | 6 ++++- 6 files changed, 77 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 372d514..59b9da7 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,14 @@ In `Runner/Info.plist` add or modify the `LSApplicationQueriesSchemes` key so it #### Get list of installed apps ```dart -final List appMetaList = await UpiPay.getInstalledUpiApps(); +final List appMetaList = await UpiPay.getInstalledUpiApplications(); +``` + +Filter for mandate-capable apps: + +```dart +final List mandateApps = + await UpiPay.getInstalledUpiApplications(isForMandateApps: true); ``` #### Show an app's details @@ -96,6 +103,22 @@ Future doUpiTransation(ApplicationMeta appMeta) { } ``` +Send a mandate request (uses `upi://mandate`): + +```dart +Future doUpiMandate(ApplicationMeta appMeta) { + return UpiPay.initiateTransaction( + amount: '100.00', + app: appMeta.application, + receiverName: 'John Doe', + receiverUpiAddress: 'john@doe', + transactionRef: 'UPIMANDATE0001', + transactionNote: 'UPI Mandate', + isForMandate: true, + ); +} +``` + ## Behaviour, Limitations & Measures ### Android @@ -116,7 +139,7 @@ It is advised that you implement a server-side payment verification on top of th #### Flow - On iOS, the [UPI Deep Linking And Proximity Integration Specification](https://github.com/reeteshranjan/upi_pay/files/6338127/UPI.Linking.Specs_ver.1.6.pdf) is implemented using iOS custom schemes. -- Each UPI payment app can listen to a payment request of the form `upi://pay?...` sent by a caller app to iOS. +- Each UPI payment app can listen to a payment request of the form `upi://pay?...` or `upi://mandate?...` sent by a caller app to iOS. - The specification does not let you specify the target app's identifier in this request. On iOS, there is no other disambiguation measure available such as any ordering of the UPI payment apps that can be retrieved using any iOS APIs. Hence, it's impossible to know which UPI payment app will be invoked. - One of the applicable apps gets invoked and it processes the payment. The custom schemes mechanism has no way to return a transaction status to your calling code. The calling code can only know if a UPI payment app was launched successfully or not. diff --git a/android/src/main/kotlin/com/trident/flutter_upi_india/UpiPayPlugin.kt b/android/src/main/kotlin/com/trident/flutter_upi_india/UpiPayPlugin.kt index ee1fe52..cc7c68c 100644 --- a/android/src/main/kotlin/com/trident/flutter_upi_india/UpiPayPlugin.kt +++ b/android/src/main/kotlin/com/trident/flutter_upi_india/UpiPayPlugin.kt @@ -40,7 +40,7 @@ class UpiPayPlugin: FlutterPlugin, MethodCallHandler, ActivityAware, PluginRegis when (call.method) { "initiateTransaction" -> this.initiateTransaction(call) - "getInstalledUpiApps" -> this.getInstalledUpiApps() + "getInstalledUpiApps" -> this.getInstalledUpiApps(call) else -> result.notImplemented() } } @@ -55,6 +55,7 @@ class UpiPayPlugin: FlutterPlugin, MethodCallHandler, ActivityAware, PluginRegis val am: String? = call.argument("am") val cu: String? = call.argument("cu") val url: String? = call.argument("url") + val isForMandate: Boolean? = call.argument("isForMandate") try { /* @@ -63,7 +64,8 @@ class UpiPayPlugin: FlutterPlugin, MethodCallHandler, ActivityAware, PluginRegis * 'abc 40upi' by these apps. The URI building logic is changed to avoid URL encoding * of the value of 'pa' parameter. - Prince */ - var uriStr: String? = "upi://pay?pa=" + pa + + val authority = if (isForMandate == true) "mandate" else "pay" + var uriStr: String? = "upi://$authority?pa=" + pa + "&pn=" + Uri.encode(pn) + "&tr=" + Uri.encode(tr) + "&am=" + Uri.encode(am) + @@ -97,9 +99,14 @@ class UpiPayPlugin: FlutterPlugin, MethodCallHandler, ActivityAware, PluginRegis } } - private fun getInstalledUpiApps() { + private fun getInstalledUpiApps(call: MethodCall) { + val isForMandateApps: Boolean? = call.argument("isForMandateApps") val uriBuilder = Uri.Builder() - uriBuilder.scheme("upi").authority("pay") + if(isForMandateApps == true){ + uriBuilder.scheme("upi").authority("mandate") + } else { + uriBuilder.scheme("upi").authority("pay") + } val uri = uriBuilder.build() val intent = Intent(Intent.ACTION_VIEW, uri) @@ -197,4 +204,4 @@ class UpiPayPlugin: FlutterPlugin, MethodCallHandler, ActivityAware, PluginRegis activity = null } -} \ No newline at end of file +} diff --git a/lib/src/api.dart b/lib/src/api.dart index 6e17b86..bb4fa29 100644 --- a/lib/src/api.dart +++ b/lib/src/api.dart @@ -52,6 +52,8 @@ class UpiPay { /// [UPI Linking Specification](https://www.npci.org.in/sites/default/files/UPI%20Linking%20Specs_ver%201.6.pdf). /// /// [url]: See `url` parameter in [UPI Linking Specification](https://www.npci.org.in/sites/default/files/UPI%20Linking%20Specs_ver%201.6.pdf) + /// + /// [isForMandate] switches the request to use the `upi://mandate` authority. static Future initiateTransaction({ required UpiApplication app, required String receiverUpiAddress, @@ -61,6 +63,7 @@ class UpiPay { String? url, String? merchantCode, String? transactionNote, + bool isForMandate = false, }) async { final transactionDetails = TransactionDetails( upiApplication: app, @@ -71,6 +74,7 @@ class UpiPay { url: url, merchantCode: merchantCode, transactionNote: transactionNote, + isForMandate: isForMandate, ); return await _transactionHelper.transact(_channel, transactionDetails); } @@ -94,11 +98,15 @@ class UpiPay { /// /// [paymentType] must be [UpiApplicationDiscoveryAppPaymentType.nonMerchant] /// for now. Setting it to any other value will lead to [UnsupportedError]. + /// + /// [isForMandateApps] filters apps that advertise UPI mandate support where + /// platform discovery supports it. static Future> getInstalledUpiApplications({ UpiApplicationDiscoveryAppPaymentType paymentType = UpiApplicationDiscoveryAppPaymentType.nonMerchant, UpiApplicationDiscoveryAppStatusType statusType = UpiApplicationDiscoveryAppStatusType.working, + bool isForMandateApps = false, }) async { if (paymentType != UpiApplicationDiscoveryAppPaymentType.nonMerchant) { throw UnsupportedError('The parameter `paymentType` must be ' @@ -112,6 +120,7 @@ class UpiPay { applicationStatusMap: _upiApplicationStatuses, paymentType: paymentType, statusType: statusType, + isForMandateApps: isForMandateApps, ); } diff --git a/lib/src/discovery.dart b/lib/src/discovery.dart index a2751f8..1e948ec 100644 --- a/lib/src/discovery.dart +++ b/lib/src/discovery.dart @@ -26,6 +26,7 @@ class UpiApplicationDiscovery implements _PlatformDiscoveryBase { UpiApplicationDiscoveryAppPaymentType.nonMerchant, UpiApplicationDiscoveryAppStatusType statusType = UpiApplicationDiscoveryAppStatusType.working, + bool isForMandateApps = false, }) async { if (io.Platform.isAndroid || io.Platform.isIOS) { return await discovery!.discover( @@ -33,6 +34,7 @@ class UpiApplicationDiscovery implements _PlatformDiscoveryBase { applicationStatusMap: applicationStatusMap, paymentType: paymentType, statusType: statusType, + isForMandateApps: isForMandateApps, ); } throw UnsupportedError('Discovery is available only on Android and iOS'); @@ -54,8 +56,11 @@ class _AndroidDiscovery implements _PlatformDiscoveryBase { UpiApplicationDiscoveryAppPaymentType.nonMerchant, UpiApplicationDiscoveryAppStatusType statusType = UpiApplicationDiscoveryAppStatusType.working, + bool isForMandateApps = false, }) async { - final appsList = await upiMethodChannel.getInstalledUpiApps(); + final appsList = await upiMethodChannel.getInstalledUpiApps( + isForMandateApps: isForMandateApps, + ); if (appsList == null) return []; final List retList = []; appsList.forEach((app) { @@ -127,7 +132,15 @@ class _IosDiscovery implements _PlatformDiscoveryBase { UpiApplicationDiscoveryAppPaymentType.nonMerchant, UpiApplicationDiscoveryAppStatusType statusType = UpiApplicationDiscoveryAppStatusType.working, + bool isForMandateApps = false, }) async { + if (isForMandateApps) { + final bool? supportsMandate = + await upiMethodChannel.canLaunch('upi://mandate'); + if (supportsMandate != true) { + return []; + } + } Map discoveryMap = {}; List discovered = []; applicationStatusMap.forEach((app, status) { @@ -203,6 +216,7 @@ abstract class _PlatformDiscoveryBase { UpiApplicationDiscoveryAppPaymentType.nonMerchant, UpiApplicationDiscoveryAppStatusType statusType = UpiApplicationDiscoveryAppStatusType.working, + bool isForMandateApps = false, }); } diff --git a/lib/src/method_channel.dart b/lib/src/method_channel.dart index d7ec5c2..d773636 100644 --- a/lib/src/method_channel.dart +++ b/lib/src/method_channel.dart @@ -28,19 +28,25 @@ class UpiMethodChannel { throw UnsupportedError('The `launch` call is supported only on iOS'); } - Future>?> getInstalledUpiApps() async { + Future>?> getInstalledUpiApps({ + bool isForMandateApps = false, + }) async { if (io.Platform.isAndroid) { return await _channel - .invokeListMethod>('getInstalledUpiApps'); + .invokeListMethod>('getInstalledUpiApps', { + 'isForMandateApps': isForMandateApps, + }); } throw UnsupportedError('The `getInstalledUpiApps` call is supported only ' 'on Android'); } - Future canLaunch(String scheme) async { + Future canLaunch(String uriOrScheme) async { if (io.Platform.isIOS) { - return await _channel - .invokeMethod('canLaunch', {'uri': scheme + "://"}); + final uri = uriOrScheme.contains('://') + ? uriOrScheme + : uriOrScheme + '://'; + return await _channel.invokeMethod('canLaunch', {'uri': uri}); } throw UnsupportedError('The `canLaunch` call is supported only on iOS'); } diff --git a/lib/src/transaction_details.dart b/lib/src/transaction_details.dart index b19f767..68244c7 100644 --- a/lib/src/transaction_details.dart +++ b/lib/src/transaction_details.dart @@ -15,6 +15,7 @@ class TransactionDetails { final String? url; final String? merchantCode; final String? transactionNote; + final bool isForMandate; TransactionDetails({ required this.upiApplication, @@ -26,6 +27,7 @@ class TransactionDetails { this.url, this.merchantCode = '', this.transactionNote = 'UPI Transaction', + this.isForMandate = false, }) : amount = Decimal.parse(amount) { if (!_checkIfUpiAddressIsValid(payeeAddress)) { throw InvalidUpiAddressException(); @@ -56,11 +58,13 @@ class TransactionDetails { 'url': url, 'mc': merchantCode, 'tn': transactionNote, + 'isForMandate': isForMandate, }; } String toString() { - String uri = 'upi://pay?pa=$payeeAddress' + final authority = isForMandate ? 'mandate' : 'pay'; + String uri = 'upi://$authority?pa=$payeeAddress' '&pn=${Uri.encodeComponent(payeeName)}' '&tr=$transactionRef' '&tn=${Uri.encodeComponent(transactionNote!)}' From dae7cc913973a101193fe6652e620ec69c157853 Mon Sep 17 00:00:00 2001 From: lokesh-univest Date: Mon, 5 Jan 2026 19:07:36 +0530 Subject: [PATCH 2/5] Add mandate support with additional parameters for UPI transactions --- .../trident/flutter_upi_india/UpiPayPlugin.kt | 67 +++++++++++++++++- lib/src/api.dart | 44 ++++++++++++ lib/src/transaction_details.dart | 69 ++++++++++++++++++- 3 files changed, 178 insertions(+), 2 deletions(-) diff --git a/android/src/main/kotlin/com/trident/flutter_upi_india/UpiPayPlugin.kt b/android/src/main/kotlin/com/trident/flutter_upi_india/UpiPayPlugin.kt index cc7c68c..77dd7cf 100644 --- a/android/src/main/kotlin/com/trident/flutter_upi_india/UpiPayPlugin.kt +++ b/android/src/main/kotlin/com/trident/flutter_upi_india/UpiPayPlugin.kt @@ -57,6 +57,22 @@ class UpiPayPlugin: FlutterPlugin, MethodCallHandler, ActivityAware, PluginRegis val url: String? = call.argument("url") val isForMandate: Boolean? = call.argument("isForMandate") + // Mandate-specific parameters + val amrule: String? = call.argument("amrule") + val block: String? = call.argument("block") + val mn: String? = call.argument("mn") + val mode: String? = call.argument("mode") + val orgid: String? = call.argument("orgid") + val purpose: String? = call.argument("purpose") + val recur: String? = call.argument("recur") + val recurtype: String? = call.argument("recurtype") + val recurvalue: String? = call.argument("recurvalue") + val rev: String? = call.argument("rev") + val tid: String? = call.argument("tid") + val txnType: String? = call.argument("txnType") + val validitystart: String? = call.argument("validitystart") + val validityend: String? = call.argument("validityend") + try { /* * Some UPI apps extract incorrect format VPA due to url encoding of `pa` parameter. @@ -79,7 +95,56 @@ class UpiPayPlugin: FlutterPlugin, MethodCallHandler, ActivityAware, PluginRegis if(tn != null) { uriStr += ("&tn=" + Uri.encode(tn)) } - uriStr += "&mode=00" // &orgid=000000" + + // Add mandate-specific parameters if available + if(isForMandate == true) { + if(amrule != null) { + uriStr += ("&amrule=" + Uri.encode(amrule)) + } + if(block != null) { + uriStr += ("&block=" + Uri.encode(block)) + } + if(mn != null) { + uriStr += ("&mn=" + Uri.encode(mn)) + } + if(mode != null) { + uriStr += ("&mode=" + Uri.encode(mode)) + } + if(orgid != null) { + uriStr += ("&orgid=" + Uri.encode(orgid)) + } + if(purpose != null) { + uriStr += ("&purpose=" + Uri.encode(purpose)) + } + if(recur != null) { + uriStr += ("&recur=" + Uri.encode(recur)) + } + if(recurtype != null) { + uriStr += ("&recurtype=" + Uri.encode(recurtype)) + } + if(recurvalue != null) { + uriStr += ("&recurvalue=" + Uri.encode(recurvalue)) + } + if(rev != null) { + uriStr += ("&rev=" + Uri.encode(rev)) + } + if(tid != null) { + uriStr += ("&tid=" + Uri.encode(tid)) + } + if(txnType != null) { + uriStr += ("&txnType=" + Uri.encode(txnType)) + } + if(validitystart != null) { + uriStr += ("&validitystart=" + Uri.encode(validitystart)) + } + if(validityend != null) { + uriStr += ("&validityend=" + Uri.encode(validityend)) + } + } else { + // For non-mandate transactions, add default mode if not provided + uriStr += "&mode=00" + } + val uri = Uri.parse(uriStr) Log.d("flutter_upi_india", "initiateTransaction URI: " + uri.toString()) diff --git a/lib/src/api.dart b/lib/src/api.dart index bb4fa29..0ba8255 100644 --- a/lib/src/api.dart +++ b/lib/src/api.dart @@ -54,6 +54,22 @@ class UpiPay { /// [url]: See `url` parameter in [UPI Linking Specification](https://www.npci.org.in/sites/default/files/UPI%20Linking%20Specs_ver%201.6.pdf) /// /// [isForMandate] switches the request to use the `upi://mandate` authority. + /// + /// Mandate-specific parameters: + /// [amountRule] - Amount rule (MAX, EXACT, etc.) + /// [blockFlag] - Block flag (Y/N) + /// [merchantName] - Merchant name or identifier + /// [mode] - Payment mode + /// [orgId] - Organization ID + /// [purpose] - Payment purpose code + /// [recurrence] - Recurrence pattern (ASPRESENTED, MONTHLY, etc.) + /// [recurrenceType] - Recurrence type (AFTER, BEFORE, etc.) + /// [recurrenceValue] - Recurrence value/count + /// [revocable] - Revocable flag (Y/N) + /// [transactionId] - Transaction ID + /// [txnType] - Transaction type (CREATE, REVOKE, etc.) + /// [validityStart] - Validity start date (DDMMYYYY) + /// [validityEnd] - Validity end date (DDMMYYYY) static Future initiateTransaction({ required UpiApplication app, required String receiverUpiAddress, @@ -64,6 +80,20 @@ class UpiPay { String? merchantCode, String? transactionNote, bool isForMandate = false, + String? amountRule, + String? blockFlag, + String? merchantName, + String? mode, + String? orgId, + String? purpose, + String? recurrence, + String? recurrenceType, + String? recurrenceValue, + String? revocable, + String? transactionId, + String? txnType, + String? validityStart, + String? validityEnd, }) async { final transactionDetails = TransactionDetails( upiApplication: app, @@ -75,6 +105,20 @@ class UpiPay { merchantCode: merchantCode, transactionNote: transactionNote, isForMandate: isForMandate, + amountRule: amountRule, + blockFlag: blockFlag, + merchantName: merchantName, + mode: mode, + orgId: orgId, + purpose: purpose, + recurrence: recurrence, + recurrenceType: recurrenceType, + recurrenceValue: recurrenceValue, + revocable: revocable, + transactionId: transactionId, + txnType: txnType, + validityStart: validityStart, + validityEnd: validityEnd, ); return await _transactionHelper.transact(_channel, transactionDetails); } diff --git a/lib/src/transaction_details.dart b/lib/src/transaction_details.dart index 68244c7..71078bc 100644 --- a/lib/src/transaction_details.dart +++ b/lib/src/transaction_details.dart @@ -17,6 +17,22 @@ class TransactionDetails { final String? transactionNote; final bool isForMandate; + // Mandate-specific parameters + final String? amountRule; + final String? blockFlag; + final String? merchantName; + final String? mode; + final String? orgId; + final String? purpose; + final String? recurrence; + final String? recurrenceType; + final String? recurrenceValue; + final String? revocable; + final String? transactionId; + final String? txnType; + final String? validityStart; + final String? validityEnd; + TransactionDetails({ required this.upiApplication, required this.payeeAddress, @@ -28,6 +44,20 @@ class TransactionDetails { this.merchantCode = '', this.transactionNote = 'UPI Transaction', this.isForMandate = false, + this.amountRule, + this.blockFlag, + this.merchantName, + this.mode, + this.orgId, + this.purpose, + this.recurrence, + this.recurrenceType, + this.recurrenceValue, + this.revocable, + this.transactionId, + this.txnType, + this.validityStart, + this.validityEnd, }) : amount = Decimal.parse(amount) { if (!_checkIfUpiAddressIsValid(payeeAddress)) { throw InvalidUpiAddressException(); @@ -48,7 +78,7 @@ class TransactionDetails { } Map toJson() { - return { + final json = { 'app': upiApplication.toString(), 'pa': payeeAddress, 'pn': payeeName, @@ -60,6 +90,24 @@ class TransactionDetails { 'tn': transactionNote, 'isForMandate': isForMandate, }; + + // Add mandate-specific parameters if available + if (amountRule != null) json['amrule'] = amountRule; + if (blockFlag != null) json['block'] = blockFlag; + if (merchantName != null) json['mn'] = merchantName; + if (mode != null) json['mode'] = mode; + if (orgId != null) json['orgid'] = orgId; + if (purpose != null) json['purpose'] = purpose; + if (recurrence != null) json['recur'] = recurrence; + if (recurrenceType != null) json['recurtype'] = recurrenceType; + if (recurrenceValue != null) json['recurvalue'] = recurrenceValue; + if (revocable != null) json['rev'] = revocable; + if (transactionId != null) json['tid'] = transactionId; + if (txnType != null) json['txnType'] = txnType; + if (validityStart != null) json['validitystart'] = validityStart; + if (validityEnd != null) json['validityend'] = validityEnd; + + return json; } String toString() { @@ -76,6 +124,25 @@ class TransactionDetails { if (merchantCode!.isNotEmpty) { uri += '&mc=${Uri.encodeComponent(merchantCode!)}'; } + + // Add mandate-specific parameters if available + if (isForMandate) { + if (amountRule != null) uri += '&amrule=$amountRule'; + if (blockFlag != null) uri += '&block=$blockFlag'; + if (merchantName != null) uri += '&mn=${Uri.encodeComponent(merchantName!)}'; + if (mode != null) uri += '&mode=$mode'; + if (orgId != null) uri += '&orgid=$orgId'; + if (purpose != null) uri += '&purpose=$purpose'; + if (recurrence != null) uri += '&recur=$recurrence'; + if (recurrenceType != null) uri += '&recurtype=$recurrenceType'; + if (recurrenceValue != null) uri += '&recurvalue=$recurrenceValue'; + if (revocable != null) uri += '&rev=$revocable'; + if (transactionId != null) uri += '&tid=$transactionId'; + if (txnType != null) uri += '&txnType=$txnType'; + if (validityStart != null) uri += '&validitystart=$validityStart'; + if (validityEnd != null) uri += '&validityend=$validityEnd'; + } + return uri; } } From 3a57ca44b115fbbe17a4c0d54aee15b6ef157d23 Mon Sep 17 00:00:00 2001 From: lokesh-univest Date: Thu, 22 Jan 2026 18:00:31 +0530 Subject: [PATCH 3/5] Refactor UPI application discovery schemes to lowercase and update URI scheme handling in transaction details --- lib/src/applications.dart | 20 ++++++++++---------- lib/src/transaction_details.dart | 8 +++++++- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/lib/src/applications.dart b/lib/src/applications.dart index 441f139..3e90763 100644 --- a/lib/src/applications.dart +++ b/lib/src/applications.dart @@ -81,7 +81,7 @@ class UpiApplication { androidPackageName: 'in.org.npci.upiapp', iosBundleId: 'in.org.npci.ios.upiapp', appName: 'BHIM', - discoveryCustomScheme: 'BHIM', + discoveryCustomScheme: 'bhim', ); /// MiPay from Xiomi @@ -95,7 +95,7 @@ class UpiApplication { androidPackageName: 'in.amazon.mShop.android.shopping', iosBundleId: 'com.amazon.AmazonIN', appName: 'Amazon Pay', - discoveryCustomScheme: 'com.amazon.mobile.shopping', + discoveryCustomScheme: 'amazonpay', ); /// Truecaller @@ -146,7 +146,7 @@ class UpiApplication { androidPackageName: 'com.fisglobal.bandhanupi.app', iosBundleId: 'com.bandhan.bandhanupi', appName: 'Bandhan UPI', - discoveryCustomScheme: 'Bandhan', + discoveryCustomScheme: 'bandhan', ); /// BHIM BOB Pay (Bank of Baroda's BHIM UPI app) @@ -168,7 +168,7 @@ class UpiApplication { androidPackageName: 'com.infrasofttech.centralbankupi', iosBundleId: 'com.centralbank.centupi', appName: 'Cent UPI', - discoveryCustomScheme: 'CentUPI', + discoveryCustomScheme: 'centupi', ); /// BHIM CORP UPI (Corporation Bank's BHIM UPI app) @@ -364,7 +364,7 @@ class UpiApplication { androidPackageName: 'com.canarabank.mobility', iosBundleId: 'com.canarabank.mobility', appName: 'Canara Bank App', - discoveryCustomScheme: 'CanaraMobility', + discoveryCustomScheme: 'canaramobility', ); /// Cointab BHIM UPI payments app @@ -410,7 +410,7 @@ class UpiApplication { androidPackageName: 'com.fampay.in', iosBundleId: 'in.fampay.app', appName: 'FamPay', - discoveryCustomScheme: 'in.fampay.app', + discoveryCustomScheme: 'fampay', ); /// Freecharge @@ -448,7 +448,7 @@ class UpiApplication { androidPackageName: 'com.citrus.citruspay', iosBundleId: 'com.payu.citrusapp', appName: 'LazyPay', - discoveryCustomScheme: 'www.citruspay.com', + discoveryCustomScheme: 'citruspay', ); /// MahaUPI (Maharashtra Bank's BHIM UPI app) @@ -476,7 +476,7 @@ class UpiApplication { androidPackageName: 'com.jio.myjio', iosBundleId: 'com.jio.myjio', appName: 'MyJio', - discoveryCustomScheme: 'myJio', + discoveryCustomScheme: 'jiopay', ); /// OmegaPay BHIM UPI app from Omegaon @@ -490,7 +490,7 @@ class UpiApplication { androidPackageName: 'com.enstage.wibmo.hdfc', // iosBundleId: 'com.enstage.Wibmo.hdfc', appName: 'PayZapp', - // discoveryCustomScheme: 'payzapp', + discoveryCustomScheme: 'payzapp', ); /// RBL's app for account holders that also includes BHIM UPI transactions for @@ -499,7 +499,7 @@ class UpiApplication { androidPackageName: 'com.rblbank.mobank', iosBundleId: 'com.rbl.mobilebankingiphone', appName: 'RBL MoBank', - discoveryCustomScheme: 'com.rbl.rblimplicitjourney', + discoveryCustomScheme: 'rblbank', ); /// Realme PaySa BHIM UPI app diff --git a/lib/src/transaction_details.dart b/lib/src/transaction_details.dart index 71078bc..4659685 100644 --- a/lib/src/transaction_details.dart +++ b/lib/src/transaction_details.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:decimal/decimal.dart'; import 'package:flutter_upi_india/src/applications.dart'; import 'package:flutter_upi_india/src/exceptions.dart'; @@ -111,8 +113,12 @@ class TransactionDetails { } String toString() { + String scheme = 'upi'; + if(Platform.isIOS && (upiApplication.discoveryCustomScheme ?? "").isNotEmpty){ + scheme = upiApplication.discoveryCustomScheme ?? 'upi'; + } final authority = isForMandate ? 'mandate' : 'pay'; - String uri = 'upi://$authority?pa=$payeeAddress' + String uri = '$scheme://$authority?pa=$payeeAddress' '&pn=${Uri.encodeComponent(payeeName)}' '&tr=$transactionRef' '&tn=${Uri.encodeComponent(transactionNote!)}' From 5e12bece770f7df2312a7198af976e70332814b4 Mon Sep 17 00:00:00 2001 From: lokesh-univest Date: Thu, 22 Jan 2026 19:13:49 +0530 Subject: [PATCH 4/5] Add support for UPI mandate transactions with additional parameters and update documentation --- README.md | 39 +++++++++++ doc/api/flutter_upi_india/UpiPay-class.html | 5 +- .../UpiPay/getInstalledUpiApplications.html | 8 ++- .../UpiPay/initiateTransaction.html | 70 ++++++++++++++++++- doc/api/index.html | 55 ++++++++++++++- 5 files changed, 168 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 59b9da7..9f3150e 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,45 @@ Future doUpiMandate(ApplicationMeta appMeta) { } ``` +#### Mandate parameters + +Mandate requests can include additional parameters when `isForMandate: true`. +Only non-null values are sent to the UPI app. + +```dart +Future doUpiMandateWithParams(ApplicationMeta appMeta) { + return UpiPay.initiateTransaction( + amount: '250.00', + app: appMeta.application, + receiverName: 'Acme Subscriptions', + receiverUpiAddress: 'acme@upi', + transactionRef: 'UPIMANDATE0002', + transactionNote: 'Monthly plan', + isForMandate: true, + // Mandate-specific parameters + amountRule: 'MAX', // amrule + blockFlag: 'Y', // block + merchantName: 'ACME', // mn + mode: 'UPI', // mode + orgId: 'ACME001', // orgid + purpose: '00', // purpose + recurrence: 'MONTHLY', // recur + recurrenceType: 'BEFORE', // recurtype + recurrenceValue: '1', // recurvalue + revocable: 'Y', // rev + transactionId: 'TID12345', // tid + txnType: 'CREATE', // txnType + validityStart: '01012025', // validitystart + validityEnd: '31122025', // validityend + ); +} +``` + +Mandate-capable apps can be filtered via +`UpiPay.getInstalledUpiApplications(isForMandateApps: true)`. Availability +varies by app and platform, so verify with the target UPI app before production +use. + ## Behaviour, Limitations & Measures ### Android diff --git a/doc/api/flutter_upi_india/UpiPay-class.html b/doc/api/flutter_upi_india/UpiPay-class.html index 7199719..a59510a 100644 --- a/doc/api/flutter_upi_india/UpiPay-class.html +++ b/doc/api/flutter_upi_india/UpiPay-class.html @@ -170,7 +170,7 @@

Operators

Static Methods

- getInstalledUpiApplications({UpiApplicationDiscoveryAppPaymentType paymentType = UpiApplicationDiscoveryAppPaymentType.nonMerchant, UpiApplicationDiscoveryAppStatusType statusType = UpiApplicationDiscoveryAppStatusType.working}) + getInstalledUpiApplications({UpiApplicationDiscoveryAppPaymentType paymentType = UpiApplicationDiscoveryAppPaymentType.nonMerchant, UpiApplicationDiscoveryAppStatusType statusType = UpiApplicationDiscoveryAppStatusType.working, bool isForMandateApps = false}) Future<List<ApplicationMeta>> @@ -183,7 +183,7 @@

Static Methods

- initiateTransaction({required UpiApplication app, required String receiverUpiAddress, required String receiverName, required String transactionRef, required String amount, String? url, String? transactionNote}) + initiateTransaction({required UpiApplication app, required String receiverUpiAddress, required String receiverName, required String transactionRef, required String amount, String? url, String? merchantCode, String? transactionNote, bool isForMandate = false, String? amountRule, String? blockFlag, String? merchantName, String? mode, String? orgId, String? purpose, String? recurrence, String? recurrenceType, String? recurrenceValue, String? revocable, String? transactionId, String? txnType, String? validityStart, String? validityEnd}) Future<UpiTransactionResponse> @@ -290,4 +290,3 @@
flutter_upi_india library
- diff --git a/doc/api/flutter_upi_india/UpiPay/getInstalledUpiApplications.html b/doc/api/flutter_upi_india/UpiPay/getInstalledUpiApplications.html index 9f4fab9..94c74dc 100644 --- a/doc/api/flutter_upi_india/UpiPay/getInstalledUpiApplications.html +++ b/doc/api/flutter_upi_india/UpiPay/getInstalledUpiApplications.html @@ -61,7 +61,8 @@

getInstalledUpiApplications static method Future<List<ApplicationMeta>> getInstalledUpiApplications(
  1. {UpiApplicationDiscoveryAppPaymentType paymentType = UpiApplicationDiscoveryAppPaymentType.nonMerchant,
  2. -
  3. UpiApplicationDiscoveryAppStatusType statusType = UpiApplicationDiscoveryAppStatusType.working}
  4. +
  5. UpiApplicationDiscoveryAppStatusType statusType = UpiApplicationDiscoveryAppStatusType.working,
  6. +
  7. bool isForMandateApps = false}
) @@ -85,6 +86,8 @@

getInstalledUpiApplications static method UPI apps this package can detect.

paymentType must be UpiApplicationDiscoveryAppPaymentType.nonMerchant for now. Setting it to any other value will lead to UnsupportedError.

+

isForMandateApps filters apps that advertise UPI mandate support where +platform discovery supports it.

@@ -96,6 +99,7 @@

Implementation

UpiApplicationDiscoveryAppPaymentType.nonMerchant, UpiApplicationDiscoveryAppStatusType statusType = UpiApplicationDiscoveryAppStatusType.working, + bool isForMandateApps = false, }) async { if (paymentType != UpiApplicationDiscoveryAppPaymentType.nonMerchant) { throw UnsupportedError('The parameter `paymentType` must be ' @@ -109,6 +113,7 @@

Implementation

applicationStatusMap: _upiApplicationStatuses, paymentType: paymentType, statusType: statusType, + isForMandateApps: isForMandateApps, ); } @@ -185,4 +190,3 @@
UpiPay class
- diff --git a/doc/api/flutter_upi_india/UpiPay/initiateTransaction.html b/doc/api/flutter_upi_india/UpiPay/initiateTransaction.html index 676ad75..609f206 100644 --- a/doc/api/flutter_upi_india/UpiPay/initiateTransaction.html +++ b/doc/api/flutter_upi_india/UpiPay/initiateTransaction.html @@ -66,7 +66,23 @@

initiateTransaction static method
  • required String transactionRef,
  • required String amount,
  • String? url,
  • -
  • String? transactionNote}
  • +
  • String? merchantCode,
  • +
  • String? transactionNote,
  • +
  • bool isForMandate = false,
  • +
  • String? amountRule,
  • +
  • String? blockFlag,
  • +
  • String? merchantName,
  • +
  • String? mode,
  • +
  • String? orgId,
  • +
  • String? purpose,
  • +
  • String? recurrence,
  • +
  • String? recurrenceType,
  • +
  • String? recurrenceValue,
  • +
  • String? revocable,
  • +
  • String? transactionId,
  • +
  • String? txnType,
  • +
  • String? validityStart,
  • +
  • String? validityEnd}
  • ) @@ -96,6 +112,25 @@

    initiateTransaction static method

    transactionNote is a short description of the transaction. See tn in UPI Linking Specification.

    url: See url parameter in UPI Linking Specification

    +

    merchantCode: See mc parameter in UPI Linking Specification

    +

    isForMandate switches the request to use the upi://mandate authority.

    +

    Mandate-specific parameters:

    +
      +
    • amountRule - Amount rule (MAX, EXACT, etc.)
    • +
    • blockFlag - Block flag (Y/N)
    • +
    • merchantName - Merchant name or identifier
    • +
    • mode - Payment mode
    • +
    • orgId - Organization ID
    • +
    • purpose - Payment purpose code
    • +
    • recurrence - Recurrence pattern (ASPRESENTED, MONTHLY, etc.)
    • +
    • recurrenceType - Recurrence type (AFTER, BEFORE, etc.)
    • +
    • recurrenceValue - Recurrence value/count
    • +
    • revocable - Revocable flag (Y/N)
    • +
    • transactionId - Transaction ID
    • +
    • txnType - Transaction type (CREATE, REVOKE, etc.)
    • +
    • validityStart - Validity start date (DDMMYYYY)
    • +
    • validityEnd - Validity end date (DDMMYYYY)
    • +
    @@ -109,7 +144,23 @@

    Implementation

    required String transactionRef, required String amount, String? url, + String? merchantCode, String? transactionNote, + bool isForMandate = false, + String? amountRule, + String? blockFlag, + String? merchantName, + String? mode, + String? orgId, + String? purpose, + String? recurrence, + String? recurrenceType, + String? recurrenceValue, + String? revocable, + String? transactionId, + String? txnType, + String? validityStart, + String? validityEnd, }) async { final transactionDetails = TransactionDetails( upiApplication: app, @@ -118,7 +169,23 @@

    Implementation

    transactionRef: transactionRef, amount: amount, url: url, + merchantCode: merchantCode, transactionNote: transactionNote, + isForMandate: isForMandate, + amountRule: amountRule, + blockFlag: blockFlag, + merchantName: merchantName, + mode: mode, + orgId: orgId, + purpose: purpose, + recurrence: recurrence, + recurrenceType: recurrenceType, + recurrenceValue: recurrenceValue, + revocable: revocable, + transactionId: transactionId, + txnType: txnType, + validityStart: validityStart, + validityEnd: validityEnd, ); return await _transactionHelper.transact(_channel, transactionDetails); } @@ -196,4 +263,3 @@
    UpiPay class
    - diff --git a/doc/api/index.html b/doc/api/index.html index 61aeea7..ef2fc6e 100644 --- a/doc/api/index.html +++ b/doc/api/index.html @@ -99,7 +99,12 @@

    iOS configuration

    Usage

    Get list of installed apps

    -
    final List<ApplicationMeta> appMetaList = await UpiPay.getInstalledUpiApps();
    +
    final List<ApplicationMeta> appMetaList =
    +    await UpiPay.getInstalledUpiApplications();
    +
    +

    Filter for mandate-capable apps:

    +
    final List<ApplicationMeta> mandateApps =
    +    await UpiPay.getInstalledUpiApplications(isForMandateApps: true);
     

    Show an app's details

    Widget appWidget(ApplicationMeta appMeta) {
    @@ -132,6 +137,53 @@ 

    Do a UPI transaction

    print(response.status); }
    +

    Send a mandate request (uses upi://mandate)

    +
    Future doUpiMandate(ApplicationMeta appMeta) {
    +  return UpiPay.initiateTransaction(
    +    amount: '100.00',
    +    app: appMeta.application,
    +    receiverName: 'John Doe',
    +    receiverUpiAddress: 'john@doe',
    +    transactionRef: 'UPIMANDATE0001',
    +    transactionNote: 'UPI Mandate',
    +    isForMandate: true,
    +  );
    +}
    +
    +

    Mandate parameters

    +

    Mandate requests can include additional parameters when isForMandate: true. +Only non-null values are sent to the UPI app.

    +
    Future doUpiMandateWithParams(ApplicationMeta appMeta) {
    +  return UpiPay.initiateTransaction(
    +    amount: '250.00',
    +    app: appMeta.application,
    +    receiverName: 'Acme Subscriptions',
    +    receiverUpiAddress: 'acme@upi',
    +    transactionRef: 'UPIMANDATE0002',
    +    transactionNote: 'Monthly plan',
    +    isForMandate: true,
    +    // Mandate-specific parameters
    +    amountRule: 'MAX', // amrule
    +    blockFlag: 'Y', // block
    +    merchantName: 'ACME', // mn
    +    mode: 'UPI', // mode
    +    orgId: 'ACME001', // orgid
    +    purpose: '00', // purpose
    +    recurrence: 'MONTHLY', // recur
    +    recurrenceType: 'BEFORE', // recurtype
    +    recurrenceValue: '1', // recurvalue
    +    revocable: 'Y', // rev
    +    transactionId: 'TID12345', // tid
    +    txnType: 'CREATE', // txnType
    +    validityStart: '01012025', // validitystart
    +    validityEnd: '31122025', // validityend
    +  );
    +}
    +
    +

    Mandate-capable apps can be filtered via +UpiPay.getInstalledUpiApplications(isForMandateApps: true). Availability +varies by app and platform, so verify with the target UPI app before production +use.

    Behaviour, Limitations & Measures

    Android

    Flow

    @@ -264,4 +316,3 @@