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
print("Session ID: \(sessionData?.sessionId != nil ? "Present" : "Missing")")
print("HMAC key: \(sessionData?.hmacKey != nil ? "Present" : "Missing")")
print("Encryption key: \(sessionData?.encryptionKey != nil ? "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("===================================")
}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 rendering buildContent(). Until create() finishes, the SDK returns EmptyView() with no thrown error. If getCheckoutDropInConfig fails, create() returns early and fires onError (for example SDK1100 or SDK0204) — without a wired onError handler, failures look like a blank drop-in with no console error. |
| 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. | Verify that the session data includes sessionId, hmacKey, and allowedFundingTypes. Check the 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. |
| 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 = ""
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: must not be nil
if sessionData.allowedFundingTypes?.cards != nil {
paymentMethodCount += 1
}
// 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
}
if paymentMethodCount == 0 {
diagnostics += "ERROR: No payment methods enabled (SDK0204)\n"
} else {
diagnostics += "\(paymentMethodCount) payment method(s) available\n"
}
}
// Initialise Drop-in
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-1",
onSuccess: { result in
print("Success: \(result.systemTransactionId)")
},
onError: { paymentMethod, error in
print("Error: \(error.errorMessage)")
}
)
let instance = try CheckoutDropIn(config: config)
await instance.create()
dropIn = instance
} catch {
diagnostics += "ERROR: \(error.localizedDescription)\n"
print("Diagnostic error: \(error)")
}
}
}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 dropIn = viewModel.dropIn {
dropIn.buildContent()
} else if let errorMessage = viewModel.errorMessage {
VStack {
Text(errorMessage)
.foregroundColor(.red)
Button("Refresh") {
Task {
await viewModel.refreshSession()
}
}
}
} else {
ProgressView("Loading checkout...")
}
}
.task {
await viewModel.loadDropIn()
}
}
}
@MainActor
final class CheckoutViewModel: ObservableObject {
@Published var dropIn: CheckoutDropIn?
@Published var errorMessage: String?
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 {
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-1",
onSuccess: { result in
Task {
await verifyPaymentOnBackend(result)
}
},
onError: { paymentMethod, error in
// 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)"
}
}
}
)
let instance = try CheckoutDropIn(config: config)
await instance.create()
dropIn = instance
}
}
func fetchSessionFromBackend() async throws -> SessionData {
// Implementation to fetch session from your backend
// ...
}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 | Cards aren't enabled in the Unity Portal or are missing from the session configuration. | Enable the Card service in the Unity Portal and verify that the session includes allowedFundingTypes.cards. |
| PayPal | PayPal onboarding wasn't completed or is missing from session configuration. | Complete PayPal onboarding in Unity Portal and verify that the session includes allowedFundingTypes.wallets.paypal. |
| 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, add test cards to Wallet, and test on a physical device or simulator with Apple Pay support. |
Diagnostic steps:
import PassKit
import PXPCheckoutSDK
func diagnosePaymentMethodVisibility(
sessionData: SessionData?,
config: CheckoutDropInConfig
) {
print("=== Payment method diagnostics ===")
let fundingTypes = sessionData?.allowedFundingTypes
print("Session funding types: \(String(describing: fundingTypes))")
// Check cards - must not be nil
if fundingTypes?.cards != nil {
print("Cards enabled: \(String(describing: fundingTypes?.cards))")
} else {
print("WARNING: Cards not enabled in session (allowedFundingTypes.cards must not be nil)")
}
// Check PayPal - must have allowedFundingOptions present and non-empty
if let paypalConfig = fundingTypes?.wallets?.paypal,
let allowedFundingOptions = paypalConfig.allowedFundingOptions,
!allowedFundingOptions.isEmpty {
print("PayPal enabled with funding options: \(allowedFundingOptions)")
} else {
print("WARNING: PayPal not enabled (wallets.paypal.allowedFundingOptions must be present and non-empty)")
}
// 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)")
// Check Apple Pay availability
if PKPaymentAuthorizationController.canMakePayments() {
print("Device supports Apple Pay")
// Check if user has cards configured for networks from Drop-in config
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 Pay for accepted networks")
}
} 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 device info
print("iOS version: \(UIDevice.current.systemVersion)")
print("Device: \(UIDevice.current.model)")
print("===================================")
}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. | Add test cards to Apple Wallet manually. |
| 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. |
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(" Add cards to Wallet to use Apple Pay")
}
// Check iOS version
if #available(iOS 14.0, *) {
print("✓ iOS version compatible (14.0+)")
} else {
print("✗ iOS version too old (requires 14.0+)")
}
print("=============================")
}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' });
}
});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 dropIn = viewModel.dropIn {
dropIn.buildContent()
} else if let errorMessage = viewModel.errorMessage {
Text(errorMessage)
.foregroundColor(.red)
} else {
ProgressView("Loading...")
}
}
.task {
await viewModel.loadDropIn()
}
.onChange(of: scenePhase) { oldPhase, newPhase in
switch newPhase {
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")
}
}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(.interactively)
.ignoresSafeArea(.keyboard, edges: .bottom)
}
}Alternatively, use KeyboardAvoidingView:
struct CheckoutView: View {
var body: some View {
GeometryReader { geometry in
ScrollView {
VStack {
// Your checkout content
}
.frame(minHeight: geometry.size.height)
}
}
.ignoresSafeArea(.keyboard)
}
}When troubleshooting issues, start by checking for these common error codes:
SDK0107: Card is missing in allow funding typesSDK0108: PayPal is missing in allow funding typesSDK0109: Apple Pay is missing in allow funding typesSDK0204: No payment methods available aftercreate(). FiresonError(nil, …)withpaymentMethod: nil(initialisation error, not tied to a specific method). An inlinenoPaymentMethodsErrormessage is still shown in the UI.SDK1100: Failed to retrieve checkout drop-in configurationSDK1116: Card payment failedSDK1117: PayPal payment failedSDK1119: Apple Pay payment failedSDK1120: Invalid PayPal entry type
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
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
var eventData: [String: Any] = [
"eventName": event.eventName,
"sessionId": event.sessionId,
"timestamp": event.timestamp
]
if let propertiesJson = try? encoder.encode(event),
let propertiesString = String(data: propertiesJson, encoding: .utf8) {
logger.debug("Event: \(propertiesString)")
} else {
logger.debug("Event: \(event.eventName)")
}
#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 information - source from package metadata or SDK constant
// info["sdkVersion"] = CheckoutDropIn.sdkVersion (if available from SDK)
info["environment"] = String(describing: config.environment)
// Session info (sanitised - DO NOT log hmacKey, encryptionKey, or API secrets)
info["sessionIdPresent"] = sessionData?.sessionId != nil
info["hmacKeyPresent"] = sessionData?.hmacKey != nil
info["encryptionKeyPresent"] = sessionData?.encryptionKey != nil
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 production), iOS version, device model, and any relevant error messages or console logs. For Paze issues, also note whether wallets.paze is in the session, currency is USD, merchantCategoryCode is present, and the URL callback scheme is registered.