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

Support confirmation of save LinkPaymentDetails #10041

Merged
merged 10 commits into from
Jan 30, 2025
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@ package com.stripe.android.link.confirmation
import com.stripe.android.core.Logger
import com.stripe.android.core.strings.resolvableString
import com.stripe.android.link.LinkConfiguration
import com.stripe.android.link.LinkPaymentDetails
import com.stripe.android.link.model.LinkAccount
import com.stripe.android.model.ConfirmPaymentIntentParams
import com.stripe.android.model.ConsumerPaymentDetails
import com.stripe.android.model.PaymentMethod
import com.stripe.android.model.PaymentMethodCreateParams
import com.stripe.android.model.PaymentMethodOptionsParams
import com.stripe.android.model.wallets.Wallet
import com.stripe.android.paymentelement.confirmation.ConfirmationHandler
import com.stripe.android.paymentelement.confirmation.PaymentMethodConfirmationOption
import com.stripe.android.paymentsheet.PaymentSheet
Expand All @@ -21,6 +26,25 @@ internal class DefaultLinkConfirmationHandler @Inject constructor(
paymentDetails: ConsumerPaymentDetails.PaymentDetails,
linkAccount: LinkAccount,
cvc: String?
): Result {
return runCatching {
Copy link
Collaborator

Choose a reason for hiding this comment

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

can you update to share this code with the fn below it? this is a lot of duplication and should be very simple to share

val args = newConfirmationArgs(paymentDetails, linkAccount, cvc)
confirmationHandler.start(args)
val result = confirmationHandler.awaitResult()
transformResult(result)
}.getOrElse { error ->
logger.error(
msg = "DefaultLinkConfirmationHandler: Failed to confirm payment",
t = error
)
Result.Failed(R.string.stripe_something_went_wrong.resolvableString)
}
}

