Skip to content

Commit

Permalink
PayPal: Remove Vault Listener Pattern (#308)
Browse files Browse the repository at this point in the history
* Migrate PayPalWebCheckoutClient away from listener pattern.

* Fix broken tests after migrating away from listener pattern.

* Clean up lint errors.

* Remove PayPalWebStatus type.

* Remove obselete classes.

* Update CHANGELOG.

* Update documentation for PayPalWebCheckoutClient.
  • Loading branch information
sshropshire authored Dec 19, 2024
1 parent 4afcf91 commit 99bc477
Show file tree
Hide file tree
Showing 14 changed files with 155 additions and 259 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,13 @@
* PayPalWebPayments
* Remove `PayPalWebCheckoutClient.listener` property
* Add `PayPalWebCheckoutClient.finishStart(Intent, String)` method
* Remove `PayPalWebCheckoutResult` type
* Add `PayPalWebCheckoutFinishStartResult` type
* Remove `PayPalWebCheckoutClient.vaultListener` type
* Add `PayPalWebCheckoutClient.finishVault(Intent, String)` method
* Remove `PayPalWebCheckoutClient.removeObservers()` method
* Add `PayPalWebCheckoutFinishVaulResult` type
* Remove `PayPalWebVaultResult` type

## 2.0.0-beta1 (2024-11-20)
* Breaking Changes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,11 +148,6 @@ class PayPalWebViewModel @Inject constructor(
}
}

override fun onCleared() {
super.onCleared()
paypalClient?.removeObservers()
}

