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

  1. Set the payment URLs to the views in your mobile app
  2. Load the checkout URL in the web view
  3. Open the bank URL in the standard browser
  4. 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. – iOSAndroid

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.

Example: Android (Java)
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,
    },
});