Skip to main content

Card data

Card payments are made via secure input components — Secure Fields. These components handle secure entry, validation, and protection of card data. The host application never has access to sensitive data in unprotected form.

Secure Fields

The library provides four components for entering data in the payment form:

SecurePanField

Field for the card number (PAN — Primary Account Number).

  • Accepts digits only
  • Automatically detects the card network (Visa, Mastercard)
  • Formats the card number with spaces according to the network (e.g. 4111 1111 1111 1111)
  • Limits input length according to the detected network
  • Validates the card number using the Luhn algorithm
  • Exposes the last4 property — last 4 digits of the card (safe for UI display)
import SwiftUI
import ComgateSDK

@StateObject private var panState = SecurePanFieldState()

var body: some View {
SecurePanField(state: panState)
}

SecureExpiryField

Field for the card expiry date.

  • Accepts digits only
  • Automatically formats input as MM/YY
  • Validates the month (1–12) and checks whether the card is not expired
@StateObject private var expiryState = SecureExpiryFieldState()

SecureExpiryField(state: expiryState)

SecureCvvField

Field for the card security code (CVV/CVC).

  • Accepts digits only
  • Length is 3 digits
  • Validates the minimum required length
@StateObject private var cvvState = SecureCvvFieldState()

SecureCvvField(state: cvvState)

SecureFullNameField

Field for the cardholder's full name.

  • Accepts text input (including spaces)
  • Considered valid when non-empty
  • After attaching to ComgateSecureSession, its value is automatically used as fullName
@StateObject private var fullNameState = SecureFullNameFieldState()

SecureFullNameField(state: fullNameState)
.onAppear { fullNameState.attachTo(session) }
.onDisappear { fullNameState.detachFrom(session) }
Information

PaymentParams.fullName always takes precedence over the value from SecureFullNameField — when fullName in PaymentParams is non-empty, the SecureFullNameField is not required. When fullName is empty, the value from the attached field is used (in which case the field is required). When both are empty, the payment fails with MISSING_CARDHOLDER_NAME.

Common properties

All Secure Field states (SecurePanFieldState, SecureExpiryFieldState, SecureCvvFieldState, SecureFullNameFieldState) share these properties and methods:

Property / MethodTypeDescription
isValidBoolCurrent validation state of the field (read-only, @Published).
errorTextString?Current error message — localized via translation.
translationTranslationTranslations used for placeholder and error messages. Set this to session.translation to match the session.
onFieldError((String?) -> Void)?Callback invoked when the error state changes.
clear()Clears the field and resets validation.
requestFocus()@discardableResult BoolProgrammatically focuses the field.

Field labels

Each field has a label above the input. The default label text is taken from translation. To set a custom label, override the matching key in Translation:

panState.translation = Translation(panLabel: "Payment card number")

SecureCardDataCollector

SecureCardDataCollector ties three separate Secure Fields together and tracks their overall validation state. It is required for payment processing. Create it once via the factory function secureCardDataCollector(pan:expiry:cvv:) and keep it in a @StateObject ObservableObject wrapper so it survives view re-renders:

@MainActor
final class CollectorHolder: ObservableObject {
let collector: SecureCardDataCollector
init(pan: SecurePanFieldState, expiry: SecureExpiryFieldState, cvv: SecureCvvFieldState) {
self.collector = secureCardDataCollector(pan: pan, expiry: expiry, cvv: cvv)
}
}

struct PaymentForm: View {
@StateObject private var panState: SecurePanFieldState
@StateObject private var expiryState: SecureExpiryFieldState
@StateObject private var cvvState: SecureCvvFieldState
@StateObject private var holder: CollectorHolder

init() {
let pan = SecurePanFieldState()
let expiry = SecureExpiryFieldState()
let cvv = SecureCvvFieldState()
_panState = StateObject(wrappedValue: pan)
_expiryState = StateObject(wrappedValue: expiry)
_cvvState = StateObject(wrappedValue: cvv)
_holder = StateObject(wrappedValue: CollectorHolder(pan: pan, expiry: expiry, cvv: cvv))
}

private var collector: SecureCardDataCollector { holder.collector }
}
Warning

