Skip to content

Implementation

Learn how to use card components in your iOS project.

Overview

Every card flow follows the same pattern:

  1. Build CheckoutConfig and call PxpCheckout.initialize(config:) (once per checkout session).
  2. Create components with try pxpCheckout.create(_:componentConfig:), passing the matching *ComponentConfig, and wire weak references where required (see About configuration).
  3. Render in SwiftUI with component.buildContent().

Before you start

Add PXPCheckoutSDK to your app. See Install.

You'll need a session from your server (session ID, HMAC key, and encryption key) and a merchant shopper ID that matches your backend model.

Step 1: Initialise the SDK

Call PxpCheckout.initialize(config:) with a CheckoutConfig. This can throw if mandatory fields are missing or inconsistent.

import PXPCheckoutSDK

let sessionData = SessionData(
    sessionId: "your-session-id",
    hmacKey: "your-hmac-key",
    encryptionKey: "your-encryption-key"
)

let transactionData = TransactionData(
    amount: 10,
    currency: "USD",
    entryType: .ecom,
    intent: TransactionIntentData(card: .authorisation),
    merchantTransactionId: "unique-transaction-id",
    merchantTransactionDate: { Date() }
)

let checkoutConfig = CheckoutConfig(
    environment: .test,
    session: sessionData,
    transactionData: transactionData,
    merchantShopperId: "shopper-id",
    ownerType: "MerchantGroup",
    ownerId: "your-owner-id",
    onGetShopper: { async in TransactionShopper(id: "shopper-id") },
    analyticsEvent: { event in
        _ = event.eventName
    }
)

let pxpCheckout = try PxpCheckout.initialize(config: checkoutConfig)

The following table describes the main properties in CheckoutConfig:

Property Description
environment
Environment
required
The environment type.

Possible values:
  • .test: For development and testing.
  • .live: For live transactions.
session
SessionData
required
Details about the checkout session returned from the Unity Sessions API. Includes sessionId, hmacKey, encryptionKey, and optional allowedFundingTypes.
transactionData
TransactionData
required
Details about the transaction. See the properties below for required fields.
transactionData.amount
Decimal
required
The transaction amount. Must match the currency's expected decimal places.
transactionData.currency
String
required
The currency code associated with the transaction, in ISO 4217 format. See Supported payment currencies.
transactionData.entryType
EntryType
required
The entry type.

Possible values:
  • .ecom: E-commerce transactions.
  • .moto: Mail order/telephone order transactions.
transactionData.intent
TransactionIntentData
required
The transaction intents for each payment method. See Supported transaction intents for details.
transactionData.merchantTransactionId
String
required
A unique identifier for this transaction. Use a UUID or order ID.
transactionData.merchantTransactionDate
() -> Date
required
A function that returns the current date and time.
transactionData.recurring
RecurringType?
Optional recurring payment data (frequencyInDays, frequencyExpiration).
transactionData.linkId
String?
Optional link identifier for related transactions.
transactionData.cardAcceptorName
String?
Optional card acceptor name for the transaction.
merchantShopperId
String
required
A unique identifier for the shopper in your system. Used for card tokenisation and Card-on-File functionality.
ownerType
String?
Always set to "MerchantGroup" for most integrations. Required for token and some API flows.
ownerId
String
required
Your unique merchant or merchant group identifier, as assigned by PXP. You can find it in the Unity Portal.
localisation
Localisation?
Optional UI string overrides for customising displayed text.
locale
String?
Optional locale code in BCP 47 format (for example en-GB).
paypalConfig
PayPalConfig?
PayPal-specific settings when you use PayPal components.
restrictions
Restrictions?
Optional card restrictions for owner types and funding sources. When both session and config restrictions are provided, they are merged as a union (session values first, then config values not already present). See Card restrictions for details.
kountDisabled
Bool
Whether to disable Kount fraud detection. Defaults to false.
onGetShippingAddress
() async -> ShippingAddress?
async closure returning shipping address information. Used for pre-filling billing address from shipping address.
onGetShopper
() async -> TransactionShopper?
async closure returning shopper information. Required for card tokenisation and Card-on-File functionality. Returns shopper object containing id, email, firstName, and lastName.
analyticsEvent
(BaseAnalyticsEvent) -> Void
Optional analytics callback. See Analytics for details.

Example for transactionData.recurring:

transactionData: TransactionData(
    // ...
    recurring: RecurringType(frequencyInDays: 30, frequencyExpiration: "2025-01-01")
)

Supported card transaction intents

The transactionData.intent.card property accepts the following intent types:

