diff --git a/README.md b/README.md index 372d514..9f3150e 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,61 @@ 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, + ); +} +``` + +#### 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 @@ -116,7 +178,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..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 @@ -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,23 @@ 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") + + // 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 { /* @@ -63,7 +80,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) + @@ -77,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()) @@ -97,9 +164,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 +269,4 @@ class UpiPayPlugin: FlutterPlugin, MethodCallHandler, ActivityAware, PluginRegis activity = null } -} \ No newline at end of file +} 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 @@