Skip to content

Commit

Permalink
Remove 3DS URL Parsing (#294)
Browse files Browse the repository at this point in the history
* Remove url parsing from 3DS deep link return URLs to match feature parity with iOS.

* Fix unit tests broken from removing deprecated methods.

* Clean up CHANGELOG post-v2 beta merge.

* Update CHANGELOG.

* Update CHANGELOG and fix build errors from internal constructors.

* Add 3DS result to CardVaultResult View.

* Fix VaultCardView to use CardVaultResultView.
  • Loading branch information
sshropshire authored Nov 19, 2024
1 parent ef559fd commit bb18e81
Show file tree
Hide file tree
Showing 8 changed files with 33 additions and 183 deletions.
14 changes: 7 additions & 7 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# PayPal Android SDK Release Notes

# beta - unreleased
## unreleased
* Breaking Changes
* PayPalNativePayments
* Remove entire PayPalNativePayments module
Expand All @@ -17,6 +17,9 @@
* Add `ApproveOrderListener.onApproveOrderAuthorizationRequired(CardAuthChallenge)` method
* Add `CardVaultListener.onVaultAuthorizationRequired(CardAuthChallenge)` method
* Remove `authChallenge` property from `CardVaultResult`
* Remove `deepLinkUrl` and `liabilityShift` properties from `CardResult`
* Annotate `CardResult` and `CardVaultResult` constructors as restricted to the library group
* Add `didAttemptThreeDSecureAuthentication` property to `CardVaultResult`
* PayPalWebPayments
* Remove `PayPalWebCheckoutClient(FragmentActivity, CoreConfig, String)` constructor
* Add `PayPalWebCheckoutClient(Context, CoreConfig, String)` constructor
Expand All @@ -29,12 +32,9 @@
* Add `PayPalWebStatus` type
* Gradle
* Update `browser-switch` version to `3.0.0-beta`

# unreleased
* Gradle
* Update Kotlin version to `1.9.24`
* Update Android Gradle Plugin (AGP) to version `8.7.1`
* Explicitly declare Java 17 version as the target JVM toolchain
* Update Kotlin version to `1.9.24`
* Update Android Gradle Plugin (AGP) to version `8.7.1`
* Explicitly declare Java 17 version as the target JVM toolchain

## 1.7.1 (2024-10-29)
* Gradle
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,59 +75,27 @@ internal class CardAuthLauncher(
}

private fun parseVaultSuccessResult(finalResult: BrowserSwitchFinalResult.Success): CardStatus {
val deepLinkUrl = finalResult.returnUrl
val requestMetadata = finalResult.requestMetadata

return if (requestMetadata == null) {
val setupTokenId = finalResult.requestMetadata?.optString(METADATA_KEY_SETUP_TOKEN_ID)
return if (setupTokenId == null) {
CardStatus.VaultError(CardError.unknownError)
} else {
// TODO: see if there's a way that we can require the merchant to make their
// return and cancel urls conform to a strict schema

// NOTE: this assumes that when the merchant created a setup token, they used a
// return_url with word "success" in it (or a cancel_url with the word "cancel" in it)
val setupTokenId =
finalResult.requestMetadata?.optString(METADATA_KEY_SETUP_TOKEN_ID)
val deepLinkUrlString = deepLinkUrl.toString()
val didSucceed = deepLinkUrlString.contains("success")
if (didSucceed) {
val result = CardVaultResult(setupTokenId!!, "SCA_COMPLETE")
CardStatus.VaultSuccess(result)
} else {
val didCancel = deepLinkUrlString.contains("cancel")
if (didCancel) {
CardStatus.VaultCanceled(setupTokenId)
} else {
CardStatus.VaultError(CardError.unknownError)
}
}
val result =
CardVaultResult(setupTokenId, null, didAttemptThreeDSecureAuthentication = true)
CardStatus.VaultSuccess(result)
}
}

private fun parseApproveOrderSuccessResult(
finalResult: BrowserSwitchFinalResult.Success,
): CardStatus {
val deepLinkUrl = finalResult.returnUrl
val orderId = finalResult.requestMetadata?.optString(METADATA_KEY_ORDER_ID)

return if (orderId == null) {
CardStatus.ApproveOrderError(CardError.unknownError, null)
} else if (deepLinkUrl.getQueryParameter("error") != null) {
CardStatus.ApproveOrderError(CardError.threeDSVerificationError, orderId)
} else {
val state = deepLinkUrl.getQueryParameter("state")
val code = deepLinkUrl.getQueryParameter("code")
if (state == null || code == null) {
CardStatus.ApproveOrderError(CardError.malformedDeepLinkError, orderId)
} else {
val liabilityShift = deepLinkUrl.getQueryParameter("liability_shift")
val result = CardResult(
orderId = orderId,
liabilityShift = liabilityShift,
didAttemptThreeDSecureAuthentication = true
)
CardStatus.ApproveOrderSuccess(result)
}
val result = CardResult(orderId = orderId, didAttemptThreeDSecureAuthentication = true)
CardStatus.ApproveOrderSuccess(result)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,8 @@ class CardClient internal constructor(
)

if (response.payerActionHref == null) {
val result = CardResult(
orderId = response.orderId,
status = response.status?.name,
didAttemptThreeDSecureAuthentication = false
)
val result =
response.run { CardResult(orderId = orderId, status = status?.name) }
notifyApproveOrderSuccess(result)
} else {
analyticsService.sendAnalyticsEvent(
Expand Down Expand Up @@ -127,8 +124,7 @@ class CardClient internal constructor(

val approveHref = updateSetupTokenResult.approveHref
if (approveHref == null) {
val result =
updateSetupTokenResult.run { CardVaultResult(setupTokenId, status) }
val result = updateSetupTokenResult.run { CardVaultResult(setupTokenId, status) }
cardVaultListener?.onVaultSuccess(result)
} else {
val url = Uri.parse(approveHref)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,29 +1,17 @@
package com.paypal.android.cardpayments

import android.net.Uri
import androidx.annotation.RestrictTo

/**
* A result returned by [CardClient] when an order was successfully approved with a [Card].
*
* @property [orderId] associated order ID.
* @property [liabilityShift] Liability shift value returned from 3DS verification
* @property [status] status of the order
* @property [didAttemptThreeDSecureAuthentication] 3DS verification was attempted.
* Use v2/checkout/orders/{orderId} in your server to get verification results.
*/
data class CardResult(
data class CardResult @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) constructor(
val orderId: String,

/**
* @suppress
*/
@Deprecated("Use status instead.")
val deepLinkUrl: Uri? = null,

@Deprecated("Use didAttemptThreeDSecureAuthentication instead.")
val liabilityShift: String? = null,

val status: String? = null,

val didAttemptThreeDSecureAuthentication: Boolean = false
)
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
package com.paypal.android.cardpayments

import androidx.annotation.RestrictTo

/**
* @suppress
*
* A result returned by [CardClient] when an a successful vault occurs.
*
* @param setupTokenId the id for the setup token that was recently updated
* @param status the status of the updated setup token
* @property [didAttemptThreeDSecureAuthentication] 3DS verification was attempted.
* Use v2/checkout/orders/{orderId} in your server to get verification results.
*/
// NEXT MAJOR VERSION: make `CardVaultResult` constructor private
data class CardVaultResult(
data class CardVaultResult @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) constructor(
val setupTokenId: String,
val status: String,
val status: String? = null,
val didAttemptThreeDSecureAuthentication: Boolean = false
)
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ class CardAuthLauncherUnitTest {
}

@Test
fun `completeAuthRequest() returns approve order success when liability shift available`() {
fun `completeAuthRequest() returns approve order success`() {
sut = CardAuthLauncher(browserSwitchClient)

val scheme = "com.paypal.android.demo"
Expand All @@ -169,85 +169,12 @@ class CardAuthLauncherUnitTest {

val cardResult = status.result
assertEquals("fake-order-id", cardResult.orderId)
assertEquals("NO", cardResult.liabilityShift)
assertTrue(cardResult.didAttemptThreeDSecureAuthentication)
assertNull(cardResult.status)
}

@Test
fun `completeAuthRequest() returns approve order error when deep link contains an error`() {
sut = CardAuthLauncher(browserSwitchClient)

val scheme = "com.paypal.android.demo"
val domain = "example.com"
val successDeepLink = "$scheme://$domain/return_url?error=error"

val finalResult = createBrowserSwitchSuccessFinalResult(
BrowserSwitchRequestCodes.CARD_APPROVE_ORDER,
approveOrderMetadata,
Uri.parse(successDeepLink)
)
every {
browserSwitchClient.completeRequest(intent, "pending request")
} returns finalResult

val status =
sut.completeAuthRequest(intent, "pending request") as CardStatus.ApproveOrderError
val error = status.error
assertEquals(0, error.code)
assertEquals("3DS Verification is returning an error.", error.errorDescription)
}

@Test
fun `completeAuthRequest() returns approve order error when deep link is missing code parameter`() {
sut = CardAuthLauncher(browserSwitchClient)

val scheme = "com.paypal.android.demo"
val domain = "example.com"
val successDeepLink = "$scheme://$domain/return_url?state=undefined&liability_shift=NO"

val finalResult = createBrowserSwitchSuccessFinalResult(
BrowserSwitchRequestCodes.CARD_APPROVE_ORDER,
approveOrderMetadata,
Uri.parse(successDeepLink)
)
every {
browserSwitchClient.completeRequest(intent, "pending request")
} returns finalResult

val status =
sut.completeAuthRequest(intent, "pending request") as CardStatus.ApproveOrderError
val error = status.error
assertEquals(1, error.code)
assertEquals("Malformed deeplink URL.", error.errorDescription)
}

@Test
fun `completeAuthRequest() returns approve order error when deep link is missing state parameter`() {
sut = CardAuthLauncher(browserSwitchClient)

val scheme = "com.paypal.android.demo"
val domain = "example.com"
val successDeepLink = "$scheme://$domain/return_url?code=undefined&liability_shift=NO"

val finalResult = createBrowserSwitchSuccessFinalResult(
BrowserSwitchRequestCodes.CARD_APPROVE_ORDER,
approveOrderMetadata,
Uri.parse(successDeepLink)
)
every {
browserSwitchClient.completeRequest(intent, "pending request")
} returns finalResult

val status =
sut.completeAuthRequest(intent, "pending request") as CardStatus.ApproveOrderError
val error = status.error
assertEquals(1, error.code)
assertEquals("Malformed deeplink URL.", error.errorDescription)
}

@Test
fun `completeAuthRequest() returns vault success when deep link url contains the word success`() {
fun `completeAuthRequest() returns vault success`() {
sut = CardAuthLauncher(browserSwitchClient)

val scheme = "com.paypal.android.demo"
Expand All @@ -266,28 +193,7 @@ class CardAuthLauncherUnitTest {
val status = sut.completeAuthRequest(intent, "pending request") as CardStatus.VaultSuccess
val vaultResult = status.result
assertEquals("fake-setup-token-id", vaultResult.setupTokenId)
assertEquals("SCA_COMPLETE", vaultResult.status)
}

@Test
fun `completeAuthRequest() returns vault canceled when deep link url contains the word cancel`() {
sut = CardAuthLauncher(browserSwitchClient)

val scheme = "com.paypal.android.demo"
val domain = "example.com"
val successDeepLink = "$scheme://$domain/canceled"

val finalResult = createBrowserSwitchSuccessFinalResult(
BrowserSwitchRequestCodes.CARD_VAULT,
vaultMetadata,
Uri.parse(successDeepLink)
)
every {
browserSwitchClient.completeRequest(intent, "pending request")
} returns finalResult

val status = sut.completeAuthRequest(intent, "pending request") as CardStatus.VaultCanceled
assertEquals("fake-setup-token-id", status.setupTokenId)
assertNull(vaultResult.status)
}

private fun createBrowserSwitchSuccessFinalResult(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,13 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.paypal.android.R
import com.paypal.android.cardpayments.CardVaultResult
import com.paypal.android.uishared.components.ActionButtonColumn
import com.paypal.android.uishared.components.CardForm
import com.paypal.android.uishared.components.CardPaymentTokenView
import com.paypal.android.uishared.components.CardSetupTokenView
import com.paypal.android.uishared.components.CardVaultResultView
import com.paypal.android.uishared.components.EnumOptionList
import com.paypal.android.uishared.components.ErrorView
import com.paypal.android.uishared.components.PropertyView
import com.paypal.android.uishared.components.StepHeader
import com.paypal.android.uishared.state.CompletedActionState
import com.paypal.android.utils.OnLifecycleOwnerResumeEffect
Expand Down Expand Up @@ -73,17 +72,6 @@ fun VaultCardView(
}
}

@Composable
fun VaultSuccessView(cardVaultResult: CardVaultResult) {
Column(
verticalArrangement = UIConstants.spacingMedium,
modifier = Modifier.padding(UIConstants.paddingMedium)
) {
PropertyView(name = "Setup Token Id", value = cardVaultResult.setupTokenId)
PropertyView(name = "Status", value = cardVaultResult.status)
}
}

@Composable
private fun Step_CreateSetupToken(
uiState: VaultCardUiState,
Expand Down Expand Up @@ -144,7 +132,7 @@ private fun Step_VaultCard(
) { state ->
when (state) {
is CompletedActionState.Failure -> ErrorView(error = state.value)
is CompletedActionState.Success -> VaultSuccessView(cardVaultResult = state.value)
is CompletedActionState.Success -> CardVaultResultView(result = state.value)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,7 @@ fun CardVaultResultView(result: CardVaultResult) {
) {
PropertyView(name = "Setup Token ID", value = result.setupTokenId)
PropertyView(name = "Status", value = result.status)
val didAttemptText = if (result.didAttemptThreeDSecureAuthentication) "YES" else "NO"
PropertyView(name = "Did Attempt 3DS Authentication", value = didAttemptText)
}
}

0 comments on commit bb18e81

Please sign in to comment.