Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PayPal: Remove Vault Listener Pattern #308

Merged
merged 7 commits into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
Copy link
Collaborator

@KunJeongPark KunJeongPark Dec 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think in iOS, we just have "challenge-required" event before launching a web view.
So if we wanted to track success/failure of the entire 3ds flow including the web view launch, we would have a starting point.

I guess Android equivalent is "challenge-received" so you just have an extra "start" on the auth.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I'm hoping we'll be able to align analytics on iOS and Android sometime soon in an upcoming sprint.


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
Loading