# Update iOS SDK to latest version (/sdk/ios/update-ios)

Update your iOS integration from PayPal Mobile SDK v1 to v2. Move from delegate callbacks to completion handlers and async/await. Adopt the unified CoreSDKError type. Handle cancellations as errors.



Update your iOS integration from PayPal Mobile SDK v1 to v2 to continue using `CardClient` and `PayPalWebCheckoutClient`. SDK v2 replaces delegate-based callbacks with completion handlers and async/await, introduces the unified `CoreSDKError` type across the SDK, and delivers cancellations as errors instead of separate delegate methods.

This migration doesn't require server-side changes or additional platform enrollment.

## 1. Review your current integration [#1-review-your-current-integration]

Search your project for the following v1 identifiers to scope the change. You'll replace each one in the steps that follow.

#### CardClient (v1) [#cardclient-v1]

* `CardDelegate`
* `cardClient.delegate`
* `card(_:didFinishWithResult:)`
* `card(_:didFinishWithError:)`
* `cardDidCancel`

#### PayPalWebCheckoutClient (v1) [#paypalwebcheckoutclient-v1]

* `PayPalWebCheckoutDelegate`
* `payPalClient.delegate`
* `payPal(_:didFinishWithResult:)`
* `payPal(_:didFinishWithError:)`
* `payPalDidCancel`

### What carries forward from v1 [#what-carries-forward-from-v1]

Most of your v1 configuration and request objects carry over unchanged. Only the delegate-based callback surface changes.