Do not use var collector: SecureCardDataCollector { secureCardDataCollector(...) } as a computed property — it would create a new collector on every render, losing focus advance and validation state.

Property / MethodTypeDescription
isValidBooltrue when all three fields are valid.
autoAdvanceFocusBoolEnables/disables automatic focus advance between fields (PAN → Expiry → CVV). Default: true.
onValidationChanged((Bool) -> Void)?Callback invoked when the validation state of any field changes.

Focus advance between fields

By default the collector automatically advances focus between fields in the order:

PAN → Expiry → CVV

The advance happens once the current field is fully filled and valid. To control focus yourself, disable the automatic behavior:

let collector = secureCardDataCollector(pan: panState, expiry: expiryState, cvv: cvvState)
collector.autoAdvanceFocus = false

SecurePayButton

SecurePayButton is a pre-built payment button that integrates automatically with the session and the collector:

  • Automatically enabled/disabled based on the validation state of the card fields
  • Stays disabled until the session has been successfully initialized (session.state == .ready)
  • On tap, triggers payment processing via ComgateSecureSession
  • Displays a shimmer animation while processing (can be disabled via PayButtonStyle)
  • Returns the payment result via the onResult callback

Button setup

SecurePayButton(
session: session,
collector: collector,
paymentParams: {
try! PaymentParams(
email: "customer@example.com",
price: 100,
curr: "CZK",
country: "CZ",
label: "Payment label",
refId: "ref-123",
fullName: "John Doe",
billingAddrCity: "Hradec Králové",
billingAddrStreet: "Jiráskova 115",
billingAddrPostalCode: "50304",
billingAddrCountry: "CZ"
)
},
onResult: { result in
// Handle payment result
}
)

The paymentParams parameter is a closure invoked at the moment the button is tapped. This lets you read current values from the UI dynamically (e.g. amount from a text field).

PaymentParams

ParameterTypeRequiredDescription
emailStringYesPayer's email address.
priceIntYesPayment amount in the smallest currency unit (e.g. 10000 = 100.00 CZK). Must be a positive integer.
currStringYesCurrency code — ISO 4217 (e.g. "CZK", "EUR"). See PaymentParams.supportedCurrencies for the full list.
labelStringYesShort product description (1–16 characters).
refIdStringYesVariable symbol or order number (your internal ID).
fullNameStringConditionallyPayer's full name. When non-empty, takes precedence over the value from SecureFullNameField. When empty, the value from the attached SecureFullNameField is used (then required).
countryStringNoCountry code per ISO 3166-1 alpha-2 (e.g. "CZ", "SK"). Default: "CZ".
accountString?NoIdentifier of the client's bank account in the Comgate system.
nameString?NoProduct identifier (appears in the daily CSV as "Product").
preauthBool?NoMarks the payment as a preauthorization. Cannot be combined with initRecurring = true.
initRecurringBool?NoMarks the payment as the first in a series of recurring payments. Cannot be combined with preauth = true.
billingAddrCityString?NoBilling address — city.
billingAddrStreetString?NoBilling address — street.
billingAddrPostalCodeString?NoBilling address — postal code.
billingAddrCountryString?NoBilling address — country code (ISO 3166-1 alpha-2).
deliveryString?NoDelivery method ("HOME_DELIVERY", "PICKUP", "ELECTRONIC_DELIVERY").
homeDeliveryCityString?NoDelivery address — city (only when delivery = "HOME_DELIVERY").
homeDeliveryStreetString?NoDelivery address — street (only when delivery = "HOME_DELIVERY").
homeDeliveryPostalCodeString?NoDelivery address — postal code (only when delivery = "HOME_DELIVERY").
homeDeliveryCountryString?NoDelivery address — country code (only when delivery = "HOME_DELIVERY").
categoryString?NoProduct category ("PHYSICAL_GOODS_ONLY", "OTHER").
require3dsBool?NoDev mode only (devMode = true). Forces a particular 3DS flow. Ignored in production.
errorReasonErrorReason?NoDev mode only. Simulates a particular decline/failure reason. Ignored in production. See ErrorReason.
Information

