Handling UPI Intent in WebView

Introduction

UPI intent is a functionality which allows the customer to make a UPI payment without entering their UPI ID. The user is instead shown a list of UPI PSPs (Payment Service Providers) which are installed on their device and has to select the UPI PSP from where they wish to complete the payment.

Applicability

This document is applicable for merchants who have integrated the Neo - Full Redirect or Ace - SDK in a WebView for any of the following workflows:

  • One-time Payments
  • Payment + Mandate
  • Only Mandate Setup

The document contains the steps to handle UPI intent in WebView on following platforms/frameworks:

Note for UPI intent on iOS:

BillDesk supports the following UPI PSPs for UPI intent transactions on iOS and these apps appear on the Neo - Full Redirect or Ace - SDK irrespective of the installation status of these apps on the customers' device:

  1. BHIM (only for One-time Payments)
  2. Google Pay
  3. Paytm
  4. PhonePe

Sample Code

Native Android

Step 1: Add the below given code in the AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest
	xmlns:android="http://schemas.android.com/apk/res/android" >
	<application>
	...
	...
	</application>
	<queries>
		<intent>
			<action android:name="android.intent.action.VIEW" />
			<data android:scheme="upi" android:host="pay"/>
		</intent>
		<intent>
			<action android:name="android.intent.action.VIEW" />
			<data android:scheme="upi"  android:host="mandate" />
		</intent>
		<intent>
			<action android:name="android.intent.action.VIEW" />
			<data android:scheme="nb"    android:host="pay" />
		</intent>
	</queries>
</manifest>

Step 2: Add WebView to your Android app

Include a WebView in your app's layout XML file:

Xml

<WebView
 android:id="@+id/webview"
 android:layout_width="match_parent"
 android:layout_height="match_parent" />

Step 3: Setup WebViewClient

Implement a WebViewClient to handle URL requests:

import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.Bundle;
import android.webkit.WebResourceRequest;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import java.net.URISyntaxException;
public class MyWebViewClient extends WebViewClient {
    private Activity activity;
    public MyWebViewClient(Activity activity) {
        this.activity = activity;
    }
    @Override
    public Boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
        return false;
    }
}
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.ResolveInfo
import android.net.Uri
import android.os.Bundle
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import java.net.URISyntaxException
class MyWebViewClient(private val activity: Activity): WebViewClient() {
    override fun shouldOverrideUrlLoading(view: WebView ? , request : WebResourceRequest ? ):
        Boolean {
            request ? .let {
                return false
            }
        }
}

Step 4: Set WebViewClient in MainActivity

import android.app.Activity;
import android.os.Bundle;
import android.webkit.WebView;
public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        WebView webView = findViewById(R.id.webview);
        webView.setWebViewClient(new MyWebViewClient(MainActivity.this));
    }
}
import android.app.Activity
import android.os.Bundle
import android.webkit.WebView
class MainActivity: Activity() {
    override fun onCreate(savedInstanceState: Bundle ? ) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val webView: WebView = findViewById(R.id.webview)
        webView.webViewClient = MyWebViewClient(this)
    }
}

Step 5: Handle Deep Links

Handle deep links within shouldOverrideUrlLoading() method of WebViewClient to
trigger the UPI payment process:

@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
    if (url.startsWith("upi://pay") || url.startsWith("bhim://upi") || url.startsWith("upi://mandate")  || url.startsWith("nb://pay") ) {
        try {
            Context context = view.getContext();
            Intent intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);
            if (intent != null) {
                PackageManager packageManager = context.getPackageManager();
                ResolveInfo info = packageManager.resolveActivity(intent,
                    PackageManager.MATCH_DEFAULT_ONLY);
                if (info != null) {
                    context.startActivity(intent);
                } else {
                    // Fallback URL for other UPI apps
                    String fallbackUrl =
                        intent.getStringExtra("browser_fallback_url");
                    if (fallbackUrl != null && !fallbackUrl.isEmpty()) {
                        view.loadUrl(fallbackUrl);
                    } else {
                        Log.e(TAG, "Fallback URL is null or empty");
                    }
                }
                return true;
            }
        } catch (URISyntaxException e) {
            e.printStackTrace();
        }
    }
    return false;
}



