Mobile redirects (app-to-app)
Mobile redirects enable payers to authorise payments using their banking app and return to your application afterward. This guide covers how to integrate Volt checkout within a mobile WebView and handle redirect callbacks.
How it works
- Your app loads the Volt checkout URL inside a WebView.
- The checkout flow triggers redirects (to bank apps or back to merchant).
- Your app intercepts these redirects via an injected JavaScript callback.
- Your app handles the redirect natively (e.g. opening the bank app, closing the WebView).
Callback contract
To listen to redirect requests from Volt checkout, inject window.voltRedirectCallback as a function on the WebView's window object. Volt checkout will call this function with a JSON payload when a redirect occurs.
Callback payload
Prop
Type
Example payloads:
// Bank redirect — payer is being sent to their banking app
{ "type": "bank_redirect", "url": "https://bank-app.example.com/authorize?token=abc" }
// Merchant redirect — payer cancelled the payment
{ "type": "merchant_redirect", "url": "your-app://checkout/cancel", "result": "cancel" }
// Merchant redirect with payment status
{ "type": "merchant_redirect", "url": "your-app://checkout/success", "result": "success" }
// Merchant redirect with exception status
{ "type": "merchant_redirect", "url": "your-app://checkout/error", "result": "exception" }Implementation steps
Configure return URLs
Set your payment success/failure URLs to point to views in your mobile app. Use either:
- App URL schemes:
your-app://checkout/success - Universal/App Links (HTTPS):
https://your-domain.com/checkout/success
Your app must claim these URL schemes in its OS configuration:
Load checkout in a WebView
Load the Volt checkout URL in a WebView and inject the voltRedirectCallback JavaScript function. The injection should happen at document end to ensure the checkout page is ready.
Handle redirects natively
When your callback receives a bank_redirect, open the URL in the system browser or via a deep link to the banking app. When you receive a merchant_redirect, handle the result accordingly (e.g. close the WebView on cancel).
Payer returns to your app
After the payer authenticates in their banking app, they are redirected back to one of your configured payment URLs. Close the WebView and continue the checkout flow in your app.
Platform examples
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)
])
let contentController = webView.configuration.userContentController
contentController.add(self, name: Self.jsHandlerName)
let script = WKUserScript(source: Self.injectedJS, injectionTime: .atDocumentEnd, forMainFrameOnly: false)
contentController.addUserScript(script)
webView.load(URLRequest(url: Self.checkoutURL))
}
func handleRedirect(_ redirectParams: Volt.RedirectParams) {
switch redirectParams {
case .bankRedirect(let url):
guard UIApplication.shared.canOpenURL(url) else {
print("Error: URL can not be opened")
return
}
UIApplication.shared.open(url, options: [.universalLinksOnly: true]) { success in
if !success {
UIApplication.shared.open(url, options: [:])
}
}
case .merchantRedirect(let merchantRedirectResult, let url):
if merchantRedirectResult == .cancel {
// Handle the cancellation without leaving the app
}
if merchantRedirectResult == .exception {
// Handle the exception (e.g. check webhooks)
}
}
}
}
extension CheckoutWebViewController: WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
guard message.name == Self.jsHandlerName else {
return
}
if let messageBody = message.body as? [String: Any] {
do {
let jsonData = try JSONSerialization.data(withJSONObject: messageBody, options: [])
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
case failure
case pending
case exception
}
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) {
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
}
if (data.getString("result") == "exception") {
// Handle the exception (e.g. check webhooks)
}
}
}
}
}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() {
inAppBrowserRef = cordova.InAppBrowser.open(
"https://checkout.volt.io/...",
"_blank",
"location=yes"
);
inAppBrowserRef.addEventListener("loadstop", function () {
inAppBrowserRef.executeScript({ code: INJECTED_JAVASCRIPT });
});
inAppBrowserRef.addEventListener("message", messageCallback);
inAppBrowserRef.addEventListener('exit', function () {
inAppBrowserRef.removeEventListener('loadstop');
inAppBrowserRef.removeEventListener('message', messageCallback);
});
}
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") {
if (data.result === "cancel") {
// Handle the cancellation without leaving the app
}
if (data.result === "exception") {
// Handle the exception (e.g. check webhooks)
}
}
} 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/...";
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
}
if (data.result === "exception") {
// Handle the exception (e.g. check webhooks)
}
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,
},
});How is this guide?
Last updated on