# Update Android SDK to latest version (/sdk/update-android)

Update your Android integration from PayPal Mobile SDK v1 to v2. Move to sealed result callbacks, manual 3D Secure handling, and remove PayPal Native Payments.



Update your Android integration from PayPal Mobile SDK v1 to v2 to continue using `CardClient` and `PayPalWebCheckoutClient`. SDK v2 replaces listener-based callbacks with sealed result types, requires you to present 3D Secure auth challenges, and removes PayPal Native Payments.

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

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

Search your project for the following v1 identifiers to scope the change.

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

* `ApproveOrderListener`
* `CardVaultListener`
* `cardClient.approveOrderListener`
* `cardClient.vaultListener`
* `cardClient.removeObservers`
* `onApproveOrderSuccess`
* `onApproveOrderFailure`
* `onVaultSuccess`
* `onVaultFailure`

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

* `PayPalWebCheckoutListener`
* `PayPalWebVaultListener`
* `payPalClient.listener`
* `payPalClient.vaultListener`
* `payPalClient.removeObservers`
* `onPayPalWebSuccess`
* `onPayPalWebFailure`
* `onPayPalWebCanceled`

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

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

| Item                                                      | Carries forward? | Notes                                                               |
| --------------------------------------------------------- | ---------------- | ------------------------------------------------------------------- |
| `CoreConfig` with `CLIENT_ID`                             | ✅ Yes            | Unchanged                                                           |
| `Environment.LIVE` and `Environment.SANDBOX`              | ✅ Yes            | Enum values unchanged                                               |
| `CardRequest` and `CardVaultRequest`                      | ✅ Yes            | Request types unchanged                                             |
| `PayPalWebCheckoutRequest`                                | ✅ Yes            | Unchanged                                                           |
| Deep link URL scheme                                      | ✅ Yes            | Same scheme in constructor                                          |
| Backend order and capture calls                           | ✅ Yes            | No server changes required                                          |
| Listener interfaces (for example, `ApproveOrderListener`) | ❌ No             | Replaced by sealed result types                                     |
| `cardClient.removeObservers()`                            | ❌ No             | Method removed                                                      |
| `Activity` in client constructors                         | ❌ No             | Constructors now accept `Context`; pass `Activity` per call instead |

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

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

| v1 operation                                        | v2 replacement                                                      | Category          |
| --------------------------------------------------- | ------------------------------------------------------------------- | ----------------- |
| `CardClient(activity, config)`                      | `CardClient(context, config)`                                       | Restructured      |
| `cardClient.approveOrderListener = this`            | Removed                                                             | No replacement    |
| `cardClient.vaultListener = this`                   | Removed                                                             | No replacement    |
| `cardClient.approveOrder(this, request)`            | `cardClient.approveOrder(request)` returns `CardApproveOrderResult` | Restructured      |
| `cardClient.vault(this, request)`                   | `cardClient.vault(request)` returns `CardVaultResult`               | Restructured      |
| `onApproveOrderSuccess`                             | `CardApproveOrderResult.Success`                                    | Restructured      |
| `onApproveOrderFailure`                             | `CardApproveOrderResult.Failure`                                    | Restructured      |
| `onVaultSuccess` with a manual 3D Secure check      | `CardVaultResult.AuthorizationRequired`                             | Restructured      |
| `cardClient.finishApproveOrder(intent)`             | New in v2                                                           | Split             |
| `cardClient.finishVault(intent)`                    | New in v2                                                           | Split             |
| Reassigning `intent` in `onNewIntent`               | Explicit call to `checkForCardAuthCompletion(newIntent)`            | Behavioral change |
| `cardClient.removeObservers()`                      | Removed                                                             | No replacement    |
| `PayPalWebCheckoutClient(activity, config, scheme)` | `PayPalWebCheckoutClient(context, config, scheme)`                  | Restructured      |
| `payPalClient.start(request)`                       | `paypalClient.start(this, request) { result -> }`                   | Restructured      |
| `payPalClient.vault(request)`                       | `paypalClient.vault(this, request) { result -> }`                   | Restructured      |
| `payPalClient.finishStart(intent)`                  | New in v2                                                           | Split             |
| `payPalClient.finishVault(intent)`                  | New in v2                                                           | Split             |
| `onPayPalWeb*` delegate methods                     | Removed                                                             | No replacement    |
| `PayPalNativeClient` (entire module)                | Removed                                                             | No replacement    |

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

