Test your Drop-in integration in sandbox before going live.
Test your Drop-in integration in the sandbox environment before accepting real payments. The sandbox environment allows you to test all payment methods without processing actual transactions.
Set the environment to .test when initialising Drop-in:
CheckoutDropInConfig(
environment: .test, // Use sandbox
// ... rest of config
)Key points about sandbox testing:
- No real money is charged.
- Use test card numbers and sandbox accounts.
- Payment methods behave like production, with the same SDK constraints.
- Backend verification still required using test API credentials.
Use the following test cards in sandbox to simulate different scenarios. These PANs are for manual card form entry in Drop-in. They aren't Apple Pay / Apple Wallet test cards. See Test Apple Pay for wallet testing.
The test card numbers below are examples. Actual available test cards depend on your merchant and test processor configuration.
The following test cards represent successful payment scenarios:
| Card number | Network | 3DS behaviour |
|---|---|---|
4111 1111 1111 1111 | Visa | No challenge |
5555 5555 5555 4444 | Mastercard | No challenge |
3782 822463 10005 | American Express | No challenge |
4000 0000 0000 0002 | Visa | Challenge required |
For all test cards:
- Expiry date: Any future date (e.g.,
12/30). - CVV: Any 3 digits (e.g.,
123). - Cardholder name: Any name.
The following test cards simulate payment failures:
| Card number | Network | Result |
|---|---|---|
4000 0000 0000 0077 | Visa | Insufficient funds |
4000 0000 0000 0051 | Visa | Card declined |
4000 0000 0000 0069 | Visa | Expired card |
4000 0000 0000 0101 | Visa | CVV failure |
When using the challenge test card (4000 0000 0000 0002), a 3D Secure authentication screen appears. The exact 3DS flow depends on your payment processor and card network provider.
- To pass: enter any code (e.g.,
1234). - To fail: leave empty or enter wrong code multiple times.
- To timeout: wait for authentication to timeout.
Expected callback behaviour:
- Success:
onSuccesscallback fires withDropInSubmitResultcontaining transaction details. - Failure:
onErrorcallback fires with the authentication error (e.g.,SDK1114for authentication failed).
Verify that your backend receives the transaction details after successful authentication.
Use DropInCardConfig to control which card sections appear. Confirm each scenario with cards enabled in the session:
| Configuration | Expected result |
|---|---|
showCOF and showNewCard omitted or true | Both card-on-file and new card entry appear when pre-requisites are met. |
showCOF: true, showNewCard: false, with a non-empty shopper id from onGetShopper | Only saved cards and the card-on-file submit button appear. |
showCOF: false, showNewCard: true | Only the new card form appears. Saved cards aren't shown. |
showCOF: false, showNewCard: false | The Card payment method panel is hidden. With no wallets available, Drop-in can fire SDK0204. |
Card-on-file also requires onGetShopper to return a shopper with a non-empty id. For configuration details, see Cards.
You need a PayPal sandbox account for testing. Create one in the PayPal Developer Dashboard.
Create a personal (buyer) account for testing customer payments.
The PayPal button only appears when session.allowedFundingTypes.wallets.paypal.allowedFundingOptions is present and non-empty in your session configuration.
- Tap the PayPal button in the drop-in.
- Log in with your PayPal sandbox account in the web view.
- Approve the payment.
- Verify that
onSuccessfires with transaction details. - Verify your backend receives the payment notification.
Test both transaction intents:
- Purchase intent: Funds captured immediately.
- Authorisation intent: Funds authorised but not captured (capture later via backend API).
Apple Pay testing needs:
- iOS 14.0 or higher
- Apple Pay capability enabled in Xcode
- Valid Apple Pay merchant identifier
- A sandbox tester Apple ID with supported test credentials in Wallet (see Apple's sandbox guide below)
Manual card-entry PANs from Test card numbers are for typing into Drop-in's card form. Apple Pay uses cards provisioned in Apple Wallet through Apple's sandbox, not those form PANs.
Follow Apple's Apple Pay sandbox testing guide to:
- Create an App Store Connect sandbox tester account.
- Sign in on a supported test device with that sandbox tester.
- Add Apple's sandbox test credentials to Wallet (FPAN / CVV / expiry from Apple's table, not the Drop-in manual-entry PANs above).
- Open your app, tap the Apple Pay button in Drop-in, and authenticate with Face ID, Touch ID, or passcode.
- Verify that
onSuccessfires with transaction details.
The Apple Pay button only appears when:
- The user has cards in their Apple Wallet and Apple Pay is properly configured.
session.allowedFundingTypes.wallets.applePay.merchantIdis present in your session configuration.
Full Apple Pay sandbox card provisioning and Wallet flows are intended for physical devices with a sandbox tester account. Simulator support for Apple Pay is limited and does not use the same Wallet test-card setup as a device.
Use a physical device for end-to-end Apple Pay sandbox testing whenever possible. For simulator-only UI smoke tests, confirm the Apple Pay button visibility rules above, but don't expect to add Apple sandbox Wallet cards using the manual Drop-in PAN table.
Apple Pay sandbox testing requires proper Xcode project configuration with the Apple Pay capability and a valid merchant identifier registered in your Apple Developer account. See Apple Pay sandbox testing.
Aeropay only appears when the session includes allowedFundingTypes.payByBanks.aeropay, the currency is USD, and entryType is .ecom.
Run through this Aeropay path in sandbox:
- Confirm Aeropay is enabled in the Unity Portal and present in your session response under
payByBanks.aeropay. - Use
environment: .test, settransactionData.currencyto"USD", andentryTypeto.ecom. - Set
transactionData.intent.aeropayto a supported intent (.authorisation,.purchase, or.estimatedAuthorisation). - Provide valid
onGetShopperconsumer fields when you supply values, or setmethodConfig.aeropay.userId. - Select Pay by bank via Aeropay in Drop-in.
- Complete the flow and verify
onSuccesswithpaymentMethod == .aeropay. To test failure or cancellation, triggeronErroror close the popup and verifymethodConfig.global.onCancel.
Use approved UAT shopper data and Aeropay test credentials from your PXP or Aeropay integration contact. Don't invent sandbox bank login or OTP steps beyond what your test environment provides.
Test that your error handling works correctly by simulating failures:
Use the failed payment test cards to trigger different error types:
- Insufficient funds:
4000 0000 0000 0077 - Card declined:
4000 0000 0000 0051 - Expired card:
4000 0000 0000 0069 - CVV failure:
4000 0000 0000 0101
Verify that your onError callback receives the error and displays an appropriate message to the customer.
Your onError callback may receive the following SDK error codes:
Check these general codes first:
SDK0204: No payment methods available. Drop-in callsonErrorwithpaymentMethod: nilduringcreate()when no payment methods can be rendered (an inlinenoPaymentMethodsErrormessage is still shown). To test in sandbox, use a session with no wallets andDropInCardConfig(showCOF: false, showNewCard: false).SDK1114: Authentication failed (3DS or similar authentication failure).SDK1115: Card authorisation evaluation failed in Drop-in. For declined card payments, expectSDK1116and/or a provider code onerror.errorCode, and inspecterror.errorMessage.SDK1116: Card payment failed (general card payment error).SDK1117: PayPal payment failed (general PayPal error).SDK1119: Apple Pay payment failed (general Apple Pay error).SDK1120: Invalid PayPal entry type (configuration mismatch).SDK1125: Aeropay create or render failure fallback (underlyingSDK0113orSDK0115are often preserved onerror.errorCode).SDK1126: Aeropay payment failed (generic fallback).SDK0114: Aeropay entry type isn't.ecom.SDK0116: Aeropay currency isn'tUSD.SDK1300: Invalid Aeropay shopper data at load or during the popup flow.
These Apple Pay codes may appear on onError when wallet payments fail (except cancellation):
SDK0602: Apple Pay is not supported on this device or iOS version (not a merchant configuration mistake).SDK0616: Apple Pay merchant validation failed.SDK0617: Custom API processing failed during Apple Pay payment.
SDK0615 (Apple Pay session cancelled by the user) arrives as the error argument to methodConfig.global.onCancel(.applePay, error). It is not delivered through onError. See User cancellation.
Test offline behaviour:
- Enable airplane mode on your device.
- Try to submit a payment.
- Verify that
onErrorreceiveserror.errorCode == "SDK0500"("Network error."), or handle a message-based fallback if your app maps network failures differently, and shows a network error message to the customer.
Alternatively, use Network Link Conditioner in Xcode or your app's backend/mock layer to simulate network failures.
// Pseudocode example - adapt to your app's architecture
// Use your app's backend mock layer or dependency injection
// to simulate network failures in tests
class NetworkErrorTest: XCTestCase {
func testNetworkError() async throws {
// Configure your backend mock to return network errors
// or use URLProtocol/URLSession mocks in your app
// Verify error handling in your app — expect SDK0500 for Drop-in network failures
// ... test implementation
}
}To test cancellation handling:
- PayPal: close the web view without completing payment. This doesn't trigger
methodConfig.global.onCancelbecause Drop-in overrides the PayPal cancel handler with an internal handler to clear processing state. - Apple Pay: tap outside the payment sheet or cancel the authentication. This triggers
methodConfig.global.onCancel(.applePay, error)if configured. - Aeropay: close the Aeropay popup without completing. This triggers
methodConfig.global.onCancel(.aeropay, nil)if configured. Cancellation doesn't callonError.
Always verify payments on your backend before fulfilling orders. The frontend callbacks can be manipulated by users.
Test these critical security checks:
- Transaction exists: query the PXP API with
systemTransactionIdto verify that the transaction exists. - Amount matches: verify that the transaction amount matches your expected amount.
- Transaction succeeded: check that the transaction state is
AuthorisedorCaptured. - Prevent replay attacks: ensure the same transaction can't be processed twice.
Example verification:
// Backend verification endpoint (Node.js/Express example)
app.post('/api/verify-payment', async (req, res) => {
const { systemTransactionId, merchantTransactionId } = req.body;
// Get expected amount from your database
const order = await db.orders.findOne({ merchantTransactionId });
// Guard against missing order
if (!order) {
return res.status(404).json({ success: false, error: 'Order not found' });
}
const expectedAmount = order.amount;
// Query PXP API to verify transaction
const transaction = await unityApi.getTransaction(systemTransactionId);
// Verify transaction is successful
if (transaction.state !== 'Authorised' && transaction.state !== 'Captured') {
return res.json({ success: false, error: 'Transaction not authorised' });
}
// Verify amount matches
const txnAmount = transaction.amounts?.transactionValue || transaction.amount;
if (Math.abs(txnAmount - expectedAmount) > 0.01) {
return res.json({ success: false, error: 'Amount mismatch' });
}
// Check for duplicate processing
const alreadyProcessed = await db.transactions.findOne({ systemTransactionId });
if (alreadyProcessed) {
return res.json({ success: true, orderId: alreadyProcessed.orderId });
}
// Process order
const orderId = await fulfillOrder(order);
await db.transactions.create({ systemTransactionId, orderId });
res.json({ success: true, orderId });
});Test on real iOS devices to ensure the best user experience:
Test on devices that represent your user base:
- Budget device: iPhone SE (2nd/3rd generation)
- Standard device: iPhone 13, iPhone 14
- Pro device: iPhone 15 Pro, iPhone 14 Pro
- Tablet: iPad Air, iPad Pro
Test the following on each device:
- Card field rendering and keyboard input.
- 3D Secure authentication UI.
- Apple Pay integration and authentication.
- PayPal web view interaction.
- Aeropay popup flow (consumer data, OTP, and bank selection when required).
- Screen rotation handling.
- Different screen sizes (iPhone vs iPad).
- Performance and responsiveness.
Test on different screen sizes:
// Use SwiftUI Previews for different screen sizes
struct CheckoutView_Previews: PreviewProvider {
static var previews: some View {
Group {
CheckoutView()
.previewDevice(PreviewDevice(rawValue: "iPhone SE (3rd generation)"))
.previewDisplayName("iPhone SE")
CheckoutView()
.previewDevice(PreviewDevice(rawValue: "iPhone 15 Pro"))
.previewDisplayName("iPhone 15 Pro")
CheckoutView()
.previewDevice(PreviewDevice(rawValue: "iPad Pro (12.9-inch)"))
.previewDisplayName("iPad Pro")
}
}
}Write automated tests for your Drop-in integration:
Test your configuration and callback logic. Card-only examples below use GBP.
import XCTest
import PXPCheckoutSDK
class CheckoutConfigTests: XCTestCase {
func testConfigCreation() {
let config = CheckoutDropInConfig(
environment: .test,
session: mockSessionData(),
transactionData: DropInTransactionData(
amount: Decimal(string: "99.99") ?? 0,
currency: "GBP",
entryType: .ecom,
intent: DropInTransactionIntentData(
card: .authorisation
),
merchantTransactionId: "test-txn-123",
merchantTransactionDate: { Date() }
),
merchantShopperId: "shopper-123",
ownerId: "MERCHANT-1"
)
XCTAssertEqual(config.environment, .test)
XCTAssertEqual(config.ownerId, "MERCHANT-1")
XCTAssertEqual(config.transactionData.currency, "GBP")
}
func testCallbackExecution() {
var successCalled = false
var errorCalled = false
let config = CheckoutDropInConfig(
environment: .test,
session: mockSessionData(),
transactionData: DropInTransactionData(
amount: Decimal(string: "99.99") ?? 0,
currency: "GBP",
entryType: .ecom,
intent: DropInTransactionIntentData(
card: .authorisation
),
merchantTransactionId: "test-txn-callback-123",
merchantTransactionDate: { Date() }
),
merchantShopperId: "shopper-callback-123",
ownerId: "MERCHANT-1",
onSuccess: { _ in
successCalled = true
},
onError: { _, _ in
errorCalled = true
}
)
// Test callbacks
config.onSuccess?(mockSuccessResult())
XCTAssertTrue(successCalled)
XCTAssertFalse(errorCalled)
}
func mockSessionData() -> SessionData {
return makeMockSessionData()
}
func mockSuccessResult(paymentMethod: DropInPaymentMethod = .card) -> DropInSubmitResult {
return DropInSubmitResult(
systemTransactionId: "sys-txn-123",
merchantTransactionId: "test-txn-123",
paymentMethod: paymentMethod
// paymentData is typically nil for card, PayPal, and Apple Pay
)
}
}
private func makeMockSessionData() -> SessionData {
return SessionData(
sessionId: "test-session-id",
hmacKey: "test-hmac-key",
encryptionKey: "test-encryption-key",
sessionExpiry: nil,
allowedFundingTypes: AllowedFundingType(
cardSchemes: ["Visa", "Mastercard"],
cards: [],
wallets: Wallets(
paypal: Paypal(
allowedFundingOptions: ["paypal"],
merchantId: "test-merchant-id"
),
applePay: ApplePay(merchantId: "merchant.test.app")
)
),
restrictions: nil
)
}Test async Drop-in initialisation. await dropIn.create() fetches Unity site configuration and wires components. Placeholder SessionData with fake hmacKey / encryptionKey alone isn't enough for a passing integration test. Use a mocked backend/Unity layer or a valid sandbox session from your test API.
class AsyncDropInTests: XCTestCase {
func testAsyncDropInCreation() async throws {
let config = CheckoutDropInConfig(
environment: .test,
session: makeMockSessionData(), // Must be backed by mocks or a valid sandbox session
transactionData: DropInTransactionData(
amount: Decimal(string: "49.99") ?? 0,
currency: "GBP",
entryType: .ecom,
intent: DropInTransactionIntentData(
card: .authorisation
),
merchantTransactionId: "test-async-txn-123",
merchantTransactionDate: { Date() }
),
merchantShopperId: "shopper-async-123",
ownerId: "MERCHANT-1"
)
// Test async initialisation (requires mocked network or valid sandbox session)
let dropIn = try CheckoutDropIn(config: config)
await dropIn.create()
// Verify Drop-in is ready
XCTAssertNotNil(dropIn)
}
}Test the full payment flow:
The UI test selectors shown below (e.g., "Card", "PayPal", "Card number") are examples and aren't guaranteed by the SDK. The SDK may change button labels, field names, or accessibility identifiers in future releases. For robust UI testing, use your merchant app's own stable accessibility identifiers rather than relying on localised text or SDK-provided labels.
import XCTest
class CheckoutFlowUITests: XCTestCase {
var app: XCUIApplication!
override func setUp() {
super.setUp()
continueAfterFailure = false
app = XCUIApplication()
app.launch()
}
func testCardPaymentFlow() {
// Wait for Drop-in to render
let cardButton = app.buttons["Card"]
XCTAssertTrue(cardButton.waitForExistence(timeout: 5))
// Verify payment methods are displayed (assert only methods enabled in your test session)
XCTAssertTrue(app.buttons["Card"].exists)
XCTAssertTrue(app.buttons["PayPal"].exists)
// Select card payment
cardButton.tap()
// Verify card form is displayed
XCTAssertTrue(app.textFields["Card number"].exists)
XCTAssertTrue(app.textFields["Expiry date"].exists)
XCTAssertTrue(app.textFields["CVV"].exists)
}
func testPaymentSubmission() {
let cardButton = app.buttons["Card"]
XCTAssertTrue(cardButton.waitForExistence(timeout: 5))
// Fill card details and submit
cardButton.tap()
let cardNumberField = app.textFields["Card number"]
cardNumberField.tap()
cardNumberField.typeText("4111111111111111")
let expiryField = app.textFields["Expiry date"]
expiryField.tap()
expiryField.typeText("1230")
let cvvField = app.textFields["CVV"]
cvvField.tap()
cvvField.typeText("123")
// Submit payment
app.buttons["Pay"].tap()
// Verify success screen appears
let successMessage = app.staticTexts["Payment successful"]
XCTAssertTrue(successMessage.waitForExistence(timeout: 10))
}
}Before going live, verify the following:
Confirm production configuration:
- Change the environment to
.live. - Use production API credentials.
- Verify that the
ownerIdis correct. - Confirm the currency and amount formatting.
- Verify that all required session fields are present and valid:
sessionId: Valid session identifier from your backend.hmacKey: Valid HMAC key for request signing.encryptionKey: Valid encryption key for sensitive data.allowedFundingTypes.cards: Non-nilrequired for Card to appear in Drop-in (may be empty[]). Brand lists still resolve as: merchantacceptedCardNetworks→ sessioncardSchemes→ sessioncards.cardSchemesalone does not enable the Card panel.allowedFundingTypes.wallets.paypal.allowedFundingOptions: Present and non-empty if PayPal is enabled.allowedFundingTypes.wallets.applePay.merchantId: Present if Apple Pay is enabled.allowedFundingTypes.payByBanks.aeropay: Present with non-blankexternalMerchantIdandconfigurationIdif Aeropay is enabled. Also useUSD,.ecom, andintent.aeropay.restrictions: Correct merchant-specific restrictions (if any).
Confirm sandbox and device coverage:
- Test all enabled payment methods.
- Test successful payments.
- Test failed payments and error handling.
- Verify backend verification works correctly.
- Test on physical devices (not just simulators).
Confirm security controls:
- Backend verification implemented.
- Replay attack prevention in place.
- HTTPS enabled on all backend endpoints.
- No sensitive data logged.
- Code signing and provisioning profiles configured for production.
Confirm UX quality:
- Test on different screen sizes and orientations.
- Loading states display correctly.
- Success screen implemented.
- Error messages are user-friendly.
- Accessibility features tested (VoiceOver, Dynamic Type).
Confirm performance:
- App startup time is acceptable.
- Payment submission is responsive.
- No memory leaks detected (use Xcode Instruments).
- Network timeouts configured appropriately.
Confirm code quality:
- SwiftLint checks pass.
- Unit tests pass.
- UI tests pass.
- Code review completed.
- Documentation updated.