Skip to content

Commit

Permalink
Show promo badge in vertical mode form header (#9895)
Browse files Browse the repository at this point in the history
* Show promo badge in vertical mode form header

* Address code review feedback

Add `takeIfMatches()` method to `PaymentMethodIncentive`.
  • Loading branch information
tillh-stripe authored Jan 14, 2025
1 parent c7c5595 commit 1faaf8d
Show file tree
Hide file tree
Showing 18 changed files with 100 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ internal data class FormHeaderInformation(
val lightThemeIconUrl: String?,
val darkThemeIconUrl: String?,
val iconRequiresTinting: Boolean,
val promoBadge: String?,
)
Original file line number Diff line number Diff line change
Expand Up @@ -73,14 +73,17 @@ internal data class SupportedPaymentMethod(
subtitle = subtitle,
)

fun asFormHeaderInformation(): FormHeaderInformation {
fun asFormHeaderInformation(
incentive: PaymentMethodIncentive?,
): FormHeaderInformation {
return FormHeaderInformation(
displayName = displayName,
shouldShowIcon = true,
iconResource = iconResource,
lightThemeIconUrl = lightThemeIconUrl,
darkThemeIconUrl = darkThemeIconUrl,
iconRequiresTinting = iconRequiresTinting,
promoBadge = incentive?.displayText,
)
}

Expand All @@ -107,7 +110,7 @@ internal data class SupportedPaymentMethod(
darkThemeIconUrl = darkThemeIconUrl,
iconRequiresTinting = iconRequiresTinting,
subtitle = subtitle,
promoBadge = incentive?.takeIf { it.matches(code) }?.displayText,
promoBadge = incentive?.displayText,
onClick = onClick,
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,8 @@ internal data class PaymentMethodMetadata(
): FormHeaderInformation? {
return if (isExternalPaymentMethod(code)) {
getUiDefinitionFactoryForExternalPaymentMethod(code)?.createFormHeaderInformation(
customerHasSavedPaymentMethods = customerHasSavedPaymentMethods
customerHasSavedPaymentMethods = customerHasSavedPaymentMethods,
incentive = null,
)
} else {
val definition = supportedPaymentMethodDefinitions().firstOrNull { it.type.code == code } ?: return null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import com.stripe.android.model.PaymentMethodCreateParams
import com.stripe.android.model.PaymentMethodExtraParams
import com.stripe.android.paymentsheet.PaymentSheet
import com.stripe.android.paymentsheet.addresselement.toIdentifierMap
import com.stripe.android.paymentsheet.model.PaymentMethodIncentive
import com.stripe.android.ui.core.cbc.CardBrandChoiceEligibility
import com.stripe.android.ui.core.elements.SharedDataSpec
import com.stripe.android.uicore.elements.FormElement
Expand Down Expand Up @@ -75,8 +76,11 @@ internal sealed interface UiDefinitionFactory {
sharedDataSpec: SharedDataSpec,
): SupportedPaymentMethod

fun createFormHeaderInformation(sharedDataSpec: SharedDataSpec): FormHeaderInformation {
return createSupportedPaymentMethod(sharedDataSpec).asFormHeaderInformation()
fun createFormHeaderInformation(
sharedDataSpec: SharedDataSpec,
incentive: PaymentMethodIncentive?,
): FormHeaderInformation {
return createSupportedPaymentMethod(sharedDataSpec).asFormHeaderInformation(incentive)
}

fun createFormElements(
Expand All @@ -93,8 +97,11 @@ internal sealed interface UiDefinitionFactory {
interface Simple : UiDefinitionFactory {
fun createSupportedPaymentMethod(): SupportedPaymentMethod

fun createFormHeaderInformation(customerHasSavedPaymentMethods: Boolean): FormHeaderInformation {
return createSupportedPaymentMethod().asFormHeaderInformation()
fun createFormHeaderInformation(
customerHasSavedPaymentMethods: Boolean,
incentive: PaymentMethodIncentive?,
): FormHeaderInformation {
return createSupportedPaymentMethod().asFormHeaderInformation(incentive)
}

fun createFormElements(metadata: PaymentMethodMetadata, arguments: Arguments): List<FormElement>
Expand Down Expand Up @@ -138,13 +145,19 @@ internal sealed interface UiDefinitionFactory {
customerHasSavedPaymentMethods: Boolean,
): FormHeaderInformation? = when (this) {
is Simple -> {
createFormHeaderInformation(customerHasSavedPaymentMethods = customerHasSavedPaymentMethods)
createFormHeaderInformation(
customerHasSavedPaymentMethods = customerHasSavedPaymentMethods,
incentive = metadata.paymentMethodIncentive,
)
}

is RequiresSharedDataSpec -> {
val sharedDataSpec = sharedDataSpecs.firstOrNull { it.type == definition.type.code }
if (sharedDataSpec != null) {
createFormHeaderInformation(sharedDataSpec)
createFormHeaderInformation(
sharedDataSpec = sharedDataSpec,
incentive = metadata.paymentMethodIncentive,
)
} else {
null
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import com.stripe.android.lpmfoundations.paymentmethod.UiDefinitionFactory
import com.stripe.android.lpmfoundations.paymentmethod.link.LinkFormElement
import com.stripe.android.model.PaymentMethod
import com.stripe.android.paymentsheet.PaymentSheet
import com.stripe.android.paymentsheet.model.PaymentMethodIncentive
import com.stripe.android.ui.core.BillingDetailsCollectionConfiguration
import com.stripe.android.ui.core.elements.CardBillingAddressElement
import com.stripe.android.ui.core.elements.CardDetailsSectionElement
Expand Down Expand Up @@ -52,13 +53,16 @@ private object CardUiDefinitionFactory : UiDefinitionFactory.Simple {
iconRequiresTinting = true,
)

override fun createFormHeaderInformation(customerHasSavedPaymentMethods: Boolean): FormHeaderInformation {
override fun createFormHeaderInformation(
customerHasSavedPaymentMethods: Boolean,
incentive: PaymentMethodIncentive?,
): FormHeaderInformation {
val displayName = if (customerHasSavedPaymentMethods) {
PaymentsUiCoreR.string.stripe_paymentsheet_add_new_card
} else {
PaymentsUiCoreR.string.stripe_paymentsheet_add_card
}
return createSupportedPaymentMethod().asFormHeaderInformation().copy(
return createSupportedPaymentMethod().asFormHeaderInformation(incentive).copy(
displayName = displayName.resolvableString,
shouldShowIcon = false,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodDefinition
import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodMetadata
import com.stripe.android.lpmfoundations.paymentmethod.UiDefinitionFactory
import com.stripe.android.model.PaymentMethod
import com.stripe.android.paymentsheet.model.PaymentMethodIncentive
import com.stripe.android.ui.core.R
import com.stripe.android.uicore.elements.FormElement

Expand Down Expand Up @@ -39,8 +40,11 @@ private object UsBankAccountUiDefinitionFactory : UiDefinitionFactory.Simple {
darkThemeIconUrl = null,
)

override fun createFormHeaderInformation(customerHasSavedPaymentMethods: Boolean): FormHeaderInformation {
return createSupportedPaymentMethod().asFormHeaderInformation().copy(
override fun createFormHeaderInformation(
customerHasSavedPaymentMethods: Boolean,
incentive: PaymentMethodIncentive?,
): FormHeaderInformation {
return createSupportedPaymentMethod().asFormHeaderInformation(incentive).copy(
displayName = R.string.stripe_paymentsheet_add_us_bank_account.resolvableString,
shouldShowIcon = false,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ internal data class PaymentMethodIncentive(
val displayText: String,
) : Parcelable {

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ internal fun NewPaymentMethodTabLayoutUI(
isSelected = index == selectedIndex,
isEnabled = isEnabled,
iconRequiresTinting = item.iconRequiresTinting,
promoBadge = incentive?.takeIf { it.matches(item.code) }?.displayText,
promoBadge = incentive?.takeIfMatches(item.code)?.displayText,
onItemSelectedListener = {
onItemSelectedListener(paymentMethods[index])
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ internal class BankFormInteractor(

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

// TODO(tillh-stripe): Update incentive badge here
paymentMethodIncentiveInteractor.setEligible(selection == null)
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ internal class PaymentMethodIncentiveInteractor(
private val _displayedIncentive = MutableStateFlow(incentive)
val displayedIncentive: StateFlow<PaymentMethodIncentive?> = _displayedIncentive.asStateFlow()

fun setEligible(eligible: Boolean) {
_displayedIncentive.value = if (eligible) incentive else null
}

companion object {

fun create(viewModel: BaseSheetViewModel): PaymentMethodIncentiveInteractor {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,8 @@ internal class DefaultPaymentMethodVerticalLayoutInteractor(
incentive: PaymentMethodIncentive?,
): List<DisplayablePaymentMethod> {
val lpms = supportedPaymentMethods.map { supportedPaymentMethod ->
supportedPaymentMethod.asDisplayablePaymentMethod(paymentMethods, incentive) {
val paymentMethodIncentive = incentive?.takeIfMatches(supportedPaymentMethod.code)
supportedPaymentMethod.asDisplayablePaymentMethod(paymentMethods, paymentMethodIncentive) {
handleViewAction(ViewAction.PaymentMethodSelected(supportedPaymentMethod.code))
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ import com.stripe.android.paymentsheet.CustomerStateHolder
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.paymentdatacollection.FormArguments
import com.stripe.android.paymentsheet.paymentdatacollection.ach.USBankAccountFormArguments
import com.stripe.android.paymentsheet.viewmodels.BaseSheetViewModel
import com.stripe.android.uicore.elements.FormElement
import com.stripe.android.uicore.utils.mapAsStateFlow
import com.stripe.android.uicore.utils.combineAsStateFlow
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
Expand Down Expand Up @@ -55,16 +56,22 @@ internal class DefaultVerticalModeFormInteractor(
private val canGoBackDelegate: () -> Boolean,
override val isLiveMode: Boolean,
processing: StateFlow<Boolean>,
paymentMethodIncentive: StateFlow<PaymentMethodIncentive?>,
private val coroutineScope: CoroutineScope,
) : VerticalModeFormInteractor {
override val state: StateFlow<VerticalModeFormInteractor.State> = processing.mapAsStateFlow { isProcessing ->
override val state: StateFlow<VerticalModeFormInteractor.State> = combineAsStateFlow(
processing,
paymentMethodIncentive,
) { isProcessing, paymentMethodIncentive ->
VerticalModeFormInteractor.State(
selectedPaymentMethodCode = selectedPaymentMethodCode,
isProcessing = isProcessing,
usBankAccountFormArguments = usBankAccountArguments,
formArguments = formArguments,
formElements = formElements,
headerInformation = headerInformation,
headerInformation = headerInformation?.copy(
promoBadge = paymentMethodIncentive?.takeIfMatches(selectedPaymentMethodCode)?.displayText,
),
)
}

Expand Down Expand Up @@ -122,6 +129,7 @@ internal class DefaultVerticalModeFormInteractor(
isLiveMode = paymentMethodMetadata.stripeIntent.isLiveMode,
canGoBackDelegate = { viewModel.navigationHandler.canGoBack },
processing = viewModel.processing,
paymentMethodIncentive = bankFormInteractor.paymentMethodIncentiveInteractor.displayedIncentive,
reportFieldInteraction = viewModel.analyticsListener::reportFieldInteraction,
coroutineScope = coroutineScope,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import com.stripe.android.lpmfoundations.FormHeaderInformation
import com.stripe.android.paymentsheet.R
import com.stripe.android.paymentsheet.ui.FormElement
import com.stripe.android.paymentsheet.ui.PaymentMethodIcon
import com.stripe.android.paymentsheet.ui.PromoBadge
import com.stripe.android.uicore.image.StripeImageLoader
import com.stripe.android.uicore.strings.resolve
import com.stripe.android.uicore.stripeColors
Expand Down Expand Up @@ -109,5 +110,12 @@ internal fun VerticalModeFormHeaderUI(
overflow = TextOverflow.Ellipsis,
modifier = Modifier.testTag(TEST_TAG_HEADER_TITLE)
)

if (formHeaderInformation.promoBadge != null) {
PromoBadge(
text = formHeaderInformation.promoBadge,
modifier = Modifier.padding(start = 12.dp),
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ class PaymentMethodIncentiveTest {
displayText = "$5"
)

val matchesLink = paymentMethodIncentive.matches(PaymentMethod.Type.Link.code)
assertThat(matchesLink).isTrue()
val applicableIncentive = paymentMethodIncentive.takeIfMatches(PaymentMethod.Type.Link.code)
assertThat(applicableIncentive).isNotNull()
}

@Test
Expand All @@ -24,8 +24,8 @@ class PaymentMethodIncentiveTest {
displayText = "$5"
)

val matchesLink = paymentMethodIncentive.matches(PaymentMethod.Type.USBankAccount.code)
assertThat(matchesLink).isFalse()
val applicableIncentive = paymentMethodIncentive.takeIfMatches(PaymentMethod.Type.USBankAccount.code)
assertThat(applicableIncentive).isNull()
}

@Test
Expand All @@ -35,10 +35,10 @@ class PaymentMethodIncentiveTest {
displayText = "$5"
)

val matchesAny = PaymentMethod.Type.entries.any {
paymentMethodIncentive.matches(it.code)
val matches = PaymentMethod.Type.entries.mapNotNull {
paymentMethodIncentive.takeIfMatches(it.code)
}

assertThat(matchesAny).isFalse()
assertThat(matches).isEmpty()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ 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.VerticalModeFormInteractor.ViewAction
import com.stripe.android.uicore.utils.stateFlowOf
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.UnconfinedTestDispatcher
Expand Down Expand Up @@ -98,6 +99,7 @@ internal class DefaultVerticalModeFormInteractorTest {
canGoBackDelegate = canGoBackDelegate,
processing = processing,
coroutineScope = CoroutineScope(UnconfinedTestDispatcher()),
paymentMethodIncentive = stateFlowOf(null),
)

TestParams(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ internal class VerticalModeFormHeaderUITest {
lightThemeIconUrl = null,
darkThemeIconUrl = null,
iconRequiresTinting = false,
promoBadge = null,
)
)
}
Expand All @@ -40,6 +41,25 @@ internal class VerticalModeFormHeaderUITest {
lightThemeIconUrl = null,
darkThemeIconUrl = null,
iconRequiresTinting = false,
promoBadge = null,
)
)
}
}

@Test
fun testBank() {
paparazziRule.snapshot {
VerticalModeFormHeaderUI(
isEnabled = true,
formHeaderInformation = FormHeaderInformation(
displayName = "Bank".resolvableString,
shouldShowIcon = true,
iconResource = R.drawable.stripe_ic_paymentsheet_pm_bank,
lightThemeIconUrl = null,
darkThemeIconUrl = null,
iconRequiresTinting = false,
promoBadge = "$5",
)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ internal class VerticalModeFormUITest {
val headerInformation =
(CardDefinition.uiDefinitionFactory() as UiDefinitionFactory.Simple).createFormHeaderInformation(
customerHasSavedPaymentMethods = customerHasSavedPaymentMethods,
incentive = null,
)
return VerticalModeFormInteractor.State(
selectedPaymentMethodCode = PaymentMethod.Type.Card.code,
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 1faaf8d

Please sign in to comment.