override suspend fun confirm(
paymentDetails: LinkPaymentDetails,
linkAccount: LinkAccount,
cvc: String?
): Result {
return runCatching {
val args = confirmationArgs(paymentDetails, linkAccount, cvc)
Expand Down Expand Up @@ -55,6 +79,28 @@ internal class DefaultLinkConfirmationHandler @Inject constructor(
}

private fun confirmationArgs(
paymentDetails: LinkPaymentDetails,
linkAccount: LinkAccount,
cvc: String?
): ConfirmationHandler.Args {
return when (paymentDetails) {
is LinkPaymentDetails.New -> {
newConfirmationArgs(
paymentDetails = paymentDetails.paymentDetails,
linkAccount = linkAccount,
cvc = cvc
)
}
is LinkPaymentDetails.Saved -> {
savedConfirmationArgs(
paymentDetails = paymentDetails,
cvc = cvc
)
}
}
}

private fun newConfirmationArgs(
paymentDetails: ConsumerPaymentDetails.PaymentDetails,
linkAccount: LinkAccount,
cvc: String?
Expand All @@ -76,6 +122,37 @@ internal class DefaultLinkConfirmationHandler @Inject constructor(
)
}

private fun savedConfirmationArgs(
paymentDetails: LinkPaymentDetails,
cvc: String?
): ConfirmationHandler.Args {
return ConfirmationHandler.Args(
intent = configuration.stripeIntent,
confirmationOption = PaymentMethodConfirmationOption.Saved(
paymentMethod = PaymentMethod.Builder()
.setId(paymentDetails.paymentDetails.id)
.setCode(paymentDetails.paymentMethodCreateParams.typeCode)
.setCard(
PaymentMethod.Card(
last4 = paymentDetails.paymentDetails.last4,
wallet = Wallet.LinkWallet(paymentDetails.paymentDetails.last4),
)
)
.setType(PaymentMethod.Type.Card)
.build(),
optionsParams = PaymentMethodOptionsParams.Card(
setupFutureUsage = ConfirmPaymentIntentParams.SetupFutureUsage.OffSession,
cvc = cvc?.takeIf {
configuration.passthroughModeEnabled.not()
}
)
),
appearance = PaymentSheet.Appearance(),
initializationMode = configuration.initializationMode,
shippingDetails = configuration.shippingDetails
)
}

private fun createPaymentMethodCreateParams(
selectedPaymentDetails: ConsumerPaymentDetails.PaymentDetails,
linkAccount: LinkAccount,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.stripe.android.link.confirmation

import com.stripe.android.core.strings.ResolvableString
import com.stripe.android.link.LinkPaymentDetails
import com.stripe.android.link.model.LinkAccount
import com.stripe.android.model.ConsumerPaymentDetails
import com.stripe.android.paymentelement.confirmation.ConfirmationHandler
Expand All @@ -12,6 +13,12 @@ internal interface LinkConfirmationHandler {
cvc: String? = null
): Result

suspend fun confirm(
paymentDetails: LinkPaymentDetails,
linkAccount: LinkAccount,
cvc: String? = null
): Result

fun interface Factory {
fun create(confirmationHandler: ConfirmationHandler): LinkConfirmationHandler
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import com.stripe.android.common.exception.stripeErrorMessage
import com.stripe.android.core.Logger
import com.stripe.android.link.LinkActivityResult
import com.stripe.android.link.LinkConfiguration
import com.stripe.android.link.LinkPaymentDetails
import com.stripe.android.link.account.LinkAccountManager
import com.stripe.android.link.confirmation.LinkConfirmationHandler
import com.stripe.android.link.confirmation.Result
Expand All @@ -17,7 +18,6 @@ import com.stripe.android.link.model.LinkAccount
import com.stripe.android.link.ui.PrimaryButtonState
import com.stripe.android.link.ui.completePaymentButtonLabel
import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodMetadata
import com.stripe.android.model.ConsumerPaymentDetails
import com.stripe.android.model.PaymentMethod
import com.stripe.android.paymentsheet.DefaultFormHelper
import com.stripe.android.paymentsheet.FormHelper
Expand Down Expand Up @@ -78,7 +78,7 @@ internal class PaymentMethodViewModel @Inject constructor(
onSuccess = { linkPaymentDetails ->
val cardMap = paymentMethodCreateParams.toParamMap()["card"] as? Map<*, *>?
performConfirmation(
paymentDetails = linkPaymentDetails.paymentDetails,
paymentDetails = linkPaymentDetails,
cvc = cardMap?.get("cvc") as? String?
)
updateButtonState(PrimaryButtonState.Enabled)
Expand All @@ -100,7 +100,7 @@ internal class PaymentMethodViewModel @Inject constructor(
}

private suspend fun performConfirmation(
paymentDetails: ConsumerPaymentDetails.PaymentDetails,
paymentDetails: LinkPaymentDetails,
cvc: String?
) {
val result = linkConfirmationHandler.confirm(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,11 @@ internal object TestFactory {
originalParams = mock()
)

val LINK_SAVED_PAYMENT_DETAILS = LinkPaymentDetails.Saved(
paymentDetails = CONSUMER_PAYMENT_DETAILS_CARD,
paymentMethodCreateParams = PAYMENT_METHOD_CREATE_PARAMS,
)

val LINK_ACCOUNT = LinkAccount(CONSUMER_SESSION)

val CONSUMER_PAYMENT_DETAILS: ConsumerPaymentDetails = ConsumerPaymentDetails(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import com.google.common.truth.Truth.assertThat
import com.stripe.android.core.Logger
import com.stripe.android.core.strings.resolvableString
import com.stripe.android.link.LinkConfiguration
import com.stripe.android.link.LinkPaymentDetails
import com.stripe.android.link.TestFactory
import com.stripe.android.link.model.LinkAccount
import com.stripe.android.model.ConsumerPaymentDetails
import com.stripe.android.model.PaymentMethodCreateParams
import com.stripe.android.model.PaymentMethodOptionsParams
import com.stripe.android.model.SetupIntentFixtures
import com.stripe.android.paymentelement.confirmation.ConfirmationHandler
import com.stripe.android.paymentelement.confirmation.FakeConfirmationHandler
Expand Down Expand Up @@ -169,6 +171,98 @@ internal class DefaultLinkConfirmationHandlerTest {
.containsExactly("DefaultLinkConfirmationHandler: Payment confirmation returned null" to null)
}

@Test
fun `confirm with New LinkPaymentDetails calls uses correct confirmation args`() = runTest(dispatcher) {
val configuration = TestFactory.LINK_CONFIGURATION
val confirmationHandler = FakeConfirmationHandler()
val handler = createHandler(
confirmationHandler = confirmationHandler,
configuration = configuration
)

confirmationHandler.awaitResultTurbine.add(
item = ConfirmationHandler.Result.Succeeded(
intent = configuration.stripeIntent,
deferredIntentConfirmationType = null
)
)

val result = handler.confirm(
paymentDetails = TestFactory.LINK_NEW_PAYMENT_DETAILS,
linkAccount = TestFactory.LINK_ACCOUNT,
cvc = CVC
)

assertThat(result).isEqualTo(Result.Succeeded)
confirmationHandler.startTurbine.awaitItem().assertConfirmationArgs(
configuration = configuration,
linkAccount = TestFactory.LINK_ACCOUNT,
paymentDetails = TestFactory.CONSUMER_PAYMENT_DETAILS_CARD,
cvc = CVC
)
}

@Test
fun `confirm with saved LinkPaymentDetails creates correct confirmation args`() = runTest(dispatcher) {
val configuration = TestFactory.LINK_CONFIGURATION
val confirmationHandler = FakeConfirmationHandler()
val handler = createHandler(
confirmationHandler = confirmationHandler,
configuration = configuration
)

confirmationHandler.awaitResultTurbine.add(
item = ConfirmationHandler.Result.Succeeded(
intent = configuration.stripeIntent,
deferredIntentConfirmationType = null
)
)

val savedPaymentDetails = TestFactory.LINK_SAVED_PAYMENT_DETAILS
val result = handler.confirm(
paymentDetails = savedPaymentDetails,
linkAccount = TestFactory.LINK_ACCOUNT,
cvc = CVC
)

assertThat(result).isEqualTo(Result.Succeeded)
confirmationHandler.startTurbine.awaitItem().assertSavedConfirmationArgs(
configuration = configuration,
paymentDetails = TestFactory.LINK_SAVED_PAYMENT_DETAILS,
cvc = CVC
)
}

@Test
fun `confirm with saved LinkPaymentDetails in passthrough mode omits CVC`() = runTest(dispatcher) {
val configuration = TestFactory.LINK_CONFIGURATION.copy(passthroughModeEnabled = true)
val confirmationHandler = FakeConfirmationHandler()
val handler = createHandler(
confirmationHandler = confirmationHandler,
configuration = configuration
)

confirmationHandler.awaitResultTurbine.add(
item = ConfirmationHandler.Result.Succeeded(
intent = configuration.stripeIntent,
deferredIntentConfirmationType = null
)
)

val result = handler.confirm(
paymentDetails = TestFactory.LINK_SAVED_PAYMENT_DETAILS,
linkAccount = TestFactory.LINK_ACCOUNT,
cvc = CVC
)

assertThat(result).isEqualTo(Result.Succeeded)
confirmationHandler.startTurbine.awaitItem().assertSavedConfirmationArgs(
configuration = configuration,
paymentDetails = TestFactory.LINK_SAVED_PAYMENT_DETAILS,
cvc = null
)
}

private fun ConfirmationHandler.Args.assertConfirmationArgs(
configuration: LinkConfiguration,
paymentDetails: ConsumerPaymentDetails.PaymentDetails,
Expand All @@ -188,6 +282,21 @@ internal class DefaultLinkConfirmationHandlerTest {
assertThat(initializationMode).isEqualTo(configuration.initializationMode)
}

private fun ConfirmationHandler.Args.assertSavedConfirmationArgs(
configuration: LinkConfiguration,
paymentDetails: LinkPaymentDetails.Saved,
cvc: String?,
) {
assertThat(intent).isEqualTo(configuration.stripeIntent)
val option = confirmationOption as PaymentMethodConfirmationOption.Saved
assertThat(option.paymentMethod.id).isEqualTo(paymentDetails.paymentDetails.id)

val optionsCard = option.optionsParams as? PaymentMethodOptionsParams.Card
assertThat(optionsCard?.cvc).isEqualTo(cvc)
assertThat(shippingDetails).isEqualTo(configuration.shippingDetails)
assertThat(initializationMode).isEqualTo(configuration.initializationMode)
}

private fun createHandler(
configuration: LinkConfiguration = TestFactory.LINK_CONFIGURATION,
logger: Logger = FakeLogger(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package com.stripe.android.link.confirmation

import com.stripe.android.link.LinkPaymentDetails
import com.stripe.android.link.model.LinkAccount
import com.stripe.android.model.ConsumerPaymentDetails

internal class FakeLinkConfirmationHandler : LinkConfirmationHandler {
var confirmResult: Result = Result.Succeeded
var confirmWithLinkPaymentDetailsResult: Result = Result.Succeeded
val calls = arrayListOf<Call>()
val confirmWithLinkPaymentDetailsCall = arrayListOf<ConfirmWithLinkPaymentDetailsCall>()

override suspend fun confirm(
paymentDetails: ConsumerPaymentDetails.PaymentDetails,
Expand All @@ -22,9 +25,30 @@ internal class FakeLinkConfirmationHandler : LinkConfirmationHandler {
return confirmResult
}

override suspend fun confirm(
paymentDetails: LinkPaymentDetails,
linkAccount: LinkAccount,
cvc: String?
): Result {
confirmWithLinkPaymentDetailsCall.add(
element = ConfirmWithLinkPaymentDetailsCall(
paymentDetails = paymentDetails,
linkAccount = linkAccount,
cvc = cvc
)
)
return confirmWithLinkPaymentDetailsResult
}

data class Call(
val paymentDetails: ConsumerPaymentDetails.PaymentDetails,
val linkAccount: LinkAccount,
val cvc: String?
)

data class ConfirmWithLinkPaymentDetailsCall(
val paymentDetails: LinkPaymentDetails,
val linkAccount: LinkAccount,
val cvc: String?
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,8 @@ internal class PaymentMethodScreenTest {

fillCardDetails()

linkConfirmationHandler.confirmResult = LinkConfirmationResult.Failed("oops".resolvableString)
linkConfirmationHandler.confirmWithLinkPaymentDetailsResult =
LinkConfirmationResult.Failed("oops".resolvableString)

onPayButton()
.scrollToAndAssertDisplayed()
Expand Down
Loading
Loading