Take these steps to update your `CardClient` integration.

### Update the constructor [#update-the-constructor]

`CardClient` now takes a `Context` instead of an `Activity`.

#### Before (v1) [#before-v1]

```kotlin
val cardClient = CardClient(requireActivity(), config)
```

#### After (v2) [#after-v2]

```kotlin
val cardClient = CardClient(requireContext(), config)
```

Your project compiles after every `CardClient` constructor call passes a `Context`.

### Remove listener registration and teardown [#remove-listener-registration-and-teardown]

Remove the listener interfaces, the listener assignments, and the `removeObservers()` call from `onDestroy()`:

```kotlin
// Remove this class declaration and the listener assignments below it
class SampleActivity: ComponentActivity(), ApproveOrderListener, CardVaultListener {

    cardClient.approveOrderListener = this
    cardClient.vaultListener = this

    override fun onDestroy() {
        super.onDestroy()
        cardClient.removeObservers()
    }
}
```

`removeObservers()` doesn't exist in v2. If the call remains in your code, the resulting compile error indicates that you still need to remove references to the v1 listener implementation.

### Replace approveOrder with the sealed result pattern [#replace-approveorder-with-the-sealed-result-pattern]

The listener callbacks are gone. `approveOrder()` now returns a sealed result you handle inline.

#### Before (v1) [#before-v1-1]

```kotlin
fun approveOrder() {
    cardClient.approveOrder(this, cardRequest)
}

override fun onApproveOrderSuccess(result: CardResult) {
    // Capture or authorize on server
}

override fun onApproveOrderFailure(error: PayPalSDKError) {
    // Handle failure
}
```

#### After (v2) [#after-v2-1]

```kotlin
fun approveOrder() {
    when (val result = cardClient.approveOrder(cardRequest)) {
        is CardApproveOrderResult.Success -> {
            // Capture or authorize the order on your server
        }
        is CardApproveOrderResult.Failure -> {
            // Handle failure
        }
        is CardApproveOrderResult.AuthorizationRequired -> {
            presentAuthChallenge(result.authChallenge)
        }
    }
}
```

> **Info:** In v1, the SDK launched 3D Secure auth challenges automatically inside `approveOrder()`. In v2, `CardApproveOrderResult.AuthorizationRequired` is returned, and your code must call `presentAuthChallenge()` when this state occurs.

Confirm the `when` block handles all three result types. Cards that need 3D Secure produce no outcome until you add the `AuthorizationRequired` branch.

### Replace vault with the sealed result pattern [#replace-vault-with-the-sealed-result-pattern]

`vault()` follows the same sealed result pattern as `approveOrder()`, including the explicit `AuthorizationRequired` state.

#### Before (v1) [#before-v1-2]

```kotlin
override fun onVaultSuccess(result: CardVaultResult) {
    val authChallenge = result.authChallenge
    if (authChallenge != null) {
        cardClient?.presentAuthChallenge(activity, authChallenge)
    } else {
        // Create payment token on server
    }
}
```

#### After (v2) [#after-v2-2]

```kotlin
when (val vaultResult = cardClient.vault(cardVaultRequest)) {
    is CardVaultResult.Success -> {
        // Create the payment token on your server
    }
    is CardVaultResult.Failure -> {
        // Handle failure
    }
    is CardVaultResult.AuthorizationRequired -> {
        presentAuthChallenge(vaultResult.authChallenge)
    }
}
```

### Present and finish the auth challenge [#present-and-finish-the-auth-challenge]

