Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Embedding Tebex Checkout in an iOS App

A reference for iOS developers selling Tebex packages inside their own app. It shows how to hand the shopper off to Tebex's hosted checkout, detect when they finish, and confirm the payment. You build no payment UI yourself.

This repo ships a working SwiftUI example (iOS 16+, no third party dependencies). To run it, see CONTRIBUTING.md.

The checkout flow

  1. Create a basket via the Headless API and add packages to it.
  2. Open pay.tebex.io/<basketIdent> in an in-app browser.
  3. The shopper pays. Tebex redirects to your complete_url (or cancel_url).
  4. The app closes the browser and re-fetches the basket.
  5. The basket's complete flag confirms the outcome.

Two pieces make this work, whichever browser you choose:

  • Auto redirect. Create the basket with complete_auto_redirect: true so Tebex redirects on success instead of showing a "return to store" button. That redirect is the signal the app watches for.
  • Authoritative completion. A client side redirect is not proof of payment. Always re-fetch the basket and read its complete flag.
// Creating the basket (Headless API)
let body: [String: Any] = [
    "complete_url": completeURL,
    "cancel_url": cancelURL,
    "complete_auto_redirect": true,
]

Two ways to present checkout

The example ships both, selectable with Config.checkoutStrategy, so you can compare them. ASWebAuthenticationSession is the preferred option: it is the system Safari browser, so Apple Pay and 3DS work reliably and you write almost no UI. SFSafariViewController is not an option here, as it cannot observe the completion redirect needed to auto close.

ASWebAuthenticationSession (preferred) WKWebView
Browser chrome System provided You build it (including a Cancel button)
Apple Pay / 3DS Robust (real Safari) Workable, needs a WKUIDelegate for popups
Auto close on redirect Yes, on the tebexpay:// callback scheme Yes, on any navigation including the tebexpay:// scheme
Project setup None None
One time system alert Shown before first present None

Option A: ASWebAuthenticationSession (preferred)

The system runs the web flow and closes the moment checkout redirects to the callback scheme.

import AuthenticationServices

final class CheckoutSession: NSObject, ASWebAuthenticationPresentationContextProviding {
    private var session: ASWebAuthenticationSession?

    func start(url: URL, onClose: @escaping () -> Void) {
        let session = ASWebAuthenticationSession(
            url: url,
            callbackURLScheme: "tebexpay"  // matches complete_url / cancel_url
        ) { _, _ in onClose() }            // fires on redirect or dismiss
        session.presentationContextProvider = self
        self.session = session
        session.start()
    }

    func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
        UIApplication.shared.connectedScenes
            .compactMap { $0 as? UIWindowScene }
            .first { $0.activationState == .foregroundActive }?
            .keyWindow ?? ASPresentationAnchor()
    }
}

Create the basket with a custom scheme callback so the session can intercept it:

let completeURL = "tebexpay://complete"
let cancelURL   = "tebexpay://cancel"

ASWebAuthenticationSession can only intercept a custom scheme, not an https URL. The Headless API accepts tebexpay:// in complete_url / cancel_url.

Reference: CheckoutSession.swift.

Option B: WKWebView

WKWebView's navigation delegate sees every navigation, so it intercepts the tebexpay:// redirect and closes. The cost is that you own the browser chrome and must handle Apple Pay / 3DS popups yourself.

1. Wrap WKWebView in SwiftUI and provide your own Cancel button.

import SwiftUI
import WebKit

struct CheckoutWebView: View {
    let url: URL
    let onFinish: () -> Void   // called once, on redirect or Cancel

    var body: some View {
        VStack(spacing: 0) {
            HStack {
                Button("Cancel", action: onFinish)   // your only exit
                Spacer()
            }
            .padding()
            WebView(url: url, onFinish: onFinish)
        }
    }
}

private struct WebView: UIViewRepresentable {
    let url: URL
    let onFinish: () -> Void

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

    func makeUIView(context: Context) -> WKWebView {
        let webView = WKWebView()
        webView.navigationDelegate = context.coordinator
        webView.uiDelegate = context.coordinator
        webView.load(URLRequest(url: url))
        return webView
    }

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

2. Intercept the redirect. Cancel the navigation to the callback URL and fire onFinish once. Match on prefix, since the redirect may carry query params.

final class Coordinator: NSObject, WKNavigationDelegate, WKUIDelegate {
    private let onFinish: () -> Void
    private var finished = false

    init(onFinish: @escaping () -> Void) { self.onFinish = onFinish }

    func webView(
        _ webView: WKWebView,
        decidePolicyFor navigationAction: WKNavigationAction,
        decisionHandler: @escaping (WKNavigationActionPolicy) -> Void
    ) {
        let target = navigationAction.request.url?.absoluteString ?? ""
        if target.hasPrefix(completeURL) || target.hasPrefix(cancelURL) {
            decisionHandler(.cancel)
            guard !finished else { return }   // callback can fire more than once
            finished = true
            onFinish()
            return
        }
        decisionHandler(.allow)
    }

3. Keep Apple Pay / 3DS popups alive. These often open with target=_blank. WKWebView silently drops them unless you load them back into the same view.

    func webView(
        _ webView: WKWebView,
        createWebViewWith configuration: WKWebViewConfiguration,
        for navigationAction: WKNavigationAction,
        windowFeatures: WKWindowFeatures
    ) -> WKWebView? {
        if navigationAction.targetFrame == nil {
            webView.load(navigationAction.request)
        }
        return nil
    }
}

The callback URLs are the same tebexpay:// scheme both strategies share — the delegate matches them by prefix and never lets them load:

let completeURL = "tebexpay://complete"
let cancelURL   = "tebexpay://cancel"

Reference: CheckoutWebView.swift.

Confirming completion

Present either browser off the checkout URL. When it closes, dismiss and confirm the outcome by re-fetching the basket and reading its complete flag. Do not trust the redirect alone as proof of payment.

CheckoutWebView(url: url) {
    Task { await viewModel.checkoutClosed() }   // re-fetch, then read `complete`
}

About

A demo app displaying the capabilities of the Tebex Checkout on native mobile devices.

Resources

Contributing

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages