On this page
No Headings
Last updated: August 19, 2026
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.
Search your project for the following v1 identifiers to scope the change. You'll replace each one in the steps that follow.
CardDelegatecardClient.delegatecard(_:didFinishWithResult:)card(_:didFinishWithError:)cardDidCancelPayPalWebCheckoutDelegatepayPalClient.delegatepayPal(_:didFinishWithResult:)payPal(_:didFinishWithError:)payPalDidCancelMost 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 |
(SomeResult?, CoreSDKError?) beta completion signature | ❌ No | Replaced by Result<T, CoreSDKError> in the GA release |
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 |
Take these steps for every CardClient change.
Remove the delegate protocol conformance, the delegate assignment, and each delegate method:
// 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
}
}After you remove the delegate methods, approveOrder() returns its result through a completion handler or through async/await.
Switch on the result to handle a successful capture, and check for a 3D Secure cancellation before falling through to your generic error handler.
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)
}
}
}
}
}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.
Check known platform issues before choosing async/await
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 before you pick this pattern.
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
}
}
}vault() follows the same completion handler pattern as approveOrder().
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)
}
}
}The same delegate-removal and completion handler changes from CardClient apply here, with one naming difference.
Remove the delegate protocol conformance, the delegate assignment, and each delegate method:
// 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) { }
}Method renamed
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.
After you remove the delegate methods, start() returns its result through a completion handler or, if your app supports it, through async/await.
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.
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)
}
}
}
}
}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.
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
}
}Most significant behavioral change in v2
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.
Check the failure case for each client's cancellation signal before falling through to your generic error handler.
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)Simpler for cancellation-only handling
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.
} 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
}
}SDK v2 introduces CoreSDKError as the unified error type across the SDK. Every error the SDK returns is an instance of CoreSDKError.
// 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) -> BoolUse 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 |
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.
Async/await unaffected
Async/await function signatures are unchanged between beta versions and the GA release. Only completion handler signatures changed.
The beta completion handler takes two optional parameters instead of a Result. Check each one separately.
cardClient.approveOrder(request: cardRequest) { cardResult, error in
if let error {
// Handle error
return
}
if let cardResult {
// Handle success
}
}The GA release replaces those two optionals with a single Result<T, CoreSDKError> parameter that you switch on.
cardClient.approveOrder(request: cardRequest) { result in
switch result {
case .success(let cardResult):
// Handle success
case .failure(let error):
// Handle error
}
}Known platform issue
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 |
Use sandbox for testing
Set Environment.SANDBOX in CoreConfig while you test. Create sandbox buyer accounts at 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 |
Confirm each of the following before you release the update:
CoreConfig is set to Environment.LIVECardDelegate, PayPalWebCheckoutDelegate) is removedcardClient.delegate = self and payPalClient.delegate = self assignment is removedCoreSDKError details from payment failures for PayPal support escalationConfirm you've completed every row of the API surface mapping table for both clients, plus these call sites the table doesn't cover:
CardError.threeDSecureCanceledError, PayPalError.isCheckoutCanceled, PayPalError.isVaultCanceled(Result?, Error?) completion signature to Result<T, CoreSDKError>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. 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 |