Learn how to diagnose and fix common issues with Checkout Drop-in.
If you're experiencing issues with Drop-in, start with these quick diagnostic checks:
import PXPCheckoutSDK
import UIKit
import Network
// Drop-in diagnostic helper
func diagnoseDropIn(
sessionData: SessionData?,
config: CheckoutDropInConfig
) {
print("=== Checkout Drop-in diagnostics ===")
// Check SDK import
print("SDK imported: \(CheckoutDropIn.self)")
// Check session data
// Required for init: non-empty sessionId and hmacKey (empty → SDK0103 / SDK0104 throws).
// encryptionKey is needed for card secured fields; empty may not throw at init.
print("Session ID: \((sessionData?.sessionId.isEmpty == false) ? "Present" : "Missing")")
print("HMAC key: \((sessionData?.hmacKey.isEmpty == false) ? "Present" : "Missing")")
print("Encryption key: \((sessionData?.encryptionKey.isEmpty == false) ? "Present" : "Missing")")
if let sessionExpiry = sessionData?.sessionExpiry {
print("Session expiry: \(sessionExpiry)")
}
if let restrictions = sessionData?.restrictions {
print("Restrictions: \(String(describing: restrictions))")
}
print("Allowed funding types: \(String(describing: sessionData?.allowedFundingTypes))")
// Check environment
print("Environment: \(config.environment)")
print("Owner ID: \(config.ownerId)")
print("Merchant Shopper ID: \(config.merchantShopperId)")
// Check device info
print("iOS version: \(UIDevice.current.systemVersion)")
print("Device: \(UIDevice.current.model)")
print("Screen scale: \(UIScreen.main.scale)")
// Check network connectivity
let monitor = NWPathMonitor()
let queue = DispatchQueue(label: "NetworkMonitor")
monitor.pathUpdateHandler = { path in
print("Network available: \(path.status == .satisfied)")
}
monitor.start(queue: queue)
print("===================================")
}Look for these symptoms:
- The component doesn't appear in your SwiftUI view.
- No payment methods are visible.
- There are no errors in the console.
| Cause | Solution |
|---|---|
create() not completed. | Always await dropIn.create() before expecting the full Drop-in UI. While create() is running, buildContent() shows a built-in loading indicator (CheckoutDropInInitializingView). If getCheckoutDropInConfig fails or no payment methods can be created, create() finishes without a component and onError fires (for example SDK1100 or SDK0204); buildContent() may then return an empty view. Wire onError so failures are not mistaken for a silent blank screen. |
| SwiftUI view update issue. | Ensure the session data is loaded before rendering CheckoutDropIn. Use .task or onAppear to fetch session data and conditional rendering. |
| Session data invalid. | Required for init: non-empty sessionId and hmacKey (empty values throw SDK0103 / SDK0104 from CheckoutDropIn(config:)). Also supply a valid encryptionKey from the session response for card tokenisation — an empty encryption key may not throw at init but will break secured fields later. Verify allowedFundingTypes and backend session creation logs. |
| No payment methods enabled. | Check allowedFundingTypes in your session. At least one payment method must be enabled in the Unity Portal. For cards, Drop-in requires the cards key (an empty array is valid). cardSchemes alone doesn't enable the card panel. |
| Card panel hidden by display config. | If wallets aren't available and DropInCardConfig sets both showCOF and showNewCard to false, Drop-in creates no payment methods and can fire SDK0204. Enable at least one card section, or enable another payment method in the session. |
| Incorrect configuration. | Verify environment and ownerId are set correctly. |
| SDK not properly linked. | Verify that the Swift Package Manager dependency is properly resolved in Xcode. |
Diagnostic steps:
import SwiftUI
import PXPCheckoutSDK
struct DiagnoseRenderingIssueView: View {
@StateObject private var viewModel = DiagnosticViewModel()
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 8) {
Text(viewModel.diagnostics)
.font(.system(.caption, design: .monospaced))
if let dropIn = viewModel.dropIn {
dropIn.buildContent()
} else {
ProgressView("Loading...")
}
}
.padding()
}
.task {
await viewModel.diagnose()
}
}
}
@MainActor
final class DiagnosticViewModel: ObservableObject {
@Published var dropIn: CheckoutDropIn?
@Published var diagnostics: String = ""
private var isCreatingDropIn = false
private var createFailed = false
func diagnose() async {
diagnostics += "Step 1: Fetching session...\n"
do {
let sessionData = try await fetchSessionFromBackend()
diagnostics += "Step 2: Session fetched successfully\n"
diagnostics += "Session ID: \(sessionData.sessionId)\n"
// Step 3: Validate session data
diagnostics += "Step 3: Validating session data...\n"
if sessionData.sessionId.isEmpty {
diagnostics += "ERROR: Session ID missing\n"
} else {
diagnostics += "Session ID present\n"
}
if sessionData.allowedFundingTypes == nil {
diagnostics += "ERROR: No allowed funding types\n"
} else {
var paymentMethodCount = 0
// Cards: Drop-in requires the cards key (empty array is valid).
// cardSchemes alone does not enable the card panel.
if sessionData.allowedFundingTypes?.cards != nil {
paymentMethodCount += 1
diagnostics += "Cards key present\n"
} else if sessionData.allowedFundingTypes?.cardSchemes != nil {
diagnostics += "WARNING: cardSchemes is set but does not enable the Card panel (cards key missing)\n"
}
// PayPal: must have allowedFundingOptions present and non-empty
if let paypalConfig = sessionData.allowedFundingTypes?.wallets?.paypal,
let allowedFundingOptions = paypalConfig.allowedFundingOptions,
!allowedFundingOptions.isEmpty {
paymentMethodCount += 1
}
// Apple Pay: must have merchantId present and non-empty
if let applePayConfig = sessionData.allowedFundingTypes?.wallets?.applePay,
let merchantId = applePayConfig.merchantId,
!merchantId.isEmpty {
paymentMethodCount += 1
}
// Aeropay: funding object present (credentials validated at create)
if sessionData.allowedFundingTypes?.payByBanks?.aeropay != nil {
paymentMethodCount += 1
}
if paymentMethodCount == 0 {
diagnostics += "ERROR: No payment methods enabled in session (SDK0204 risk)\n"
} else {
diagnostics += "\(paymentMethodCount) payment method(s) available from session checks\n"
diagnostics += "Note: session checks alone can still produce SDK0204 if card display config hides Card and no wallets render\n"
}
}
// Card display overrides (defaults true when omitted)
let showCOF = true
let showNewCard = true
diagnostics += "showCOF=\(showCOF) showNewCard=\(showNewCard)\n"
if !showCOF && !showNewCard {
diagnostics += "WARNING: Both showCOF and showNewCard are false — Card panel hidden even if session.cards is present\n"
}
// Initialise Drop-in
isCreatingDropIn = true
createFailed = false
let config = CheckoutDropInConfig(
environment: .test,
session: sessionData,
transactionData: DropInTransactionData(
amount: Decimal(string: "1.00") ?? 0,
currency: "USD",
entryType: .ecom,
intent: DropInTransactionIntentData(
card: .authorisation
),
merchantTransactionId: "test-\(Date().timeIntervalSince1970)",
merchantTransactionDate: { Date() }
),
merchantShopperId: "shopper-123",
ownerId: "MERCHANT_GROUP_1", // Merchant group ID (ownerType is always "MerchantGroup")
methodConfig: DropInMethodConfig(
card: DropInCardConfig(showCOF: showCOF, showNewCard: showNewCard)
),
onSuccess: { result in
print("Success: \(result.systemTransactionId)")
},
onError: { paymentMethod, error in
if isCreatingDropIn {
createFailed = true
}
diagnostics += "onError: \(error.errorCode): \(error.errorMessage)\n"
print("Error: \(error.errorMessage)")
}
)
let instance = try CheckoutDropIn(config: config)
// Create-time failures (including SDK1100 and method setup errors) are delivered
// through onError, not thrown from create().
await instance.create()
isCreatingDropIn = false
if !createFailed {
dropIn = instance
} else {
diagnostics += "create() finished but Drop-in did not mount. See onError above.\n"
}
} catch {
diagnostics += "ERROR: \(error.localizedDescription)\n"
print("Diagnostic error: \(error)")
}
}
}Look for these symptoms:
- A "Session expired" error message is displayed.
- The drop-in loads but payment fails immediately.
- Console shows session timeout errors.
| Cause | Solution |
|---|---|
| Session has expired. | Sessions expire based on your backend configuration. Check the optional sessionExpiry field returned by your backend when creating the session. Create a new session when needed. |
| Clock skew | Ensure that the server and device clocks are synchronised. |
| Session reused | Treat sessions as checkout-attempt scoped unless your backend explicitly supports reuse. Always fetch a fresh session for new checkout attempts. |
| Invalid HMAC signature | Verify that the HMAC key matches between session creation and SDK initialisation. |
Solution: Implement session refresh
import SwiftUI
import PXPCheckoutSDK
struct CheckoutViewWithSessionRefresh: View {
@StateObject private var viewModel = CheckoutViewModel()
var body: some View {
Group {
if let errorMessage = viewModel.errorMessage {
VStack {
Text(errorMessage)
.foregroundColor(.red)
Button("Refresh") {
Task {
await viewModel.refreshSession()
}
}
}
}
if let dropIn = viewModel.dropIn {
dropIn.buildContent()
} else if viewModel.errorMessage == nil {
ProgressView("Loading checkout...")
}
}
.task {
await viewModel.loadDropIn()
}
}
}
@MainActor
final class CheckoutViewModel: ObservableObject {
@Published var dropIn: CheckoutDropIn?
@Published var errorMessage: String?
private var isCreatingDropIn = false
private var createFailed = false
func loadDropIn() async {
do {
let sessionData = try await fetchSessionFromBackend()
try await initializeDropIn(with: sessionData)
} catch {
print("Failed to fetch session: \(error)")
errorMessage = "Failed to load checkout. Please try again."
}
}
func refreshSession() async {
errorMessage = nil
dropIn = nil
await loadDropIn()
}
private func initializeDropIn(with sessionData: SessionData) async throws {
isCreatingDropIn = true
createFailed = false
let config = CheckoutDropInConfig(
environment: .live,
session: sessionData,
transactionData: DropInTransactionData(
amount: Decimal(string: "99.99") ?? 0,
currency: "USD",
entryType: .ecom,
intent: DropInTransactionIntentData(
card: .authorisation
),
merchantTransactionId: UUID().uuidString,
merchantTransactionDate: { Date() }
),
merchantShopperId: "shopper-123",
ownerId: "MERCHANT_GROUP_1", // Merchant group ID (ownerType is always "MerchantGroup")
onSuccess: { result in
Task {
await verifyPaymentOnBackend(result)
}
},
onError: { paymentMethod, error in
if isCreatingDropIn {
createFailed = true
}
// Handle session expiry
if error.errorMessage.localizedCaseInsensitiveContains("expired") ||
error.errorMessage.localizedCaseInsensitiveContains("session") {
print("Session expired, refreshing...")
Task { @MainActor in
self.errorMessage = "Session expired. Please refresh."
}
} else {
print("Payment error: \(error.errorMessage)")
Task { @MainActor in
self.errorMessage = "Payment failed: \(error.errorMessage)"
}
}
}
)
// try only covers SDK0103 / SDK0104 (empty sessionId / hmacKey).
// Create-time failures (including SDK1100 and method setup errors) are delivered
// through onError, not thrown from create().
let instance = try CheckoutDropIn(config: config)
await instance.create()
isCreatingDropIn = false
if !createFailed {
dropIn = instance
}
}
}
func fetchSessionFromBackend() async throws -> SessionData {
// Implementation to fetch session from your backend
// ...
}Look for these symptoms:
- An expected payment method isn't shown.
- Only the card payment method is visible.
- Apple Pay doesn't appear even though it's enabled.
| Payment method | Common causes | Solutions |
|---|---|---|
| Cards | Session missing the cards key (having only cardSchemes is not enough), or both showCOF and showNewCard are false. | Enable the Card service in the Unity Portal and verify that the session includes allowedFundingTypes.cards (an empty array is valid). cardSchemes is used for network resolution but does not substitute for cards. Check methodConfig.card: at least one of showCOF or showNewCard must resolve to true for the card panel to appear. For card-on-file only, also return a non-empty shopper id from onGetShopper. Optional session customerProfileId doesn't replace merchantShopperId or onGetShopper for card-on-file. See Implementation — Customer Profile. |
| PayPal | PayPal onboarding wasn't completed, allowedFundingOptions is missing or empty, or entryType isn't .ecom. | Complete PayPal onboarding in the Unity Portal. Verify the session includes allowedFundingTypes.wallets.paypal and a non-empty allowedFundingOptions (or a methodConfig.paypal.fundingSources override). PayPal also requires entryType: .ecom (SDK1120 otherwise). |
| Apple Pay | Apple Pay not configured in Xcode, no cards in Apple Wallet, or device doesn't support Apple Pay. | Enable Apple Pay capability in Xcode. Provision cards in Apple Wallet via Apple Pay sandbox testing, not the manual card-form PANs from the Testing page. Prefer a physical device for end-to-end Wallet testing. |
| Aeropay | Missing payByBanks.aeropay (panel hidden with no onError), currency isn't USD, entry type isn't .ecom, missing intent, blank credentials, or invalid non-empty shopper data at load. | Enable Aeropay in the Unity Portal and confirm the session response includes allowedFundingTypes.payByBanks.aeropay with externalMerchantId and configurationId. If payByBanks.aeropay is missing, the Aeropay panel is not shown and Drop-in does not fire onError for Aeropay. Use currency = "USD" and entryType: .ecom. Set intent.aeropay. Blank credentials or missing intent typically surface as SDK0113 or SDK0115 on onError (not only SDK1125). Malformed non-empty shopper fields fire SDK1300 and hide the panel; empty fields are OK. See Aeropay. |
Diagnostic steps:
import PassKit
import PXPCheckoutSDK
func diagnosePaymentMethodVisibility(
sessionData: SessionData?,
config: CheckoutDropInConfig
) async {
print("=== Payment method diagnostics ===")
let fundingTypes = sessionData?.allowedFundingTypes
print("Session funding types: \(String(describing: fundingTypes))")
// Check cards — requires the cards key (empty array OK). cardSchemes alone is insufficient.
if fundingTypes?.cards != nil {
print("Cards key present: \(String(describing: fundingTypes?.cards))")
print("cardSchemes (networks only): \(String(describing: fundingTypes?.cardSchemes))")
} else {
print("WARNING: Cards not enabled in session (allowedFundingTypes.cards must not be nil)")
if fundingTypes?.cardSchemes != nil {
print("WARNING: cardSchemes is set but does not enable the Card panel")
}
}
let showCOF = config.methodConfig?.card?.showCOF ?? true
let showNewCard = config.methodConfig?.card?.showNewCard ?? true
print("showCOF=\(showCOF) showNewCard=\(showNewCard)")
if showCOF == false && showNewCard == false {
print("WARNING: Both showCOF and showNewCard are false — Card panel will be hidden even if session.cards is present")
}
if showCOF {
let shopper = await config.onGetShopper?()
if let shopperId = shopper?.id, !shopperId.isEmpty {
print("onGetShopper returned non-empty id")
} else {
print("WARNING: Card-on-file needs onGetShopper to return TransactionShopper(id:) with a non-empty id")
}
}
// Check PayPal - must have allowedFundingOptions present and non-empty
if let paypalConfig = fundingTypes?.wallets?.paypal {
print("PayPal wallet present")
if let allowedFundingOptions = paypalConfig.allowedFundingOptions,
!allowedFundingOptions.isEmpty {
print("PayPal enabled with funding options: \(allowedFundingOptions)")
} else if let fundingSources = config.methodConfig?.paypal?.fundingSources,
!fundingSources.isEmpty {
print("PayPal fundingSources override present: \(fundingSources)")
} else {
print("WARNING: PayPal wallet present but allowedFundingOptions is missing/empty (and no fundingSources override)")
}
if config.transactionData.entryType != .ecom {
print("WARNING: PayPal requires entryType .ecom (SDK1120)")
}
} else {
print("WARNING: PayPal not enabled (wallets.paypal missing)")
}
// Check Apple Pay - must have merchantId present and non-empty
if let applePayConfig = fundingTypes?.wallets?.applePay,
let merchantId = applePayConfig.merchantId,
!merchantId.isEmpty {
print("Apple Pay configuration present with merchant ID: \(merchantId)")
if PKPaymentAuthorizationController.canMakePayments() {
print("Device supports Apple Pay")
let networks = config.methodConfig?.global?.acceptedCardNetworks?.compactMap { network -> PKPaymentNetwork? in
switch network {
case .visa: return .visa
case .mastercard: return .masterCard
case .amex: return .amex
case .discover: return .discover
default: return nil
}
} ?? [.visa, .masterCard, .amex]
if PKPaymentAuthorizationController.canMakePayments(usingNetworks: networks) {
print("Apple Pay has cards configured for accepted networks: \(networks)")
} else {
print("WARNING: No cards configured in Apple Wallet for accepted networks")
print(" Provision Apple sandbox Wallet cards — not Drop-in manual-entry PANs")
}
} else {
print("WARNING: Device does not support Apple Pay")
}
} else {
print("WARNING: Apple Pay not configured (wallets.applePay.merchantId must be present and non-empty)")
}
// Check Aeropay - funding object present; credentials must be non-blank
if let aeropay = fundingTypes?.payByBanks?.aeropay {
print("Aeropay funding present: \(aeropay)")
let externalMerchantId = aeropay.externalMerchantId ?? ""
let configurationId = aeropay.configurationId ?? ""
if externalMerchantId.isEmpty || configurationId.isEmpty {
print("WARNING: Aeropay credentials incomplete (externalMerchantId or configurationId). Expect SDK0113 via onError")
}
if config.transactionData.currency != "USD" {
print("WARNING: Aeropay requires USD (SDK0116 if not)")
}
if config.transactionData.entryType != .ecom {
print("WARNING: Aeropay requires entryType .ecom (SDK0114 if not)")
}
if config.transactionData.intent.aeropay == nil {
print("WARNING: Aeropay intent missing (expect SDK0115 via onError)")
}
} else {
print("WARNING: Aeropay not configured in session (payByBanks.aeropay missing)")
}
print("iOS version: \(UIDevice.current.systemVersion)")
print("Device: \(UIDevice.current.model)")
print("===================================")
}Look for these symptoms:
- Apple Pay button doesn't appear.
- Apple Pay sheet doesn't open when tapped.
- Payment fails with Apple Pay errors.
| Cause | Solution |
|---|---|
| Apple Pay capability not enabled. | Enable Apple Pay capability in Xcode project settings. |
| No cards in Apple Wallet. | Provision Apple sandbox test credentials in Apple Wallet via Apple Pay sandbox testing. Don't reuse Drop-in manual card-entry PANs from the Testing page. |
| Minimum iOS version not met. | Apple Pay requires iOS 14.0 or higher. |
| Merchant ID missing or invalid. | Add a valid Apple Pay merchant ID in your Xcode project and Unity Portal. |
| Device not supported. | Use a physical device for final validation. Simulator is useful for UI checks only but may not fully represent production Apple Pay behaviour (SDK0602 means Apple Pay isn't supported on the current device or iOS version). |
Solution:
import PassKit
func checkApplePayAvailability() {
print("=== Apple Pay Diagnostics ===")
// Check if device supports Apple Pay
if PKPaymentAuthorizationController.canMakePayments() {
print("✓ Device supports Apple Pay")
} else {
print("✗ Device does not support Apple Pay")
return
}
// Check if user has cards configured
let networks: [PKPaymentNetwork] = [.visa, .masterCard, .amex]
if PKPaymentAuthorizationController.canMakePayments(usingNetworks: networks) {
print("✓ Apple Pay has cards configured")
} else {
print("✗ No cards configured in Apple Wallet")
print(" Provision Apple sandbox Wallet cards — not Drop-in manual-entry PANs")
}
// Check iOS version
if #available(iOS 14.0, *) {
print("✓ iOS version compatible (14.0+)")
} else {
print("✗ iOS version too old (requires 14.0+)")
}
print("=============================")
}Look for these symptoms:
- The Aeropay payment method isn't visible.
- Other methods appear, but Pay by bank via Aeropay doesn't.
Verify these conditions:
- Session includes
allowedFundingTypes.payByBanks.aeropaywith non-blankexternalMerchantIdandconfigurationId. transactionData.currencyis"USD".transactionData.entryTypeis.ecom.transactionData.intent.aeropayis set.
If payByBanks.aeropay is missing from the session, the Aeropay panel isn't shown and Drop-in doesn't fire onError for Aeropay. Enable Aeropay in the Unity Portal and confirm the session response includes the funding object.
If onError fires with SDK0114 (entry type), SDK0116 (currency), SDK0113 or SDK0115 (create failures — typically the codes merchants see for blank credentials or missing intent), rare SDK1125 (unexpected non-BaseSdkException render failures), or SDK1300 (shopper data), see Aeropay.
Tips for common Aeropay codes:
SDK0114: setentryType: .ecom.SDK0116: setcurrencyto"USD".SDK0113: session Aeropay credentials are blank. FixexternalMerchantIdandconfigurationIdon the session response. This is the usual code merchants see for blank credentials (not onlySDK1125).SDK0115: setintent.aeropayonDropInTransactionIntentData. This is Aeropay intent missing, not card authorisation (SDK1115).SDK1125: fallback for unexpected Aeropay render failures that aren't aBaseSdkException. Blank credentials or missing intent usually surface asSDK0113orSDK0115instead.SDK1300: fix malformed non-emptyonGetShopperfields, or providemethodConfig.aeropay.userId. Empty fields and empty phone are OK at load.- Missing verified user ID on bank-list load typically surfaces as
SDK1303, notSDK1311. See Aeropay — Error codes for mid-flow codes (SDK1301–SDK1325).
Look for these symptoms:
- Frontend
onSuccessfires but backend verification fails. - Orders aren't being fulfilled.
- "Payment verification failed" errors.
| Cause | Solution |
|---|---|
| Webhook not configured. | Set up a webhook URL in the Unity Portal and implement a webhook handler on your backend. |
| Webhook authentication failing. | Verify your webhook signature/authentication. Check your HMAC implementation. |
| Race condition (GET before webhook). | Implement a fallback to the Get transaction details API if the webhook hasn't arrived yet. |
| Amount mismatch. | Ensure that the amount in the verification request exactly matches the transaction amount. |
| Transaction ID mismatch | Verify that the systemTransactionId and merchantTransactionId match database records. |
Solution: Robust backend verification
// Backend webhook handler (Node.js/Express example)
app.post('/webhooks/pxp', async (req, res) => {
try {
const events = req.body;
// Verify webhook authenticity using HMAC
if (!verifyWebhookSignature(req)) {
console.error('Invalid webhook signature');
return res.status(401).json({ error: 'Unauthorised' });
}
for (const event of events) {
if (event.eventCategory === 'Transaction') {
const txn = event.eventData;
console.log('Processing transaction:', txn.systemTransactionId);
// Idempotency check
const existing = await db.transactions.findOne({
systemTransactionId: txn.systemTransactionId
});
if (existing) {
console.log('Transaction already processed, skipping');
continue;
}
// Verify transaction state
if (txn.state === 'Authorised' || txn.state === 'Captured') {
// Find order by merchant transaction ID
const order = await db.orders.findOne({
merchantTransactionId: txn.merchantTransactionId
});
if (!order) {
console.error('Order not found:', txn.merchantTransactionId);
continue;
}
// Verify amount matches
const transactionAmount = txn.amounts?.transactionValue || txn.amount;
if (Math.abs(transactionAmount - order.amount) > 0.01) {
console.error('Amount mismatch:', {
expected: order.amount,
actual: transactionAmount
});
continue;
}
// Mark transaction as processed
await db.transactions.create({
systemTransactionId: txn.systemTransactionId,
merchantTransactionId: txn.merchantTransactionId,
amount: transactionAmount,
state: txn.state,
processedAt: new Date()
});
// Fulfill order
await fulfillOrder(order.id, txn.systemTransactionId);
console.log('Order fulfilled:', order.id);
}
}
}
// Always return success
res.json({ state: 'Success' });
} catch (error) {
console.error('Webhook processing error:', error);
res.status(500).json({ error: 'Internal server error' });
}
});Look for these symptoms:
- App performance degrades over time.
- Xcode Instruments shows growing memory usage.
- Out of memory crashes after multiple checkout attempts.
Solution: Proper lifecycle management
import SwiftUI
import PXPCheckoutSDK
struct CheckoutView: View {
@StateObject private var viewModel = CheckoutViewModel()
@Environment(\.scenePhase) private var scenePhase
var body: some View {
Group {
if let errorMessage = viewModel.errorMessage {
Text(errorMessage)
.foregroundColor(.red)
}
if let dropIn = viewModel.dropIn {
dropIn.buildContent()
} else if viewModel.errorMessage == nil {
ProgressView("Loading...")
}
}
.task {
await viewModel.loadDropIn()
}
// Single-value onChange compiles with the SDK's iOS 14 deployment floor
.onChange(of: scenePhase) { phase in
switch phase {
case .background:
print("App backgrounded")
case .inactive:
print("App inactive")
case .active:
print("App active")
@unknown default:
break
}
}
.onDisappear {
viewModel.cleanup()
}
}
}
@MainActor
final class CheckoutViewModel: ObservableObject {
@Published var dropIn: CheckoutDropIn?
@Published var errorMessage: String?
func loadDropIn() async {
// Implementation
}
func cleanup() {
dropIn?.destroy()
dropIn = nil
errorMessage = nil
print("Checkout view cleaned up")
}
deinit {
print("CheckoutViewModel deallocated")
}
}Look for these symptoms:
- Keyboard covers the submit button.
- User can't see what they're typing.
- Layout doesn't adjust when keyboard appears.
Solution: Proper keyboard avoidance
import SwiftUI
struct CheckoutView: View {
@StateObject private var viewModel = CheckoutViewModel()
@FocusState private var focusedField: Field?
enum Field {
case cardNumber, expiry, cvv
}
var body: some View {
ScrollView {
VStack(spacing: 16) {
if let dropIn = viewModel.dropIn {
dropIn.buildContent()
}
}
.padding()
}
// `.scrollDismissesKeyboard` requires iOS 16+. On iOS 14–15, omit this modifier
// and use keyboard frame notifications for insets (see below).
.modifier(ScrollDismissesKeyboardIfAvailable())
// Do not use `.ignoresSafeArea(.keyboard)` here — that prevents keyboard insets
// and can leave fields or the submit area covered.
}
}
private struct ScrollDismissesKeyboardIfAvailable: ViewModifier {
func body(content: Content) -> some View {
if #available(iOS 16.0, *) {
content.scrollDismissesKeyboard(.interactively)
} else {
content
}
}
}If you need more control over keyboard insets (especially on iOS 14–15), observe keyboard frame notifications and apply padding yourself. There's no SwiftUI KeyboardAvoidingView type (that name is from React Native).
When troubleshooting issues, start by checking for these common error codes:
For Drop-in, missing Card / PayPal / Apple Pay / Aeropay funding in the session usually means the method is omitted with no onError. Diagnose via session allowedFundingTypes (and for Card, methodConfig.card showCOF / showNewCard). Treat SDK0107–SDK0109 as Component-level create errors when you use standalone components — not the primary Drop-in “method not appearing” signal. Blank Aeropay credentials or missing intent still surface via onError as SDK0113 / SDK0115.
Check these funding and initialisation codes when onError does fire:
SDK0113: Aeropay credentials missing or blank in allow funding types (usually whatonErrorreports when credentials fail create). See Aeropay.SDK0114: Aeropay only supports.ecomentry type.SDK0115: Aeropay intent missing on transaction data. Not the same asSDK1115(card authorisation failure).SDK0116: Aeropay only supportsUSDcurrency.SDK0204: No payment methods available aftercreate(). FiresonError(nil, …)withpaymentMethod: nil(initialisation error, not tied to a specific method). Duringcreate(), the drop-in component is typically not mounted, sobuildContent()often returns an empty view — wireonErrorso this isn't mistaken for a silent blank screen. InlinenoPaymentMethodsErrorUI appears only when aCheckoutDropInComponentis mounted with zero available methods (a rarer path). This can also occur when the session has no wallets and bothshowCOFandshowNewCardarefalse.SDK0500: Network error.SDK0602: Apple Pay is not supported on this device or iOS version.SDK1100: Failed to retrieve checkout drop-in configuration.SDK1120: Invalid PayPal entry type (entryTypemust be.ecom).SDK1125: Unexpected Aeropay render fallback (non-BaseSdkException). Blank credentials or missing intent → usuallySDK0113orSDK0115ononError, notSDK1125.SDK1300: Invalid Aeropay shopper data at load or during the popup flow.SDK1303: Missing or invalid Aerosync payload (for example bank-list load without a verified user ID). Prefer this overSDK1311when diagnosing that path. See Aeropay — Error codes.
Check these payment failure codes:
SDK1115: Drop-in card authorisation failed (not AeropaySDK0115).SDK1116: Card payment failed.SDK1117: PayPal payment failed.SDK1119: Apple Pay payment failed.SDK1126: Aeropay payment failed (generic fallback when no APIerrorCodeis present).
If you're still experiencing issues, try these troubleshooting steps.
Add detailed logging using print statements or OSLog:
import OSLog
let logger = Logger(subsystem: "com.yourapp.checkout", category: "DropIn")
CheckoutDropInConfig(
// ... other config
analyticsEvent: { event in
#if DEBUG
// BaseAnalyticsEvent encodes eventName, sessionId, and timestamp only.
// Subclass-specific properties aren't included unless you cast to the concrete event type.
let timestamp = ISO8601DateFormatter().string(from: event.timestamp)
logger.debug("Event: \(event.eventName) sessionId=\(event.sessionId) timestamp=\(timestamp)")
#endif
}
)When contacting support, include:
import UIKit
func collectDiagnosticInfo(sessionData: SessionData?, config: CheckoutDropInConfig) -> String {
var info: [String: Any] = [:]
// Device information
info["device"] = UIDevice.current.model
info["systemName"] = UIDevice.current.systemName
info["systemVersion"] = UIDevice.current.systemVersion
info["identifierForVendor"] = UIDevice.current.identifierForVendor?.uuidString ?? "N/A"
// Screen information
let screen = UIScreen.main
info["screenWidth"] = screen.bounds.width
info["screenHeight"] = screen.bounds.height
info["screenScale"] = screen.scale
// App information
if let bundleInfo = Bundle.main.infoDictionary {
info["appVersion"] = bundleInfo["CFBundleShortVersionString"] ?? "N/A"
info["buildNumber"] = bundleInfo["CFBundleVersion"] ?? "N/A"
}
// SDK version — there is no CheckoutDropIn.sdkVersion API.
// Record the Swift Package resolved version from Xcode (Package Dependencies) or Package.resolved.
info["environment"] = String(describing: config.environment)
// Session info (sanitised - DO NOT log hmacKey, encryptionKey, or API secrets)
info["sessionIdPresent"] = sessionData?.sessionId.isEmpty == false
info["hmacKeyPresent"] = sessionData?.hmacKey.isEmpty == false
info["encryptionKeyPresent"] = sessionData?.encryptionKey.isEmpty == false
info["allowedFundingTypes"] = String(describing: sessionData?.allowedFundingTypes)
// Timestamp
info["timestamp"] = ISO8601DateFormatter().string(from: Date())
// Convert to JSON string
if let jsonData = try? JSONSerialization.data(withJSONObject: info, options: .prettyPrinted),
let jsonString = String(data: jsonData, encoding: .utf8) {
print(jsonString)
// Copy to pasteboard
UIPasteboard.general.string = jsonString
return jsonString
}
return "Failed to generate diagnostics"
}When contacting support, always include your merchant ID, environment (test or live), iOS version, device model, the Swift Package resolved SDK version, and any relevant error messages or console logs.