The PaymentParams initializer throws ComgateError.invalidPrice when price is not a positive integer, or ComgateError.conflictingPaymentOptions when initRecurring = true is combined with preauth = true.

Supported currencies and countries

Supported currencies (PaymentParams.supportedCurrencies)

PaymentParams.supportedCurrencies returns the list of ISO 4217 currency codes accepted by the Comgate payment gateway. You can use this list to populate a currency picker in your UI.

BGN, CHF, CZK, DKK, EUR, GBP, HUF, NOK, PLN, RON, SEK, USD

Supported countries (PaymentParams.supportedCountries)

PaymentParams.supportedCountries returns the list of ISO 3166-1 alpha-2 country codes accepted for the country parameter. The value "ALL" represents no country restriction.

ALL, AT, BE, CY, CZ, DE, EE, EL, ES, FI, FR, GB, HR, HU, IE, IT, LT, LU, LV, MT, NL, NO, PL, PT, RO, SE, SI, SK, US

Example — populating currency and country pickers

let currencies = PaymentParams.supportedCurrencies
let countries = PaymentParams.supportedCountries

let params = try PaymentParams(
email: "customer@example.com",
price: 10000,
curr: selectedCurrency,
country: selectedCountry,
label: "Order",
refId: "order-123",
fullName: "John Doe"
)

Recurring payments and preauthorization

initRecurring

initRecurring = true marks the payment as the first (initial) transaction in a series of recurring payments. The bank gets a signal that further automatic payments will follow (e.g. subscriptions, periodic fees).

let params = try PaymentParams(
email: "customer@example.com",
price: 9900,
curr: "CZK",
label: "Subscription",
refId: "sub-001",
fullName: "John Doe",
initRecurring: true
)
Information

initRecurring cannot be combined with preauth = true.

preauth

preauth = true marks the payment as a preauthorization — the bank temporarily reserves the requested amount on the payer's card, but funds are not immediately captured. Capture (settlement) happens when the transaction is later confirmed on the backend.

let params = try PaymentParams(
email: "customer@example.com",
price: 50000,
curr: "CZK",
label: "Reservation",
refId: "reservation-42",
fullName: "John Doe",
preauth: true
)
Information

preauth cannot be combined with initRecurring = true.

Custom button and direct processPayment call

If the pre-built SecurePayButton doesn't fit your needs, you can trigger a payment by calling session.processPayment() directly.

Tip

The processPayment method encrypts card data, sends it to the payment gateway, and automatically performs 3D Secure authentication when needed. Sensitive data never leaves the library in unprotected form.

Method signature

public func processPayment(
collector: SecureCardDataCollector,
params: PaymentParams,
presenter: UIViewController? = nil
) async -> PaymentResult
ParameterTypeDescription
collectorSecureCardDataCollectorCollector tying together the Secure Fields (PAN, expiry, CVV).
paramsPaymentParamsPayment parameters (price, currency, label, refId, payer name, etc.).
presenterUIViewController?Optional presenter for the 3DS challenge UI. When nil, the library uses the current top view controller.

Implementation example

