Skip to content

Commit

Permalink
Fix mandate handling in Payment Element
Browse files Browse the repository at this point in the history
  • Loading branch information
samer-stripe committed Feb 14, 2025
1 parent 65d3422 commit c40b474
Show file tree
Hide file tree
Showing 11 changed files with 205 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import com.stripe.android.model.PaymentMethod
import com.stripe.android.model.PaymentMethodCreateParams
import com.stripe.android.model.PaymentMethodOptionsParams
import com.stripe.android.model.SetupIntent
import com.stripe.android.model.StripeIntent

/**
* Factory class for creating [ConfirmPaymentIntentParams] or [ConfirmSetupIntentParams].
Expand Down Expand Up @@ -44,13 +45,14 @@ sealed class ConfirmStripeIntentParamsFactory<out T : ConfirmStripeIntentParams>

fun createFactory(
clientSecret: String,
intent: StripeIntent,
shipping: ConfirmPaymentIntentParams.Shipping?,
) = when {
PaymentIntent.ClientSecret.isMatch(clientSecret) -> {
ConfirmPaymentIntentParamsFactory(clientSecret, shipping)
intent is PaymentIntent && PaymentIntent.ClientSecret.isMatch(clientSecret) -> {
ConfirmPaymentIntentParamsFactory(clientSecret, intent, shipping)
}
SetupIntent.ClientSecret.isMatch(clientSecret) -> {
ConfirmSetupIntentParamsFactory(clientSecret)
intent is SetupIntent && SetupIntent.ClientSecret.isMatch(clientSecret) -> {
ConfirmSetupIntentParamsFactory(clientSecret, intent)
}
else -> {
error("Encountered an invalid client secret \"$clientSecret\"")
Expand All @@ -61,6 +63,7 @@ sealed class ConfirmStripeIntentParamsFactory<out T : ConfirmStripeIntentParams>

internal class ConfirmPaymentIntentParamsFactory(
private val clientSecret: String,
private val intent: PaymentIntent,
private val shipping: ConfirmPaymentIntentParams.Shipping?
) : ConfirmStripeIntentParamsFactory<ConfirmPaymentIntentParams>() {

Expand All @@ -73,8 +76,7 @@ internal class ConfirmPaymentIntentParamsFactory(
paymentMethodId = paymentMethodId,
clientSecret = clientSecret,
paymentMethodOptions = optionsParams,
mandateData = MandateDataParams(MandateDataParams.Type.Online.DEFAULT)
.takeIf { paymentMethodType?.requiresMandate == true },
mandateData = mandateData(intent, paymentMethodType),
shipping = shipping
)
}
Expand All @@ -94,6 +96,7 @@ internal class ConfirmPaymentIntentParamsFactory(

internal class ConfirmSetupIntentParamsFactory(
private val clientSecret: String,
private val intent: SetupIntent,
) : ConfirmStripeIntentParamsFactory<ConfirmSetupIntentParams>() {

override fun create(
Expand All @@ -104,9 +107,7 @@ internal class ConfirmSetupIntentParamsFactory(
return ConfirmSetupIntentParams.create(
paymentMethodId = paymentMethodId,
clientSecret = clientSecret,
mandateData = paymentMethodType?.requiresMandate?.let {
MandateDataParams(MandateDataParams.Type.Online.DEFAULT)
}
mandateData = mandateData(intent, paymentMethodType),
)
}

Expand All @@ -120,3 +121,25 @@ internal class ConfirmSetupIntentParamsFactory(
)
}
}

private fun mandateData(intent: StripeIntent, paymentMethodType: PaymentMethod.Type?): MandateDataParams? {
return paymentMethodType?.let { type ->
val supportsAddingMandateData = when (intent) {
is PaymentIntent -> intent.canSetupFutureUsage()
is SetupIntent -> true
}

return MandateDataParams(MandateDataParams.Type.Online.DEFAULT).takeIf {
supportsAddingMandateData && type.requiresMandate
}
}
}

private fun PaymentIntent.canSetupFutureUsage(): Boolean {
return when (setupFutureUsage) {
null,
StripeIntent.Usage.OneTime -> false
StripeIntent.Usage.OnSession,
StripeIntent.Usage.OffSession -> true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ constructor(
"klarna",
isReusable = false,
isVoucher = false,
requiresMandate = false,
requiresMandate = true,
hasDelayedSettlement = false,
),
Affirm(
Expand All @@ -358,7 +358,7 @@ constructor(
"revolut_pay",
isReusable = false,
isVoucher = false,
requiresMandate = false,
requiresMandate = true,
hasDelayedSettlement = false,
afterRedirectAction = AfterRedirectAction.Poll(),
),
Expand Down Expand Up @@ -394,7 +394,7 @@ constructor(
"amazon_pay",
isReusable = false,
isVoucher = false,
requiresMandate = false,
requiresMandate = true,
hasDelayedSettlement = false,
afterRedirectAction = AfterRedirectAction.Poll(),
),
Expand Down Expand Up @@ -437,7 +437,7 @@ constructor(
code = "cashapp",
isReusable = false,
isVoucher = false,
requiresMandate = false,
requiresMandate = true,
hasDelayedSettlement = false,
afterRedirectAction = AfterRedirectAction.Refresh(),
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,22 @@ package com.stripe.android
import com.google.common.truth.Truth.assertThat
import com.stripe.android.model.Address
import com.stripe.android.model.ConfirmPaymentIntentParams
import com.stripe.android.model.ConfirmStripeIntentParams
import com.stripe.android.model.MandateDataParams
import com.stripe.android.model.PaymentIntent
import com.stripe.android.model.PaymentMethodCreateParamsFixtures
import com.stripe.android.model.PaymentMethodFixtures
import com.stripe.android.model.PaymentMethodOptionsParams
import com.stripe.android.model.StripeIntent
import com.stripe.android.testing.PaymentIntentFactory
import com.stripe.android.testing.PaymentMethodFactory
import org.junit.Test

class ConfirmPaymentIntentParamsFactoryTest {

private val factory = ConfirmPaymentIntentParamsFactory(
clientSecret = CLIENT_SECRET,
intent = createPaymentIntent(),
shipping = null,
)

Expand Down Expand Up @@ -87,6 +94,7 @@ class ConfirmPaymentIntentParamsFactoryTest {

val factoryWithConfig = ConfirmPaymentIntentParamsFactory(
clientSecret = CLIENT_SECRET,
intent = createPaymentIntent(),
shipping = shippingDetails,
)

Expand All @@ -107,6 +115,7 @@ class ConfirmPaymentIntentParamsFactoryTest {

val factoryWithConfig = ConfirmPaymentIntentParamsFactory(
clientSecret = CLIENT_SECRET,
intent = createPaymentIntent(),
shipping = shippingDetails,
)

Expand All @@ -121,6 +130,7 @@ class ConfirmPaymentIntentParamsFactoryTest {
fun `create() with saved card and does not require save on confirmation`() {
val factoryWithConfig = ConfirmPaymentIntentParamsFactory(
clientSecret = CLIENT_SECRET,
intent = createPaymentIntent(),
shipping = null,
)

Expand All @@ -142,6 +152,7 @@ class ConfirmPaymentIntentParamsFactoryTest {
fun `create() with saved card and requires save on confirmation`() {
val factoryWithConfig = ConfirmPaymentIntentParamsFactory(
clientSecret = CLIENT_SECRET,
intent = createPaymentIntent(),
shipping = null,
)

Expand All @@ -159,6 +170,66 @@ class ConfirmPaymentIntentParamsFactoryTest {
)
}

@Test
fun `create() without SFU should not contain any mandate data`() = mandateDataTest(
setupFutureUsage = null,
expectedMandateDataParams = null,
)

@Test
fun `create() with 'OneTime' SFU should not contain any mandate data`() = mandateDataTest(
setupFutureUsage = StripeIntent.Usage.OneTime,
expectedMandateDataParams = null,
)

@Test
fun `create() with 'OnSession' SFU should contain any mandate data`() = mandateDataTest(
setupFutureUsage = StripeIntent.Usage.OnSession,
expectedMandateDataParams = MandateDataParams(MandateDataParams.Type.Online.DEFAULT),
)

@Test
fun `create() with 'OffSession' SFU should contain any mandate data`() = mandateDataTest(
setupFutureUsage = StripeIntent.Usage.OffSession,
expectedMandateDataParams = MandateDataParams(MandateDataParams.Type.Online.DEFAULT),
)

private fun mandateDataTest(
setupFutureUsage: StripeIntent.Usage?,
expectedMandateDataParams: MandateDataParams?
) {
val factoryWithConfig = ConfirmPaymentIntentParamsFactory(
clientSecret = CLIENT_SECRET,
intent = createPaymentIntent(
setupFutureUsage = setupFutureUsage,
),
shipping = null,
)

val result = factoryWithConfig.create(
paymentMethod = PaymentMethodFactory.cashAppPay(),
optionsParams = null,
)

assertThat(result).isInstanceOf(ConfirmPaymentIntentParams::class.java)

val params = result.asConfirmPaymentIntentParams()

assertThat(params.mandateData).isEqualTo(expectedMandateDataParams)
}

private fun createPaymentIntent(
setupFutureUsage: StripeIntent.Usage? = null,
): PaymentIntent {
return PaymentIntentFactory.create(
setupFutureUsage = setupFutureUsage,
)
}

private fun ConfirmStripeIntentParams.asConfirmPaymentIntentParams(): ConfirmPaymentIntentParams {
return this as ConfirmPaymentIntentParams
}

private companion object {
private const val CLIENT_SECRET = "client_secret"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.stripe.android

import com.google.common.truth.Truth.assertThat
import com.stripe.android.model.ConfirmSetupIntentParams
import com.stripe.android.model.ConfirmStripeIntentParams
import com.stripe.android.model.MandateDataParams
import com.stripe.android.testing.PaymentMethodFactory
import com.stripe.android.testing.SetupIntentFactory
import org.junit.Test

class ConfirmSetupIntentParamsFactoryTest {
@Test
fun `create() should not contain mandate data`() {
val factoryWithConfig = ConfirmSetupIntentParamsFactory(
clientSecret = CLIENT_SECRET,
intent = SetupIntentFactory.create(),
)

val result = factoryWithConfig.create(
paymentMethod = PaymentMethodFactory.cashAppPay(),
optionsParams = null,
)

assertThat(result).isInstanceOf(ConfirmSetupIntentParams::class.java)

val params = result.asConfirmSetupIntentParams()

assertThat(params.mandateData).isEqualTo(MandateDataParams(MandateDataParams.Type.Online.DEFAULT))
}

private fun ConfirmStripeIntentParams.asConfirmSetupIntentParams(): ConfirmSetupIntentParams {
return this as ConfirmSetupIntentParams
}

private companion object {
private const val CLIENT_SECRET = "client_secret"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import com.stripe.android.paymentsheet.example.playground.settings.Country
import com.stripe.android.paymentsheet.example.playground.settings.CountrySettingsDefinition
import com.stripe.android.paymentsheet.example.playground.settings.Currency
import com.stripe.android.paymentsheet.example.playground.settings.CurrencySettingsDefinition
import com.stripe.android.paymentsheet.example.playground.settings.InitializationType
import com.stripe.android.paymentsheet.example.playground.settings.InitializationTypeSettingsDefinition
import com.stripe.android.paymentsheet.example.playground.settings.PlaygroundConfigurationData
import com.stripe.android.paymentsheet.example.playground.settings.SupportedPaymentMethodsSettingsDefinition
import com.stripe.android.paymentsheet.ui.PAYMENT_SHEET_ERROR_TEXT_TEST_TAG
import com.stripe.android.test.core.AuthorizeAction
Expand Down Expand Up @@ -87,4 +90,14 @@ internal class TestCashApp : BasePlaygroundTest() {
testParameters = testParameters,
)
}

@Test
fun testCashAppPayDeferredCscWithSfu() {
testDriver.confirmNewOrGuestComplete(
testParameters = testParameters.copyPlaygroundSettings { settings ->
settings[InitializationTypeSettingsDefinition] = InitializationType.DeferredClientSideConfirmation
settings[CheckoutModeSettingsDefinition] = CheckoutMode.PAYMENT_WITH_SETUP
},
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ internal class IntentConfirmationDefinition(
): ConfirmationDefinition.Action<Args> {
val nextStep = intentConfirmationInterceptor.intercept(
confirmationOption = confirmationOption,
intent = confirmationParameters.intent,
initializationMode = confirmationParameters.initializationMode,
shippingDetails = confirmationParameters.shippingDetails,
)
Expand Down
Loading

0 comments on commit c40b474

Please sign in to comment.