public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
Uri uri = request.getUrl();
String url = uri.toString();
if (url.startsWith("upi://pay") || url.startsWith("bhim://upi") || url.startsWith("upi://mandate")  || url.startsWith("nb://pay") ) {
    try {
        Context context = view.getContext();
        Intent intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);
        if (intent != null) {
            PackageManager packageManager = context.getPackageManager();
            ResolveInfo info = packageManager.resolveActivity(intent,
                PackageManager.MATCH_DEFAULT_ONLY);
            if (info != null) {
                context.startActivity(intent);
            } else {
                // Fallback URL for other UPI apps
                String fallbackUrl =
                    intent.getStringExtra("browser_fallback_url");
                if (fallbackUrl != null && !fallbackUrl.isEmpty()) {
                    view.loadUrl(fallbackUrl);
                } else {
                    Log.e("WebViewClient", "Fallback URL is null or 
                        empty ");
                    }
                }
                return true;
            }
        } catch (URISyntaxException e) {
            e.printStackTrace();
        }
    }
    return false;
}

import android.content.Intent
import android.content.pm.PackageManager
import android.util.Log
import android.webkit.WebView
import java.net.URISyntaxException

override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
    if (url.startsWith("upi://pay") || url.startsWith("bhim://upi")
        || url.startsWith("upi://mandate") || url.startsWith("nb://pay") ) {
        try {
            val context = view.context
            val intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME)
            if (intent != null) {
                val packageManager = context.packageManager
                val info = packageManager.resolveActivity(intent,PackageManager.MATCH_DEFAULT_ONLY)
                if (info != null) {
                    context.startActivity(intent)
                } else {
                    // Fallback URL for other UPI apps
                    val fallbackUrl =
                        intent.getStringExtra("browser_fallback_url")
                    if (fallbackUrl != null && !fallbackUrl.isEmpty()) {
                        view.loadUrl(fallbackUrl)
                    } else {
                        Log.e(TAG, "Fallback URL is null or empty")
                    }
                }
                return true
            }
        } catch (e: URISyntaxException) {
            e.printStackTrace()
        }
    }
    return false
}


override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
    val uri = request.url
    val url = uri.toString()
    if (url.startsWith("upi://pay") ||
        url.startsWith("bhim://upi") ||
        url.startsWith("upi://mandate") ||
        url.startsWith("nb://pay")
    ) {
        try {
            val context = view.context
            val intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME)
            val packageManager = context.packageManager
            val info = packageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY)

            if (info != null) {
                context.startActivity(intent)
            } else {
                // Fallback URL for other UPI apps
                val fallbackUrl = intent.getStringExtra("browser_fallback_url")
                if (!fallbackUrl.isNullOrEmpty()) {
                    view.loadUrl(fallbackUrl)
                } else {
                    Log.e("WebViewClient", "Fallback URL is null or empty")
                }
            }
            return true
        } catch (e: URISyntaxException) {
            e.printStackTrace()
        }
    }
    return false
}




Native iOS

Step 1: Handle deep links in your app

Delegate, override the application(_:open:options:) method to handle deep link URLs:

Swift
func application(
  _ app: UIApplication, open url: URL,
  options:
    [UIApplication.OpenURLOptionsKey: Any] = [:]
) -> Bool {  // Handle deep link URL here return
  true
}

Step 2: Setup WKNavigationDelegate for WKWebView

Set up the WKNavigationDelegate for your WKWebView to handle navigation events:

Swift 
import UIKit 
import WebKit 
class BilldeskViewController: UIViewController { var checkoutUrl: String? @IBOutlet weak var 
wkWebView: WKWebView! override func viewDidLoad() { super.viewDidLoad() 
wkWebView.navigationDelegate = self loadWebPage() } func loadWebPage() { guard let 
urlString = checkoutUrl, let url = URL(string: urlString) else { return } 
wkWebView.load(URLRequest(url: url)) } }

Step 3: Implement WKNavigationDelegate

Implement WKNavigationDelegate methods to handle navigation events in your WKWebView:

Swift 
import UIKit 
import WebKit 
extension BilldeskViewController: WKNavigationDelegate { func webView(_ webView: 
WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: 
@escaping (WKNavigationActionPolicy) -> Void) { guard let url = navigationAction.request.url 
else { decisionHandler(.cancel) return } let regex = try! NSRegularExpression(pattern: 
"(upi|intent)", options: []) if (regex.firstMatch(in: url.absoluteString, options: [], range: 
NSRange(location: 0, length: url.absoluteString.count)) != nil) && 
!url.absoluteString.hasPrefix("http://") && !url.absoluteString.hasPrefix("https://") { 
UIApplication.shared.open(url) decisionHandler(.cancel) } else { decisionHandler(.allow) } 
print("navigationAction load: \(String(describing: navigationAction.request.url))") } }

React Native

Step 1: Add a WebView

Include a WebView in your app's layout view file:

<View style={{ flex: 1 }}>
      <WebView
        originWhitelist={['*']}
        javaScriptEnabled={true}
        domStorageEnabled={true}
        scalesPageToFit={true}
        allowFileAccess={true}
        allowFileAccessFromFileURLs={true}
        source={{ html: htmlContent }}
       onShouldStartLoadWithRequest={handleShouldStartLoadWithRequest}
        onMessage={(event) => {
          console.log(event.nativeEvent.data);
        }}
      />
    </View>
import { StyleSheet, Text, View, Linking } from 'react-native'; // Import Linking
import { WebView } from 'react-native-webview';
import { useEffect, useState } from 'react';
import { Asset } from 'expo-asset';
import { readAsStringAsync } from 'expo-file-system';

export default function App() {
  const [html, setHtml] = useState('');
  const [error, setError] = useState(null);

  useEffect(() => {
    (async () => {
      const files = await Asset.loadAsync(require('./src/index.html'));
      const fileContents = await readAsStringAsync(files[0].localUri);
      setHtml(fileContents);
    })();
  }, []);

  const handleShouldStartLoadWithRequest = (event) => {
    const url = event.url;
    const regex =  new RegExp('(upi|intent)');
    if (Platform.OS === 'android' || Platform.OS === 'ios') {
      if (regex.test(url)) {
        Linking.openURL(url);
        return false;
      }
    }

    return true; 
  };

  return (
    <View style={{ flex: 1 }}>
      <WebView
        originWhitelist={['*']}
        javaScriptEnabled={true}
        domStorageEnabled={true}
        scalesPageToFit={true}
        allowFileAccess={true}
        allowFileAccessFromFileURLs={true}
        source={{ html }}
       onShouldStartLoadWithRequest={handleShouldStartLoadWithRequest}
        onMessage={(event) => {
          console.log(event.nativeEvent.data); 
        }}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
});

Step 2: Handle intent links

const handleShouldStartLoadWithRequest = (event: any) => {
    const url = event.url;
    const regex = new RegExp('(upi|intent)');
    if ((Platform.OS === 'android' || Platform.OS === 'ios') && regex.test(url))          {
      Linking.openURL(url);
      return false;
    }
    return true;
  };

Flutter

Step 1: Add below dependencies in pubspec.yaml file

 ...
 ...
 dependencies:
 flutter:
 sdk: flutter
 url_launcher: ^6.0.20
 flutter_inappwebview: ^5.7.2+3
...
...

Step 2: Import necessary packages

import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:url_launcher/url_launcher.dart';

Step 3: Set up InAppWebView with UPI launch capability

InAppWebView(
 ….
 shouldOverrideUrlLoading: (controller, navigationAction) async {
 var uri = navigationAction.request.url!;
 
 if (!["http", "https", "file", "chrome", "data", "javascript", "about"].contains(uri.scheme)) {
 // Check if it's a UPI URL
 bool isUpiUrl = RegExp(r"(upi)", caseSensitive: false).hasMatch(uri.toString());
 
 if (isUpiUrl) {
 
 if (await canLaunchUrl(uri)) {
 // Launch the UPI app
 await launchUrl(uri, mode: LaunchMode.externalApplication);
 // Cancel the request in WebView
 return NavigationActionPolicy.CANCEL;
 }
 }
 }
 
 return NavigationActionPolicy.ALLOW;
 },
 // ... other InAppWebView parameters ...
)