struct CustomPaymentScreen: View {
@ObservedObject var session: ComgateSecureSession

@StateObject private var panState: SecurePanFieldState
@StateObject private var expiryState: SecureExpiryFieldState
@StateObject private var cvvState: SecureCvvFieldState
@StateObject private var holder: CollectorHolder
@State private var isProcessing = false
@State private var resultText = ""

init(session: ComgateSecureSession) {
self._session = ObservedObject(wrappedValue: session)
let pan = SecurePanFieldState()
let expiry = SecureExpiryFieldState()
let cvv = SecureCvvFieldState()
_panState = StateObject(wrappedValue: pan)
_expiryState = StateObject(wrappedValue: expiry)
_cvvState = StateObject(wrappedValue: cvv)
_holder = StateObject(wrappedValue: CollectorHolder(pan: pan, expiry: expiry, cvv: cvv))
}

private var collector: SecureCardDataCollector { holder.collector }

var body: some View {
VStack(spacing: 12) {
SecurePanField(state: panState)
SecureExpiryField(state: expiryState)
SecureCvvField(state: cvvState)

Button(isProcessing ? "Processing…" : "Pay") {
Task {
isProcessing = true
let params = try! PaymentParams(
email: "customer@example.com",
price: 100,
curr: "CZK",
country: "CZ",
label: "Order #123",
refId: "order-123",
fullName: "John Doe"
)
let result = await session.processPayment(collector: collector, params: params)
isProcessing = false

switch result {
case .paid(let t): resultText = "Paid (\(t))"
case .authorized(let t): resultText = "Authorized (\(t))"
case .pending(let t): resultText = "Pending (\(t))"
case .cancelled(let reason, _): resultText = "Cancelled: \(reason ?? "-")"
case .failed(let err): resultText = "Error: \(err.message)"
}
}
}
.disabled(!collector.isValid || isProcessing || session.state != .ready)

Text(resultText)
}
.padding()
}
}
Warning

Before calling processPayment, make sure that:

  1. The session has been initialized (session.state == .ready).
  2. The card data is valid (collector.isValid == true).

If these conditions are not met, the method returns .failed(.sessionNotInitialized) or .failed(.invalidCardData) immediately.

3D Secure

The library provides full 3D Secure authentication support. When 3DS is configured, the library automatically:

  1. Prepares authentication parameters during payment processing
  2. Evaluates the server response (frictionless / challenge)
  3. Displays the challenge screen if needed
  4. Returns the authentication result through PaymentResult

Configuration

3DS is configured via the ThreeDSConfig struct passed to the ComgateSecureSession initializer:

let threeDSConfig = ThreeDSConfig(
uiCustomization: threeDSUi,
defaultMessageVersion: "2.2.0",
challengeTimeoutMinutes: 5,
challengeWindowCornerRadiusDp: 16
)

let session = ComgateSecureSession(
checkoutId: "your-checkout-id",
threeDSConfig: threeDSConfig
)

ThreeDSConfig parameters

ParameterTypeDefaultDescription
uiCustomizationThreeDSUiCustomization?nilCustomization of the challenge screen appearance. When nil, default styles are used.
defaultMessageVersionString"2.2.0"3DS protocol version. Supported values: see ThreeDSConfig.supportedMessageVersions.
challengeTimeoutMinutesInt5Maximum time (1–30) to wait for challenge completion in minutes.
challengeWindowCornerRadiusDpInt?nilCorner radius of the challenge window (0–64). When nil, the default is used.

Customizing the challenge screen appearance

The ThreeDSUiCustomization struct allows detailed customization of the 3DS challenge screen appearance:

let threeDSUi = ThreeDSUiCustomization(
buttons: [
.submit: ThreeDSButtonStyle(
backgroundColor: "#4287F5",
textColor: "#FFFFFF",
textFontSize: 16,
cornerRadius: 48
),
.continue: ThreeDSButtonStyle(
backgroundColor: "#4287F5",
textColor: "#FFFFFF",
textFontSize: 16,
cornerRadius: 48
),
.resend: ThreeDSButtonStyle(
backgroundColor: "#F0F0F0",
textColor: "#4287F5",
textFontSize: 16,
cornerRadius: 48
)
],
labelStyle: ThreeDSLabelStyle(
headingTextColor: "#4287F5",
textColor: "#333333"
),
textBoxStyle: ThreeDSTextBoxStyle(
borderColor: "#4287F5",
borderWidth: 4,
cornerRadius: 16,
textColor: "#333333"
),
toolbarStyle: ThreeDSToolbarStyle(
headerText: "Payment verification",
buttonText: "Cancel",
backgroundColor: "#F0F0F0",
textColor: "#333333"
)
)

