From 49a559f9a11ffdcce5f535ee90a03408539ef2e8 Mon Sep 17 00:00:00 2001 From: sshropshire Date: Mon, 16 Dec 2024 10:00:50 -0600 Subject: [PATCH 1/7] Migrate PayPalWebCheckoutClient away from listener pattern. --- .../ui/paypalweb/PayPalWebViewModel.kt | 5 -- .../paypalwebvault/PayPalWebVaultUiState.kt | 3 +- .../ui/paypalwebvault/PayPalWebVaultView.kt | 4 +- .../paypalwebvault/PayPalWebVaultViewModel.kt | 47 +++++++------ .../PayPalWebCheckoutClient.kt | 69 +++++-------------- .../PayPalWebCheckoutFinishVaultResult.kt | 11 +++ .../paypalwebpayments/PayPalWebLauncher.kt | 47 +++++++------ 7 files changed, 87 insertions(+), 99 deletions(-) create mode 100644 PayPalWebPayments/src/main/java/com/paypal/android/paypalwebpayments/PayPalWebCheckoutFinishVaultResult.kt diff --git a/Demo/src/main/java/com/paypal/android/ui/paypalweb/PayPalWebViewModel.kt b/Demo/src/main/java/com/paypal/android/ui/paypalweb/PayPalWebViewModel.kt index 7758b981a..81ff63160 100644 --- a/Demo/src/main/java/com/paypal/android/ui/paypalweb/PayPalWebViewModel.kt +++ b/Demo/src/main/java/com/paypal/android/ui/paypalweb/PayPalWebViewModel.kt @@ -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) { diff --git a/Demo/src/main/java/com/paypal/android/ui/paypalwebvault/PayPalWebVaultUiState.kt b/Demo/src/main/java/com/paypal/android/ui/paypalwebvault/PayPalWebVaultUiState.kt index f1a7c8432..78db42590 100644 --- a/Demo/src/main/java/com/paypal/android/ui/paypalwebvault/PayPalWebVaultUiState.kt +++ b/Demo/src/main/java/com/paypal/android/ui/paypalwebvault/PayPalWebVaultUiState.kt @@ -2,12 +2,13 @@ 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.PayPalWebCheckoutFinishVaultResult import com.paypal.android.paypalwebpayments.PayPalWebVaultResult import com.paypal.android.uishared.state.ActionState data class PayPalWebVaultUiState( val createSetupTokenState: ActionState = ActionState.Idle, - val vaultPayPalState: ActionState = ActionState.Idle, + val vaultPayPalState: ActionState = ActionState.Idle, val createPaymentTokenState: ActionState = ActionState.Idle, ) { val isCreateSetupTokenSuccessful: Boolean diff --git a/Demo/src/main/java/com/paypal/android/ui/paypalwebvault/PayPalWebVaultView.kt b/Demo/src/main/java/com/paypal/android/ui/paypalwebvault/PayPalWebVaultView.kt index 64d2ca232..558f0133a 100644 --- a/Demo/src/main/java/com/paypal/android/ui/paypalwebvault/PayPalWebVaultView.kt +++ b/Demo/src/main/java/com/paypal/android/ui/paypalwebvault/PayPalWebVaultView.kt @@ -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 @@ -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) diff --git a/Demo/src/main/java/com/paypal/android/ui/paypalwebvault/PayPalWebVaultViewModel.kt b/Demo/src/main/java/com/paypal/android/ui/paypalwebvault/PayPalWebVaultViewModel.kt index b6cb4e37a..6bfa50039 100644 --- a/Demo/src/main/java/com/paypal/android/ui/paypalwebvault/PayPalWebVaultViewModel.kt +++ b/Demo/src/main/java/com/paypal/android/ui/paypalwebvault/PayPalWebVaultViewModel.kt @@ -17,6 +17,7 @@ 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 com.paypal.android.paypalwebpayments.PayPalWebCheckoutFinishVaultResult import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -96,16 +97,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 } } } @@ -115,7 +115,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 { @@ -126,25 +127,29 @@ class PayPalWebVaultViewModel @Inject constructor( } override fun onPayPalWebVaultSuccess(result: PayPalWebVaultResult) { - vaultPayPalState = ActionState.Success(result) } override fun onPayPalWebVaultFailure(error: PayPalSDKError) { - vaultPayPalState = ActionState.Failure(error) } override fun onPayPalWebVaultCanceled() { - vaultPayPalState = ActionState.Failure(Exception("USER CANCELED")) - } - - override fun onCleared() { - super.onCleared() - paypalClient?.removeObservers() } fun handleBrowserSwitchResult(activity: ComponentActivity) { - authState?.let { - paypalClient?.completeAuthChallenge(activity.intent, it) + val result = authState?.let { paypalClient?.finishVault(activity.intent, it) } + when (result) { + is PayPalWebCheckoutFinishVaultResult.Success -> + vaultPayPalState = ActionState.Success(result) + + is PayPalWebCheckoutFinishVaultResult.Failure -> + vaultPayPalState = ActionState.Failure(result.error) + + PayPalWebCheckoutFinishVaultResult.Canceled -> + vaultPayPalState = ActionState.Failure(Exception("USER CANCELED")) + + null, PayPalWebCheckoutFinishVaultResult.NoResult -> { + // do nothing + } } } } diff --git a/PayPalWebPayments/src/main/java/com/paypal/android/paypalwebpayments/PayPalWebCheckoutClient.kt b/PayPalWebPayments/src/main/java/com/paypal/android/paypalwebpayments/PayPalWebCheckoutClient.kt index ac1eda3b7..684d39dfd 100644 --- a/PayPalWebPayments/src/main/java/com/paypal/android/paypalwebpayments/PayPalWebCheckoutClient.kt +++ b/PayPalWebPayments/src/main/java/com/paypal/android/paypalwebpayments/PayPalWebCheckoutClient.kt @@ -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 @@ -30,11 +29,6 @@ class PayPalWebCheckoutClient internal constructor( PayPalWebLauncher(urlScheme, configuration), ) - /** - * 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]. * @@ -47,12 +41,11 @@ 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 } @@ -69,13 +62,11 @@ 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 } @@ -83,17 +74,14 @@ class PayPalWebCheckoutClient internal constructor( 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 @@ -102,42 +90,23 @@ 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 + fun finishVault(intent: Intent, authState: String): PayPalWebCheckoutFinishVaultResult { + val result = payPalWebLauncher.completeCheckoutVaultRequest(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 } } diff --git a/PayPalWebPayments/src/main/java/com/paypal/android/paypalwebpayments/PayPalWebCheckoutFinishVaultResult.kt b/PayPalWebPayments/src/main/java/com/paypal/android/paypalwebpayments/PayPalWebCheckoutFinishVaultResult.kt new file mode 100644 index 000000000..68ea9b179 --- /dev/null +++ b/PayPalWebPayments/src/main/java/com/paypal/android/paypalwebpayments/PayPalWebCheckoutFinishVaultResult.kt @@ -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() +} diff --git a/PayPalWebPayments/src/main/java/com/paypal/android/paypalwebpayments/PayPalWebLauncher.kt b/PayPalWebPayments/src/main/java/com/paypal/android/paypalwebpayments/PayPalWebLauncher.kt index 996fbefeb..e23d79099 100644 --- a/PayPalWebPayments/src/main/java/com/paypal/android/paypalwebpayments/PayPalWebLauncher.kt +++ b/PayPalWebPayments/src/main/java/com/paypal/android/paypalwebpayments/PayPalWebLauncher.kt @@ -126,19 +126,24 @@ internal class PayPalWebLauncher( } } - fun completeAuthRequest(intent: Intent, authState: String): PayPalWebStatus { + fun completeCheckoutVaultRequest( + intent: Intent, + authState: String + ): PayPalWebCheckoutFinishVaultResult { return when (val finalResult = browserSwitchClient.completeRequest(intent, authState)) { - is BrowserSwitchFinalResult.Success -> parseBrowserSwitchSuccessResult(finalResult) - is BrowserSwitchFinalResult.Failure -> PayPalWebStatus.UnknownError(finalResult.error) - BrowserSwitchFinalResult.NoResult -> PayPalWebStatus.NoResult - } - } + is BrowserSwitchFinalResult.Success -> parseVaultSuccessResult(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) + PayPalWebCheckoutFinishVaultResult.Failure(browserSwitchError) + } - private fun parseBrowserSwitchSuccessResult(result: BrowserSwitchFinalResult.Success) = - when (result.requestCode) { - BrowserSwitchRequestCodes.PAYPAL_VAULT -> parseVaultSuccessResult(result) - else -> PayPalWebStatus.NoResult + BrowserSwitchFinalResult.NoResult -> PayPalWebCheckoutFinishVaultResult.NoResult } + } private fun parseWebCheckoutSuccessResult( finalResult: BrowserSwitchFinalResult.Success @@ -164,20 +169,22 @@ internal class PayPalWebLauncher( } } - private fun parseVaultSuccessResult(finalResult: BrowserSwitchFinalResult.Success): PayPalWebStatus { + private fun parseVaultSuccessResult(finalResult: BrowserSwitchFinalResult.Success): PayPalWebCheckoutFinishVaultResult { val deepLinkUrl = finalResult.returnUrl val requestMetadata = finalResult.requestMetadata - - return if (requestMetadata == null) { - PayPalWebStatus.VaultError(PayPalWebCheckoutError.unknownError) - } else { - val approvalSessionId = deepLinkUrl.getQueryParameter(URL_PARAM_APPROVAL_SESSION_ID) - if (approvalSessionId.isNullOrEmpty()) { - PayPalWebStatus.VaultError(PayPalWebCheckoutError.malformedResultError) + return if (finalResult.requestCode == BrowserSwitchRequestCodes.PAYPAL_VAULT) { + if (requestMetadata == null) { + PayPalWebCheckoutFinishVaultResult.Failure(PayPalWebCheckoutError.unknownError) } else { - val result = PayPalWebVaultResult(approvalSessionId) - PayPalWebStatus.VaultSuccess(result) + val approvalSessionId = deepLinkUrl.getQueryParameter(URL_PARAM_APPROVAL_SESSION_ID) + if (approvalSessionId.isNullOrEmpty()) { + PayPalWebCheckoutFinishVaultResult.Failure(PayPalWebCheckoutError.malformedResultError) + } else { + PayPalWebCheckoutFinishVaultResult.Success(approvalSessionId) + } } + } else { + PayPalWebCheckoutFinishVaultResult.NoResult } } } From fbfd2fad3ad8edc8235573f30d4eaed7ed5fbdd0 Mon Sep 17 00:00:00 2001 From: sshropshire Date: Mon, 16 Dec 2024 10:26:34 -0600 Subject: [PATCH 2/7] Fix broken tests after migrating away from listener pattern. --- .../PayPalWebCheckoutClient.kt | 2 +- .../paypalwebpayments/PayPalWebLauncher.kt | 2 +- .../PayPalWebCheckoutClientUnitTest.kt | 74 +++++++------------ .../PayPalWebLauncherUnitTest.kt | 53 ++----------- 4 files changed, 37 insertions(+), 94 deletions(-) diff --git a/PayPalWebPayments/src/main/java/com/paypal/android/paypalwebpayments/PayPalWebCheckoutClient.kt b/PayPalWebPayments/src/main/java/com/paypal/android/paypalwebpayments/PayPalWebCheckoutClient.kt index 684d39dfd..1965df0cc 100644 --- a/PayPalWebPayments/src/main/java/com/paypal/android/paypalwebpayments/PayPalWebCheckoutClient.kt +++ b/PayPalWebPayments/src/main/java/com/paypal/android/paypalwebpayments/PayPalWebCheckoutClient.kt @@ -91,7 +91,7 @@ class PayPalWebCheckoutClient internal constructor( } fun finishVault(intent: Intent, authState: String): PayPalWebCheckoutFinishVaultResult { - val result = payPalWebLauncher.completeCheckoutVaultRequest(intent, authState) + val result = payPalWebLauncher.completeVaultAuthRequest(intent, authState) // TODO: see if we can get setup token id from somewhere for tracking when (result) { is PayPalWebCheckoutFinishVaultResult.Success -> diff --git a/PayPalWebPayments/src/main/java/com/paypal/android/paypalwebpayments/PayPalWebLauncher.kt b/PayPalWebPayments/src/main/java/com/paypal/android/paypalwebpayments/PayPalWebLauncher.kt index e23d79099..9806e26f9 100644 --- a/PayPalWebPayments/src/main/java/com/paypal/android/paypalwebpayments/PayPalWebLauncher.kt +++ b/PayPalWebPayments/src/main/java/com/paypal/android/paypalwebpayments/PayPalWebLauncher.kt @@ -126,7 +126,7 @@ internal class PayPalWebLauncher( } } - fun completeCheckoutVaultRequest( + fun completeVaultAuthRequest( intent: Intent, authState: String ): PayPalWebCheckoutFinishVaultResult { diff --git a/PayPalWebPayments/src/test/java/com/paypal/android/paypalwebpayments/PayPalWebCheckoutClientUnitTest.kt b/PayPalWebPayments/src/test/java/com/paypal/android/paypalwebpayments/PayPalWebCheckoutClientUnitTest.kt index d833f54dd..ac0623453 100644 --- a/PayPalWebPayments/src/test/java/com/paypal/android/paypalwebpayments/PayPalWebCheckoutClientUnitTest.kt +++ b/PayPalWebPayments/src/test/java/com/paypal/android/paypalwebpayments/PayPalWebCheckoutClientUnitTest.kt @@ -3,12 +3,11 @@ package com.paypal.android.paypalwebpayments import android.content.Intent import androidx.fragment.app.FragmentActivity import com.paypal.android.corepayments.PayPalSDKError -import io.mockk.Called import io.mockk.every import io.mockk.mockk -import io.mockk.slot import io.mockk.verify import junit.framework.TestCase.assertSame +import junit.framework.TestCase.assertTrue import kotlinx.coroutines.ExperimentalCoroutinesApi import org.junit.Before import org.junit.Test @@ -22,8 +21,6 @@ class PayPalWebCheckoutClientUnitTest { private val activity: FragmentActivity = mockk(relaxed = true) private val analytics = mockk(relaxed = true) - private val vaultListener = mockk(relaxed = true) - private val intent = Intent() private lateinit var payPalWebLauncher: PayPalWebLauncher @@ -58,8 +55,6 @@ class PayPalWebCheckoutClientUnitTest { @Test fun `vault() launches PayPal web checkout`() { - sut.vaultListener = vaultListener - val launchResult = PayPalPresentAuthChallengeResult.Success("auth state") every { payPalWebLauncher.launchPayPalWebVault(any(), any()) } returns launchResult @@ -67,25 +62,19 @@ class PayPalWebCheckoutClientUnitTest { PayPalWebVaultRequest("fake-setup-token-id") sut.vault(activity, request) verify(exactly = 1) { payPalWebLauncher.launchPayPalWebVault(activity, request) } - verify(exactly = 0) { vaultListener.onPayPalWebVaultFailure(any()) } } @Test fun `vault() notifies merchant of browser switch failure`() { - sut.vaultListener = vaultListener - val sdkError = PayPalSDKError(123, "fake error description") val launchResult = PayPalPresentAuthChallengeResult.Failure(sdkError) every { payPalWebLauncher.launchPayPalWebVault(any(), any()) } returns launchResult val request = PayPalWebVaultRequest("fake-setup-token-id") - sut.vault(activity, request) - - val slot = slot() - verify(exactly = 1) { vaultListener.onPayPalWebVaultFailure(capture(slot)) } + val result = sut.vault(activity, request) as PayPalPresentAuthChallengeResult.Failure - assertSame(slot.captured, sdkError) + assertSame(sdkError, result.error) } @Test @@ -125,58 +114,49 @@ class PayPalWebCheckoutClientUnitTest { } @Test - fun `completeAuthChallenge notifies merchant of vault success`() { - sut.vaultListener = vaultListener - - val successResult = PayPalWebVaultResult("fake-approval-session-id") + fun `finishVault() forwards vault success from PayPal web launcher`() { + val successResult = + PayPalWebCheckoutFinishVaultResult.Success("fake-approval-session-id") every { - payPalWebLauncher.completeAuthRequest(intent, "auth state") - } returns PayPalWebStatus.VaultSuccess(successResult) + payPalWebLauncher.completeVaultAuthRequest(intent, "auth state") + } returns successResult - sut.completeAuthChallenge(intent, "auth state") + val result = sut.finishVault(intent, "auth state") + as PayPalWebCheckoutFinishVaultResult.Success - val slot = slot() - verify(exactly = 1) { vaultListener.onPayPalWebVaultSuccess(capture(slot)) } - assertSame(successResult, slot.captured) + assertSame("fake-approval-session-id", result.approvalSessionId) } @Test - fun `completeAuthChallenge notifies merchant of vault failure`() { - sut.vaultListener = vaultListener - + fun `finishVault() notifies merchant of vault failure`() { val error = PayPalSDKError(123, "fake-error-description") every { - payPalWebLauncher.completeAuthRequest(intent, "auth state") - } returns PayPalWebStatus.VaultError(error) + payPalWebLauncher.completeVaultAuthRequest(intent, "auth state") + } returns PayPalWebCheckoutFinishVaultResult.Failure(error) - sut.completeAuthChallenge(intent, "auth state") + val result = sut.finishVault(intent, "auth state") + as PayPalWebCheckoutFinishVaultResult.Failure - val slot = slot() - verify(exactly = 1) { vaultListener.onPayPalWebVaultFailure(capture(slot)) } - assertSame(error, slot.captured) + assertSame(error, result.error) } @Test - fun `completeAuthChallenge notifies merchant of vault cancelation`() { - sut.vaultListener = vaultListener - + fun `finishVault forwards vault cancellation`() { every { - payPalWebLauncher.completeAuthRequest(intent, "auth state") - } returns PayPalWebStatus.VaultCanceled + payPalWebLauncher.completeVaultAuthRequest(intent, "auth state") + } returns PayPalWebCheckoutFinishVaultResult.Canceled - sut.completeAuthChallenge(intent, "auth state") - verify(exactly = 1) { vaultListener.onPayPalWebVaultCanceled() } + val result = sut.finishVault(intent, "auth state") + assertTrue(result is PayPalWebCheckoutFinishVaultResult.Canceled) } @Test - fun `completeAuthChallenge doesn't deliver result when browserSwitchResult is null`() { - sut.vaultListener = vaultListener - + fun `finishVault forwards no result`() { every { - payPalWebLauncher.completeAuthRequest(intent, "auth state") - } returns PayPalWebStatus.NoResult + payPalWebLauncher.completeVaultAuthRequest(intent, "auth state") + } returns PayPalWebCheckoutFinishVaultResult.NoResult - sut.completeAuthChallenge(intent, "auth state") - verify { vaultListener.wasNot(Called) } + val result = sut.finishVault(intent, "auth state") + assertTrue(result is PayPalWebCheckoutFinishVaultResult.NoResult) } } diff --git a/PayPalWebPayments/src/test/java/com/paypal/android/paypalwebpayments/PayPalWebLauncherUnitTest.kt b/PayPalWebPayments/src/test/java/com/paypal/android/paypalwebpayments/PayPalWebLauncherUnitTest.kt index 4466664d9..155754508 100644 --- a/PayPalWebPayments/src/test/java/com/paypal/android/paypalwebpayments/PayPalWebLauncherUnitTest.kt +++ b/PayPalWebPayments/src/test/java/com/paypal/android/paypalwebpayments/PayPalWebLauncherUnitTest.kt @@ -4,7 +4,6 @@ import android.content.Intent import android.net.Uri import androidx.fragment.app.FragmentActivity import com.braintreepayments.api.BrowserSwitchClient -import com.braintreepayments.api.BrowserSwitchException import com.braintreepayments.api.BrowserSwitchFinalResult import com.braintreepayments.api.BrowserSwitchOptions import com.braintreepayments.api.BrowserSwitchStartResult @@ -16,8 +15,6 @@ import io.mockk.mockk import io.mockk.slot import junit.framework.TestCase.assertEquals import org.json.JSONObject -import org.junit.Assert.assertSame -import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -237,40 +234,6 @@ class PayPalWebLauncherUnitTest { assertEquals("error message from browser switch", result.error.errorDescription) } - @Test - fun `completeAuthRequest() returns unknown error when browser switch fails`() { - val browserSwitchError = BrowserSwitchException("browser switch error") - val finalResult = mockk(relaxed = true) - every { finalResult.error } returns browserSwitchError - every { - browserSwitchClient.completeRequest(intent, "pending request") - } returns finalResult - - sut = PayPalWebLauncher("custom_url_scheme", liveConfig, browserSwitchClient) - - val status = sut.completeAuthRequest(intent, "pending request") - as PayPalWebStatus.UnknownError - assertSame(browserSwitchError, status.error) - } - - @Test - fun `completeAuthRequest() returns no result when request code is not for PayPal`() { - val browserSwitchResult = createCheckoutSuccessBrowserSwitchResult( - requestCode = BrowserSwitchRequestCodes.CARD_APPROVE_ORDER, - orderId = "fake-order-id", - payerId = "fake-payer-id" - ) - - every { - browserSwitchClient.completeRequest(intent, "pending request") - } returns browserSwitchResult - - sut = PayPalWebLauncher("custom_url_scheme", liveConfig, browserSwitchClient) - - val status = sut.completeAuthRequest(intent, "pending request") - assertTrue(status is PayPalWebStatus.NoResult) - } - @Test fun `completeCheckoutAuthRequest() parses successful checkout result`() { val browserSwitchResult = createCheckoutSuccessBrowserSwitchResult( @@ -349,7 +312,7 @@ class PayPalWebLauncherUnitTest { } @Test - fun `completeRequest() parses successful vault result`() { + fun `completeVaultAuthRequest() parses successful vault result`() { val browserSwitchResult = createVaultSuccessBrowserSwitchResult( requestCode = BrowserSwitchRequestCodes.PAYPAL_VAULT, setupTokenId = "fake-setup-token-id", @@ -360,13 +323,13 @@ class PayPalWebLauncherUnitTest { } returns browserSwitchResult sut = PayPalWebLauncher("custom_url_scheme", liveConfig, browserSwitchClient) - val status = sut.completeAuthRequest(intent, "pending request") - as PayPalWebStatus.VaultSuccess - assertEquals("fake-approval-session-id", status.result.approvalSessionId) + val result = sut.completeVaultAuthRequest(intent, "pending request") + as PayPalWebCheckoutFinishVaultResult.Success + assertEquals("fake-approval-session-id", result.approvalSessionId) } @Test - fun `completeRequest() parses vault failure when approval session id is blank`() { + fun `completeVaultAuthRequest() parses vault failure when approval session id is blank`() { val browserSwitchResult = createVaultSuccessBrowserSwitchResult( requestCode = BrowserSwitchRequestCodes.PAYPAL_VAULT, setupTokenId = "fake-setup-token-id", @@ -377,11 +340,11 @@ class PayPalWebLauncherUnitTest { } returns browserSwitchResult sut = PayPalWebLauncher("custom_url_scheme", liveConfig, browserSwitchClient) - val status = sut.completeAuthRequest(intent, "pending request") - as PayPalWebStatus.VaultError + val result = sut.completeVaultAuthRequest(intent, "pending request") + as PayPalWebCheckoutFinishVaultResult.Failure val expectedDescription = "Result did not contain the expected data. Payer ID or Order ID is null." - assertEquals(expectedDescription, status.error.errorDescription) + assertEquals(expectedDescription, result.error.errorDescription) } private fun createCheckoutMetadata(orderId: String) = JSONObject() From 104e65e0654da7c8cb40cbaabeec5706160e8f46 Mon Sep 17 00:00:00 2001 From: sshropshire Date: Mon, 16 Dec 2024 10:29:56 -0600 Subject: [PATCH 3/7] Clean up lint errors. --- .../paypalwebvault/PayPalWebVaultUiState.kt | 1 - .../paypalwebvault/PayPalWebVaultViewModel.kt | 20 ++++--------------- .../paypalwebpayments/PayPalWebLauncher.kt | 4 +++- 3 files changed, 7 insertions(+), 18 deletions(-) diff --git a/Demo/src/main/java/com/paypal/android/ui/paypalwebvault/PayPalWebVaultUiState.kt b/Demo/src/main/java/com/paypal/android/ui/paypalwebvault/PayPalWebVaultUiState.kt index 78db42590..16e2607ad 100644 --- a/Demo/src/main/java/com/paypal/android/ui/paypalwebvault/PayPalWebVaultUiState.kt +++ b/Demo/src/main/java/com/paypal/android/ui/paypalwebvault/PayPalWebVaultUiState.kt @@ -3,7 +3,6 @@ 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.PayPalWebCheckoutFinishVaultResult -import com.paypal.android.paypalwebpayments.PayPalWebVaultResult import com.paypal.android.uishared.state.ActionState data class PayPalWebVaultUiState( diff --git a/Demo/src/main/java/com/paypal/android/ui/paypalwebvault/PayPalWebVaultViewModel.kt b/Demo/src/main/java/com/paypal/android/ui/paypalwebvault/PayPalWebVaultViewModel.kt index 6bfa50039..64c706b62 100644 --- a/Demo/src/main/java/com/paypal/android/ui/paypalwebvault/PayPalWebVaultViewModel.kt +++ b/Demo/src/main/java/com/paypal/android/ui/paypalwebvault/PayPalWebVaultViewModel.kt @@ -4,20 +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 com.paypal.android.paypalwebpayments.PayPalWebCheckoutFinishVaultResult import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -30,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" @@ -126,15 +123,6 @@ class PayPalWebVaultViewModel @Inject constructor( } } - override fun onPayPalWebVaultSuccess(result: PayPalWebVaultResult) { - } - - override fun onPayPalWebVaultFailure(error: PayPalSDKError) { - } - - override fun onPayPalWebVaultCanceled() { - } - fun handleBrowserSwitchResult(activity: ComponentActivity) { val result = authState?.let { paypalClient?.finishVault(activity.intent, it) } when (result) { diff --git a/PayPalWebPayments/src/main/java/com/paypal/android/paypalwebpayments/PayPalWebLauncher.kt b/PayPalWebPayments/src/main/java/com/paypal/android/paypalwebpayments/PayPalWebLauncher.kt index 9806e26f9..ebe12e91e 100644 --- a/PayPalWebPayments/src/main/java/com/paypal/android/paypalwebpayments/PayPalWebLauncher.kt +++ b/PayPalWebPayments/src/main/java/com/paypal/android/paypalwebpayments/PayPalWebLauncher.kt @@ -169,7 +169,9 @@ internal class PayPalWebLauncher( } } - private fun parseVaultSuccessResult(finalResult: BrowserSwitchFinalResult.Success): PayPalWebCheckoutFinishVaultResult { + private fun parseVaultSuccessResult( + finalResult: BrowserSwitchFinalResult.Success + ): PayPalWebCheckoutFinishVaultResult { val deepLinkUrl = finalResult.returnUrl val requestMetadata = finalResult.requestMetadata return if (finalResult.requestCode == BrowserSwitchRequestCodes.PAYPAL_VAULT) { From ac2724b8dd7efcf898c54fc1ad94dba78dfe8d0a Mon Sep 17 00:00:00 2001 From: sshropshire Date: Mon, 16 Dec 2024 10:32:38 -0600 Subject: [PATCH 4/7] Remove PayPalWebStatus type. --- .../android/paypalwebpayments/PayPalWebStatus.kt | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 PayPalWebPayments/src/main/java/com/paypal/android/paypalwebpayments/PayPalWebStatus.kt diff --git a/PayPalWebPayments/src/main/java/com/paypal/android/paypalwebpayments/PayPalWebStatus.kt b/PayPalWebPayments/src/main/java/com/paypal/android/paypalwebpayments/PayPalWebStatus.kt deleted file mode 100644 index 9b7090cc9..000000000 --- a/PayPalWebPayments/src/main/java/com/paypal/android/paypalwebpayments/PayPalWebStatus.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.paypal.android.paypalwebpayments - -import com.paypal.android.corepayments.PayPalSDKError - -sealed class PayPalWebStatus { - - class VaultError(val error: PayPalSDKError) : PayPalWebStatus() - class VaultSuccess(val result: PayPalWebVaultResult) : PayPalWebStatus() - data object VaultCanceled : PayPalWebStatus() - - class UnknownError(val error: Throwable) : PayPalWebStatus() - data object NoResult : PayPalWebStatus() -} From 59f0ee9c9178d40b9bb94def759a33be432ee950 Mon Sep 17 00:00:00 2001 From: sshropshire Date: Mon, 16 Dec 2024 10:37:10 -0600 Subject: [PATCH 5/7] Remove obselete classes. --- .../PayPalWebCheckoutResult.kt | 6 ----- .../PayPalWebVaultListener.kt | 26 ------------------- .../paypalwebpayments/PayPalWebVaultResult.kt | 7 ----- 3 files changed, 39 deletions(-) delete mode 100644 PayPalWebPayments/src/main/java/com/paypal/android/paypalwebpayments/PayPalWebCheckoutResult.kt delete mode 100644 PayPalWebPayments/src/main/java/com/paypal/android/paypalwebpayments/PayPalWebVaultListener.kt delete mode 100644 PayPalWebPayments/src/main/java/com/paypal/android/paypalwebpayments/PayPalWebVaultResult.kt diff --git a/PayPalWebPayments/src/main/java/com/paypal/android/paypalwebpayments/PayPalWebCheckoutResult.kt b/PayPalWebPayments/src/main/java/com/paypal/android/paypalwebpayments/PayPalWebCheckoutResult.kt deleted file mode 100644 index 64e1ded9f..000000000 --- a/PayPalWebPayments/src/main/java/com/paypal/android/paypalwebpayments/PayPalWebCheckoutResult.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.paypal.android.paypalwebpayments - -/** - * A result passed to a [PayPalWebCheckoutListener] when the PayPal flow completes successfully. - */ -data class PayPalWebCheckoutResult(val orderId: String?, val payerId: String?) diff --git a/PayPalWebPayments/src/main/java/com/paypal/android/paypalwebpayments/PayPalWebVaultListener.kt b/PayPalWebPayments/src/main/java/com/paypal/android/paypalwebpayments/PayPalWebVaultListener.kt deleted file mode 100644 index 167879bcf..000000000 --- a/PayPalWebPayments/src/main/java/com/paypal/android/paypalwebpayments/PayPalWebVaultListener.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.paypal.android.paypalwebpayments - -import com.paypal.android.corepayments.PayPalSDKError - -/** - * Implement this callback to receive results from [PayPalWebCheckoutClient.vault]. - */ -interface PayPalWebVaultListener { - - /** - * Called when vaulting a PayPal payment method completes successfully. - * @param result [PayPalWebVaultResult] with order information. - */ - fun onPayPalWebVaultSuccess(result: PayPalWebVaultResult) - - /** - * Called when vaulting a PayPal payment method completes with an error. - * @param error [PayPalSDKError] explaining the reason for failure. - */ - fun onPayPalWebVaultFailure(error: PayPalSDKError) - - /** - * Called when a user cancels PayPal payment method vaulting. - */ - fun onPayPalWebVaultCanceled() -} diff --git a/PayPalWebPayments/src/main/java/com/paypal/android/paypalwebpayments/PayPalWebVaultResult.kt b/PayPalWebPayments/src/main/java/com/paypal/android/paypalwebpayments/PayPalWebVaultResult.kt deleted file mode 100644 index 61d45737e..000000000 --- a/PayPalWebPayments/src/main/java/com/paypal/android/paypalwebpayments/PayPalWebVaultResult.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.paypal.android.paypalwebpayments - -/** - * A result passed to a [PayPalWebVaultListener] when the PayPal flow completes successfully. - * @property [approvalSessionId] session ID associated with vault approval by the PayPal account owner - */ -data class PayPalWebVaultResult internal constructor(val approvalSessionId: String) From d1caa1f26f6f707a9471e099be0633fa747d0d5a Mon Sep 17 00:00:00 2001 From: sshropshire Date: Mon, 16 Dec 2024 14:08:18 -0600 Subject: [PATCH 6/7] Update CHANGELOG. --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 95b861ccc..0a7e65215 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 From 04a85eecae2f24bd1a0d9abf145b38b8bb075c65 Mon Sep 17 00:00:00 2001 From: sshropshire Date: Mon, 16 Dec 2024 14:21:16 -0600 Subject: [PATCH 7/7] Update documentation for PayPalWebCheckoutClient. --- .../PayPalWebCheckoutClient.kt | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/PayPalWebPayments/src/main/java/com/paypal/android/paypalwebpayments/PayPalWebCheckoutClient.kt b/PayPalWebPayments/src/main/java/com/paypal/android/paypalwebpayments/PayPalWebCheckoutClient.kt index 1965df0cc..339b0d7c8 100644 --- a/PayPalWebPayments/src/main/java/com/paypal/android/paypalwebpayments/PayPalWebCheckoutClient.kt +++ b/PayPalWebPayments/src/main/java/com/paypal/android/paypalwebpayments/PayPalWebCheckoutClient.kt @@ -30,7 +30,7 @@ class PayPalWebCheckoutClient internal constructor( ) /** - * 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 */ @@ -51,7 +51,7 @@ class PayPalWebCheckoutClient internal constructor( } /** - * 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 */ @@ -71,6 +71,17 @@ class PayPalWebCheckoutClient internal constructor( 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) { @@ -90,6 +101,17 @@ class PayPalWebCheckoutClient internal constructor( return result } + /** + * 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