| Item                                                     | Carries forward? | Notes                                                                                                 |
| -------------------------------------------------------- | ---------------- | ----------------------------------------------------------------------------------------------------- |
| `CoreConfig` with client ID and environment              | ✅ Yes            | Construction unchanged                                                                                |
| `CardRequest` and `CardVaultRequest`                     | ✅ Yes            | Request types unchanged                                                                               |
| `PayPalWebCheckoutRequest`                               | ✅ Yes            | Unchanged                                                                                             |
| Backend order creation and capture logic                 | ✅ Yes            | No server-side changes required                                                                       |
| Delegate protocol conformance                            | ❌ No             | Replaced by completion handlers and async/await                                                       |
| `cardClient.delegate = self`                             | ❌ No             | Removed                                                                                               |
| Separate cancellation delegate methods                   | ❌ No             | Cancellations are now errors. See [Handle cancellations as errors](#5-handle-cancellations-as-errors) |
| `(SomeResult?, CoreSDKError?)` beta completion signature | ❌ No             | Replaced by `Result<T, CoreSDKError>` in the GA release                                               |

## 2. Review the API surface mapping [#2-review-the-api-surface-mapping]

The following table maps every v1 operation to its v2 replacement, so you can plan each call site's replacement before you start editing.

| v1 operation                                    | v2 replacement                                                       | Category          |
| ----------------------------------------------- | -------------------------------------------------------------------- | ----------------- |
| `CardDelegate` protocol                         | Removed                                                              | No replacement    |
| `cardClient.delegate = self`                    | Removed                                                              | No replacement    |
| `card(_:didFinishWithResult:)`                  | `.success` in a completion handler, or `try await`                   | Restructured      |
| `card(_:didFinishWithError:)`                   | `.failure(CoreSDKError)` in a completion handler, or a `catch` block | Restructured      |
| `cardDidCancel(_:)`                             | `CardError.threeDSecureCanceledError` as `.failure`                  | Behavioral change |
| `cardClient.approveOrder(request:)`             | `cardClient.approveOrder(request:) { result in }`, or `try await`    | Restructured      |
| `cardClient.vault(request:)`                    | `cardClient.vault(request:) { result in }`, or `try await`           | Restructured      |
| `PayPalWebCheckoutDelegate` protocol            | Removed                                                              | No replacement    |
| `payPalClient.delegate = self`                  | Removed                                                              | No replacement    |
| `payPalClient.approveOrder(request:)` (v1 name) | `payPalClient.start(request:completion:)`                            | Restructured      |
| `payPal(_:didFinishWithResult:)`                | `.success` in a completion handler                                   | Restructured      |
| `payPalDidCancel(_:)`                           | `PayPalError.isCheckoutCanceled` as `.failure`                       | Behavioral change |
| Generic `Error` in `didFinishWithError:`        | `CoreSDKError` (unified type)                                        | Restructured      |
| `CardError`, `NetworkingError` (private)        | Now public, with static properties                                   | Restructured      |

## 3. Update CardClient [#3-update-cardclient]

Take these steps for every `CardClient` change.

### Remove CardClient delegate conformance [#remove-cardclient-delegate-conformance]

Remove the delegate protocol conformance, the delegate assignment, and each delegate method:

```swift
// Remove this class conformance and every method below it
class MyViewController: CardDelegate {
    func setupPayment() {
        let cardClient = CardClient(config: config)
        cardClient.delegate = self
        cardClient.approveOrder(request: cardRequest)
    }

    func card(_ cardClient: CardClient, didFinishWithResult result: CardResult) {
        // Handle success
    }

    func card(_ cardClient: CardClient, didFinishWithError error: Error) {
        // Handle error
    }

    func cardDidCancel(_ cardClient: CardClient) {
        // Handle cancellation
    }
}
```

### Replace approveOrder with a completion handler or async/await [#replace-approveorder-with-a-completion-handler-or-asyncawait]

After you remove the delegate methods, `approveOrder()` returns its result through a completion handler or through async/await.

#### CardClient completion handler [#cardclient-completion-handler]

Switch on the result to handle a successful capture, and check for a 3D Secure cancellation before falling through to your generic error handler.

```swift
class MyViewController {
    func setupPayment() {
        let cardClient = CardClient(config: config)
        cardClient.approveOrder(request: cardRequest) { [weak self] result in
            switch result {
            case .success(let cardResult):
                // Capture or authorize on your server
            case .failure(let error):
                if error == CardError.threeDSecureCanceledError {
                    // 3D Secure canceled by the reader
                } else {
                    self?.handleError(error)
                }
            }
        }
    }
}
```

#### CardClient async/await [#cardclient-asyncawait]

Wrap the `try await` call in a `do`/`catch` block. Cast the caught error to `CoreSDKError`, then check it against `CardError.threeDSecureCanceledError` before handling other errors.

> **Warn:** Async/await methods can crash on some Xcode 16 beta and iOS 18 beta configurations. See [Avoid async/await crashes on early Xcode 16 and iOS 18 beta versions](#8-avoid-asyncawait-crashes-on-early-xcode-16-and-ios-18-beta-versions) before you pick this pattern.

```swift
class MyViewController {
    func setupPayment() async {
        let cardClient = CardClient(config: config)
        do {
            let result = try await cardClient.approveOrder(request: cardRequest)
            handleSuccess(result)
        } catch let error as CoreSDKError {
            if error == CardError.threeDSecureCanceledError {
                // 3D Secure canceled
            } else {
                handleError(error)
            }
        } catch {
            // Handle an unexpected, non-SDK error
        }
    }
}
```

### Replace vault [#replace-vault]

`vault()` follows the same completion handler pattern as `approveOrder()`.

```swift
cardClient.vault(request: cardVaultRequest) { [weak self] result in
    switch result {
    case .success(let vaultResult):
        // Create a payment token on your server
    case .failure(let error):
        if error == CardError.threeDSecureCanceledError {
            // 3D Secure canceled by the reader
        } else {
            self?.handleError(error)
        }
    }
}
```

## 4. Update PayPalWebCheckoutClient [#4-update-paypalwebcheckoutclient]

The same delegate-removal and completion handler changes from `CardClient` apply here, with one naming difference.

### Remove PayPalWebCheckoutClient delegate conformance [#remove-paypalwebcheckoutclient-delegate-conformance]

Remove the delegate protocol conformance, the delegate assignment, and each delegate method:

```swift
// Remove this class conformance and every method below it
class MyViewController: PayPalWebCheckoutDelegate {
    func startPayPalFlow() {
        let payPalClient = PayPalWebCheckoutClient(config: config)
        payPalClient.delegate = self
        payPalClient.approveOrder(request: paypalRequest)  // v1 method name
    }

    func payPal(_ payPalClient: PayPalWebCheckoutClient, didFinishWithResult result: PayPalWebCheckoutResult) { }
    func payPal(_ payPalClient: PayPalWebCheckoutClient, didFinishWithError error: Error) { }
    func payPalDidCancel(_ payPalClient: PayPalCheckoutClient) { }
}
```

> **Warn:** The v1 method for starting PayPal web checkout on `PayPalWebCheckoutClient` was `approveOrder(request:)`. In v2, it's renamed to `start(request:completion:)`. `CardClient` still uses `approveOrder`. These are different clients with different method names.

### Replace start with a completion handler or async/await [#replace-start-with-a-completion-handler-or-asyncawait]

After you remove the delegate methods, `start()` returns its result through a completion handler or, if your app supports it, through async/await.

#### PayPalWebCheckoutClient completion handler [#paypalwebcheckoutclient-completion-handler]

Switch on the result to handle a successful checkout, and check for a cancellation with `PayPalError.isCheckoutCanceled` before falling through to your generic error handler.

```swift
class MyViewController {
    func startPayPalFlow() {
        let payPalClient = PayPalWebCheckoutClient(config: config)
        payPalClient.start(request: paypalRequest) { [weak self] result in
            switch result {
            case .success(let checkoutResult):
                // Capture or authorize the order on your server
            case .failure(let error):
                if PayPalError.isCheckoutCanceled(error) {
                    // Reader canceled PayPal checkout
                } else {
                    self?.handleError(error)
                }
            }
        }
    }
}
```

#### PayPalWebCheckoutClient async/await [#paypalwebcheckoutclient-asyncawait]

Wrap the `try await` call in a `do`/`catch` block. Cast the caught error to `CoreSDKError`, then check it with `PayPalError.isCheckoutCanceled` before handling other errors.

```swift
func startPayPalFlow() async {
    let payPalClient = PayPalWebCheckoutClient(config: config)
    do {
        let result = try await payPalClient.start(request: paypalRequest)
        handleSuccess(result)
    } catch let error as CoreSDKError {
        if PayPalError.isCheckoutCanceled(error) {
            // Reader canceled
        } else {
            handleError(error)
        }
    } catch {
        // Handle an unexpected error
    }
}
```

## 5. Handle cancellations as errors [#5-handle-cancellations-as-errors]

> **Warn:** In v1, cancellations went through dedicated delegate methods: `cardDidCancel(_:)` and `payPalDidCancel(_:)`. In v2, cancellations return as `.failure(CoreSDKError)`, which is the same path as every other error. If your failure handler doesn't check for cancellation, a canceled payment reaches your generic error handler and can show the reader an incorrect error message.

### Detect cancellation with a completion handler [#detect-cancellation-with-a-completion-handler]

Check the failure case for each client's cancellation signal before falling through to your generic error handler.

```swift
case .failure(let error):
    // Card 3D Secure cancellation
    if error == CardError.threeDSecureCanceledError {
        // Show a "payment canceled" state
        return
    }

    // PayPal checkout cancellation
    if PayPalError.isCheckoutCanceled(error) {
        // Show a "payment canceled" state
        return
    }

    // PayPal vault cancellation
    if PayPalError.isVaultCanceled(error) {
        // Show a "vault canceled" state
        return
    }

    // All other errors
    handleError(error)
```

### Detect cancellation with async/await [#detect-cancellation-with-asyncawait]

> **Info:** The helper methods `CardError.isThreeDSecureCanceled(_:)`, `PayPalError.isCheckoutCanceled(_:)`, and `PayPalError.isVaultCanceled(_:)` don't need a cast to `CoreSDKError`. Use these in `catch` blocks when you only need to check for cancellation.

```swift
} catch {
    if CardError.isThreeDSecureCanceled(error) {
        // Cancellation confirmed, no cast required
    } else if let sdkError = error as? CoreSDKError {
        // Other known SDK error
        handleError(sdkError)
    } else {
        // Unexpected error
    }
}
```

## 6. Handle errors with CoreSDKError [#6-handle-errors-with-coresdkerror]

SDK v2 introduces `CoreSDKError` as the unified error type across the SDK. Every error the SDK returns is an instance of `CoreSDKError`.

```swift
// Previously private, now public in v2
public enum CardError {
    public static let threeDSecureCanceledError: CoreSDKError
    public static let encodingError: CoreSDKError
    // Additional static properties
}

public enum NetworkingError {
    public static let urlSessionError: CoreSDKError
    public static let serverResponseError: (String) -> CoreSDKError
}

// Helper methods, no cast required
PayPalError.isCheckoutCanceled(_ error: Error) -> Bool
PayPalError.isVaultCanceled(_ error: Error) -> Bool
CardError.isThreeDSecureCanceled(_ error: Error) -> Bool
```

Use this table to choose the right error-handling pattern for each scenario:

| Scenario                                         | Recommended pattern                                                               |
| ------------------------------------------------ | --------------------------------------------------------------------------------- |
| Detect cancellation only                         | No cast to `CoreSDKError` required                                                |
| Distinguish between multiple error types         | `if error == CardError.someError`, which requires a `CoreSDKError`-typed variable |
| Cover every error case with async/await          | `catch let error as CoreSDKError`, followed by a generic `catch`                  |
| Cover every error case with a completion handler | `switch result`, then inspect the `CoreSDKError` in `.failure`                    |

## 7. Update from 2.0.0 beta to GA [#7-update-from-200-beta-to-ga]

**Beta only:** If you integrated a 2.0.0-beta version and are upgrading to the 2.0.0 GA release, one additional breaking change applies to every completion block.

> **Info:** Async/await function signatures are unchanged between beta versions and the GA release. Only completion handler signatures changed.

#### 2.0.0-beta [#200-beta]

The beta completion handler takes two optional parameters instead of a `Result`. Check each one separately.

```swift
cardClient.approveOrder(request: cardRequest) { cardResult, error in
    if let error {
        // Handle error
        return
    }
    if let cardResult {
        // Handle success
    }
}
```

#### 2.0.0 GA [#200-ga]

The GA release replaces those two optionals with a single `Result<T, CoreSDKError>` parameter that you switch on.

```swift
cardClient.approveOrder(request: cardRequest) { result in
    switch result {
    case .success(let cardResult):
        // Handle success
    case .failure(let error):
        // Handle error
    }
}
```

## 8. Avoid async/await crashes on early Xcode 16 and iOS 18 beta versions [#8-avoid-asyncawait-crashes-on-early-xcode-16-and-ios-18-beta-versions]

> **Warn:** The SDK's async/await methods use `withCheckedContinuation` internally. Some Xcode 16 beta 5 and later builds, combined with early iOS 18 beta versions, have reported crashes. Mac Catalyst apps built with Xcode 16 and running on macOS 14 and earlier are also affected. The fix ships in Xcode 16.2 beta.

| Affected configuration                            | Status           | Mitigation                                         |
| ------------------------------------------------- | ---------------- | -------------------------------------------------- |
| Xcode 16 beta 5 and later, with early iOS 18 beta | Crashes reported | Use completion handler APIs instead of async/await |
| Mac Catalyst, Xcode 16, macOS 14 and earlier      | Crashes reported | Upgrade to Xcode 16.2 beta or later                |
| Xcode 16.2 and later, with stable iOS 18          | Fixed            | No action needed                                   |

## 9. Test your integration [#9-test-your-integration]

> **Info:** Set `Environment.SANDBOX` in `CoreConfig` while you test. Create sandbox buyer accounts at [developer.paypal.com](https://developer.paypal.com).

Run through each scenario and confirm the result matches what's expected:

| Scenario                               | Client                    | What to verify                                                                                                     |
| -------------------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| Card approve order, success            | `CardClient`              | `result.success` returns, and the server capture succeeds                                                          |
| Card approve order, 3D Secure canceled | `CardClient`              | `.failure(CardError.threeDSecureCanceledError)` returns, and the reader sees a canceled state, not a generic error |
| Card vault, success                    | `CardClient`              | `result.success` returns with a vault token                                                                        |
| Card vault, 3D Secure canceled         | `CardClient`              | The cancellation returns as `.failure` and your code handles it without reaching the generic error path            |
| PayPal web checkout, success           | `PayPalWebCheckoutClient` | `result.success` returns with an order or capture ID                                                               |
| PayPal checkout, reader cancels        | `PayPalWebCheckoutClient` | `PayPalError.isCheckoutCanceled` returns `true`, with no crash or unexpected error message                         |
| PayPal vault, success                  | `PayPalWebCheckoutClient` | `result.success` returns with a vault token                                                                        |
| PayPal vault, reader cancels           | `PayPalWebCheckoutClient` | `PayPalError.isVaultCanceled` returns `true`                                                                       |
| Network error                          | Both clients              | `NetworkingError.urlSessionError` surfaces in `.failure`                                                           |

## 10. Go live [#10-go-live]

Confirm each of the following before you release the update:

* `CoreConfig` is set to `Environment.LIVE`
* Every delegate protocol conformance (`CardDelegate`, `PayPalWebCheckoutDelegate`) is removed
* Every `cardClient.delegate = self` and `payPalClient.delegate = self` assignment is removed
* Cancellation handling is implemented for every payment flow
* If you use async/await, your Xcode and iOS versions fall outside the affected range in [Avoid async/await crashes on early Xcode 16 and iOS 18 beta versions](#8-avoid-asyncawait-crashes-on-early-xcode-16-and-ios-18-beta-versions)
* The full test scenario checklist passes against live credentials in a pre-production environment
* You log `CoreSDKError` details from payment failures for PayPal support escalation

### Migration checklist [#migration-checklist]

Confirm you've completed every row of the [API surface mapping](#2-review-the-api-surface-mapping) table for both clients, plus these call sites the table doesn't cover:

* Update the SDK to v2.0.0 GA through CocoaPods or Swift Package Manager
* Add explicit cancellation checks: `CardError.threeDSecureCanceledError`, `PayPalError.isCheckoutCanceled`, `PayPalError.isVaultCanceled`
* Confirm cancellations don't reach the generic error handler
* If migrating from a beta version, update every `(Result?, Error?)` completion signature to `Result<T, CoreSDKError>`
* If you use async/await, confirm your Xcode and iOS versions fall outside the [affected crash range](#8-avoid-asyncawait-crashes-on-early-xcode-16-and-ios-18-beta-versions)
* Run the full test scenario checklist in sandbox

## Avoid these common migration mistakes [#avoid-these-common-migration-mistakes]

These are the failure modes teams run into most often when a step above gets skipped or half-applied.

| Mistake                                                                          | Why it happens                                                                                                                       | Resolution                                                                                                                                                                                                                                                                                                |
| -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Casting `error` to `CoreSDKError` in error comparisons without an explicit cast  | `.failure(let error)` in a completion handler already delivers a `CoreSDKError`, but in a `catch` block, `error` is typed as `Error` | Use `catch let error as CoreSDKError` in `catch` blocks. In completion handlers, `.failure(let error)` needs no cast                                                                                                                                                                                      |
| Leaving dead delegate method implementations in place                            | After you remove the protocol conformance declaration, Swift no longer requires the methods, so they compile silently as dead code   | Remove the delegate method bodies. Dead code that references v1 error or result types can mask an incomplete migration                                                                                                                                                                                    |
| Not casting to `CoreSDKError` in async/await `catch` blocks                      | Writing `catch { handleError(error) }` without casting first                                                                         | Use `catch let error as CoreSDKError` as the primary catch block, followed by a generic `catch` for unexpected errors. Without the cast, you can't use `CoreSDKError`'s `Equatable` conformance to compare against static enum properties                                                                 |
| Not handling cancellation as an error                                            | In v1, cancellations had dedicated delegate methods, so developers expect cancellations to stay separate from other errors           | In v2, check every `.failure` case for cancellation first, using `error == CardError.threeDSecureCanceledError`, `PayPalError.isCheckoutCanceled(error)`, or `PayPalError.isVaultCanceled(error)`. An unchecked cancellation reaches your generic error handler and shows the reader an incorrect message |
| Shipping async/await on an affected Xcode or iOS configuration                   | The `withCheckedContinuation` crash is intermittent and might not appear in every test run                                           | Check your Xcode and iOS versions against the affected ranges in [Avoid async/await crashes on early Xcode 16 and iOS 18 beta versions](#8-avoid-asyncawait-crashes-on-early-xcode-16-and-ios-18-beta-versions). Fall back to completion handler APIs for affected configurations                         |
| Assuming server-side changes are required                                        | The major version bump suggests a larger change than it is                                                                           | No server-side changes are required. This migration is client-side, and your existing order creation and capture server logic is unaffected                                                                                                                                                               |
| Keeping the beta `(Result?, Error?)` completion signature after upgrading to GA  | Developers carry the beta pattern forward without checking for a signature change                                                    | Every completion block in the GA release returns a single `Result<T, CoreSDKError>`. Replace `if let error` and `if let result` patterns with `switch result { case .success: ... case .failure: ... }`                                                                                                   |
| Calling the v1 method name `approveOrder(request:)` on `PayPalWebCheckoutClient` | `CardClient` still uses `approveOrder`, so developers assume `PayPalWebCheckoutClient` follows the same naming                       | In v2, `PayPalWebCheckoutClient` renames the method to `start(request:completion:)`. This causes a compilation error in v2, unless you're conditionally compiling for v1 targets                                                                                                                          |