ThreeDSButtonStyle

Customizes buttons on the challenge screen.

ParameterTypeDescription
backgroundColorString?Background color in #RRGGBB or #AARRGGBB format. Default "#4287F5".
textColorString?Text color in #RRGGBB or #AARRGGBB format. Default "#FFFFFF".
textFontSizeInt?Font size (8–48).
cornerRadiusInt?Corner radius of the button (0–64).

Buttons are configured in a dictionary keyed by button type:

Button typeDescription
.submitSubmit (confirm) button.
.continueContinue button.
.nextNext-step button.
.cancelCancel button.
.resendResend-code button.

ThreeDSLabelStyle

Customizes text labels.

ParameterTypeDescription
headingTextColorString?Heading text color.
headingTextFontSizeInt?Heading font size (8–48).
textColorString?Body text color.
textFontSizeInt?Body font size (8–48).

ThreeDSTextBoxStyle

Customizes input fields on the challenge screen.

ParameterTypeDescription
borderColorString?Border color.
borderWidthInt?Border width (0–16).
cornerRadiusInt?Corner radius (0–64).
textColorString?Text color.
textFontSizeInt?Text size (8–48).

ThreeDSToolbarStyle

Customizes the challenge screen toolbar.

ParameterTypeDescription
headerTextString?Toolbar header text.
buttonTextString?Toolbar button text (typically "Cancel").
backgroundColorString?Toolbar background color.
textColorString?Text color.
textFontSizeInt?Text size (8–48).
Information

Colors are provided as strings in #RRGGBB or #AARRGGBB format. Invalid formats raise the runtime validation error ThreeDSConfigurationError.invalidHexColor.

Testing 3DS payments

To make development and testing easier, the library offers — in dev mode (devMode = true) — the ability to simulate different 3DS flows without using a real card.

The test behavior is controlled by the require3ds parameter in PaymentParams:

require3ds valuePayment flow
trueServer returns a challenge — the 3DS challenge screen is shown and the user must enter an OTP or otherwise verify.
falseServer returns a frictionless result — payment goes through without a challenge screen.
nil (default)Server decides; standard behavior in production.
Warning

The require3ds parameter is functional only in dev mode (devMode = true). In production (devMode = false) it is ignored and the payment proceeds in the standard way.

Simulating a 3DS challenge

Set require3ds = true. The library shows the 3DS challenge screen and the user verifies. The payment result depends on the user's action:

  • Verification completed → .paid(transId:)
  • Challenge cancelled → .failed(.threeDSChallengeCancelled)
  • Timeout reached → .failed(.threeDSChallengeTimeout)
let params = try PaymentParams(
email: "customer@example.com",
price: 100,
curr: "CZK",
label: "Test payment",
refId: "test-001",
fullName: "John Doe",
require3ds: true
)

Simulating a frictionless flow

Set require3ds = false. The payment proceeds without a challenge screen.

let params = try PaymentParams(
email: "customer@example.com",
price: 100,
curr: "CZK",
label: "Test payment",
refId: "test-001",
fullName: "John Doe",
require3ds: false
)

Simulating an error reason — ErrorReason

In dev mode (devMode = true) you can use the errorReason parameter in PaymentParams to simulate a specific decline/failure reason.

let params = try PaymentParams(
email: "customer@example.com",
price: 100,
curr: "CZK",
label: "Test payment",
refId: "test-001",
fullName: "John Doe",
errorReason: .noFunds
)
Warning

The errorReason parameter is functional only in dev mode (devMode = true). In production it is fully ignored.

Available ErrorReason values:

