On this page
No Headings
Last updated: August 21, 2026
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.
Search your project for the following v1 identifiers to scope the change.
ApproveOrderListenerCardVaultListenercardClient.approveOrderListenercardClient.vaultListenercardClient.removeObserversonApproveOrderSuccessonApproveOrderFailureonVaultSuccessonVaultFailurePayPalWebCheckoutListenerPayPalWebVaultListenerpayPalClient.listenerpayPalClient.vaultListenerpayPalClient.removeObserversonPayPalWebSuccessonPayPalWebFailureonPayPalWebCanceledMost 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 |
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 |
Take these steps to update your CardClient integration.
CardClient now takes a Context instead of an Activity.
val cardClient = CardClient(requireActivity(), config)val cardClient = CardClient(requireContext(), config)Your project compiles after every CardClient constructor call passes a Context.
Remove the listener interfaces, the listener assignments, and the removeObservers() call from onDestroy():
// 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.
The listener callbacks are gone. approveOrder() now returns a sealed result you handle inline.
fun approveOrder() {
cardClient.approveOrder(this, cardRequest)
}
override fun onApproveOrderSuccess(result: CardResult) {
// Capture or authorize on server
}
override fun onApproveOrderFailure(error: PayPalSDKError) {
// Handle failure
}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)
}
}
}AuthorizationRequired is new in v2
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.
vault() follows the same sealed result pattern as approveOrder(), including the explicit AuthorizationRequired state.
override fun onVaultSuccess(result: CardVaultResult) {
val authChallenge = result.authChallenge
if (authChallenge != null) {
cardClient?.presentAuthChallenge(activity, authChallenge)
} else {
// Create payment token on server
}
}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)
}
}When a result returns AuthorizationRequired, present the challenge and then resume it after the deep link redirects back to your app.
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.
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
}
}
}
}Dispatch the intent in onNewIntent
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().
The same constructor, listener-removal, and sealed result changes from CardClient apply here, with a few PayPal-specific differences.
PayPalWebCheckoutClient also moves from Activity to Context, and drops its listener interfaces.
val payPalClient = PayPalWebCheckoutClient(requireActivity(), config, "my-scheme")val payPalClient = PayPalWebCheckoutClient(requireContext(), config, "my-scheme")Remove any PayPalWebCheckoutListener and PayPalWebVaultListener implementations and their listener assignments.
start() and vault() move from listener callbacks to an inline callback that receives a sealed result.
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
}// 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.
Like CardClient, PayPalWebCheckoutClient requires a call to pick up the result after the redirect completes.
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.
Success confirms the web view opened, not the payment
PayPalPresentAuthChallengeResult.Success from start() or vault() only confirms the Chrome Custom Tab opened. The payment or vault result arrives later, through finishStart() or finishVault().
No v2 replacement exists
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:
PayPalNativeClientPayPalNativeCheckoutRequestPayPalNativeCheckoutListenerIf your project contains any of these identifiers, you'll need to complete this step. If it doesn't, you can 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.
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, 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 |
Confirm each of the following before you release the update:
CoreConfig is set to Environment.LIVEAndroidManifest.xml with a production intent filterPayPalNativePayments dependency is removed from build.gradleremoveObservers() calls, and other deprecated references are removeddebug_id from any SDK errors for PayPal support escalationConfirm you've completed every row of the API surface mapping table, plus the following call sites:
PayPalNativeClient and migrate to PayPalWebCheckoutClient if foundbuild.gradlecheckForCardAuthCompletion(intent) and checkForPayPalAuthCompletion(intent) to both onResume() and onNewIntent()Canceled and NoResult states are handled for both clientsThese 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 -> } |