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.
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/25). - 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., SDK1114 for authentication failed).
Verify that your backend receives the transaction details after successful authentication.
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).
- iOS 14.0 or higher
- Apple Pay capability enabled in Xcode
- Valid Apple Pay merchant identifier
- Test card added to Apple Wallet
- Open your app on an iOS device or simulator.
- Ensure Apple Pay is set up with test cards.
- Add a test card to Apple Wallet (use test card numbers from above).
- Tap the Apple Pay button in Drop-in.
- 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.
To test Apple Pay on an iOS simulator:
- Use Simulator with iOS 14.0 or higher.
- In the Simulator menu, click Wallet > Add Credit or Debit Card.
- Add test cards manually using the card numbers above.
- Run your app and test Apple Pay integration.
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.
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:
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: Authorisation failed (payment declined by issuer/processor).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).
SDK0602: Apple Pay configuration error.SDK0615: Apple Pay session cancelled by the user.SDK0616: Apple Pay merchant validation failed.SDK0617: Custom API processing failed during Apple Pay payment.
Test offline behaviour:
- Enable airplane mode on your device.
- Try to submit a payment.
- Verify that your error handling shows a network error message.
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
// ... 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.
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.
- 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: { result in
Task { @MainActor in
successCalled = true
}
},
onError: { paymentMethod, error in
Task { @MainActor in
errorCalled = true
}
}
)
// Test callbacks
config.onSuccess?(mockSuccessResult())
XCTAssertTrue(successCalled)
XCTAssertFalse(errorCalled)
}
func mockSessionData(includePaze: Bool = false) -> 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"),
paze: includePaze
? Paze(clientId: "test-paze-client-id", merchantCategoryCode: "5812")
: nil
)
),
restrictions: nil
)
}
func testPazeConfigUsesUSD() {
let config = CheckoutDropInConfig(
environment: .test,
session: mockSessionData(includePaze: true),
transactionData: DropInTransactionData(
amount: Decimal(string: "49.99") ?? 0,
currency: "USD",
entryType: .ecom,
intent: DropInTransactionIntentData(card: .authorisation),
merchantTransactionId: "test-paze-txn-123",
merchantTransactionDate: { Date() }
),
merchantShopperId: "shopper-123",
ownerId: "MERCHANT-1",
onSuccess: { _ in },
onError: { _, _ in }
)
XCTAssertEqual(config.transactionData.currency, "USD")
XCTAssertEqual(config.transactionData.entryType, .ecom)
}
func mockSuccessResult(paymentMethod: DropInPaymentMethod = .card) -> DropInSubmitResult {
return DropInSubmitResult(
systemTransactionId: "sys-txn-123",
merchantTransactionId: "test-txn-123",
paymentMethod: paymentMethod
// paymentData is nil for card, PayPal, and Apple Pay; optional .paze for Paze
)
}
}Test async Drop-in initialisation:
class AsyncDropInTests: XCTestCase {
func testAsyncDropInCreation() async throws {
let config = CheckoutDropInConfig(
environment: .test,
session: mockSessionData(),
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
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
XCTAssertTrue(app.buttons["Card"].exists)
XCTAssertTrue(app.buttons["PayPal"].exists)
// If Paze is enabled in your session:
// XCTAssertTrue(app.buttons["Paze"].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("1225")
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:
- 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.cardSchemesand/orallowedFundingTypes.cards: At least one non-empty list of enabled card networks (or setacceptedCardNetworksinDropInGlobalConfig). The SDK resolves networks in priority order: merchantacceptedCardNetworks→ sessioncardSchemes→ sessioncards.allowedFundingTypes.wallets.paypal.allowedFundingOptions: Present and non-empty if PayPal is enabled.allowedFundingTypes.wallets.applePay.merchantId: Present if Apple Pay is enabled.restrictions: Correct merchant-specific restrictions (if any).
- 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).
- 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.
- 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).
- App startup time is acceptable.
- Payment submission is responsive.
- No memory leaks detected (use Xcode Instruments).
- Network timeouts configured appropriately.
- SwiftLint checks pass.
- Unit tests pass.
- UI tests pass.
- Code review completed.
- Documentation updated.