ValueRaw valueDescription
.customerClickCUSTOMER_CLICKCancelled by the payer.
.fraudSuspectedFRAUD_SUSPECTEDSuspected fraud.
.eshopCancelledESHOP_CANCELLEDCancelled by the merchant.
.providerReportPROVIDER_REPORTCancelled by the provider.
.providerTimeoutPROVIDER_TIMEOUTProvider timeout.
.customerTimeoutCUSTOMER_TIMEOUTPayment timed out.
.acsTimeoutACS_TIMEOUTVerification timeout.
.invalidCardnoExpiryINVALID_CARDNO_EXPIRYInvalid card number or expiry date.
.invalidCvcINVALID_CVCInvalid CVC / CVV code.
.limitExceededLIMIT_EXCEEDEDCard limit exceeded.
.noFundsNO_FUNDSInsufficient funds.
.rejectedByBankREJECTED_BY_BANKRejected by the bank.
.threeDSAuthFail3DS_AUTH_FAIL3DS authentication failed.
.notSpecifiedNOT_SPECIFIEDNot specified.

Complete example

The following example shows a complete card payment implementation with 3D Secure, from initialization to result handling:

import SwiftUI
import ComgateSDK

@main
struct CardPaymentApp: App {
@StateObject private var session = ComgateSecureSession(
checkoutId: "your-checkout-id",
threeDSConfig: ThreeDSConfig(
uiCustomization: ThreeDSUiCustomization(
toolbarStyle: ThreeDSToolbarStyle(
headerText: "Payment verification",
buttonText: "Cancel"
)
),
challengeTimeoutMinutes: 5
)
)

var body: some Scene {
WindowGroup {
PaymentScreen(session: session)
.task {
if case .notInitialized = session.state {
try? await session.initialize()
}
}
}
}
}

struct PaymentScreen: View {
@ObservedObject var session: ComgateSecureSession

@StateObject private var panState: SecurePanFieldState
@StateObject private var expiryState: SecureExpiryFieldState
@StateObject private var cvvState: SecureCvvFieldState
@StateObject private var nameState: SecureFullNameFieldState
@StateObject private var statusState = PaymentStatusState()
@StateObject private var holder: CollectorHolder
@State private var cardInfo = "Enter card details"

init(session: ComgateSecureSession) {
self._session = ObservedObject(wrappedValue: session)
let pan = SecurePanFieldState()
let expiry = SecureExpiryFieldState()
let cvv = SecureCvvFieldState()
let name = SecureFullNameFieldState()
_panState = StateObject(wrappedValue: pan)
_expiryState = StateObject(wrappedValue: expiry)
_cvvState = StateObject(wrappedValue: cvv)
_nameState = StateObject(wrappedValue: name)
_holder = StateObject(wrappedValue: CollectorHolder(pan: pan, expiry: expiry, cvv: cvv))
}

private var collector: SecureCardDataCollector { holder.collector }

var body: some View {
ScrollView {
VStack(spacing: 12) {
Text(cardInfo)

SecureFullNameField(state: nameState)
SecurePanField(state: panState)
SecureExpiryField(state: expiryState)
SecureCvvField(state: cvvState)

SecurePayButton(
session: session,
collector: collector,
paymentParams: {
try! PaymentParams(
email: "customer@example.com",
price: 100,
curr: "CZK",
country: "CZ",
label: "Order #123",
refId: "order-123",
fullName: "John Doe",
billingAddrCity: "Hradec Králové",
billingAddrStreet: "Jiráskova 115",
billingAddrPostalCode: "50304",
billingAddrCountry: "CZ"
)
},
onResult: { result in
statusState.translation = session.translation
statusState.show(result: result)
}
)

SecurePaymentStatusView(state: statusState)
}
.padding()
}
.onChange(of: panState.isValid) { isValid in
cardInfo = isValid ? "✓ Card ending in \(panState.last4)" : "Enter card details"
}
.onAppear {
panState.translation = session.translation
expiryState.translation = session.translation
cvvState.translation = session.translation
nameState.translation = session.translation
nameState.attachTo(session)
}
.onDisappear { nameState.detachFrom(session) }
.secureLoadingOverlay(session: session)
}
}