Skip to content

Commit

Permalink
Add promo badge for incentives in MPE (#9662)
Browse files Browse the repository at this point in the history
* Add promo badge for incentives in MPE

* Address code review feedback

- Add tests for PaymentMethodIncentive logic
- Add tests for vertical layout interactor
  • Loading branch information
tillh-stripe authored Dec 2, 2024
1 parent 865db79 commit a11400b
Show file tree
Hide file tree
Showing 31 changed files with 295 additions and 27 deletions.
8 changes: 8 additions & 0 deletions paymentsheet/api/paymentsheet.api
Original file line number Diff line number Diff line change
Expand Up @@ -1822,6 +1822,14 @@ public final class com/stripe/android/paymentsheet/model/PaymentIntentClientSecr
public synthetic fun newArray (I)[Ljava/lang/Object;
}

public final class com/stripe/android/paymentsheet/model/PaymentMethodIncentive$Creator : android/os/Parcelable$Creator {
public fun <init> ()V
public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/paymentsheet/model/PaymentMethodIncentive;
public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object;
public final fun newArray (I)[Lcom/stripe/android/paymentsheet/model/PaymentMethodIncentive;
public synthetic fun newArray (I)[Ljava/lang/Object;
}

public final class com/stripe/android/paymentsheet/model/PaymentOption {
public static final field $stable I
public fun <init> (ILjava/lang/String;)V
Expand Down
3 changes: 2 additions & 1 deletion paymentsheet/detekt-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@
<ID>LongMethod:EditPaymentMethod.kt$@Composable internal fun EditPaymentMethodUi( viewState: EditPaymentMethodViewState, viewActionHandler: (action: EditPaymentMethodViewAction) -> Unit, modifier: Modifier = Modifier )</ID>
<ID>LongMethod:FormViewModelTest.kt$FormViewModelTest$@Test fun `Verify params are set when element address fields are complete`()</ID>
<ID>LongMethod:FormViewModelTest.kt$FormViewModelTest$@Test fun `Verify params are set when required address fields are complete`()</ID>
<ID>LongMethod:PaymentMethodVerticalLayoutInteractor.kt$DefaultPaymentMethodVerticalLayoutInteractor.Companion$fun create( viewModel: BaseSheetViewModel, paymentMethodMetadata: PaymentMethodMetadata, customerStateHolder: CustomerStateHolder, ): PaymentMethodVerticalLayoutInteractor</ID>
<ID>LongMethod:PaymentMethodMetadataTest.kt$PaymentMethodMetadataTest$@OptIn(ExperimentalCardBrandFilteringApi::class) @Test fun `should create metadata properly with elements session response, payment sheet config, and data specs`()</ID>
<ID>LongMethod:PaymentMethodVerticalLayoutInteractor.kt$DefaultPaymentMethodVerticalLayoutInteractor.Companion$fun create( viewModel: BaseSheetViewModel, paymentMethodMetadata: PaymentMethodMetadata, customerStateHolder: CustomerStateHolder, bankFormInteractor: BankFormInteractor, ): PaymentMethodVerticalLayoutInteractor</ID>
<ID>LongMethod:PaymentSheetConfigurationKtx.kt$internal fun PaymentSheet.Appearance.parseAppearance()</ID>
<ID>LongMethod:PaymentSheetScreen.kt$@Composable private fun PaymentSheetContent( viewModel: BaseSheetViewModel, headerText: ResolvableString?, walletsState: WalletsState?, walletsProcessingState: WalletsProcessingState?, error: ResolvableString?, currentScreen: PaymentSheetScreen, mandateText: MandateText?, modifier: Modifier )</ID>
<ID>LongMethod:PaymentSheetViewModelTest.kt$PaymentSheetViewModelTest$@Test fun `Can complete payment after switching to another LPM from card selection with inline Link signup state`()</ID>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ internal fun AddPaymentMethod(
},
formArguments = viewState.formArguments,
usBankAccountFormArguments = viewState.usBankAccountFormArguments,
incentive = null,
onFormFieldValuesChanged = {
// This only gets emitted if form field values are complete
viewActionHandler(CustomerSheetViewAction.OnFormFieldValuesCompleted(it))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodDefinition
import com.stripe.android.model.PaymentMethod
import com.stripe.android.model.PaymentMethodCode
import com.stripe.android.paymentsheet.R
import com.stripe.android.paymentsheet.model.PaymentMethodIncentive
import com.stripe.android.paymentsheet.verticalmode.DisplayablePaymentMethod
import com.stripe.android.ui.core.elements.SharedDataSpec

Expand Down Expand Up @@ -85,6 +86,7 @@ internal data class SupportedPaymentMethod(

fun asDisplayablePaymentMethod(
customerSavedPaymentMethods: List<PaymentMethod>,
incentive: PaymentMethodIncentive?,
onClick: () -> Unit,
): DisplayablePaymentMethod {
fun isTypeAndHasCustomerSavedPaymentMethodsOfType(type: PaymentMethod.Type): Boolean {
Expand All @@ -105,6 +107,7 @@ internal data class SupportedPaymentMethod(
darkThemeIconUrl = darkThemeIconUrl,
iconRequiresTinting = iconRequiresTinting,
subtitle = subtitle,
promoBadge = incentive?.takeIf { it.matches(code) }?.displayText,
onClick = onClick,
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ import com.stripe.android.payments.financialconnections.DefaultIsFinancialConnec
import com.stripe.android.payments.financialconnections.IsFinancialConnectionsAvailable
import com.stripe.android.paymentsheet.PaymentSheet
import com.stripe.android.paymentsheet.addresselement.AddressDetails
import com.stripe.android.paymentsheet.model.PaymentMethodIncentive
import com.stripe.android.paymentsheet.model.PaymentSelection
import com.stripe.android.paymentsheet.model.toPaymentMethodIncentive
import com.stripe.android.ui.core.Amount
import com.stripe.android.ui.core.cbc.CardBrandChoiceEligibility
import com.stripe.android.ui.core.elements.ExternalPaymentMethodSpec
Expand Down Expand Up @@ -49,6 +51,7 @@ internal data class PaymentMethodMetadata(
val linkInlineConfiguration: LinkInlineConfiguration?,
val paymentMethodSaveConsentBehavior: PaymentMethodSaveConsentBehavior,
val linkMode: LinkMode?,
val paymentMethodIncentive: PaymentMethodIncentive?,
val financialConnectionsAvailable: Boolean = DefaultIsFinancialConnectionsAvailable(),
val cardBrandFilter: CardBrandFilter,
) : Parcelable {
Expand Down Expand Up @@ -231,6 +234,7 @@ internal data class PaymentMethodMetadata(
isGooglePayReady: Boolean,
linkInlineConfiguration: LinkInlineConfiguration?,
): PaymentMethodMetadata {
val linkSettings = elementsSession.linkSettings
return PaymentMethodMetadata(
stripeIntent = elementsSession.stripeIntent,
billingDetailsCollectionConfiguration = configuration.billingDetailsCollectionConfiguration,
Expand All @@ -250,7 +254,8 @@ internal data class PaymentMethodMetadata(
externalPaymentMethodSpecs = externalPaymentMethodSpecs,
paymentMethodSaveConsentBehavior = elementsSession.toPaymentSheetSaveConsentBehavior(),
linkInlineConfiguration = linkInlineConfiguration,
linkMode = elementsSession.linkSettings?.linkMode,
linkMode = linkSettings?.linkMode,
paymentMethodIncentive = linkSettings?.linkConsumerIncentive?.toPaymentMethodIncentive(),
isGooglePayReady = isGooglePayReady,
cardBrandFilter = PaymentSheetCardBrandFilter(configuration.cardBrandAcceptance)
)
Expand Down Expand Up @@ -284,6 +289,7 @@ internal data class PaymentMethodMetadata(
financialConnectionsAvailable = isFinancialConnectionsAvailable(),
paymentMethodSaveConsentBehavior = paymentMethodSaveConsentBehavior,
linkMode = elementsSession.linkSettings?.linkMode,
paymentMethodIncentive = null,
externalPaymentMethodSpecs = emptyList(),
cardBrandFilter = PaymentSheetCardBrandFilter(configuration.cardBrandAcceptance)
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.stripe.android.paymentsheet.model

import android.os.Parcelable
import com.stripe.android.model.LinkConsumerIncentive
import com.stripe.android.model.PaymentMethod
import com.stripe.android.model.PaymentMethodCode
import kotlinx.parcelize.Parcelize

@Parcelize
internal data class PaymentMethodIncentive(
private val identifier: String,
val displayText: String,
) : Parcelable {

fun matches(code: PaymentMethodCode): Boolean {
return identifier == "link_instant_debits" && code == PaymentMethod.Type.Link.code
}
}

internal fun LinkConsumerIncentive.toPaymentMethodIncentive(): PaymentMethodIncentive? {
return incentiveDisplayText?.let { displayText ->
PaymentMethodIncentive(
identifier = incentiveParams.paymentMethod,
displayText = displayText,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import com.stripe.android.paymentsheet.addresselement.AddressDetails
import com.stripe.android.paymentsheet.model.PaymentSelection
import com.stripe.android.paymentsheet.state.PaymentElementLoader
import com.stripe.android.paymentsheet.ui.PrimaryButton
import com.stripe.android.paymentsheet.verticalmode.BankFormInteractor
import com.stripe.android.paymentsheet.viewmodels.BaseSheetViewModel
import kotlinx.coroutines.flow.update

Expand Down Expand Up @@ -61,6 +62,7 @@ internal class USBankAccountFormArguments(
paymentMethodMetadata: PaymentMethodMetadata,
hostedSurface: String,
selectedPaymentMethodCode: String,
bankFormInteractor: BankFormInteractor,
): USBankAccountFormArguments {
val isSaveForFutureUseValueChangeable = isSaveForFutureUseValueChangeable(
code = selectedPaymentMethodCode,
Expand Down Expand Up @@ -91,7 +93,7 @@ internal class USBankAccountFormArguments(
shippingDetails = viewModel.config.shippingDetails,
draftPaymentSelection = viewModel.newPaymentSelection?.paymentSelection,
onMandateTextChanged = viewModel.mandateHandler::updateMandateText,
onLinkedBankAccountChanged = viewModel::handleLinkedBankAccountChanged,
onLinkedBankAccountChanged = bankFormInteractor::handleLinkedBankAccountChanged,
onUpdatePrimaryButtonUIState = { viewModel.customPrimaryButtonUiState.update(it) },
onUpdatePrimaryButtonState = viewModel::updatePrimaryButtonState,
onError = viewModel::onError
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ internal fun AddPaymentMethod(
enabled = !state.processing,
supportedPaymentMethods = state.supportedPaymentMethods,
selectedItemCode = state.selectedPaymentMethodCode,
incentive = state.incentive,
formElements = state.formElements,
onItemSelectedListener = { selectedLpm ->
interactor.handleViewAction(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ import com.stripe.android.payments.bankaccount.CollectBankAccountLauncher
import com.stripe.android.paymentsheet.FormHelper
import com.stripe.android.paymentsheet.LinkInlineHandler
import com.stripe.android.paymentsheet.forms.FormFieldValues
import com.stripe.android.paymentsheet.model.PaymentMethodIncentive
import com.stripe.android.paymentsheet.model.PaymentSelection
import com.stripe.android.paymentsheet.paymentdatacollection.FormArguments
import com.stripe.android.paymentsheet.paymentdatacollection.ach.USBankAccountFormArguments
import com.stripe.android.paymentsheet.verticalmode.BankFormInteractor
import com.stripe.android.paymentsheet.viewmodels.BaseSheetViewModel
import com.stripe.android.uicore.elements.FormElement
import kotlinx.coroutines.CoroutineScope
Expand All @@ -36,6 +38,7 @@ internal interface AddPaymentMethodInteractor {
val formElements: List<FormElement>,
val paymentSelection: PaymentSelection?,
val processing: Boolean,
val incentive: PaymentMethodIncentive?,
val usBankAccountFormArguments: USBankAccountFormArguments,
)

Expand All @@ -54,6 +57,7 @@ internal class DefaultAddPaymentMethodInteractor(
private val initiallySelectedPaymentMethodType: PaymentMethodCode,
private val selection: StateFlow<PaymentSelection?>,
private val processing: StateFlow<Boolean>,
private val incentive: StateFlow<PaymentMethodIncentive?>,
private val supportedPaymentMethods: List<SupportedPaymentMethod>,
private val createFormArguments: (PaymentMethodCode) -> FormArguments,
private val formElementsForCode: (PaymentMethodCode) -> List<FormElement>,
Expand All @@ -78,10 +82,13 @@ internal class DefaultAddPaymentMethodInteractor(
linkInlineHandler = linkInlineHandler,
paymentMethodMetadata = paymentMethodMetadata
)
val bankFormInteractor = BankFormInteractor.create(viewModel)

return DefaultAddPaymentMethodInteractor(
initiallySelectedPaymentMethodType = viewModel.initiallySelectedPaymentMethodType,
selection = viewModel.selection,
processing = viewModel.processing,
incentive = bankFormInteractor.paymentMethodIncentiveInteractor.displayedIncentive,
supportedPaymentMethods = paymentMethodMetadata.sortedSupportedPaymentMethods(),
createFormArguments = formHelper::createFormArguments,
formElementsForCode = formHelper::formElementsForCode,
Expand All @@ -95,6 +102,7 @@ internal class DefaultAddPaymentMethodInteractor(
paymentMethodMetadata = paymentMethodMetadata,
hostedSurface = CollectBankAccountLauncher.HOSTED_SURFACE_PAYMENT_ELEMENT,
selectedPaymentMethodCode = it,
bankFormInteractor = bankFormInteractor,
)
},
coroutineScope = coroutineScope,
Expand Down Expand Up @@ -122,6 +130,7 @@ internal class DefaultAddPaymentMethodInteractor(
formElements = formElementsForCode(selectedPaymentMethodCode),
paymentSelection = selection.value,
processing = processing.value,
incentive = incentive.value,
usBankAccountFormArguments = createUSBankAccountFormArguments(selectedPaymentMethodCode),
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.stripe.android.lpmfoundations.luxe.SupportedPaymentMethod
import com.stripe.android.paymentsheet.model.PaymentMethodIncentive
import com.stripe.android.uicore.image.StripeImageLoader
import com.stripe.android.uicore.strings.resolve

Expand All @@ -37,6 +38,7 @@ internal fun NewPaymentMethodTabLayoutUI(
paymentMethods: List<SupportedPaymentMethod>,
selectedIndex: Int,
isEnabled: Boolean,
incentive: PaymentMethodIncentive?,
onItemSelectedListener: (SupportedPaymentMethod) -> Unit,
imageLoader: StripeImageLoader,
modifier: Modifier = Modifier,
Expand Down Expand Up @@ -88,7 +90,7 @@ internal fun NewPaymentMethodTabLayoutUI(
isSelected = index == selectedIndex,
isEnabled = isEnabled,
iconRequiresTinting = item.iconRequiresTinting,
promoBadge = null,
promoBadge = incentive?.takeIf { it.matches(item.code) }?.displayText,
onItemSelectedListener = {
onItemSelectedListener(paymentMethods[index])
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import com.stripe.android.model.PaymentMethod.Type.USBankAccount
import com.stripe.android.model.PaymentMethodCode
import com.stripe.android.paymentsheet.R
import com.stripe.android.paymentsheet.forms.FormFieldValues
import com.stripe.android.paymentsheet.model.PaymentMethodIncentive
import com.stripe.android.paymentsheet.paymentdatacollection.FormArguments
import com.stripe.android.paymentsheet.paymentdatacollection.ach.USBankAccountForm
import com.stripe.android.paymentsheet.paymentdatacollection.ach.USBankAccountFormArguments
Expand All @@ -36,6 +37,7 @@ internal fun PaymentElement(
enabled: Boolean,
supportedPaymentMethods: List<SupportedPaymentMethod>,
selectedItemCode: PaymentMethodCode,
incentive: PaymentMethodIncentive?,
formElements: List<FormElement>,
onItemSelectedListener: (SupportedPaymentMethod) -> Unit,
formArguments: FormArguments,
Expand Down Expand Up @@ -66,6 +68,7 @@ internal fun PaymentElement(
selectedIndex = selectedIndex,
isEnabled = enabled,
paymentMethods = supportedPaymentMethods,
incentive = incentive,
onItemSelectedListener = onItemSelectedListener,
imageLoader = imageLoader,
modifier = Modifier.padding(bottom = 12.dp),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.stripe.android.paymentsheet.verticalmode

import com.stripe.android.paymentsheet.model.PaymentSelection
import com.stripe.android.paymentsheet.viewmodels.BaseSheetViewModel

internal class BankFormInteractor(
private val updateSelection: (PaymentSelection.New.USBankAccount?) -> Unit,
val paymentMethodIncentiveInteractor: PaymentMethodIncentiveInteractor,
) {

fun handleLinkedBankAccountChanged(selection: PaymentSelection.New.USBankAccount?) {
updateSelection(selection)

// TODO(tillh-stripe): Update incentive badge here
}

companion object {

fun create(viewModel: BaseSheetViewModel): BankFormInteractor {
return BankFormInteractor(
updateSelection = viewModel::updateSelection,
paymentMethodIncentiveInteractor = PaymentMethodIncentiveInteractor.create(viewModel),
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.stripe.android.paymentsheet.verticalmode

import com.stripe.android.paymentsheet.model.PaymentMethodIncentive
import com.stripe.android.paymentsheet.viewmodels.BaseSheetViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow

internal class PaymentMethodIncentiveInteractor(
private val incentive: PaymentMethodIncentive?,
) {

private val _displayedIncentive = MutableStateFlow(incentive)
val displayedIncentive: StateFlow<PaymentMethodIncentive?> = _displayedIncentive.asStateFlow()

companion object {

fun create(viewModel: BaseSheetViewModel): PaymentMethodIncentiveInteractor {
return PaymentMethodIncentiveInteractor(
incentive = viewModel.paymentMethodMetadata.value?.paymentMethodIncentive,
)
}
}
}
Loading

0 comments on commit a11400b

Please sign in to comment.