IntentDescription
.authorisationReserve funds on the customer's payment method (capture later). The default for most payment flows. Use when you need to hold funds temporarily, for example pending order shipment.
.purchaseCapture funds immediately after authorisation. Use for instant-fulfilment transactions like digital goods or instant services.
.verificationVerify that a card is legitimate and active, without reserving any funds or completing a purchase. Use for validating payment methods during customer onboarding.
.estimatedAuthorisationReserve funds on the customer's payment method, based on an estimated amount. The final amount is confirmed at capture time. Use in environments like hotels, car rental agencies, or fuel stations where the final charge may vary.
.payoutSend funds to the cardholder's card (also called disbursement or withdrawal). Use for marketplace seller payments, insurance settlements, refunds, or prize distributions. Funds move from your merchant account to the recipient's card.

Card restrictions

You can restrict which cards are accepted based on owner type (corporate or consumer) and funding source (credit, debit, or prepaid). Restrictions can be set in two places:

  • Session-level: returned from the backend in SessionData.restrictions.
  • Config-level: passed directly to CheckoutConfig.restrictions.

When both are provided, they're merged as a union (session values first, then config values not already present).

Structure

let restrictions = Restrictions(
    card: Restrictions.Card(
        ownerTypes: [.corporate, .consumer],    // Optional
        fundingSources: [.credit, .debit]       // Optional
    )
)

let checkoutConfig = CheckoutConfig(
    // ... other parameters
    restrictions: restrictions
)

Owner types

ValueDescription
.corporateCorporate/business cards
.consumerConsumer/personal cards

Funding sources

ValueDescription
.creditCredit cards
.debitDebit cards
.prepaidPrepaid cards

Example: Accept only consumer credit and debit cards

let checkoutConfig = CheckoutConfig(
    environment: .test,
    session: sessionData,
    transactionData: transactionData,
    merchantShopperId: "shopper-id",
    ownerId: "your-owner-id",
    restrictions: Restrictions(
        card: Restrictions.Card(
            ownerTypes: [.consumer],
            fundingSources: [.credit, .debit]
        )
    )
)

If ownerTypes or fundingSources is nil, no restriction is applied for that dimension. Setting both to nil means all cards are accepted.

Step 2: Create a component

Use try pxpCheckout.create(componentType, componentConfig:). Cast the returned BaseComponent to the concrete type when you need buildContent() or submit APIs.

let config = NewCardComponentConfig(/* … */)
guard let newCard = try pxpCheckout.create(.newCard, componentConfig: config) as? NewCardComponent else {
    throw /* or handle configuration error */
}

Step 3: Render the component

Embed buildContent() in your SwiftUI hierarchy. For CardSubmitComponent, submit collects data through weak references on CardSubmitComponentConfig:

  • Pre-built .newCard* field refs are wired internally; merchants only pass submit inside NewCardComponentConfig.
  • Standalone .cardSubmit: assign weak refs before render/submit, e.g.,
    • newCardComponent when reusing a NewCardComponent with a separate submit button, or
    • cardNumberComponent, cardExpiryDateComponent, cardCvcComponent, cardHolderNameComponent, cardConsentComponent for fully standalone fields.

See About configuration for the full weak-reference matrix.

import SwiftUI

struct PaymentScreen: View {
    let newCard: NewCardComponent

    var body: some View {
        ScrollView {
            VStack(alignment: .leading, spacing: 16) {
                Text("Payment")
                    .font(.title2)
                newCard.buildContent()
            }
            .padding()
        }
    }
}

Step 4: View and session lifetime

  • Hold PxpCheckout for as long as the checkout session is valid (typically the lifetime of your payment screen or a coordinator you inject).
  • Recreate PxpCheckout when you start a new server session (new SessionData), not on every SwiftUI body refresh.
  • Components are reference types; avoid recreating them unnecessarily. If you use @State, store the component instance you created after initialize, not a new config on every render.
  • There's no public SDK cleanup(); dropping references when the customer leaves checkout is sufficient for typical apps.

What's next?

Customise appearance

Use FieldStyle, FieldInputStateStyles, ContainerStyle, ButtonStateStyles, and per-component configs. Each component guide lists the relevant structs — start from New card or Card number.

var numberConfig = CardNumberComponentConfig(
    label: "Card number",
    acceptedCardBrands: [.visa, .mastercard, .amex]
)

Add event handling

iOS field configs use () -> Void for onChange / focus, and ([ValidationResult]) -> Void for validation callbacks (ValidationResult.valid, errors). Submit lifecycle lives on CardSubmitComponentConfig. onPreAuthorisation is required for card payment — if it's nil, submit stops silently (loading resets, no onPostAuthorisation, no onSubmitError).

var fields = NewCardComponentFields()
fields.cardNumber = CardNumberComponentConfig(
    label: "Card number",
    onCardBrandDetected: { event in
        // CardBrandDetectionEvent: cardBrand, isCardBrandAccepted
    },
    onValidationFailed: { results in
        for r in results where !r.valid {
            r.errors?.values.forEach { print($0.code, $0.message) }
        }
    }
)

