Volley
Native WebView

iOS

Integrate Volley's hosted payment page in your iOS app with WKWebView

Embed Volley's hosted payment page in a WKWebView. Your app opens the page, the customer approves in their bank app, and the page sends a completion message to your app to let you update the UI.

1. Add the WKWebView

Create a payment request on your server, pass the request URL to your app, and load it in a WKWebView presented as a sheet.

import SwiftUI
import WebKit

struct ContentView: View {
    let requestURL = "https://app.volley.nz/pay/<requestId>"

    @State private var showRequest = false

    var body: some View {
        Button("Pay by bank") {
            showRequest = true
        }
        .sheet(isPresented: $showRequest) {
            WebView(url: requestURL) { result in
                showRequest = false
            }
        }
    }
}

2. Add the bridge

Implement the WebView with a WKScriptMessageHandler coordinator to accept messages from the hosted payment page. Call UIApplication.shared.open(url) on the redirect event to redirect to the bank app. Implement an onComplete handler for the payment result.

struct VolleyResult: Decodable {
    let status: String
    let bank: String?
    let paymentId: String?
}

struct WebView: UIViewRepresentable {
    let url: URL
    let onComplete: (VolleyResult) -> Void

    func makeCoordinator() -> Coordinator { Coordinator(onComplete: onComplete) }

    func makeUIView(context: Context) -> WKWebView {
        let config = WKWebViewConfiguration()
        config.userContentController.add(context.coordinator, name: "volleyWebview")
        let web = WKWebView(frame: .zero, configuration: config)
        web.load(URLRequest(url: url))
        return web
    }

    func updateUIView(_ web: WKWebView, context: Context) {}

    static func dismantleUIView(_ web: WKWebView, coordinator: Coordinator) {
        web.configuration.userContentController
            .removeScriptMessageHandler(forName: "volleyWebview")
    }

    final class Coordinator: NSObject, WKScriptMessageHandler {
        let onComplete: (VolleyResult) -> Void
        init(onComplete: @escaping (VolleyResult) -> Void) { self.onComplete = onComplete }

        func userContentController(_ ucc: WKUserContentController,
                                   didReceive message: WKScriptMessage) {
            guard message.name == "volleyWebview",
                  let body = message.body as? [String: Any],
                  let type = body["type"] as? String
            else { return }

            switch type {
            case "redirect":
                if let s = body["url"] as? String, let url = URL(string: s) {
                    UIApplication.shared.open(url)
                }
            case "complete":
                if let data = try? JSONSerialization.data(withJSONObject: body),
                   let result = try? JSONDecoder().decode(VolleyResult.self, from: data) {
                    onComplete(result)
                }
            default:
                break
            }
        }
    }
}

3. Redirect back to your app

After approving, your customer is redirected from their bank app to their default browser. From here Volley automatically redirects the customer to your payment request's success redirect URL. Use a deep link for this URL to get the customer back to your app. The hosted payment page will poll the payment to completion and emit the complete event.

Development - register a custom scheme in Info.plist (CFBundleURLTypes) and set the request's redirect URL to yourscheme://return.

Production - use a Universal Link:

  1. Host apple-app-site-association at https://yourdomain.com/.well-known/apple-app-site-association:
{
  "applinks": {
    "apps": [],
    "details": [
      { "appID": "com.company.app", "paths": ["/return", "/return/*"] }
    ]
  }
}
  1. Add the Associated Domains capability in Xcode: applinks:yourdomain.com.
  2. Use a path (e.g. /return), not the bare domain, so it doesn't shadow the association file.

Remember to add the redirect domain or scheme as an allowed domain in the Volley Dashboard.

Test end-to-end

The app-to-app redirect only works for real bank apps on a physical device. The simulator can't reproduce it. We recommend testing your integration against each bank app, including BNZ to test the decoupled flow.

If the bank opens in as a web URL, the hand-off isn't going through the bridge - confirm the redirect message calls UIApplication.shared.open. If you aren't returned to your app after completing a payment, check your Associated Domains and, while iterating, enable Associated Domains Development on the device so iOS fetches the association file directly instead of from its cache. Also check the success and failure URLs are set on the payment request you are testing.

On this page