Skip to content

Commit

Permalink
Attach Mandate to PaymentOption (#9881)
Browse files Browse the repository at this point in the history
* Attach Mandate to PaymentOption

Co-authored-by: jaynewstrom-stripe <jaynewstrom@stripe.com>

* Rename UiDefinitionFactoryHelper to NullUiDefinitionFactoryHelper

* Add tests

* Add google pay test

---------

Co-authored-by: jaynewstrom-stripe <jaynewstrom@stripe.com>
  • Loading branch information
tjclawson-stripe and jaynewstrom-stripe authored Jan 9, 2025
1 parent 25f4519 commit 0333f85
Show file tree
Hide file tree
Showing 4 changed files with 169 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.stripe.android.paymentelement.embedded

import com.stripe.android.cards.CardAccountRangeRepository
import com.stripe.android.cards.CardNumber
import com.stripe.android.lpmfoundations.paymentmethod.UiDefinitionFactory
import com.stripe.android.model.AccountRange
import com.stripe.android.networking.StripeRepository
import com.stripe.android.uicore.utils.stateFlowOf
import kotlinx.coroutines.flow.StateFlow

internal object NullUiDefinitionFactoryHelper {
val nullEmbeddedUiDefinitionFactory = UiDefinitionFactory.Arguments.Factory.Default(
cardAccountRangeRepositoryFactory = NullCardAccountRangeRepositoryFactory,
linkConfigurationCoordinator = null,
onLinkInlineSignupStateChanged = {
throw IllegalStateException("Not possible.")
},
)
}

private object NullCardAccountRangeRepositoryFactory : CardAccountRangeRepository.Factory {
override fun create(): CardAccountRangeRepository {
return NullCardAccountRangeRepository
}

override fun createWithStripeRepository(
stripeRepository: StripeRepository,
publishableKey: String
): CardAccountRangeRepository {
return NullCardAccountRangeRepository
}

private object NullCardAccountRangeRepository : CardAccountRangeRepository {
override suspend fun getAccountRange(cardNumber: CardNumber.Unvalidated): AccountRange? {
return null
}

override suspend fun getAccountRanges(cardNumber: CardNumber.Unvalidated): List<AccountRange>? {
return null
}

override val loading: StateFlow<Boolean> = stateFlowOf(false)
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.stripe.android.paymentelement.embedded

import android.content.Context
import androidx.compose.ui.text.AnnotatedString
import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodMetadata
import com.stripe.android.paymentelement.EmbeddedPaymentElement
import com.stripe.android.paymentelement.ExperimentalEmbeddedPaymentElementApi
import com.stripe.android.paymentsheet.model.PaymentSelection
Expand All @@ -16,11 +18,32 @@ internal class PaymentOptionDisplayDataFactory @Inject constructor(
private val iconLoader: PaymentSelection.IconLoader,
private val context: Context,
) {
fun create(selection: PaymentSelection?): EmbeddedPaymentElement.PaymentOptionDisplayData? {
fun create(
selection: PaymentSelection?,
paymentMethodMetadata: PaymentMethodMetadata,
): EmbeddedPaymentElement.PaymentOptionDisplayData? {
if (selection == null) {
return null
}

val mandate = when (selection) {
is PaymentSelection.New -> {
paymentMethodMetadata.formElementsForCode(
code = selection.paymentMethodType,
uiDefinitionFactoryArgumentsFactory = NullUiDefinitionFactoryHelper.nullEmbeddedUiDefinitionFactory
)?.firstNotNullOfOrNull { it.mandateText }
}
is PaymentSelection.Saved -> {
selection.mandateText(
paymentMethodMetadata.merchantName,
paymentMethodMetadata.hasIntentToSetup()
)
}
is PaymentSelection.ExternalPaymentMethod -> null
PaymentSelection.GooglePay -> null
PaymentSelection.Link -> null
}

return EmbeddedPaymentElement.PaymentOptionDisplayData(
label = selection.label.resolve(context),
imageLoader = {
Expand All @@ -32,7 +55,7 @@ internal class PaymentOptionDisplayDataFactory @Inject constructor(
},
billingDetails = null,
paymentMethodType = selection.paymentMethodType,
mandateText = null,
mandateText = if (mandate == null) null else AnnotatedString(mandate.resolve(context))
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,15 @@ internal class SharedPaymentElementViewModel @Inject constructor(
init {
viewModelScope.launch {
selectionHolder.selection.collect { selection ->
_paymentOption.value = paymentOptionDisplayDataFactory.create(selection)
val state = confirmationStateHolder.state
if (state == null) {
_paymentOption.value = null
} else {
_paymentOption.value = paymentOptionDisplayDataFactory.create(
selection = selection,
paymentMethodMetadata = state.paymentMethodMetadata,
)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import com.stripe.android.common.model.asCommonConfiguration
import com.stripe.android.isInstanceOf
import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodMetadataFactory
import com.stripe.android.model.PaymentIntentFixtures
import com.stripe.android.model.PaymentMethodCreateParams
import com.stripe.android.model.PaymentMethodFixtures
import com.stripe.android.model.SetupIntentFixtures
import com.stripe.android.paymentelement.EmbeddedPaymentElement
import com.stripe.android.paymentelement.ExperimentalEmbeddedPaymentElementApi
import com.stripe.android.paymentelement.confirmation.FakeConfirmationHandler
Expand Down Expand Up @@ -303,6 +306,94 @@ internal class SharedPaymentElementViewModelTest {
assertThat(embeddedContentHelper.dataLoadedTurbine.awaitItem()).isNotNull()
}

@Test
fun `selecting lpm PaymentOption with mandate attaches mandate to paymentMethodMetadata`() {
mandateTest(
paymentSelection = PaymentMethodFixtures.CASHAPP_PAYMENT_SELECTION.copy(
paymentMethodCreateParams = PaymentMethodCreateParams(code = "cashapp", requiresMandate = true)
),
validate = {
assertThat(it?.mandateText).isNotNull()
}
)
}

@Test
fun `selecting saved card does not attach mandate to paymentMethodMetadata`() {
mandateTest(
paymentSelection = PaymentSelection.Saved(PaymentMethodFixtures.CARD_PAYMENT_METHOD),
validate = {
assertThat(it?.mandateText).isNull()
}
)
}

@Test
fun `selecting new card does attach mandate to paymentMethodMetadata`() {
mandateTest(
paymentSelection = PaymentMethodFixtures.CARD_PAYMENT_SELECTION,
validate = {
assertThat(it?.mandateText).isNotNull()
}
)
}

@Test
fun `selecting google pay does not attach mandate to paymentMethodMetadata`() {
mandateTest(
paymentSelection = PaymentSelection.GooglePay,
validate = {
assertThat(it?.mandateText).isNull()
}
)
}

private fun mandateTest(
paymentSelection: PaymentSelection,
validate: (paymentOption: EmbeddedPaymentElement.PaymentOptionDisplayData?) -> Unit
) = testScenario {
val configuration = EmbeddedPaymentElement.Configuration.Builder("Example, Inc.").build()
configurationHandler.emit(
Result.success(
PaymentElementLoader.State(
config = configuration.asCommonConfiguration(),
customer = null,
linkState = null,
paymentSelection = paymentSelection,
validationError = null,
paymentMethodMetadata = PaymentMethodMetadataFactory.create(
stripeIntent = SetupIntentFixtures.SI_SUCCEEDED.copy(
paymentMethodTypes = listOf("card", "cashapp"),
),
billingDetailsCollectionConfiguration = configuration
.billingDetailsCollectionConfiguration,
allowsDelayedPaymentMethods = configuration.allowsDelayedPaymentMethods,
allowsPaymentMethodsRequiringShippingAddress = configuration
.allowsPaymentMethodsRequiringShippingAddress,
isGooglePayReady = true,
cbcEligibility = CardBrandChoiceEligibility.Ineligible,
),
)
)
)

assertThat(selectionHolder.selection.value?.paymentMethodType).isNull()
viewModel.paymentOption.test {
assertThat(awaitItem()).isNull()

assertThat(
viewModel.configure(
PaymentSheet.IntentConfiguration(
PaymentSheet.IntentConfiguration.Mode.Setup("USD"),
),
configuration = configuration,
)
).isInstanceOf<EmbeddedPaymentElement.ConfigureResult.Succeeded>()
validate.invoke(awaitItem())
}
assertThat(embeddedContentHelper.dataLoadedTurbine.awaitItem()).isNotNull()
}

private fun testScenario(
block: suspend Scenario.() -> Unit,
) = runTest {
Expand Down

0 comments on commit 0333f85

Please sign in to comment.