Redirection methods
Any method the session marks template = "redirection" or integration_mode = "redirection" renders nothing in your screen: the PSP's hosted page is the payment step. Bank transfers (Fintecture), BNPL providers (Oney, Sequra, Alma…), local schemes (Bizum, iDEAL, Bancontact…), and any card method you configure in redirection mode all take this path.
Setup
| Product | PurseHeadlessCheckout, nothing else |
| Install | Nothing. RedirectionModule is built into PluginRegistry |
| Dashboard | The method activated on the entity in redirection mode. Its return URL is part of the client session |
| Project | A RedirectPresenter passed to paymentElement(for:), and whatever its RedirectPolicy needs: an Associated Domain for a universal link, or a CFBundleURLSchemes entry for a custom scheme |
| Keys | None in the app |
Switching a method to redirection mode in the dashboard is how you offer, on iOS, a method the SDK has no native plugin for: a PSP-hosted card form, Riverty, Alma, MB Way… The Supported payment methods table marks them.
The presenter
The SDK never opens a page you did not give it somewhere to open. One presenter serves every redirect in the checkout, 3-D Secure included.
let presenter = WebAuthRedirectPresenter(
policy: RedirectPolicy(
returnDestination: .universalLink(host: "checkout.example.com", path: "/return"),
additionalAllowedHosts: [] // empty: any https host in the chain is allowed
),
presentingViewController: viewController, // needed for the POST cases
prefersInAppWebView: false
)
| Choice | Effect |
|---|---|
.universalLink(host:path:) | Preferred: only the app owning the domain can claim the return, so no other app can fabricate one. Needs an Associated Domain. The browser can observe it from iOS 17.4; below that the SDK falls back to its own web view |
.customScheme(_:) | Simpler (a CFBundleURLSchemes entry), but any app can register the same scheme, which is why reaching it is only ever returned, never approved |
prefersInAppWebView: true | Forces the in-app web view even for a GET. Useful when you claim no domain |
presentingViewController | Where the modal web view is presented from. Required for postForm and postHTML; the key window's root is the fallback |
additionalAllowedHosts | Restricts the chain further. https is enforced either way |
A GET redirect goes to ASWebAuthenticationSession, a real browser, which ACS providers that refuse embedded web views still render. postForm and postHTML have no system-browser equivalent and always use the modal web view.
Render and pay
struct RedirectionSection: View {
@ObservedObject var checkout: PurseHeadlessCheckout
let method: PrimaryMethod
/// checkout.paymentElement(for: .primaryMethod(method), listener: listener, redirect: presenter)
@ObservedObject var element: PaymentElement
var body: some View {
Button("Pay") { element.submit() }
.disabled(!element.isReady)
.task {
try? checkout.setPrimary(method)
await element.prepare()
}
}
}
hasUI is false: there is nothing to place, only your Pay button. prepare() makes no network call and element.isReady turns true at once. Unlike Android, the POST /init for a redirection method runs inside submit, as part of building the /validate payload, so creating the element consumes nothing. submit() runs /init and /validate, then presents whatever the answer asks for.
To know whether a listed method is one of these, ask the registry rather than reading the session yourself:
let plugin = PluginRegistry.shared.plugin(
partner: item.partner, method: item.method, session: checkout.session.source
)
let isRedirection = plugin is RedirectionPlugin
Outcomes
| Outcome | When |
|---|---|
returnedFromRedirect(url:) | The normal case: the shopper came back from the PSP page. Confirm the payment status on your backend |
completed(ValidationResult) | /validate returned no page to open (legitimate for some PSPs), or no presenter was passed |
canceled | The shopper dismissed the page without completing |
failed(PurseError) | /init or /validate failed, or the page could not open |
A return is never an outcome: the PSP's final page is built server-side and can land on a URL this client cannot read. Show the payment as pending in the app, and confirm its final status on your backend, from webhooks or Get a client session.
Open the page yourself
Two ways, depending on how much you want to take over:
- Pass no
redirect:presenter.submit()then reportscompleted, andcheckout.pendingRedirect(for: result)gives you the resolvedRedirectAction(.get,.postForm,.postHTML) to follow however you like. - Keep the presenter and supply the
onRedirectionhook inPurseCheckoutHooks. It receives the action before anything opens; returntrueto say you took it over, and the SDK opens nothing. Confirming the outcome on return is then yours. See Advanced guides.
Process death
iOS may kill the app while the shopper is in a bank app. Snapshot the checkout with saveState() and rebuild it with PurseHeadlessCheckout.restore(_:): a session already submitted refuses a second submit, and your backend still confirms the payment status. See Advanced guides.
Troubleshooting
| Symptom | Cause |
|---|---|
| The method never appears | The session does not mark it as redirection (a near-miss template value such as redirection_suffix does not match) |
completed right after Pay, nothing opened | Either no presenter was passed, or /validate returned no redirect. Check pendingRedirect(for:) to tell the two apart |
failed, message no view controller to present the redirect from | A postForm or postHTML redirect with no presentingViewController and no key window root |
failed, message refused redirect url | The policy rejected it: not https, or a host outside additionalAllowedHosts |
| The page opens but coming back does nothing | The return destination does not match what the PSP redirects to, or the Associated Domain / URL scheme is not declared |
| A universal-link return is never seen below iOS 17.4 | Expected: ASWebAuthenticationSession cannot observe an https callback there, so the SDK uses its own web view instead |
failed with API_ERROR, message Payment API POST /init failed: HTTP 4xx | The /init was refused for this item. Unlike the card and partner plugins, the redirection plugin does not inspect the answer's status.state, so only an HTTP-level refusal is reported here. A 200 that carries no step_id fails at /validate instead |
failed with METHOD_NOT_IMPLEMENTED | The item resolved to no plugin at all: the method is neither natively supported nor in redirection mode |