Mobile redirects (app-to-app)
Nowadays, more and more banks offer a way for their customers to verify and approve payments using their online banking app or other specific apps installed on their mobile device. This method is usually the easiest and most familiar for users to grant permission and make payments. This process is referred to as “app-to-app” or “app2app redirection”.
General implementation
- Set the payment URLs to the views in your mobile app
- Load the checkout URL in the web view
- Open the bank URL in the standard browser
- The user returns to any of the payment URLs from (1.)
1. Set payment URLs to the views in your mobile app
This step allows the user to return back to your intended flow in your app after the authorisation with their bank. This is ideally a URL or an app link.
Example
"paymentSuccessUrl":"https://your-domain.com/checkout/success"
"paymentSuccessUrl":"your-app://checkout/success"
Note that when using URLs, your app needs to claim the URL schema in the OS for a successful redirection.
For information on how to configure the association, please refer to the appropriate documentation. – iOS – Android
2. Load the checkout URL in the web view
Typically the OS will support this by using the standard load method for the web view element.
webView.loadUrl("https://www.example.com");
3. Open the bank URL in the standard browser
Volt Checkout opens the URL of the bank (https://bank.com/). The bank URL must be opened in the standard browser for app-to-app redirection to work. To listen to redirect requests sent by Volt Checkout, inject window.voltRedirectCallback
. Inside this callback, you can implement the logic for notifying your application about the redirect request, such as using the postMessage interface. Check the examples below for implementation details.
We recommend closing the web view once the user was redirected. This way, they can continue with the next step in the checkout process once they return from their mobile bank app.
4. Payer returns to one of the payment URLs
When the payer returns from their mobile banking app, they are ready to continue the checkout.
We recommend closing the web view once the user was redirected, so that they can continue with the next step in the checkout process.
Follow our implementation guide for returning the payer to you.
Examples
- iOS
- Android
- Cordova
- React Native
import SwiftUI
import UIKit
import WebKit
struct CheckoutWebView: UIViewControllerRepresentable {
typealias UIViewControllerType = CheckoutWebViewController
func makeUIViewController(context: Context) -> CheckoutWebViewController {
return CheckoutWebViewController()
}
func updateUIViewController(_ uiViewController: CheckoutWebViewController, context: Context) {}
}
class CheckoutWebViewController: UIViewController {
static let checkoutURL = URL(string: "https://checkout.volt.io/...")!
static let jsHandlerName = "iOSListener"
static let injectedJS: String = """
window.voltRedirectCallback = function(data) {
window.webkit.messageHandlers.\(jsHandlerName).postMessage(data);
}
"""
private lazy var webView: WKWebView = {
let webView = WKWebView()
webView.translatesAutoresizingMaskIntoConstraints = false
return webView
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(webView)
NSLayoutConstraint.activate([
webView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
webView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
webView.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor),
webView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor)
])
// Install communication between the loaded webpage and this controller via JS
let contentController = webView.configuration.userContentController
contentController.add(self, name: Self.jsHandlerName)
let script = WKUserScript(source: Self.injectedJS, injectionTime: .atDocumentEnd, forMainFrameOnly: false)
contentController.addUserScript(script)
// Load the checkout from URL
webView.load(URLRequest(url: Self.checkoutURL))
}
func handleRedirect(_ redirectParams: Volt.RedirectParams) {
switch redirectParams {
case .bankRedirect(let url):
// Do the app2app redirect...
guard UIApplication.shared.canOpenURL(url) else {
// TODO: error handling
print("🆘 Error: URL can not be opened")
return
}
UIApplication.shared.open(url, options: [.universalLinksOnly: true]) { success in
// Fallback
if !success {
// The `url` was not recognize by the system as a universal link, let's force system open the link either way
UIApplication.shared.open(url, options: [:])
}
}
case .merchantRedirect(let merchantRedirectResult, let url):
if merchantRedirectResult == .cancel {
// Handle the cancellation without leaving the app
// This may involve navigating to a different View or updating your UI in some way
print("✅ Got .cancel on merchant redirect")
} else {
assertionFailure("Got an unsupported value in result field for merchant_redirect.")
}
}
}
}
extension CheckoutWebViewController: WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
guard message.name == Self.jsHandlerName else {
return
}
// Use JSONDecoder to decode the message from WebView into the value objects
if let messageBody = message.body as? [String: Any] {
do {
// Convert the dictionary to JSON data
let jsonData = try JSONSerialization.data(withJSONObject: messageBody, options: [])
// Decode the object from JSON data
let redirectParams = try JSONDecoder().decode(Volt.RedirectParams.self, from: jsonData)
handleRedirect(redirectParams)
} catch {
print("🆘 Error: JSON decoding error: \(error)")
}
}
}
}
enum Volt {
enum MerchantRedirectResult: String, Decodable {
case cancel
case success // not in use yet
case failure // not in use yet
case pending // not in use yet
}
enum RedirectParams: Decodable {
case bankRedirect(URL)
case merchantRedirect(MerchantRedirectResult, URL)
enum CodingKeys: String, CodingKey {
case type
case url
case result
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
guard let type = try container.decodeIfPresent(RedirectType.self, forKey: .type) else {
throw DecodingError.dataCorruptedError(forKey: .type, in: container, debugDescription: "Invalid value in 'type' field.")
}
guard let url = try container.decodeIfPresent(URL.self, forKey: .url) else {
throw DecodingError.dataCorruptedError(forKey: .url, in: container, debugDescription: "Invalid value in 'url' field.")
}
switch type {
case .bankRedirect:
self = .bankRedirect(url)
case .merchantRedirect:
if let result = try container.decodeIfPresent(MerchantRedirectResult.self, forKey: .result) {
self = .merchantRedirect(result, url)
} else {
throw DecodingError.dataCorruptedError(forKey: .result, in: container, debugDescription: "Invalid value in 'result' field.")
}
}
}
}
private enum RedirectType: String, Decodable {
case bankRedirect = "bank_redirect"
case merchantRedirect = "merchant_redirect"
}
}
package com.example.myapplication;
import WebAppInterface
import android.os.Bundle
import android.webkit.WebView
import android.webkit.WebViewClient
import android.content.Intent
import android.net.Uri
import androidx.appcompat.app.AppCompatActivity
import org.json.JSONObject
class MainActivity : AppCompatActivity(), WebAppInterface.MessageListener {
private lateinit var myWebView: WebView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
myWebView = findViewById(R.id.webview)
myWebView.settings.javaScriptEnabled = true
myWebView.addJavascriptInterface(WebAppInterface(this, this), "Android")
myWebView.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView, url: String) {
myWebView.evaluateJavascript("""
window.voltRedirectCallback = function(data) {
Android.postMessage(JSON.stringify(data));
}
""", null)
}
}
myWebView.loadUrl("https://checkout.volt.io/...")
}
override fun onMessageReceived(message: String) {
// Handle the message here.
// You might need to use a Handler to run code on the UI thread.
val data = JSONObject(message)
when (data.getString("type")) {
"bank_redirect" -> {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(data.getString("url")))
startActivity(intent)
}
"merchant_redirect" -> {
if (data.getString("result") == "cancel") {
// Handle the cancellation without leaving the app
// This may involve navigating to a different Activity or updating your UI in some way
}
}
}
}
}
const INJECTED_JAVASCRIPT = `
window.voltRedirectCallback = function(data) {
if (!webkit.messageHandlers.cordova_iab) {
throw 'Cordova IAB postMessage API not found!';
} else {
webkit.messageHandlers.cordova_iab.postMessage(JSON.stringify(data));
}
}`;
var inAppBrowserRef;
function openInAppBrowser() {
/* Open URL */
inAppBrowserRef = cordova.InAppBrowser.open(
"https://checkout.volt.io/...",
"_blank",
"location=yes"
);
/* Inject JavaScript, make sure that JS is injected on the checkout.volt.io page */
inAppBrowserRef.addEventListener("loadstop", function () {
inAppBrowserRef.executeScript({ code: INJECTED_JAVASCRIPT });
});
inAppBrowserRef.addEventListener("message", messageCallback);
// Ensure event listeners are removed upon closing the InAppBrowser.
inAppBrowserRef.addEventListener('exit', function () {
inAppBrowserRef.removeEventListener('loadstop');
inAppBrowserRef.removeEventListener('message', messageCallback);
});
}
/* Handle message from the injected JS */
function messageCallback(params) {
try {
const data = JSON.parse(params.data);
if (typeof data !== "object") {
throw "Invalid message received";
}
if (data.type === "bank_redirect") {
inAppBrowserRef.close();
cordova.InAppBrowser.open(data.url, "_system");
}
if (data.type === "merchant_redirect" && data.result === "cancel") {
// Handle the cancellation without leaving the app
// This may involve navigating to a different Activity or updating your UI in some way
}
} catch (error) {
console.error('Error in messageCallback: ', error);
}
}
import React, { useCallback } from 'react';
import { WebView } from "react-native-webview";
import { StyleSheet, Linking, View } from "react-native";
const INJECTED_JAVASCRIPT = `
window.voltRedirectCallback = function(data) {
window.ReactNativeWebView.postMessage(JSON.stringify(data));
}`;
export default function App({ navigation }) {
const checkoutUrl = "https://checkout.volt.io/..."; // url obtained during payment creation
const messageCallback = useCallback((event) => {
try {
const data = JSON.parse(event.nativeEvent.data);
if (typeof data !== "object") {
throw new Error("Invalid message received");
}
switch (data.type) {
case "bank_redirect":
Linking.openURL(data.url);
break;
case "merchant_redirect":
if (data.result === "cancel") {
// Handle the cancellation without leaving the app
// This may involve navigating to a different Activity or updating your UI in some way
}
break;
default:
console.warn(`Unhandled message type: ${data.type}`);
}
} catch (err) {
console.error(err);
}
}, [navigation]);
return (
<View style={styles.container}>
<WebView
style={styles.webView}
source={{ uri: checkoutUrl }}
injectedJavaScript={INJECTED_JAVASCRIPT}
onMessage={messageCallback}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
webView: {
flex: 1,
},
});