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​

// Available currencies for a UI picker
let currencies = PaymentParams.supportedCurrencies // ["BGN", "CHF", "CZK", ...]

// Available countries for a UI picker
let countries = PaymentParams.supportedCountries // ["ALL", "AT", "BE", "CY", "CZ", ...]

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)
}
}