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

Show promo badge in bank form #9734

Merged
merged 2 commits into from
Dec 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion paymentsheet/detekt-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
<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>
<ID>LongMethod:PlaceholderHelperTest.kt$PlaceholderHelperTest$@Test fun `Test correct placeholder is removed for placeholder spec`()</ID>
<ID>LongMethod:USBankAccountForm.kt$@Composable private fun AccountDetailsForm( modifier: Modifier = Modifier, showCheckbox: Boolean, isProcessing: Boolean, bankName: String?, last4: String?, saveForFutureUseElement: SaveForFutureUseElement, onRemoveAccount: () -> Unit, )</ID>
<ID>LongMethod:USBankAccountForm.kt$@Composable private fun AccountDetailsForm( modifier: Modifier = Modifier, showCheckbox: Boolean, isProcessing: Boolean, bankName: String?, last4: String?, promoBadgeState: PromoBadgeState?, saveForFutureUseElement: SaveForFutureUseElement, onRemoveAccount: () -> Unit, )</ID>
<ID>LongMethod:USBankAccountForm.kt$@Composable private fun BillingDetailsForm( instantDebits: Boolean, formArgs: FormArguments, isProcessing: Boolean, isPaymentFlow: Boolean, nameController: TextFieldController, emailController: TextFieldController, phoneController: PhoneNumberController, addressController: AddressController, lastTextFieldIdentifier: IdentifierSpec?, sameAsShippingElement: SameAsShippingElement?, )</ID>
<ID>LongMethod:USBankAccountFormViewModel.kt$USBankAccountFormViewModel$private fun createNewPaymentSelection( resultIdentifier: ResultIdentifier, last4: String?, bankName: String?, billingDetails: PaymentMethod.BillingDetails, ): PaymentSelection.New.USBankAccount</ID>
<ID>MagicNumber:BaseSheetActivity.kt$BaseSheetActivity$30</ID>
Expand Down
2 changes: 2 additions & 0 deletions paymentsheet/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@
<string name="stripe_paymentsheet_bank_payment_promo_for_payment"><![CDATA[Get %s back when you pay by bank. <a href=\"https://link.com/promotion-terms\">See&#160;terms</a>]]></string>
<!-- Text shown in the bank payment form when a promo is available and the user is setting up an account, e.g. 'Get $5 back when you pay by bank. See terms'. Note that 'See terms' contains a non-breaking space. -->
<string name="stripe_paymentsheet_bank_payment_promo_for_setup"><![CDATA[Get %s back when you pay for the first time with your bank. <a href=\"https://link.com/promotion-terms\">See&#160;terms</a>]]></string>
<!-- Label shown when paying with a bank account is not eligible for the incentive, e.g. 'No $5 promo' -->
<string name="stripe_paymentsheet_bank_payment_promo_ineligible">No %s promo</string>
<!-- Text displayed on manage card screen when card details are not editable. -->
<string name="stripe_paymentsheet_card_details_cannot_be_changed">Card details cannot be changed.</string>
<!-- Text displayed below a credit card entry form when the card will be saved to make future payments. -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,18 @@ internal data class BankFormScreenState(
val isProcessing: Boolean
get() = _isProcessing && linkedBankAccount == null

val promoBadgeState: PromoBadgeState?
get() = if (promoText != null && linkedBankAccount != null) {
PromoBadgeState(promoText, eligible = linkedBankAccount.eligibleForIncentive)
} else {
null
}

val promoDisclaimerText: ResolvableString?
get() = promoText?.let {
if (isPaymentFlow) {
if (linkedBankAccount?.eligibleForIncentive == false) {
return null
} else if (isPaymentFlow) {
resolvableString(R.string.stripe_paymentsheet_bank_payment_promo_for_payment, it)
} else {
resolvableString(R.string.stripe_paymentsheet_bank_payment_promo_for_setup, it)
Expand All @@ -41,8 +50,14 @@ internal data class BankFormScreenState(
val financialConnectionsSessionId: String?,
val mandateText: ResolvableString,
val isVerifyingWithMicrodeposits: Boolean,
val eligibleForIncentive: Boolean = false,
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is currently hard-coded to false, but will eventually be set in the bank auth flow.

) : Parcelable

data class PromoBadgeState(
val promoText: String,
val eligible: Boolean,
)

sealed interface ResultIdentifier : Parcelable {
@Parcelize
data class Session(val id: String) : ResultIdentifier
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,17 @@ import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.stripe.android.paymentsheet.PaymentSheet.BillingDetailsCollectionConfiguration.AddressCollectionMode
import com.stripe.android.paymentsheet.PaymentSheet.BillingDetailsCollectionConfiguration.CollectionMode
import com.stripe.android.paymentsheet.R
import com.stripe.android.paymentsheet.model.PaymentSelection.New
import com.stripe.android.paymentsheet.paymentdatacollection.FormArguments
import com.stripe.android.paymentsheet.paymentdatacollection.ach.BankFormScreenState.PromoBadgeState
import com.stripe.android.paymentsheet.ui.Mandate
import com.stripe.android.paymentsheet.ui.PromoBadge
import com.stripe.android.ui.core.elements.SaveForFutureUseElement
import com.stripe.android.ui.core.elements.SaveForFutureUseElementUI
import com.stripe.android.ui.core.elements.SimpleDialogElementUI
Expand Down Expand Up @@ -150,6 +153,7 @@ internal fun BankAccountForm(
isProcessing = state.isProcessing,
bankName = linkedBankAccount.bankName,
last4 = linkedBankAccount.last4,
promoBadgeState = state.promoBadgeState,
saveForFutureUseElement = saveForFutureUseElement,
onRemoveAccount = onRemoveAccount,
)
Expand Down Expand Up @@ -362,6 +366,7 @@ private fun AccountDetailsForm(
isProcessing: Boolean,
bankName: String?,
last4: String?,
promoBadgeState: PromoBadgeState?,
saveForFutureUseElement: SaveForFutureUseElement,
onRemoveAccount: () -> Unit,
) {
Expand Down Expand Up @@ -392,6 +397,7 @@ private fun AccountDetailsForm(
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.weight(1f)
) {
Image(
painter = painterResource(bankIcon),
Expand All @@ -401,9 +407,19 @@ private fun AccountDetailsForm(

Text(
text = "$bankName •••• $last4",
modifier = Modifier.alpha(if (isProcessing) 0.5f else 1f),
color = MaterialTheme.stripeColors.onComponent,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.alpha(if (isProcessing) 0.5f else 1f)
.weight(1f, fill = false),
)

promoBadgeState?.let { badgeState ->
PromoBadge(
text = badgeState.promoText,
eligible = badgeState.eligible,
)
}
}

IconButton(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.stripe.android.paymentsheet.ui

import android.content.Context
import android.os.Build.VERSION.SDK_INT
import android.os.Build.VERSION_CODES.N
import androidx.compose.foundation.background
Expand All @@ -13,28 +14,40 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.dp
import com.stripe.android.paymentsheet.R
import com.stripe.android.uicore.StripeThemeDefaults
import com.stripe.android.uicore.getOnSuccessBackgroundColor
import com.stripe.android.uicore.getSuccessBackgroundColor
import com.stripe.android.uicore.stripeColors
import java.util.Locale

@Composable
internal fun PromoBadge(
text: String,
modifier: Modifier = Modifier,
eligible: Boolean = true,
tinyMode: Boolean = false,
) {
// TODO(tillh-stripe): Revisit how we want the badge text to scale in tiny mode
FixedTextSize(fixed = tinyMode) {
val backgroundColor = Color(
color = StripeThemeDefaults.primaryButtonStyle.getSuccessBackgroundColor(LocalContext.current),
)
val backgroundColor = if (eligible) {
Color(
color = StripeThemeDefaults.primaryButtonStyle.getSuccessBackgroundColor(LocalContext.current),
)
} else {
MaterialTheme.stripeColors.componentBorder
}

val foregroundColor = Color(
color = StripeThemeDefaults.primaryButtonStyle.getOnSuccessBackgroundColor(LocalContext.current),
)
val foregroundColor = if (eligible) {
Color(
color = StripeThemeDefaults.primaryButtonStyle.getOnSuccessBackgroundColor(LocalContext.current),
)
} else {
MaterialTheme.stripeColors.onComponent
}

val shape = MaterialTheme.shapes.medium

Expand All @@ -47,7 +60,7 @@ internal fun PromoBadge(
)
) {
Text(
text = formatPromoText(text),
text = formatPromoText(text, eligible),
color = foregroundColor,
style = MaterialTheme.typography.caption.copy(
fontSize = StripeThemeDefaults.typography.xSmallFontSize,
Expand All @@ -58,17 +71,21 @@ internal fun PromoBadge(
}

@Composable
private fun formatPromoText(text: String): String {
val context = LocalContext.current
val currentLocale: Locale = if (SDK_INT >= N) {
context.resources.configuration.locales[0]
private fun formatPromoText(
text: String,
eligible: Boolean,
): String {
return if (eligible) {
val context = LocalContext.current

if (context.isEnglishLanguage) {
"Get $text"
} else {
text
}
Comment on lines +81 to +85
Copy link
Collaborator

Choose a reason for hiding this comment

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

No action - I guess this is a backend limitation, but shouldn't we get complete text from backend?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We only get the formatted amount from the backend. That’s the way we intended it, since we need this in multiple places with different text (or none) around it.

} else {
@Suppress("DEPRECATION")
context.resources.configuration.locale
stringResource(R.string.stripe_paymentsheet_bank_payment_promo_ineligible, text)
}

val isEnglish = currentLocale.language == Locale.ENGLISH.language
return if (isEnglish) "Get $text" else text
}

@Composable
Expand All @@ -86,6 +103,18 @@ private fun FixedTextSize(
}
}

private val Context.isEnglishLanguage: Boolean
get() {
val currentLocale: Locale = if (SDK_INT >= N) {
resources.configuration.locales[0]
} else {
@Suppress("DEPRECATION")
resources.configuration.locale
}

return currentLocale.language == Locale.ENGLISH.language
}
Comment on lines +106 to +116
Copy link
Collaborator

Choose a reason for hiding this comment

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

[nit] Likely useful on other scenarios, could be moved to a common module

Copy link
Collaborator Author

@tillh-stripe tillh-stripe Dec 5, 2024

Choose a reason for hiding this comment

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

I honestly don’t want to further encourage this pattern, so I’ll keep it here for now 😅


private fun Density.copy(
fontScale: Float = 1f,
): Density {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,81 @@ internal class AccountPreviewScreenshotTest {
}
}

@Test
fun testWithPromoBadge() {
paparazzi.snapshot {
BankAccountForm(
state = BankFormScreenStateFactory.createWithSession(
sessionId = "session_1234",
promoText = "$5",
),
instantDebits = true,
isPaymentFlow = true,
formArgs = formArguments,
nameController = createNameController(),
emailController = createEmailController(),
phoneController = createPhoneNumberController(),
addressController = createAddressController(fillAddress = false),
sameAsShippingElement = sameAsShippingElement,
saveForFutureUseElement = saveForFutureUseElement,
showCheckbox = false,
lastTextFieldIdentifier = null,
onRemoveAccount = {},
)
}
}

@Test
fun testWithPromoBadgeNextToSuperLongAccountName() {
paparazzi.snapshot {
BankAccountForm(
state = BankFormScreenStateFactory.createWithSession(
sessionId = "session_1234",
promoText = "$5",
eligibleForPromo = false,
bankName = "SuperDuperUltraLongBankName",
),
instantDebits = true,
isPaymentFlow = true,
formArgs = formArguments,
nameController = createNameController(),
emailController = createEmailController(),
phoneController = createPhoneNumberController(),
addressController = createAddressController(fillAddress = false),
sameAsShippingElement = sameAsShippingElement,
saveForFutureUseElement = saveForFutureUseElement,
showCheckbox = false,
lastTextFieldIdentifier = null,
onRemoveAccount = {},
)
}
}

@Test
fun testWithIneligiblePromoBadge() {
paparazzi.snapshot {
BankAccountForm(
state = BankFormScreenStateFactory.createWithSession(
sessionId = "session_1234",
promoText = "$5",
eligibleForPromo = false,
),
instantDebits = true,
isPaymentFlow = true,
formArgs = formArguments,
nameController = createNameController(),
emailController = createEmailController(),
phoneController = createPhoneNumberController(),
addressController = createAddressController(fillAddress = false),
sameAsShippingElement = sameAsShippingElement,
saveForFutureUseElement = saveForFutureUseElement,
showCheckbox = false,
lastTextFieldIdentifier = null,
onRemoveAccount = {},
)
}
}

private fun createNameController(): TextFieldController {
return NameConfig.createController("John Doe")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,17 @@ internal object BankFormScreenStateFactory {
sessionId: String,
isVerifyingWithMicrodeposits: Boolean = false,
mandateText: ResolvableString = "Some legal text".resolvableString,
promoText: String? = null,
eligibleForPromo: Boolean = true,
bankName: String = "Stripe Bank",
): BankFormScreenState {
return create(
resultIdentifier = BankFormScreenState.ResultIdentifier.Session(sessionId),
isVerifyingWithMicrodeposits = isVerifyingWithMicrodeposits,
mandateText = mandateText,
promoText = promoText,
eligibleForPromo = eligibleForPromo,
bankName = bankName,
)
}

Expand All @@ -28,25 +34,32 @@ internal object BankFormScreenStateFactory {
resultIdentifier = BankFormScreenState.ResultIdentifier.PaymentMethod(paymentMethod),
isVerifyingWithMicrodeposits = isVerifyingWithMicrodeposits,
mandateText = mandateText,
promoText = null,
eligibleForPromo = false,
)
}

private fun create(
resultIdentifier: BankFormScreenState.ResultIdentifier,
isVerifyingWithMicrodeposits: Boolean,
mandateText: ResolvableString,
promoText: String?,
eligibleForPromo: Boolean,
bankName: String = "Stripe Bank",
): BankFormScreenState {
return BankFormScreenState(
isPaymentFlow = true,
linkedBankAccount = BankFormScreenState.LinkedBankAccount(
resultIdentifier = resultIdentifier,
financialConnectionsSessionId = "session_1234",
intentId = "intent_1234",
bankName = "Stripe Bank",
bankName = bankName,
last4 = "6789",
mandateText = mandateText,
isVerifyingWithMicrodeposits = isVerifyingWithMicrodeposits,
)
eligibleForIncentive = eligibleForPromo,
),
promoText = promoText,
)
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading