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
last4property — 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 asfullName
@StateObject private var fullNameState = SecureFullNameFieldState()
SecureFullNameField(state: fullNameState)
.onAppear { fullNameState.attachTo(session) }
.onDisappear { fullNameState.detachFrom(session) }
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 / Method | Type | Description |
|---|---|---|
isValid | Bool | Current validation state of the field (read-only, @Published). |
errorText | String? | Current error message — localized via translation. |
translation | Translation | Translations 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 Bool | Programmatically 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 }
}
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 / Method | Type | Description |
|---|---|---|
isValid | Bool | true when all three fields are valid. |
autoAdvanceFocus | Bool | Enables/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
onResultcallback
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
| Parameter | Type | Required | Description |
|---|---|---|---|
email | String | Yes | Payer's email address. |
price | Int | Yes | Payment amount in the smallest currency unit (e.g. 10000 = 100.00 CZK). Must be a positive integer. |
curr | String | Yes | Currency code — ISO 4217 (e.g. "CZK", "EUR"). See PaymentParams.supportedCurrencies for the full list. |
label | String | Yes | Short product description (1–16 characters). |
refId | String | Yes | Variable symbol or order number (your internal ID). |
fullName | String | Conditionally | Payer'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). |
country | String | No | Country code per ISO 3166-1 alpha-2 (e.g. "CZ", "SK"). Default: "CZ". |
account | String? | No | Identifier of the client's bank account in the Comgate system. |
name | String? | No | Product identifier (appears in the daily CSV as "Product"). |
preauth | Bool? | No | Marks the payment as a preauthorization. Cannot be combined with initRecurring = true. |
initRecurring | Bool? | No | Marks the payment as the first in a series of recurring payments. Cannot be combined with preauth = true. |
billingAddrCity | String? | No | Billing address — city. |
billingAddrStreet | String? | No | Billing address — street. |
billingAddrPostalCode | String? | No | Billing address — postal code. |
billingAddrCountry | String? | No | Billing address — country code (ISO 3166-1 alpha-2). |
delivery | String? | No | Delivery method ("HOME_DELIVERY", "PICKUP", "ELECTRONIC_DELIVERY"). |
homeDeliveryCity | String? | No | Delivery address — city (only when delivery = "HOME_DELIVERY"). |
homeDeliveryStreet | String? | No | Delivery address — street (only when delivery = "HOME_DELIVERY"). |
homeDeliveryPostalCode | String? | No | Delivery address — postal code (only when delivery = "HOME_DELIVERY"). |
homeDeliveryCountry | String? | No | Delivery address — country code (only when delivery = "HOME_DELIVERY"). |
category | String? | No | Product category ("PHYSICAL_GOODS_ONLY", "OTHER"). |
require3ds | Bool? | No | Dev mode only (devMode = true). Forces a particular 3DS flow. Ignored in production. |
errorReason | ErrorReason? | No | Dev mode only. Simulates a particular decline/failure reason. Ignored in production. See ErrorReason. |
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
)
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
)
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.
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
| Parameter | Type | Description |
|---|---|---|
collector | SecureCardDataCollector | Collector tying together the Secure Fields (PAN, expiry, CVV). |
params | PaymentParams | Payment parameters (price, currency, label, refId, payer name, etc.). |
presenter | UIViewController? | 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()
}
}
Before calling processPayment, make sure that:
- The session has been initialized (
session.state == .ready). - 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:
- Prepares authentication parameters during payment processing
- Evaluates the server response (frictionless / challenge)
- Displays the challenge screen if needed
- 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
| Parameter | Type | Default | Description |
|---|---|---|---|
uiCustomization | ThreeDSUiCustomization? | nil | Customization of the challenge screen appearance. When nil, default styles are used. |
defaultMessageVersion | String | "2.2.0" | 3DS protocol version. Supported values: see ThreeDSConfig.supportedMessageVersions. |
challengeTimeoutMinutes | Int | 5 | Maximum time (1–30) to wait for challenge completion in minutes. |
challengeWindowCornerRadiusDp | Int? | nil | Corner 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.
| Parameter | Type | Description |
|---|---|---|
backgroundColor | String? | Background color in #RRGGBB or #AARRGGBB format. Default "#4287F5". |
textColor | String? | Text color in #RRGGBB or #AARRGGBB format. Default "#FFFFFF". |
textFontSize | Int? | Font size (8–48). |
cornerRadius | Int? | Corner radius of the button (0–64). |
Buttons are configured in a dictionary keyed by button type:
| Button type | Description |
|---|---|
.submit | Submit (confirm) button. |
.continue | Continue button. |
.next | Next-step button. |
.cancel | Cancel button. |
.resend | Resend-code button. |
ThreeDSLabelStyle
Customizes text labels.
| Parameter | Type | Description |
|---|---|---|
headingTextColor | String? | Heading text color. |
headingTextFontSize | Int? | Heading font size (8–48). |
textColor | String? | Body text color. |
textFontSize | Int? | Body font size (8–48). |
ThreeDSTextBoxStyle
Customizes input fields on the challenge screen.
| Parameter | Type | Description |
|---|---|---|
borderColor | String? | Border color. |
borderWidth | Int? | Border width (0–16). |
cornerRadius | Int? | Corner radius (0–64). |
textColor | String? | Text color. |
textFontSize | Int? | Text size (8–48). |
ThreeDSToolbarStyle
Customizes the challenge screen toolbar.
| Parameter | Type | Description |
|---|---|---|
headerText | String? | Toolbar header text. |
buttonText | String? | Toolbar button text (typically "Cancel"). |
backgroundColor | String? | Toolbar background color. |
textColor | String? | Text color. |
textFontSize | Int? | Text size (8–48). |
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 value | Payment flow |
|---|---|
true | Server returns a challenge — the 3DS challenge screen is shown and the user must enter an OTP or otherwise verify. |
false | Server returns a frictionless result — payment goes through without a challenge screen. |
nil (default) | Server decides; standard behavior in production. |
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
)
The errorReason parameter is functional only in dev mode (devMode = true). In production it is fully ignored.
Available ErrorReason values:
| Value | Raw value | Description |
|---|---|---|
.customerClick | CUSTOMER_CLICK | Cancelled by the payer. |
.fraudSuspected | FRAUD_SUSPECTED | Suspected fraud. |
.eshopCancelled | ESHOP_CANCELLED | Cancelled by the merchant. |
.providerReport | PROVIDER_REPORT | Cancelled by the provider. |
.providerTimeout | PROVIDER_TIMEOUT | Provider timeout. |
.customerTimeout | CUSTOMER_TIMEOUT | Payment timed out. |
.acsTimeout | ACS_TIMEOUT | Verification timeout. |
.invalidCardnoExpiry | INVALID_CARDNO_EXPIRY | Invalid card number or expiry date. |
.invalidCvc | INVALID_CVC | Invalid CVC / CVV code. |
.limitExceeded | LIMIT_EXCEEDED | Card limit exceeded. |
.noFunds | NO_FUNDS | Insufficient funds. |
.rejectedByBank | REJECTED_BY_BANK | Rejected by the bank. |
.threeDSAuthFail | 3DS_AUTH_FAIL | 3DS authentication failed. |
.notSpecified | NOT_SPECIFIED | Not 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)
}
}