When a result returns `AuthorizationRequired`, present the challenge and then resume it after the deep link redirects back to your app.

```kotlin
fun presentAuthChallenge(authChallenge: CardAuthChallenge) {
    when (val result = cardClient.presentAuthChallenge(this, authChallenge)) {
        is CardPresentAuthChallengeResult.Success -> {
            // Chrome Custom Tab opened. The result arrives through the deep link.
        }
        is CardPresentAuthChallengeResult.Failure -> {
            // Handle failure to present
        }
    }
}
```

Dispatch any pending card auth result on `onResume()` and `onNewIntent()`, since a completed 3D Secure challenge returns through one of these lifecycle callbacks.

```kotlin
override fun onResume() {
    super.onResume()
    checkForCardAuthCompletion(intent)
}

override fun onNewIntent(newIntent: Intent) {
    super.onNewIntent(newIntent)
    checkForCardAuthCompletion(newIntent)
}

fun checkForCardAuthCompletion(intent: Intent) {
    cardClient.finishApproveOrder(intent)?.let { result ->
        when (result) {
            is CardFinishApproveOrderResult.Success -> {
                // Capture the order on your server
            }
            is CardFinishApproveOrderResult.Failure -> {
                // Handle error
            }
            CardFinishApproveOrderResult.Canceled -> {
                // Offer the reader a retry
            }
            CardFinishApproveOrderResult.NoResult -> {
                // Intent is unrelated to this client. No action needed.
            }
        }
    }

    cardClient.finishVault(intent)?.let { result ->
        when (result) {
            is CardFinishVaultResult.Success -> {
                // Create the payment token
            }
            is CardFinishVaultResult.Failure -> {
                // Handle error
            }
            CardFinishVaultResult.Canceled -> {
                // Offer the buyer a retry
            }
            CardFinishVaultResult.NoResult -> {
                // No action needed
            }
        }
    }
}
```

> **Warn:** In v1, `onNewIntent` reassigned `intent = newIntent` and the SDK's lifecycle observer processed it automatically. In v2, call `checkForCardAuthCompletion(newIntent)`. Without this call, auth challenge completions that arrive through `onNewIntent` are lost, and the buyer sees no outcome for a completed 3D Secure challenge.

Confirm your integration handles all four result states (`Success`, `Failure`, `Canceled`, and `NoResult`) in both `onResume()` and `onNewIntent()`.

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

The same constructor, listener-removal, and sealed result changes from `CardClient` apply here, with a few PayPal-specific differences.

### Update the constructor and remove listeners [#update-the-constructor-and-remove-listeners]

`PayPalWebCheckoutClient` also moves from `Activity` to `Context`, and drops its listener interfaces.

#### Before (v1) [#before-v1-3]

```kotlin
val payPalClient = PayPalWebCheckoutClient(requireActivity(), config, "my-scheme")
```

#### After (v2) [#after-v2-3]

```kotlin
val payPalClient = PayPalWebCheckoutClient(requireContext(), config, "my-scheme")
```

Remove any `PayPalWebCheckoutListener` and `PayPalWebVaultListener` implementations and their listener assignments.

### Replace start and vault calls [#replace-start-and-vault-calls]

`start()` and `vault()` move from listener callbacks to an inline callback that receives a sealed result.

#### Before (v1) [#before-v1-4]

```kotlin
payPalClient.start(checkoutRequest)
payPalClient.vault(vaultRequest)

override fun onPayPalWebSuccess(result: PayPalWebCheckoutResult) {
    // Handle success
}

override fun onPayPalWebFailure(error: PayPalSDKError) {
    // Handle failure
}

override fun onPayPalWebCanceled() {
    // Handle cancellation
}
```

#### After (v2) [#after-v2-4]