var submit = CardSubmitComponentConfig()
submit.onPreAuthorisation = { (preAuthData: PreAuthorizationData?) async -> TransactionInitiationData? in
    // preAuthData?.gatewayTokenId, preAuthData?.schemeTokenId available after tokenisation
    TransactionInitiationData(
        psd2Data: nil,
        riskScreeningData: nil
    )
}
submit.onPostAuthorisation = { result in
    if result is MerchantSubmitResult { /* success */ }
    else if let refused = result as? RefusedSubmitResult { /* decline */ }
    else if let failed = result as? FailedSubmitResult { /* API / transport failure */ }
}
submit.onSubmitError = { error in
    print(error.errorCode, error.errorMessage) // BaseSdkException
}

let newCardConfig = NewCardComponentConfig(
    fields: fields,
    submit: submit,
    onValidation: { results in
        let ok = results.allSatisfy(\.valid)
        // drive your own CTA state
    }
)

Details: Events.

Analytics

Handle CheckoutConfig.analyticsEvent — pattern matching on BaseAnalyticsEvent subclasses (for example ComponentErrorAnalyticsEvent). See Analytics.

CheckoutConfig(
    // …
    analyticsEvent: { event in
        if let err = event as? ComponentErrorAnalyticsEvent {
            // log err.errorMessage, err.errorCode, etc.
        }
    }
)

Error handling

Use onSubmitError, do/catch around initialize / create, and downcast onPostAuthorisation results.

do {
    let checkout = try PxpCheckout.initialize(config: checkoutConfig)
    _ = checkout
} catch {
    // Show configuration / session error
}

Complete example (new card)

Minimal SwiftUI screen: bootstrap checkout, create new card, render it. Replace placeholders with real session values and tighten error handling for production.

import SwiftUI
import PXPCheckoutSDK

@MainActor
final class NewCardPaymentModel: ObservableObject {
    /// Keep checkout alive for the lifetime of the payment screen.
    private var pxpCheckout: PxpCheckout?
    @Published private(set) var newCard: NewCardComponent?
    @Published var bootstrapError: String?

    func load() {
        bootstrapError = nil
        do {
            let sessionData = SessionData(
                sessionId: "your-session-id",
                hmacKey: "your-hmac-key",
                encryptionKey: "your-encryption-key"
            )
            let transactionData = TransactionData(
                amount: 10,
                currency: "USD",
                entryType: .ecom,
                intent: TransactionIntentData(card: .authorisation),
                merchantTransactionId: UUID().uuidString,
                merchantTransactionDate: { Date() }
            )
            let checkoutConfig = CheckoutConfig(
                environment: .test,
                session: sessionData,
                transactionData: transactionData,
                merchantShopperId: "shopper-id",
                ownerType: "MerchantGroup",
                ownerId: "your-owner-id",
                onGetShopper: { async in TransactionShopper(id: "shopper-id") }
            )
            let checkout = try PxpCheckout.initialize(config: checkoutConfig)
            self.pxpCheckout = checkout

            var submitConfig = CardSubmitComponentConfig()
            submitConfig.onPreAuthorisation = { (preAuthData: PreAuthorizationData?) async -> TransactionInitiationData? in
                TransactionInitiationData(
                    psd2Data: nil,
                    riskScreeningData: nil
                )
            }
            submitConfig.onPostAuthorisation = { result in
                if result is MerchantSubmitResult {
                    // Go to success screen
                } else if let refused = result as? RefusedSubmitResult {
                    _ = refused.provider?.message
                }
            }
            submitConfig.onSubmitError = { error in
                _ = error.errorCode
            }

            var fields = NewCardComponentFields()
            fields.cardNumber = CardNumberComponentConfig(label: "Card number")
            fields.expiryDate = CardExpiryDateComponentConfig(label: "Expiry")
            fields.cvc = NewCardOptionalField(
                config: CardCvcComponentConfig(label: "CVC", isRequired: true),
                isShown: true
            )

            let newCardConfig = NewCardComponentConfig(
                fields: fields,
                submit: submitConfig
            )
            newCard = try checkout.create(.newCard, componentConfig: newCardConfig) as? NewCardComponent
        } catch {
            bootstrapError = error.localizedDescription
        }
    }
}

struct NewCardPaymentView: View {
    @StateObject private var model = NewCardPaymentModel()

    var body: some View {
        Group {
            if let card = model.newCard {
                card.buildContent()
            } else if let message = model.bootstrapError {
                Text(message).foregroundStyle(.red)
            } else {
                ProgressView()
            }
        }
        .padding()
        .onAppear { model.load() }
    }
}