Accept PayPal and Pay Later payments with automatic popup handling and saved PayPal accounts for returning customers.
PayPal is automatically included in the drop-in when enabled in your session. The drop-in handles all PayPal setup, button rendering, web view interaction, and payment processing automatically.
- PayPal appears automatically in the drop-in when it's enabled in your session configuration.
- The drop-in handles all PayPal setup, so you don't need PayPal-specific code.
- PayPal uses the same
onSuccessandonErrorcallbacks as other payment methods for a unified integration. - Customers can pay using PayPal or Pay Later financing options.
- Returning customers can use saved PayPal accounts when vaulting is enabled.
- Standard PayPal branding is included automatically, ensuring a familiar checkout experience.
When a customer selects PayPal:
- The customer taps "PayPal" in the payment options list.
- Drop-in validates the configuration and creates a pending PayPal transaction/order through Unity.
- The PayPal web view opens with the order details.
- The customer logs in to their PayPal account.
- The customer approves the payment in the PayPal interface.
- The web view closes automatically.
- Drop-in fires your
onSuccesscallback with the pending transaction result. - Critical: You must verify the final transaction state on your backend before fulfilling the order.
Configure PayPal-specific settings for the PayPal button, checkout flow, and shipping integration.
The following properties are available for PayPal configuration through DropInPaypalConfig:
| Property | Description |
|---|---|
fundingSources[DropInPaypalFundingSources]? | Which PayPal funding sources to display. Falls back to session configuration if not specified. Possible values:
|
payeeEmailAddressString? | The payee email address for the PayPal transaction. |
paymentDescriptionString? | A description of the payment shown to the customer in PayPal. |
consentComponentBool? | Controls whether Drop-in creates the PayPal consent checkbox for vaulting. When true, a consent checkbox renders for logged-in shoppers (requires onGetShopper to return a shopper ID). |
shippingPreferencePayPalShippingPreference? | How shipping address is handled in PayPal. Possible values:
Note: When using .getFromFile with shipping options, configure methodConfig.global.shippingOptions. |
PayPal locale is configured through the top-level CheckoutDropInConfig.locale parameter, not through DropInPaypalConfig. Use hyphenated format (e.g., "en-US") for locale configuration.
To display Pay Later as a payment option, ensure .paylater is included in your fundingSources configuration and that Pay Later is enabled for your merchant account in the Unity Portal.
PayPal inherits the following settings from methodConfig.global:
| Property | Description |
|---|---|
onGetConsent(DropInPaymentMethod) -> Bool | methodConfig.paypal.consentComponent controls whether Drop-in creates the PayPal consent checkbox. methodConfig.global.onGetConsent(.paypal) provides a fallback consent value when no consent component is used. |
shippingOptions[DropInShippingOption]? | Shipping options for PayPal transactions. Requires shippingPreference: .getFromFile in DropInPaypalConfig. |
methodConfig.global.onCancel isn't currently wired for PayPal in Drop-in. The SDK overrides the PayPal cancel handler to clear processing state. User cancellations result in no callback being fired to the merchant app.
This example shows a full PayPal configuration with shipping options and all supported DropInPaypalConfig properties:
methodConfig: DropInMethodConfig(
// Global settings that apply to PayPal
global: DropInGlobalConfig(
// Shipping options for PayPal (requires shippingPreference: .getFromFile)
shippingOptions: [
DropInShippingOption(
id: "standard",
label: "Standard Shipping",
description: "Arrives in 3-5 business days",
amount: "10.00"
),
DropInShippingOption(
id: "express",
label: "Express Shipping",
description: "Arrives in 1-2 business days",
amount: "25.00"
)
],
// Provide fallback consent value for PayPal vaulting
onGetConsent: { paymentMethod in
return paymentMethod == .paypal
}
),
paypal: DropInPaypalConfig(
// Funding sources to display
fundingSources: [
.paypal,
.paylater
],
// Payee email address
payeeEmailAddress: "merchant@example.com",
// Payment description shown to buyer
paymentDescription: "Purchase from Demo Store",
// Show/hide consent checkbox for vaulting
consentComponent: true,
// Shipping preference
shippingPreference: .getFromFile // Required for shipping options
)
)PayPal requires the following to function correctly:
- iOS compatibility: iOS 14.0 or higher.
- Internet connection: Active internet connection required for PayPal web view.
- Customer setup: The customer must have a PayPal account (created during checkout if needed).
- HTTPS: Your backend endpoints must be served over HTTPS.
- Unity Portal configuration: PayPal must be enabled and configured in the Unity Portal.
- Entry type: PayPal only supports
entryType: .ecom.
PayPal vaulting requires the buyer to have previously authorised your merchant account. Authorisations must be captured within 29 days for PayPal.
PayPal works through the standard implementation, with no PayPal-specific code needed:
import SwiftUI
import PXPCheckoutSDK
struct CheckoutView: View {
@StateObject private var viewModel = CheckoutViewModel()
var body: some View {
Group {
if let dropIn = viewModel.dropIn {
dropIn.buildContent()
} else if let errorMessage = viewModel.errorMessage {
Text(errorMessage)
} else {
ProgressView("Loading checkout...")
}
}
.task {
await viewModel.loadDropIn()
}
}
}
@MainActor
final class CheckoutViewModel: ObservableObject {
@Published var dropIn: CheckoutDropIn?
@Published var errorMessage: String?
func loadDropIn() async {
// Fetch session from backend (with PayPal enabled)
guard let sessionData = await fetchSessionFromBackend() else {
errorMessage = "Failed to create session"
return
}
// Initialise Drop-in
let config = CheckoutDropInConfig(
environment: .test,
session: sessionData,
transactionData: DropInTransactionData(
amount: Decimal(string: "99.99") ?? 0,
currency: "GBP",
entryType: .ecom,
intent: DropInTransactionIntentData(
card: .authorisation,
paypal: .purchase // Choose .purchase or .authorisation
),
merchantTransactionId: UUID().uuidString,
merchantTransactionDate: { Date() }
),
merchantShopperId: "shopper-123",
ownerId: "MERCHANT-1",
onGetShopper: {
TransactionShopper(id: "shopper-123")
},
onSuccess: { result in
print("PayPal payment successful!")
print("System transaction ID: \(result.systemTransactionId)")
print("Payment method: \(result.paymentMethod.rawValue)") // "Paypal"
// CRITICAL: Verify on backend
Task {
await verifyPaymentOnBackend(result)
}
},
onError: { paymentMethod, error in
print("PayPal payment failed: \(error.errorMessage)")
Task { @MainActor in
self.errorMessage = "Payment failed: \(error.errorMessage)"
}
}
)
do {
let instance = try CheckoutDropIn(config: config)
await instance.create()
dropIn = instance
} catch {
errorMessage = error.localizedDescription
}
}
}Enable PayPal in your session request:
// BACKEND: Create a session with PayPal enabled
const sessionRequest = {
merchant: "MERCHANT-1",
site: "SITE-1",
sessionTimeout: 120,
merchantTransactionId: crypto.randomUUID(),
transactionMethod: {
intent: {
paypal: "Purchase" // or "Authorisation"
}
},
amounts: {
currencyCode: "GBP",
transactionValue: 99.99
},
allowedFundingTypes: {
wallets: {
paypal: {
// PayPal configuration from the Unity Portal will be used
// You can optionally specify allowedFundingOptions here
allowedFundingOptions: ["paypal", "paylater"]
}
}
},
allowTransaction: true,
serviceType: "CheckoutDropIn"
};Drop-in supports two PayPal payment flows, configured via the intent parameter:
Immediate, single-step payment where funds are captured right away.
transactionData: DropInTransactionData(
amount: Decimal(string: "99.99") ?? 0,
currency: "GBP",
entryType: .ecom,
intent: DropInTransactionIntentData(
card: .authorisation,
paypal: .purchase // Pay Now flow
),
merchantTransactionId: UUID().uuidString,
merchantTransactionDate: { Date() }
)Use this flow for:
- Digital products
- Simple orders with immediate fulfillment
- Subscriptions
- Donations
When a PayPal payment succeeds, your onSuccess callback receives the same standard result as other payment methods:
onSuccess: { result in
print("Payment details:")
print("- System transaction ID: \(result.systemTransactionId)")
print("- Merchant transaction ID: \(result.merchantTransactionId ?? "N/A")")
print("- Payment method: \(result.paymentMethod.rawValue)") // "Paypal"
// Note: Amount, currency, and other transaction details must be retrieved from backend
// PayPal authentication is handled internally by PayPal
}Handle PayPal-specific errors:
onError: { paymentMethod, error in
print("Error code: \(error.errorCode)")
print("Error message: \(error.errorMessage)")
// Handle specific error codes
let userMessage: String
switch error.errorCode {
case "SDK1117":
userMessage = "PayPal payment failed. Please try again or use another payment method."
case "SDK1120":
userMessage = "PayPal requires entryType .ecom."
default:
// Fall back to message-based detection
if error.errorMessage.localizedCaseInsensitiveContains("cancelled") ||
error.errorMessage.localizedCaseInsensitiveContains("closed") {
// User intentionally cancelled - don't show error
return
} else if error.errorMessage.localizedCaseInsensitiveContains("insufficient funds") {
userMessage = "Insufficient funds in PayPal account. Please add funds or use a different payment method."
} else if error.errorMessage.localizedCaseInsensitiveContains("restricted") {
userMessage = "Your PayPal account is restricted. Please contact PayPal support."
} else if error.errorMessage.localizedCaseInsensitiveContains("declined") {
userMessage = "Payment declined by PayPal. Please try a different payment method."
} else {
userMessage = "PayPal payment failed: \(error.errorMessage)"
}
}
Task { @MainActor in
showError(userMessage)
}
}The following table describes common PayPal error scenarios:
| Scenario | How to detect | Recommended action |
|---|---|---|
| PayPal payment failed | error.errorCode == "SDK1117" | Suggest trying again or using another payment method. |
| Invalid entry type | error.errorCode == "SDK1120" | Use entryType: .ecom for PayPal transactions. |
| User cancelled | error.errorMessage contains "cancelled" or "closed" | No alert needed because the user action was intentional. |
| Insufficient funds | error.errorMessage contains "insufficient" | Suggest adding funds or using a different payment method. |
| Account restricted | error.errorMessage contains "restricted" | Direct the user to contact PayPal support. |
| Payment declined | error.errorMessage contains "declined" | Suggest trying a different payment method. |
PayPal errors include both error codes (SDK1117, SDK1120) and descriptive messages. Use error.errorCode for programmatic handling and error.errorMessage for additional context. Note that methodConfig.global.onCancel is not currently wired for PayPal in Drop-in.
Always verify PayPal payments on your backend to ensure payment success before fulfilling orders:
onSuccess: { result in
// Send to backend for verification
Task {
do {
let response = try await apiClient.post("/api/verify-payment", body: [
"systemTransactionId": result.systemTransactionId,
"merchantTransactionId": result.merchantTransactionId ?? ""
])
if response.success {
// Navigate to success screen
await MainActor.run {
navigateToSuccess(orderId: response.orderId)
}
} else {
await MainActor.run {
showError("Payment verification failed")
}
}
} catch {
print("Verification error: \(error.localizedDescription)")
await MainActor.run {
showError("Failed to verify payment")
}
}
}
}Use the following backend code to verify PayPal transactions via the PXP API:
// BACKEND: Verify PayPal payment
app.post('/api/verify-payment', async (req, res) => {
const { systemTransactionId, merchantTransactionId } = req.body;
try {
// Query the PXP API to get transaction details
const txnPath = `api/v1/transactions/${systemTransactionId}`;
const { authHeader, requestId } = createAuthHeader(
txnPath,
'',
process.env.PXP_TOKEN_ID,
process.env.PXP_TOKEN_VALUE
);
const transaction = await fetch(
`https://api-services.pxp.io/${txnPath}`,
{
headers: {
'X-Client-Id': process.env.PXP_CLIENT_ID,
'X-Request-Id': requestId,
'Authorization': authHeader
}
}
).then(r => r.json());
// Verify transaction state
if (transaction.state !== 'Authorised' && transaction.state !== 'Captured') {
return res.json({ success: false, error: 'Transaction not successful' });
}
// Verify merchant transaction ID matches
if (transaction.merchantTransactionId !== merchantTransactionId) {
return res.json({ success: false, error: 'Transaction ID mismatch' });
}
// Verify amount matches expected amount from your order records
const order = await getOrderByMerchantTransactionId(merchantTransactionId);
const txnAmount = transaction.amounts?.transactionValue || transaction.amount || 0;
if (Math.abs(txnAmount - order.amount) > 0.01) {
return res.json({ success: false, error: 'Amount mismatch' });
}
// PayPal payments show as PayPal funding type
const fundingType = transaction.fundingData?.fundingType ||
transaction.fundingType ||
'Unknown';
if (fundingType !== 'PayPal') {
return res.json({ success: false, error: 'Invalid funding type' });
}
// Fulfill order
const orderId = await fulfillOrder(transaction);
return res.json({ success: true, orderId });
} catch (error) {
console.error('Verification error:', error);
return res.json({ success: false, error: 'Verification failed' });
}
});PayPal vaulting allows returning customers to pay with a saved PayPal account. When enabled, customers who have previously connected their PayPal account can use it for checkout.
- The customer pays with PayPal and agrees to save their account.
- PayPal vaults the account and returns a vault ID.
- On return visit,
onGetShopperprovides the shopper ID. - Drop-in displays the saved PayPal account for payment.
- The customer completes the payment flow with their saved PayPal account.
Implement onGetShopper to enable vaulting:
import SwiftUI
import PXPCheckoutSDK
@MainActor
final class CheckoutViewModel: ObservableObject {
@Published var dropIn: CheckoutDropIn?
@Published var errorMessage: String?
func loadDropIn() async {
// Fetch session from backend (with PayPal enabled)
guard let sessionData = await fetchSessionFromBackend() else {
errorMessage = "Failed to create session"
return
}
// Initialise Drop-in
let config = CheckoutDropInConfig(
environment: .test,
session: sessionData,
transactionData: DropInTransactionData(
amount: Decimal(string: "99.99") ?? 0,
currency: "GBP",
entryType: .ecom,
intent: DropInTransactionIntentData(
card: .purchase,
paypal: .purchase
),
merchantTransactionId: UUID().uuidString,
merchantTransactionDate: { Date() }
),
merchantShopperId: "shopper-123",
ownerId: "MERCHANT-1",
// REQUIRED: Provide shopper ID to enable PayPal vaulting
onGetShopper: {
let user = getCurrentUser()
return TransactionShopper(id: user.shopperId) // e.g., TransactionShopper(id: "shopper-123")
},
onSuccess: { result in
Task {
await verifyPaymentOnBackend(result)
await MainActor.run {
navigateToSuccess()
}
}
},
onError: { paymentMethod, error in
print("PayPal payment failed: \(error.errorMessage)")
Task { @MainActor in
self.errorMessage = "Payment failed: \(error.errorMessage)"
}
}
)
do {
let instance = try CheckoutDropIn(config: config)
await instance.create()
dropIn = instance
} catch {
errorMessage = error.localizedDescription
}
}
}When onGetShopper returns a shopper ID, the SDK automatically:
- Fetches the PayPal user ID token from the PXP API.
- Displays saved PayPal accounts for faster checkout.
- Handles vault setup during the first payment.
PayPal vaulting is enabled by implementing onGetShopper and configuring the consent component:
CheckoutDropInConfig(
// ... other config
onGetShopper: {
let user = getCurrentUser()
return TransactionShopper(id: user.shopperId) // Required for vaulting
},
methodConfig: DropInMethodConfig(
global: DropInGlobalConfig(
// Provide fallback consent value
onGetConsent: { paymentMethod in
return paymentMethod == .paypal
}
),
paypal: DropInPaypalConfig(
// Control whether Drop-in creates the PayPal consent checkbox
consentComponent: true
)
)
)Requirements for PayPal vaulting:
onGetShoppermust return aTransactionShopperwith a non-emptyid.methodConfig.paypal.consentComponentcontrols whether Drop-in creates the consent checkbox.- Consent is sent as
vaultingConsentwhen the shopper opts in oronGetConsent(.paypal)returnstrue. - The SDK automatically fetches the PayPal user ID token and handles vault setup during the first payment.