```kotlin
// start: the callback confirms the web view launched, not that the payment succeeded
paypalClient.start(this, checkoutRequest) { result ->
    when (result) {
        is PayPalPresentAuthChallengeResult.Success -> {
            // Web checkout launched. The outcome arrives through finishStart().
        }
        is PayPalPresentAuthChallengeResult.Failure -> {
            // Handle failure
        }
    }
}

paypalClient.vault(this, vaultRequest) { result ->
    when (result) {
        is PayPalPresentAuthChallengeResult.Success -> {
            // Web view launched
        }
        is PayPalPresentAuthChallengeResult.Failure -> {
            // Handle failure
        }
    }
}
```

`start()` and `vault()` now require an `Activity` and a callback. If your project still uses the single-argument v1 signature, the resulting compile error identifies call sites that you still need to update.

### Handle auth completion [#handle-auth-completion]

Like `CardClient`, `PayPalWebCheckoutClient` requires a call to pick up the result after the redirect completes.

```kotlin
fun checkForPayPalAuthCompletion(intent: Intent) {
    payPalClient.finishStart(intent)?.let { result ->
        when (result) {
            is PayPalWebCheckoutFinishStartResult.Success -> {
                // Capture or authorize the order
            }
            is PayPalWebCheckoutFinishStartResult.Failure -> {
                // Handle error
            }
            is PayPalWebCheckoutFinishStartResult.Canceled -> {
                // Notify the buyer
            }
            PayPalWebCheckoutFinishStartResult.NoResult -> {
                // No action needed
            }
        }
    }

    payPalClient.finishVault(intent)?.let { result ->
        when (result) {
            is PayPalWebCheckoutFinishVaultResult.Success -> {
                // Create the payment token
            }
            is PayPalWebCheckoutFinishVaultResult.Failure -> {
                // Handle error
            }
            is PayPalWebCheckoutFinishVaultResult.Canceled -> {
                // Notify the buyer
            }
            PayPalWebCheckoutFinishVaultResult.NoResult -> {
                // No action needed
            }
        }
    }
}
```

Call `checkForPayPalAuthCompletion(intent)` from both `onResume()` and `onNewIntent()`, the same way you did for `CardClient`.

> **Info:** `PayPalPresentAuthChallengeResult.Success` from `start()` or `vault()` only confirms the Chrome Custom Tab opened. The payment or vault result arrives later, through `finishStart()` or `finishVault()`.

## 5. Replace PayPal Native Payments, if applicable [#5-replace-paypal-native-payments-if-applicable]

> **Warn:** The `PayPalNativePayments` module and its classes (`PayPalNativeClient`, `PayPalNativeCheckoutRequest`, and `PayPalNativeCheckoutListener`) don't exist in v2. The underlying native checkout dependency is discontinued with no replacement.

Search your codebase for the following identifiers before you upgrade the SDK dependency:

* `PayPalNativeClient`
* `PayPalNativeCheckoutRequest`
* `PayPalNativeCheckoutListener`

