# PayPal

Accept PayPal and Pay Later payments with automatic popup handling and saved PayPal accounts for returning customers.

## Overview

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.

### Key benefits

* 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 `onSuccess` and `onError` callbacks 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.


## How it works

When a customer selects PayPal:

1. The customer taps "PayPal" in the payment options list.
2. Drop-in validates the configuration and creates a pending PayPal transaction/order through Unity.
3. The PayPal web view opens with the order details.
4. The customer logs in to their PayPal account.
5. The customer approves the payment in the PayPal interface.
6. The web view closes automatically.
7. Drop-in fires your `onSuccess` callback with the pending transaction result.
8. **Critical:** You must verify the final transaction state on your backend before fulfilling the order.


## Configuration

Configure PayPal-specific settings for the PayPal button, checkout flow, and shipping integration.

### Configuration properties

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:`.paypal``.paylater` |
| `payeeEmailAddress`String? | The payee email address for the PayPal transaction. |
| `paymentDescription`String? | A description of the payment shown to the customer in PayPal. |
| `consentComponent`Bool? | 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). |
| `shippingPreference`PayPalShippingPreference? | How shipping address is handled in PayPal.Possible values:`.noShipping` - No shipping address required (digital goods)`.getFromFile` - Get address from buyer's PayPal account`.setProvidedAddress` - Use merchant-provided addressNote: 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.

### Settings used from global configuration

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.

### Complete example

This example shows a full PayPal configuration with shipping options and all supported `DropInPaypalConfig` properties:

```swift
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 requirements

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.

## Implementation

PayPal works through the standard implementation, with no PayPal-specific code needed:

```swift
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
        }
    }
}
```

### Session configuration (backend)

Enable PayPal in your session request:

```javascript
// 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"
};
```

### Payment flows

Drop-in supports two PayPal payment flows, configured via the `intent` parameter:

Pay now (purchase)
Immediate, single-step payment where funds are captured right away.

```swift
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


Confirm payment (authorisation)
Two-step payment: authorise now, capture later (within 29 days for PayPal).

```swift
transactionData: DropInTransactionData(
    amount: Decimal(string: "149.99") ?? 0,
    currency: "GBP",
    entryType: .ecom,
    intent: DropInTransactionIntentData(
        card: .authorisation,
        paypal: .authorisation  // Confirm Payment flow
    ),
    merchantTransactionId: UUID().uuidString,
    merchantTransactionDate: { Date() }
)
```

Use this flow for:

- Physical products (capture on shipment)
- Inventory validation needed
- Final amount may change (shipping, taxes)
- Complex order workflows


## Handling responses

### PayPal callback data

When a PayPal payment succeeds, your `onSuccess` callback receives the same standard result as other payment methods:

```swift
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
}
```

### Error handling

Handle PayPal-specific errors:

```swift
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)
    }
}
```

#### Common error scenarios

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 - user action was intentional |
| Insufficient funds | `error.errorMessage` contains "insufficient" | Suggest adding funds or using different payment method |
| Account restricted | `error.errorMessage` contains "restricted" | Direct user to contact PayPal support |
| Payment declined | `error.errorMessage` contains "declined" | Suggest trying 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.

## Backend verification

Always verify PayPal payments on your backend to ensure payment success before fulfilling orders:

```swift
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")
            }
        }
    }
}
```

### Backend verification code

Use the following backend code to verify PayPal transactions via the PXP API:

```javascript
// 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' });
  }
});
```

## Advanced PayPal flows

### PayPal vaulting (saved PayPal account)

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.

#### How it works

1. The customer pays with PayPal and agrees to save their account.
2. PayPal vaults the account and returns a vault ID.
3. On return visit, `onGetShopper` provides the shopper ID.
4. Drop-in displays the saved PayPal account for payment.
5. The customer completes the payment flow with their saved PayPal account.


#### Enable PayPal vaulting

Implement `onGetShopper` to enable vaulting:

```swift
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 configuration

PayPal vaulting is enabled by implementing `onGetShopper` and configuring the consent component:

```swift
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:

- `onGetShopper` must return a `TransactionShopper` with a non-empty `id`.
- `methodConfig.paypal.consentComponent` controls whether Drop-in creates the consent checkbox.
- Consent is sent as `vaultingConsent` when the shopper opts in or `onGetConsent(.paypal)` returns `true`.
- The SDK automatically fetches the PayPal user ID token and handles vault setup during the first payment.