Skip to content

Commit

Permalink
PayPal: Remove Checkout Listener Pattern (#307)
Browse files Browse the repository at this point in the history
* Migrate PaypalWebCheckoutClient.start away from listener pattern.

* Update unit tests.

* Remove PayPalWebStatus events for Checkout.

* Clean up view model style.

* Update CHANGELOG.

* Add missing CardClient CHANGELOG bullets.
  • Loading branch information
sshropshire authored Dec 12, 2024
1 parent 898ba06 commit 4afcf91
Show file tree
Hide file tree
Showing 10 changed files with 156 additions and 164 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
* Fix issue that causes analytics version number to always be `null`
* Breaking Changes
* CardPayments
* Remove `CardClient.approveOrderListener` property
* Remove `CardClient.cardVaultListener` property
* Remove `ApproveOrderListener` type
* Add `CardApproveOrderCallback` interface
* Convert `CardApproveOrderResult` to a sealed class
Expand All @@ -21,6 +23,10 @@
* Add `CardFinishVaultResult` type
* Add `CardVaultCallback` interface
* Convert `CardVaultResult` to a sealed class
* PayPalWebPayments
* Remove `PayPalWebCheckoutClient.listener` property
* Add `PayPalWebCheckoutClient.finishStart(Intent, String)` method
* Add `PayPalWebCheckoutFinishStartResult` type

## 2.0.0-beta1 (2024-11-20)
* Breaking Changes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ package com.paypal.android.ui.paypalweb

import com.paypal.android.api.model.Order
import com.paypal.android.api.model.OrderIntent
import com.paypal.android.paypalwebpayments.PayPalWebCheckoutFinishStartResult
import com.paypal.android.paypalwebpayments.PayPalWebCheckoutFundingSource
import com.paypal.android.paypalwebpayments.PayPalWebCheckoutResult
import com.paypal.android.uishared.state.ActionState

data class PayPalWebUiState(
val intentOption: OrderIntent = OrderIntent.AUTHORIZE,
val createOrderState: ActionState<Order, Exception> = ActionState.Idle,
val payPalWebCheckoutState: ActionState<PayPalWebCheckoutResult, Exception> = ActionState.Idle,
val payPalWebCheckoutState: ActionState<PayPalWebCheckoutFinishStartResult.Success, Exception> = ActionState.Idle,
val completeOrderState: ActionState<Order, Exception> = ActionState.Idle,
val fundingSource: PayPalWebCheckoutFundingSource = PayPalWebCheckoutFundingSource.PAYPAL
) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,29 +1,26 @@
package com.paypal.android.ui.paypalweb

import android.annotation.SuppressLint
import android.content.Context
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.paypal.android.api.model.Order
import com.paypal.android.api.model.OrderIntent
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.fraudprotection.PayPalDataCollectorRequest
import com.paypal.android.models.OrderRequest
import com.paypal.android.paypalwebpayments.PayPalPresentAuthChallengeResult
import com.paypal.android.paypalwebpayments.PayPalWebCheckoutClient
import com.paypal.android.paypalwebpayments.PayPalWebCheckoutFinishStartResult
import com.paypal.android.paypalwebpayments.PayPalWebCheckoutFundingSource
import com.paypal.android.paypalwebpayments.PayPalWebCheckoutListener
import com.paypal.android.paypalwebpayments.PayPalWebCheckoutRequest
import com.paypal.android.paypalwebpayments.PayPalWebCheckoutResult
import com.paypal.android.uishared.state.ActionState
import com.paypal.android.usecase.CompleteOrderUseCase
import com.paypal.android.usecase.CreateOrderUseCase
import com.paypal.android.usecase.GetClientIdUseCase
import com.paypal.android.api.services.SDKSampleServerResult
import com.paypal.android.fraudprotection.PayPalDataCollectorRequest
import com.paypal.android.paypalwebpayments.PayPalPresentAuthChallengeResult
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
Expand All @@ -36,7 +33,7 @@ class PayPalWebViewModel @Inject constructor(
val getClientIdUseCase: GetClientIdUseCase,
val createOrderUseCase: CreateOrderUseCase,
val completeOrderUseCase: CompleteOrderUseCase
) : ViewModel(), PayPalWebCheckoutListener {
) : ViewModel() {

companion object {
private val TAG = PayPalWebViewModel::class.qualifiedName
Expand Down Expand Up @@ -118,51 +115,35 @@ class PayPalWebViewModel @Inject constructor(

paypalClient =
PayPalWebCheckoutClient(activity, coreConfig, "com.paypal.android.demo")
paypalClient?.listener = this@PayPalWebViewModel

paypalClient?.start(activity, PayPalWebCheckoutRequest(orderId, fundingSource))?.let { startResult ->
when (startResult) {
is PayPalPresentAuthChallengeResult.Success -> {
authState = startResult.authState
}
is PayPalPresentAuthChallengeResult.Failure -> {
payPalWebCheckoutState = ActionState.Failure(startResult.error)
}

val checkoutRequest = PayPalWebCheckoutRequest(orderId, fundingSource)
when (val startResult = paypalClient?.start(activity, checkoutRequest)) {
is PayPalPresentAuthChallengeResult.Success ->
authState = startResult.authState

is PayPalPresentAuthChallengeResult.Failure ->
payPalWebCheckoutState = ActionState.Failure(startResult.error)

null -> {
// do nothing
}
}
}
}
}

@SuppressLint("SetTextI18n")
override fun onPayPalWebSuccess(result: PayPalWebCheckoutResult) {
Log.i(TAG, "Order Approved: ${result.orderId} && ${result.payerId}")
payPalWebCheckoutState = ActionState.Success(result)
}

@SuppressLint("SetTextI18n")
override fun onPayPalWebFailure(error: PayPalSDKError) {
Log.i(TAG, "Checkout Error: ${error.errorDescription}")
payPalWebCheckoutState = ActionState.Failure(error)
}

@SuppressLint("SetTextI18n")
override fun onPayPalWebCanceled() {
Log.i(TAG, "User cancelled")
val error = Exception("USER CANCELED")
payPalWebCheckoutState = ActionState.Failure(error)
}

fun completeOrder(context: Context) {
val orderId = createdOrder?.id
if (orderId == null) {
completeOrderState = ActionState.Failure(Exception("Create an order to continue."))
} else {
viewModelScope.launch {
completeOrderState = ActionState.Loading
val dataCollectorRequest = PayPalDataCollectorRequest(hasUserLocationConsent = false)
val dataCollectorRequest =
PayPalDataCollectorRequest(hasUserLocationConsent = false)
val cmid = payPalDataCollector.collectDeviceData(context, dataCollectorRequest)
completeOrderState = completeOrderUseCase(orderId, intentOption, cmid).mapToActionState()
completeOrderState =
completeOrderUseCase(orderId, intentOption, cmid).mapToActionState()
}
}
}
Expand All @@ -173,6 +154,25 @@ class PayPalWebViewModel @Inject constructor(
}

fun handleBrowserSwitchResult(activity: ComponentActivity) {
authState?.let { paypalClient?.completeAuthChallenge(activity.intent, it) }
val result = authState?.let { paypalClient?.finishStart(activity.intent, it) }
when (result) {
is PayPalWebCheckoutFinishStartResult.Success -> {
payPalWebCheckoutState = ActionState.Success(result)
}

is PayPalWebCheckoutFinishStartResult.Canceled -> {
val error = Exception("USER CANCELED")
payPalWebCheckoutState = ActionState.Failure(error)
}

is PayPalWebCheckoutFinishStartResult.Failure -> {
Log.i(TAG, "Checkout Error: ${result.error.errorDescription}")
payPalWebCheckoutState = ActionState.Failure(result.error)
}

null, PayPalWebCheckoutFinishStartResult.NoResult -> {
// do nothing
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,6 @@ class PayPalWebCheckoutClient internal constructor(
PayPalWebLauncher(urlScheme, configuration),
)

/**
* Sets a listener to receive notifications when a PayPal Checkout event occurs.
*/
var listener: PayPalWebCheckoutListener? = null

/**
* Sets a listener to receive notifications when a Paypal Vault event occurs.
*/
Expand All @@ -54,7 +49,6 @@ class PayPalWebCheckoutClient internal constructor(
when (result) {
is PayPalPresentAuthChallengeResult.Failure -> {
analytics.notifyCheckoutAuthChallengeFailed(request.orderId)
listener?.onPayPalWebFailure(result.error)
}

is PayPalPresentAuthChallengeResult.Success ->
Expand Down Expand Up @@ -86,44 +80,54 @@ class PayPalWebCheckoutClient internal constructor(
return result
}

fun completeAuthChallenge(intent: Intent, authState: String): PayPalWebStatus {
val status = payPalWebLauncher.completeAuthRequest(intent, authState)
when (status) {
is PayPalWebStatus.CheckoutSuccess -> {
analytics.notifyCheckoutAuthChallengeSucceeded(status.result.orderId)
listener?.onPayPalWebSuccess(status.result)
fun finishStart(intent: Intent, authState: String): PayPalWebCheckoutFinishStartResult {
val result = payPalWebLauncher.completeCheckoutAuthRequest(intent, authState)
when (result) {
is PayPalWebCheckoutFinishStartResult.Success -> {
analytics.notifyCheckoutAuthChallengeSucceeded(result.orderId)
}

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

is PayPalWebStatus.CheckoutError -> {
analytics.notifyCheckoutAuthChallengeFailed(status.orderId)
listener?.onPayPalWebFailure(status.error)
is PayPalWebCheckoutFinishStartResult.Failure -> {
analytics.notifyCheckoutAuthChallengeFailed(result.orderId)
}

is PayPalWebStatus.CheckoutCanceled -> {
analytics.notifyCheckoutAuthChallengeCanceled(status.orderId)
listener?.onPayPalWebCanceled()
PayPalWebCheckoutFinishStartResult.NoResult -> {
// no analytics tracking required at the moment
}
}
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
analytics.notifyVaultAuthChallengeSucceeded(null)
vaultListener?.onPayPalWebVaultSuccess(status.result)
}

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

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

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

PayPalWebStatus.NoResult -> {
else -> {
// ignore
}
}
Expand All @@ -135,6 +139,5 @@ class PayPalWebCheckoutClient internal constructor(
*/
fun removeObservers() {
vaultListener = null
listener = null
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.paypal.android.paypalwebpayments

import com.paypal.android.corepayments.PayPalSDKError

sealed class PayPalWebCheckoutFinishStartResult {
class Success(val orderId: String?, val payerId: String?) : PayPalWebCheckoutFinishStartResult()
class Failure(val error: PayPalSDKError, val orderId: String?) : PayPalWebCheckoutFinishStartResult()
class Canceled(val orderId: String?) : PayPalWebCheckoutFinishStartResult()
data object NoResult : PayPalWebCheckoutFinishStartResult()
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import com.braintreepayments.api.BrowserSwitchStartResult
import com.paypal.android.corepayments.BrowserSwitchRequestCodes
import com.paypal.android.corepayments.CoreConfig
import com.paypal.android.corepayments.Environment
import com.paypal.android.corepayments.PayPalSDKError
import com.paypal.android.paypalwebpayments.errors.PayPalWebCheckoutError
import org.json.JSONObject

Expand Down Expand Up @@ -106,6 +107,25 @@ internal class PayPalWebLauncher(
.build()
}

fun completeCheckoutAuthRequest(
intent: Intent,
authState: String
): PayPalWebCheckoutFinishStartResult {
return when (val finalResult = browserSwitchClient.completeRequest(intent, authState)) {
is BrowserSwitchFinalResult.Success -> parseWebCheckoutSuccessResult(finalResult)
is BrowserSwitchFinalResult.Failure -> {
// TODO: remove error codes and error description from project; the built in
// Throwable type already has a message property and error codes are only required
// for iOS Error protocol conformance
val message = "Browser switch failed"
val browserSwitchError = PayPalSDKError(0, message, reason = finalResult.error)
PayPalWebCheckoutFinishStartResult.Failure(browserSwitchError, null)
}

BrowserSwitchFinalResult.NoResult -> PayPalWebCheckoutFinishStartResult.NoResult
}
}

fun completeAuthRequest(intent: Intent, authState: String): PayPalWebStatus {
return when (val finalResult = browserSwitchClient.completeRequest(intent, authState)) {
is BrowserSwitchFinalResult.Success -> parseBrowserSwitchSuccessResult(finalResult)
Expand All @@ -116,25 +136,31 @@ internal class PayPalWebLauncher(

private fun parseBrowserSwitchSuccessResult(result: BrowserSwitchFinalResult.Success) =
when (result.requestCode) {
BrowserSwitchRequestCodes.PAYPAL_CHECKOUT -> parseWebCheckoutSuccessResult(result)
BrowserSwitchRequestCodes.PAYPAL_VAULT -> parseVaultSuccessResult(result)
else -> PayPalWebStatus.NoResult
}

private fun parseWebCheckoutSuccessResult(finalResult: BrowserSwitchFinalResult.Success): PayPalWebStatus {
private fun parseWebCheckoutSuccessResult(
finalResult: BrowserSwitchFinalResult.Success
): PayPalWebCheckoutFinishStartResult {
val deepLinkUrl = finalResult.returnUrl
val metadata = finalResult.requestMetadata

return if (metadata == null) {
PayPalWebStatus.CheckoutError(PayPalWebCheckoutError.unknownError, null)
} else {
val payerId = deepLinkUrl.getQueryParameter("PayerID")
val orderId = metadata.optString(METADATA_KEY_ORDER_ID)
if (orderId.isNullOrBlank() || payerId.isNullOrBlank()) {
PayPalWebStatus.CheckoutError(PayPalWebCheckoutError.malformedResultError, orderId)
return if (finalResult.requestCode == BrowserSwitchRequestCodes.PAYPAL_CHECKOUT) {
if (metadata == null) {
val unknownError = PayPalWebCheckoutError.unknownError
PayPalWebCheckoutFinishStartResult.Failure(unknownError, null)
} else {
PayPalWebStatus.CheckoutSuccess(PayPalWebCheckoutResult(orderId, payerId))
val payerId = deepLinkUrl.getQueryParameter("PayerID")
val orderId = metadata.optString(METADATA_KEY_ORDER_ID)
if (orderId.isNullOrBlank() || payerId.isNullOrBlank()) {
val malformedResultError = PayPalWebCheckoutError.malformedResultError
PayPalWebCheckoutFinishStartResult.Failure(malformedResultError, orderId)
} else {
PayPalWebCheckoutFinishStartResult.Success(orderId, payerId)
}
}
} else {
PayPalWebCheckoutFinishStartResult.NoResult
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,6 @@ import com.paypal.android.corepayments.PayPalSDKError

sealed class PayPalWebStatus {

class CheckoutError(val error: PayPalSDKError, val orderId: String?) : PayPalWebStatus()
class CheckoutSuccess(val result: PayPalWebCheckoutResult) : PayPalWebStatus()
class CheckoutCanceled(val orderId: String?) : PayPalWebStatus()

class VaultError(val error: PayPalSDKError) : PayPalWebStatus()
class VaultSuccess(val result: PayPalWebVaultResult) : PayPalWebStatus()
data object VaultCanceled : PayPalWebStatus()
Expand Down
Loading

0 comments on commit 4afcf91

Please sign in to comment.