If your project contains any of these identifiers, you'll need to complete this step. If it doesn't, you can [test your integration](#6-test-your-integration).

Replace all `PayPalNativeClient` usage with `PayPalWebCheckoutClient`. The web checkout flow opens a Chrome Custom Tab and gives the buyer the same PayPal login and payment approval flow.

Remove the `PayPalNativePayments` module from your `build.gradle` dependency block. Leaving it in place alongside v2 artifacts causes a build failure.

## 6. Test your integration [#6-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, no 3D Secure    | `CardClient`              | `CardApproveOrderResult.Success` returns, and the server capture succeeds                             |
| Card approve order, 3D Secure required       | `CardClient`              | `AuthorizationRequired` returns, the challenge presents, and `finishApproveOrder()` returns `Success` |
| Card approve order, 3D Secure canceled       | `CardClient`              | `finishApproveOrder()` returns `Canceled`                                                             |
| Card vault, success, no 3D Secure            | `CardClient`              | `CardVaultResult.Success` returns                                                                     |
| Card vault, 3D Secure required and completed | `CardClient`              | `AuthorizationRequired` returns, and the vault completes through `finishVault()`                      |
| PayPal checkout, success                     | `PayPalWebCheckoutClient` | `finishStart()` returns `Success` with an order ID                                                    |
| PayPal checkout, user cancels                | `PayPalWebCheckoutClient` | `finishStart()` returns `Canceled`                                                                    |
| PayPal vault, success                        | `PayPalWebCheckoutClient` | `finishVault()` returns `Success`                                                                     |
| PayPal vault, user cancels                   | `PayPalWebCheckoutClient` | `finishVault()` returns `Canceled`                                                                    |
| App backgrounded mid-auth, then resumed      | Both                      | `onResume()` fires, and `checkFor*AuthCompletion(intent)` handles the stale intent without a crash    |

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

Confirm each of the following before you release the update:

* `CoreConfig` is set to `Environment.LIVE`
* The deep link scheme is registered in `AndroidManifest.xml` with a production intent filter
* The `PayPalNativePayments` dependency is removed from `build.gradle`
* All v1 listener interfaces, `removeObservers()` calls, and other deprecated references are removed
* The full test scenario checklist passes against live credentials in staging
* You log the `debug_id` from any SDK errors for PayPal support escalation

### Verify the migration is complete [#verify-the-migration-is-complete]

Confirm you've completed every row of the [API surface mapping](#2-review-the-api-surface-mapping) table, plus the following call sites:

* Search for `PayPalNativeClient` and migrate to `PayPalWebCheckoutClient` if found
* Update the SDK dependency to v2 in `build.gradle`
* Add `checkForCardAuthCompletion(intent)` and `checkForPayPalAuthCompletion(intent)` to both `onResume()` and `onNewIntent()`
* Confirm `Canceled` and `NoResult` states are handled for both clients
* 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                                                                                                                                                                                            |
| -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Keeping `intent = newIntent` in `onNewIntent` instead of dispatching it    | The v1 pattern relied on the SDK's lifecycle observer to process the intent automatically         | Replace the reassignment with `checkForCardAuthCompletion(newIntent)` and `checkForPayPalAuthCompletion(newIntent)`. Without this change, auth completions that arrive through `onNewIntent` are lost |
| Treating `CardApproveOrderResult` as two states instead of three           | In v1, the SDK launched the 3D Secure challenge automatically, so the explicit state is new in v2 | Handle all three states (`Success`, `Failure`, and `AuthorizationRequired`) in your `when` block                                                                                                      |
| Assuming `finishApproveOrder()` and `finishVault()` run automatically      | v1 handled this through lifecycle observers; v2 requires explicit calls                           | Call both methods in both `onResume()` and `onNewIntent()`. They return `null` when the intent is unrelated, so the calls are always safe to make                                                     |
| Leaving `removeObservers()` calls in `onDestroy()`                         | Carried over from the v1 teardown pattern                                                         | Remove all `removeObservers()` calls. The method doesn't exist in v2 and causes a compile error                                                                                                       |
| Searching for a renamed native checkout module in v2                       | Native Payments existed in v1, so it's natural to look for a v2 equivalent                        | `PayPalNativePayments` is discontinued. Migrate to `PayPalWebCheckoutClient`                                                                                                                          |
| Omitting the `NoResult` state, or treating it as an error                  | It's easy to assume every call returns a meaningful result                                        | `NoResult` means the intent held no auth challenge state for this client. This is expected on a normal app resume, and your code should handle it as a no-op                                          |
| Treating `PayPalPresentAuthChallengeResult.Success` as the payment outcome | The name suggests a completed result                                                              | `Success` only confirms the Chrome Custom Tab opened. The actual payment or vault result arrives later, through `finishStart()` or `finishVault()`                                                    |
| Calling the v1 `start()` signature on `PayPalWebCheckoutClient`            | `payPalClient.start(checkoutRequest)` no longer compiles without an `Activity` and a callback     | Use the v2 signature: `paypalClient.start(this, checkoutRequest) { result -> }`                                                                                                                       |
