Klarna
Klarna renders its native payment widget inside your screen. It is not an express button: your own Pay button submits, /validate runs first, and Klarna's authorization runs inside the same call.
Setup
| Product | PurseCheckoutKlarna (brings the Klarna Mobile SDK through SPM) |
| Install | PluginRegistry.shared.install(KlarnaModule()), once, before you build a checkout |
| Dashboard | Klarna installments activated on the entity. The Klarna client token reaches the app through /init |
| Project | A URL scheme in Info.plist, and the matching appReturnURL on every Klarna element |
| Keys | None in the app |
The app return URL
Klarna's widget can open a banking or identity app and must come back to yours. iOS has no applicationId for the SDK to synthesize a return URL from, the way Android does, so you supply it: both halves, and they must agree.
<!-- Info.plist -->
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>com.example.store.klarna</string>
<key>CFBundleURLSchemes</key>
<array><string>com.example.store.klarna</string></array>
</dict>
</array>
let element = checkout.paymentElement(
for: item,
listener: listener,
redirect: presenter,
appReturnURL: URL(string: "com.example.store.klarna://return")
)
Without appReturnURL the element renders nothing and reports failed on submit with Klarna needs an app return URL: pass appReturnURL to paymentElement(for:…).
Render and pay
Create the element when the shopper selects Klarna, then prepare() it: that runs POST /init for the client token and loads the widget.
struct KlarnaSection: View {
@ObservedObject var checkout: PurseHeadlessCheckout
let method: PrimaryMethod
@ObservedObject var element: PaymentElement
var body: some View {
VStack(spacing: 16) {
PaymentElementView(element) // the native widget, sized by Klarna
Button("Pay with Klarna") { element.submit() }
.disabled(!element.isReady)
}
.task {
try? checkout.setPrimary(method)
await element.prepare()
}
}
}
element.isReady turns true once the widget loaded. hasUI is true, selfSubmitting is false. submit() runs /validate, and only if it succeeded asks Klarna to authorize (finalizing when Klarna requires it). If /validate fails, Klarna is never asked.
Outcomes
| Outcome | When |
|---|---|
completed(ValidationResult) | /validate succeeded and Klarna authorized |
failed(PurseError) | The widget failed to load, /init or /validate failed, or Klarna declined or errored during its own authorization |
Klarna authorizes inside postValidation, so a decline arrives when /validate has already answered. The SDK reports it as failed with POST_VALIDATE_FAILED, wrapping a KlarnaAuthorizationDeclined you can match on, and the session is spent: do not offer a retry on the same session. This differs from Android, which maps a decline to Canceled.
Klarna's external bank or identity step is driven by Klarna's own widget through appReturnURL, not by the SDK's RedirectPresenter, so it does not surface as returnedFromRedirect. A failed before any submit (the widget did not load) is not a failed payment: nothing was charged, leave the shopper able to pick another method.
Instalment simulation
A Klarna method flagged simulable can show the schedule before the shopper selects it, with checkout.simulateLoan(_:product:) and simulateLoans(_:product:): Advanced guides.
Troubleshooting
| Symptom | Cause |
|---|---|
| The widget shows a generic "see our terms" card, no instalment breakdown | Klarna offers no instalment product for this amount and market. A Klarna-side configuration, not an SDK issue |
failed with PARTNER_UI_ERROR, message klarna init returned no klarna_client_token in logs | /init did not create a Klarna session for this item. Check the method's configuration |
failed with API_ERROR, message klarna init not successful | The /init was refused for this item |
failed with PARTNER_UI_ERROR, message Klarna widget failed to load | The widget could not initialize. The usual cause is a stale client token, because the session expired between selection and payment. Refresh the session |
failed with POST_VALIDATE_FAILED right after tapping Pay | /validate succeeded but Klarna declined the authorization. The session is spent |
failed, message Klarna authorize callback missing | submit() ran on an element whose prepare() never completed |
| After a bank app step, nothing comes back | The appReturnURL scheme is not declared in Info.plist, or the two do not match |
The method is listed but has no UI and submitting reports METHOD_NOT_IMPLEMENTED | KlarnaModule was never installed. See Payment methods |