fun handleBrowserSwitchResult(activity: ComponentActivity) {
val result = authState?.let { paypalClient?.finishStart(activity.intent, it) }
when (result) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ package com.paypal.android.ui.paypalwebvault

import com.paypal.android.api.model.PayPalPaymentToken
import com.paypal.android.api.model.PayPalSetupToken
import com.paypal.android.paypalwebpayments.PayPalWebVaultResult
import com.paypal.android.paypalwebpayments.PayPalWebCheckoutFinishVaultResult
import com.paypal.android.uishared.state.ActionState

data class PayPalWebVaultUiState(
val createSetupTokenState: ActionState<PayPalSetupToken, Exception> = ActionState.Idle,
val vaultPayPalState: ActionState<PayPalWebVaultResult, Exception> = ActionState.Idle,
val vaultPayPalState: ActionState<PayPalWebCheckoutFinishVaultResult.Success, Exception> = ActionState.Idle,
val createPaymentTokenState: ActionState<PayPalPaymentToken, Exception> = ActionState.Idle,
) {
val isCreateSetupTokenSuccessful: Boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.paypal.android.paypalwebpayments.PayPalWebVaultResult
import com.paypal.android.paypalwebpayments.PayPalWebCheckoutFinishVaultResult
import com.paypal.android.uishared.components.ActionButtonColumn
import com.paypal.android.uishared.components.ErrorView
import com.paypal.android.uishared.components.PayPalPaymentTokenView
Expand Down Expand Up @@ -135,7 +135,7 @@ private fun Step3_CreatePaymentToken(
}

@Composable
fun PayPalWebVaultResultView(result: PayPalWebVaultResult) {
fun PayPalWebVaultResultView(result: PayPalWebCheckoutFinishVaultResult.Success) {
Column(
verticalArrangement = UIConstants.spacingMedium,
modifier = Modifier.padding(UIConstants.paddingMedium)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,17 @@ import androidx.activity.ComponentActivity
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.paypal.android.api.model.PayPalSetupToken
import com.paypal.android.api.services.SDKSampleServerResult
import com.paypal.android.corepayments.CoreConfig
import com.paypal.android.corepayments.PayPalSDKError
import com.paypal.android.fraudprotection.PayPalDataCollector
import com.paypal.android.paypalwebpayments.PayPalPresentAuthChallengeResult
import com.paypal.android.paypalwebpayments.PayPalWebCheckoutClient
import com.paypal.android.paypalwebpayments.PayPalWebVaultListener
import com.paypal.android.paypalwebpayments.PayPalWebCheckoutFinishVaultResult
import com.paypal.android.paypalwebpayments.PayPalWebVaultRequest
import com.paypal.android.paypalwebpayments.PayPalWebVaultResult
import com.paypal.android.uishared.state.ActionState
import com.paypal.android.usecase.CreatePayPalPaymentTokenUseCase
import com.paypal.android.usecase.CreatePayPalSetupTokenUseCase
import com.paypal.android.usecase.GetClientIdUseCase
import com.paypal.android.api.services.SDKSampleServerResult
import com.paypal.android.paypalwebpayments.PayPalPresentAuthChallengeResult
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
Expand All @@ -29,7 +27,7 @@ class PayPalWebVaultViewModel @Inject constructor(
val getClientIdUseCase: GetClientIdUseCase,
val createPayPalSetupTokenUseCase: CreatePayPalSetupTokenUseCase,
val createPayPalPaymentTokenUseCase: CreatePayPalPaymentTokenUseCase,
) : ViewModel(), PayPalWebVaultListener {
) : ViewModel() {

companion object {
const val URL_SCHEME = "com.paypal.android.demo"
Expand Down Expand Up @@ -96,16 +94,15 @@ class PayPalWebVaultViewModel @Inject constructor(
payPalDataCollector = PayPalDataCollector(coreConfig)

paypalClient = PayPalWebCheckoutClient(activity, coreConfig, URL_SCHEME)
paypalClient?.vaultListener = this@PayPalWebVaultViewModel

paypalClient?.vault(activity, request)?.let { result ->
when (result) {
is PayPalPresentAuthChallengeResult.Success -> {
authState = result.authState
}
is PayPalPresentAuthChallengeResult.Failure -> {
vaultPayPalState = ActionState.Failure(result.error)
}
when (val result = paypalClient?.vault(activity, request)) {
is PayPalPresentAuthChallengeResult.Success ->
authState = result.authState

is PayPalPresentAuthChallengeResult.Failure ->
vaultPayPalState = ActionState.Failure(result.error)

null -> {
// do nothing for now
}
}
}
Expand All @@ -115,7 +112,8 @@ class PayPalWebVaultViewModel @Inject constructor(
fun createPaymentToken() {
val setupToken = createdSetupToken
if (setupToken == null) {
createPaymentTokenState = ActionState.Failure(Exception("Create a setup token to continue."))
createPaymentTokenState =
ActionState.Failure(Exception("Create a setup token to continue."))
} else {
createPaymentTokenState = ActionState.Loading
viewModelScope.launch {
Expand All @@ -125,26 +123,21 @@ class PayPalWebVaultViewModel @Inject constructor(
}
}

override fun onPayPalWebVaultSuccess(result: PayPalWebVaultResult) {
vaultPayPalState = ActionState.Success(result)
}

override fun onPayPalWebVaultFailure(error: PayPalSDKError) {
vaultPayPalState = ActionState.Failure(error)
}
fun handleBrowserSwitchResult(activity: ComponentActivity) {
val result = authState?.let { paypalClient?.finishVault(activity.intent, it) }
when (result) {
is PayPalWebCheckoutFinishVaultResult.Success ->
vaultPayPalState = ActionState.Success(result)

override fun onPayPalWebVaultCanceled() {
vaultPayPalState = ActionState.Failure(Exception("USER CANCELED"))
}
is PayPalWebCheckoutFinishVaultResult.Failure ->
vaultPayPalState = ActionState.Failure(result.error)

override fun onCleared() {
super.onCleared()
paypalClient?.removeObservers()
}
PayPalWebCheckoutFinishVaultResult.Canceled ->
vaultPayPalState = ActionState.Failure(Exception("USER CANCELED"))

fun handleBrowserSwitchResult(activity: ComponentActivity) {
authState?.let {
paypalClient?.completeAuthChallenge(activity.intent, it)
null, PayPalWebCheckoutFinishVaultResult.NoResult -> {
// do nothing
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package com.paypal.android.paypalwebpayments

import android.content.Context
import android.content.Intent
import android.util.Log
import androidx.activity.ComponentActivity
import com.paypal.android.corepayments.CoreConfig
import com.paypal.android.corepayments.analytics.AnalyticsService
Expand Down Expand Up @@ -31,12 +30,7 @@ class PayPalWebCheckoutClient internal constructor(
)

/**
* Sets a listener to receive notifications when a Paypal Vault event occurs.
*/
var vaultListener: PayPalWebVaultListener? = null

/**
* Confirm PayPal payment source for an order. Result will be delivered to your [PayPalWebCheckoutListener].
* Confirm PayPal payment source for an order.
*
* @param request [PayPalWebCheckoutRequest] for requesting an order approval
*/
Expand All @@ -47,18 +41,17 @@ class PayPalWebCheckoutClient internal constructor(
analytics.notifyCheckoutStarted(request.orderId)
val result = payPalWebLauncher.launchPayPalWebCheckout(activity, request)
when (result) {
is PayPalPresentAuthChallengeResult.Failure -> {
analytics.notifyCheckoutAuthChallengeFailed(request.orderId)
}

is PayPalPresentAuthChallengeResult.Success ->
analytics.notifyCheckoutAuthChallengeStarted(request.orderId)

is PayPalPresentAuthChallengeResult.Failure ->
analytics.notifyCheckoutAuthChallengeFailed(request.orderId)
}
return result
}

/**
* Vault PayPal as a payment method. Result will be delivered to your [PayPalWebVaultListener].
* Vault PayPal as a payment method.
*
* @param request [PayPalWebVaultRequest] for vaulting PayPal as a payment method
*/
Expand All @@ -69,31 +62,37 @@ class PayPalWebCheckoutClient internal constructor(
analytics.notifyVaultStarted(request.setupTokenId)
val result = payPalWebLauncher.launchPayPalWebVault(activity, request)
when (result) {
is PayPalPresentAuthChallengeResult.Failure -> {
analytics.notifyVaultAuthChallengeFailed(request.setupTokenId)
vaultListener?.onPayPalWebVaultFailure(result.error)
}

is PayPalPresentAuthChallengeResult.Success ->
analytics.notifyVaultAuthChallengeStarted(request.setupTokenId)

is PayPalPresentAuthChallengeResult.Failure ->
analytics.notifyVaultAuthChallengeFailed(request.setupTokenId)
}
return result
}

/**
* After a merchant app has re-entered the foreground following an auth challenge
* (@see [PayPalWebCheckoutClient.start]), call this method to see if a user has
* successfully authorized a PayPal account as a payment source.
*
* @param [intent] An Android intent that holds the deep link put the merchant app
* back into the foreground after an auth challenge.
* @param [authState] A continuation state received from [PayPalPresentAuthChallengeResult.Success]
* when calling [PayPalWebCheckoutClient.start]. This is needed to properly verify that an
* authorization completed successfully.
*/
fun finishStart(intent: Intent, authState: String): PayPalWebCheckoutFinishStartResult {
val result = payPalWebLauncher.completeCheckoutAuthRequest(intent, authState)
when (result) {
is PayPalWebCheckoutFinishStartResult.Success -> {
is PayPalWebCheckoutFinishStartResult.Success ->
analytics.notifyCheckoutAuthChallengeSucceeded(result.orderId)
}

is PayPalWebCheckoutFinishStartResult.Canceled -> {
is PayPalWebCheckoutFinishStartResult.Canceled ->
analytics.notifyCheckoutAuthChallengeCanceled(result.orderId)
}

is PayPalWebCheckoutFinishStartResult.Failure -> {
is PayPalWebCheckoutFinishStartResult.Failure ->
analytics.notifyCheckoutAuthChallengeFailed(result.orderId)
}

PayPalWebCheckoutFinishStartResult.NoResult -> {
// no analytics tracking required at the moment
Expand All @@ -102,42 +101,34 @@ class PayPalWebCheckoutClient internal constructor(
return result
}

fun completeAuthChallenge(intent: Intent, authState: String): PayPalWebStatus {
val status = payPalWebLauncher.completeAuthRequest(intent, authState)
when (status) {
is PayPalWebStatus.VaultSuccess -> {
// TODO: see if we can get setup token id from somewhere
/**
* After a merchant app has re-entered the foreground following an auth challenge
* (@see [PayPalWebCheckoutClient.vault]), call this method to see if a user has
* successfully authorized a PayPal account for vaulting.
*
* @param [intent] An Android intent that holds the deep link put the merchant app
* back into the foreground after an auth challenge.
* @param [authState] A continuation state received from [PayPalPresentAuthChallengeResult.Success]
* when calling [PayPalWebCheckoutClient.vault]. This is needed to properly verify that an
* authorization completed successfully.
*/
fun finishVault(intent: Intent, authState: String): PayPalWebCheckoutFinishVaultResult {
val result = payPalWebLauncher.completeVaultAuthRequest(intent, authState)
// TODO: see if we can get setup token id from somewhere for tracking
when (result) {
is PayPalWebCheckoutFinishVaultResult.Success ->
analytics.notifyVaultAuthChallengeSucceeded(null)
vaultListener?.onPayPalWebVaultSuccess(status.result)
}

is PayPalWebStatus.VaultError -> {
// TODO: see if we can get setup token id from somewhere
is PayPalWebCheckoutFinishVaultResult.Failure ->
analytics.notifyVaultAuthChallengeFailed(null)
vaultListener?.onPayPalWebVaultFailure(status.error)
}

PayPalWebStatus.VaultCanceled -> {
// TODO: see if we can get setup token id from somewhere
PayPalWebCheckoutFinishVaultResult.Canceled ->
analytics.notifyVaultAuthChallengeCanceled(null)
vaultListener?.onPayPalWebVaultCanceled()
}

is PayPalWebStatus.UnknownError -> {
Log.d("PayPalSDK", "An unknown error occurred: ${status.error.message}")
}

else -> {
// ignore
PayPalWebCheckoutFinishVaultResult.NoResult -> {
// no analytics tracking required at the moment
}
}
return status
}

/**
* Call this method at the end of the web checkout flow to clear out all observers and listeners
*/
fun removeObservers() {
vaultListener = null
return result
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.paypal.android.paypalwebpayments

import com.paypal.android.corepayments.PayPalSDKError

sealed class PayPalWebCheckoutFinishVaultResult {

class Success(val approvalSessionId: String) : PayPalWebCheckoutFinishVaultResult()
class Failure(val error: PayPalSDKError) : PayPalWebCheckoutFinishVaultResult()
data object Canceled : PayPalWebCheckoutFinishVaultResult()
data object NoResult : PayPalWebCheckoutFinishVaultResult()
}

This file was deleted.

Loading

0 comments on commit 99bc477

Please sign in to comment.