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

Add support for cash app afterpay. #10480

Merged
merged 1 commit into from
Mar 24, 2025
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## XX.XX.XX - 20XX-XX-XX

### PaymentSheet
* [CHANGED][10480](https://github.com/stripe/stripe-android/pull/10480) Updated Afterpay branding in the US to be Cash App Afterpay.

## 21.7.1 - 2025-03-24

### PaymentSheet
Expand Down
3 changes: 0 additions & 3 deletions financial-connections/detekt-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,6 @@
<ID>MatchingDeclarationName:ServerDrivenUi.kt$BulletUI</ID>
<ID>MatchingDeclarationName:Type.kt$FinancialConnectionsTypography</ID>
<ID>NestedBlockDepth:InstitutionPickerScreen.kt$private fun LazyListScope.searchResults( isInputEmpty: Boolean, payload: Payload, selectedInstitutionId: String?, onInstitutionSelected: (FinancialConnectionsInstitution, Boolean) -> Unit, institutions: Async&lt;InstitutionResponse>, onManualEntryClick: () -> Unit, onSearchMoreClick: () -> Unit )</ID>
<ID>SwallowedException:PollAttachPaymentAccount.kt$PollAttachPaymentAccount$e: StripeException</ID>
<ID>SwallowedException:PollAuthorizationSessionAccounts.kt$PollAuthorizationSessionAccounts$e: StripeException</ID>
<ID>SwallowedException:PostAuthorizationSession.kt$PostAuthorizationSession$e: StripeException</ID>
<ID>TooManyFunctions:FinancialConnectionsConsumerSessionRepository.kt$FinancialConnectionsConsumerSessionRepository</ID>
<ID>TooManyFunctions:FinancialConnectionsConsumerSessionRepository.kt$FinancialConnectionsConsumerSessionRepositoryImpl : FinancialConnectionsConsumerSessionRepository</ID>
<ID>TooManyFunctions:FinancialConnectionsManifestRepository.kt$FinancialConnectionsManifestRepository</ID>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,12 @@ fun AfterpayClearpayElementUI(
enabled = enabled,
imageLoader = mapOf(
"afterpay" to EmbeddableImage.Drawable(
if (isClearpay()) {
if (isClearpay(element.currency)) {
R.drawable.stripe_ic_clearpay_logo
} else {
R.drawable.stripe_ic_afterpay_logo
},
if (isClearpay()) {
if (isClearpay(element.currency)) {
R.string.stripe_paymentsheet_payment_method_clearpay
} else {
R.string.stripe_paymentsheet_payment_method_afterpay
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import kotlinx.coroutines.flow.StateFlow
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
data class AfterpayClearpayHeaderElement(
override val identifier: IdentifierSpec,
override val controller: Controller? = null
override val controller: Controller? = null,
val currency: String?
) : FormElement {
override val allowsUserInteraction: Boolean = false
override val mandateText: ResolvableString? = null
Expand Down Expand Up @@ -45,6 +46,9 @@ data class AfterpayClearpayHeaderElement(
const val NO_BREAK_SPACE = "\u00A0"

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
fun isClearpay() = setOf("GB", "ES", "FR", "IT").contains(Locale.current.region)
fun isClearpay(currency: String?) = "GBP".equals(currency, ignoreCase = true)

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
fun isCashappAfterpay(currency: String?) = "USD".equals(currency, ignoreCase = true)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@ data class AfterpayClearpayTextSpec(
@SerialName("api_path")
override val apiPath: IdentifierSpec = IdentifierSpec.Generic("afterpay_text")
) : FormItemSpec() {
fun transform(): FormElement = AfterpayClearpayHeaderElement(apiPath)
fun transform(currency: String?): FormElement = AfterpayClearpayHeaderElement(apiPath, currency = currency)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ package com.stripe.android.ui.core.elements
import android.app.Application
import androidx.test.core.app.ApplicationProvider
import com.google.common.truth.Truth.assertThat
import com.stripe.android.testing.LocaleTestRule
import com.stripe.android.ui.core.elements.AfterpayClearpayHeaderElement.Companion.isCashappAfterpay
import com.stripe.android.ui.core.elements.AfterpayClearpayHeaderElement.Companion.isClearpay
import com.stripe.android.uicore.elements.IdentifierSpec
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
Expand All @@ -13,10 +16,14 @@ import java.util.Locale
@RunWith(RobolectricTestRunner::class)
class AfterpayClearpayHeaderElementTest {

@get:Rule
val localeRule = LocaleTestRule()

@Test
fun `Verify label is correct`() {
val element = AfterpayClearpayHeaderElement(
IdentifierSpec.Generic("test"),
currency = "EUR",
)

assertThat(
Expand All @@ -28,6 +35,7 @@ class AfterpayClearpayHeaderElementTest {
fun `Verify infoUrl is correct`() {
val element = AfterpayClearpayHeaderElement(
IdentifierSpec.Generic("test"),
currency = "EUR",
)

assertThat(element.infoUrl)
Expand All @@ -36,24 +44,26 @@ class AfterpayClearpayHeaderElementTest {

@Test
fun `Verify infoUrl is updated as locale changes`() {
Locale.setDefault(Locale.UK)
localeRule.setTemporarily(Locale.UK)
val element = AfterpayClearpayHeaderElement(
IdentifierSpec.Generic("test"),
currency = "GBP",
)

assertThat(element.infoUrl)
.isEqualTo("https://static.afterpay.com/modal/en_GB.html")

Locale.setDefault(Locale.FRANCE)
localeRule.setTemporarily(Locale.FRANCE)
assertThat(element.infoUrl)
.isEqualTo("https://static.afterpay.com/modal/fr_FR.html")
}

@Test
fun `Verify infoUrl is localized for GB`() {
Locale.setDefault(Locale.UK)
localeRule.setTemporarily(Locale.UK)
val element = AfterpayClearpayHeaderElement(
IdentifierSpec.Generic("test"),
currency = "GBP",
)

assertThat(element.infoUrl)
Expand All @@ -62,9 +72,10 @@ class AfterpayClearpayHeaderElementTest {

@Test
fun `Verify infoUrl is localized for France`() {
Locale.setDefault(Locale.FRANCE)
localeRule.setTemporarily(Locale.FRANCE)
val element = AfterpayClearpayHeaderElement(
IdentifierSpec.Generic("test"),
currency = "EUR",
)

assertThat(element.infoUrl)
Expand All @@ -73,19 +84,14 @@ class AfterpayClearpayHeaderElementTest {

@Test
fun `Verify check if clearpay or afterpay`() {
Locale.setDefault(Locale.UK)
assertThat(isClearpay()).isTrue()

Locale.setDefault(Locale.FRANCE)
assertThat(isClearpay()).isTrue()

Locale.setDefault(Locale.Builder().setRegion("ES").build())
assertThat(isClearpay()).isTrue()

Locale.setDefault(Locale.Builder().setRegion("IT").build())
assertThat(isClearpay()).isTrue()
assertThat(isClearpay("GBP")).isTrue()
assertThat(isClearpay("EUR")).isFalse()
}

Locale.setDefault(Locale.US)
assertThat(isClearpay()).isFalse()
@Test
fun `Verify check if cash app afterpay`() {
assertThat(isCashappAfterpay("GBP")).isFalse()
assertThat(isCashappAfterpay("EUR")).isFalse()
assertThat(isCashappAfterpay("USD")).isTrue()
}
}
2 changes: 1 addition & 1 deletion paymentsheet/detekt-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<ID>ConstructorParameterNaming:BankFormScreenState.kt$BankFormScreenState$private val _isProcessing: Boolean = false</ID>
<ID>CyclomaticComplexMethod:CustomerSheetViewModel.kt$CustomerSheetViewModel$fun handleViewAction(viewAction: CustomerSheetViewAction)</ID>
<ID>CyclomaticComplexMethod:PlaceholderHelper.kt$PlaceholderHelper$@VisibleForTesting internal fun specForPlaceholderField( field: PlaceholderField, placeholderOverrideList: List&lt;IdentifierSpec>, requiresMandate: Boolean, configuration: PaymentSheet.BillingDetailsCollectionConfiguration, )</ID>
<ID>CyclomaticComplexMethod:TransformSpecToElements.kt$TransformSpecToElements$fun transform( specs: List&lt;FormItemSpec>, placeholderOverrideList: List&lt;IdentifierSpec> = emptyList(), ): List&lt;FormElement></ID>
<ID>CyclomaticComplexMethod:TransformSpecToElements.kt$TransformSpecToElements$fun transform( metadata: PaymentMethodMetadata?, specs: List&lt;FormItemSpec>, placeholderOverrideList: List&lt;IdentifierSpec> = emptyList(), ): List&lt;FormElement></ID>
<ID>EmptyFunctionBlock:PrimaryButtonAnimator.kt$PrimaryButtonAnimator.&lt;no name provided>${ }</ID>
<ID>EmptyFunctionBlock:VerticalModeFormUITest.kt$VerticalModeFormUITest.&lt;no name provided>${}</ID>
<ID>FunctionNaming:PaymentSheetTopBar.kt$@Preview @Composable internal fun PaymentSheetTopBar_Preview()</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 @@ -16,6 +16,8 @@
<string name="stripe_bank_account_with_last_4">Bank account ····%s</string>
<!-- Text for alert message when user needs to confirm payment in their banking app -->
<string name="stripe_blik_confirm_payment">Confirm the payment in your bank\'s app within %s to complete the purchase.</string>
<!-- This shows the message on Buy Now Pay Later LPM, afterpay. -->
<string name="stripe_cashapp_afterpay_subtitle">Buy now or pay later with Cash App Afterpay</string>
<!-- This shows the message on Buy Now Pay Later LPM, clearpay. -->
<string name="stripe_clearpay_subtitle">Buy now or pay later with Clearpay</string>
<!-- Used as the title for prompting the user if they want to close the sheet -->
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.stripe.android.lpmfoundations.luxe

import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodMetadata
import com.stripe.android.lpmfoundations.paymentmethod.UiDefinitionFactory
import com.stripe.android.paymentsheet.forms.PlaceholderHelper.specsForConfiguration
import com.stripe.android.paymentsheet.model.currency
import com.stripe.android.ui.core.elements.AddressSpec
import com.stripe.android.ui.core.elements.AffirmTextSpec
import com.stripe.android.ui.core.elements.AfterpayClearpayTextSpec
Expand Down Expand Up @@ -42,6 +44,7 @@ internal class TransformSpecToElements(
private val arguments: UiDefinitionFactory.Arguments,
) {
fun transform(
metadata: PaymentMethodMetadata?,
specs: List<FormItemSpec>,
placeholderOverrideList: List<IdentifierSpec> = emptyList(),
): List<FormElement> {
Expand All @@ -53,7 +56,7 @@ internal class TransformSpecToElements(
).flatMap { spec ->
when (spec) {
is StaticTextSpec -> listOf(spec.transform())
is AfterpayClearpayTextSpec -> listOf(spec.transform())
is AfterpayClearpayTextSpec -> listOf(spec.transform(metadata?.stripeIntent?.currency))
is AffirmTextSpec -> listOf(spec.transform())
is EmptyFormSpec -> listOf(EmptyFormElement())
is MandateTextSpec -> listOf(spec.transform(arguments.merchantName))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ internal data class PaymentMethodMetadata(
getUiDefinitionFactoryForExternalPaymentMethod(code)?.createSupportedPaymentMethod()
} else {
val definition = supportedPaymentMethodDefinitions().firstOrNull { it.type.code == code } ?: return null
definition.uiDefinitionFactory().supportedPaymentMethod(definition, sharedDataSpecs)
definition.uiDefinitionFactory().supportedPaymentMethod(this, definition, sharedDataSpecs)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,14 +82,16 @@ internal sealed interface UiDefinitionFactory {

interface RequiresSharedDataSpec : UiDefinitionFactory {
fun createSupportedPaymentMethod(
metadata: PaymentMethodMetadata,
sharedDataSpec: SharedDataSpec,
): SupportedPaymentMethod

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

fun createFormElements(
Expand All @@ -98,6 +100,7 @@ internal sealed interface UiDefinitionFactory {
transformSpecToElements: TransformSpecToElements,
): List<FormElement> {
return transformSpecToElements.transform(
metadata = metadata,
specs = sharedDataSpec.fields,
)
}
Expand Down Expand Up @@ -130,6 +133,7 @@ internal sealed interface UiDefinitionFactory {
}

fun supportedPaymentMethod(
metadata: PaymentMethodMetadata,
definition: PaymentMethodDefinition,
sharedDataSpecs: List<SharedDataSpec>,
): SupportedPaymentMethod? = when (this) {
Expand All @@ -140,7 +144,7 @@ internal sealed interface UiDefinitionFactory {
is RequiresSharedDataSpec -> {
val sharedDataSpec = sharedDataSpecs.firstOrNull { it.type == definition.type.code }
if (sharedDataSpec != null) {
createSupportedPaymentMethod(sharedDataSpec)
createSupportedPaymentMethod(metadata, sharedDataSpec)
} else {
null
}
Expand All @@ -164,6 +168,7 @@ internal sealed interface UiDefinitionFactory {
val sharedDataSpec = sharedDataSpecs.firstOrNull { it.type == definition.type.code }
if (sharedDataSpec != null) {
createFormHeaderInformation(
metadata = metadata,
sharedDataSpec = sharedDataSpec,
incentive = metadata.paymentMethodIncentive,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ internal object AffirmDefinition : PaymentMethodDefinition {
}

private object AffirmUiDefinitionFactory : UiDefinitionFactory.RequiresSharedDataSpec {
override fun createSupportedPaymentMethod(sharedDataSpec: SharedDataSpec) = SupportedPaymentMethod(
override fun createSupportedPaymentMethod(
metadata: PaymentMethodMetadata,
sharedDataSpec: SharedDataSpec,
) = SupportedPaymentMethod(
paymentMethodDefinition = AffirmDefinition,
sharedDataSpec = sharedDataSpec,
displayNameResource = UiCoreR.string.stripe_paymentsheet_payment_method_affirm,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ 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.R
import com.stripe.android.paymentsheet.model.currency
import com.stripe.android.ui.core.elements.AfterpayClearpayHeaderElement
import com.stripe.android.ui.core.elements.SharedDataSpec
import com.stripe.android.ui.core.R as UiCoreR
Expand All @@ -29,17 +30,26 @@ internal object AfterpayClearpayDefinition : PaymentMethodDefinition {
}

private object AfterpayClearpayUiDefinitionFactory : UiDefinitionFactory.RequiresSharedDataSpec {
override fun createSupportedPaymentMethod(sharedDataSpec: SharedDataSpec) = SupportedPaymentMethod(
override fun createSupportedPaymentMethod(
metadata: PaymentMethodMetadata,
sharedDataSpec: SharedDataSpec,
) = SupportedPaymentMethod(
paymentMethodDefinition = AfterpayClearpayDefinition,
sharedDataSpec = sharedDataSpec,
displayNameResource = if (AfterpayClearpayHeaderElement.isClearpay()) {
displayNameResource = if (AfterpayClearpayHeaderElement.isClearpay(metadata.stripeIntent.currency)) {
UiCoreR.string.stripe_paymentsheet_payment_method_clearpay
} else {
UiCoreR.string.stripe_paymentsheet_payment_method_afterpay
},
iconResource = UiCoreR.drawable.stripe_ic_paymentsheet_pm_afterpay_clearpay,
subtitle = if (AfterpayClearpayHeaderElement.isClearpay()) {
iconResource = if (AfterpayClearpayHeaderElement.isCashappAfterpay(metadata.stripeIntent.currency)) {
UiCoreR.drawable.stripe_ic_paymentsheet_pm_cash_app_pay
} else {
UiCoreR.drawable.stripe_ic_paymentsheet_pm_afterpay_clearpay
},
subtitle = if (AfterpayClearpayHeaderElement.isClearpay(metadata.stripeIntent.currency)) {
R.string.stripe_clearpay_subtitle.resolvableString
} else if (AfterpayClearpayHeaderElement.isCashappAfterpay(metadata.stripeIntent.currency)) {
R.string.stripe_cashapp_afterpay_subtitle.resolvableString
} else {
R.string.stripe_afterpay_subtitle.resolvableString
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ internal object AlipayDefinition : PaymentMethodDefinition {
}

private object AlipayUiDefinitionFactory : UiDefinitionFactory.RequiresSharedDataSpec {
override fun createSupportedPaymentMethod(sharedDataSpec: SharedDataSpec) = SupportedPaymentMethod(
override fun createSupportedPaymentMethod(
metadata: PaymentMethodMetadata,
sharedDataSpec: SharedDataSpec,
) = SupportedPaymentMethod(
paymentMethodDefinition = AlipayDefinition,
sharedDataSpec = sharedDataSpec,
displayNameResource = R.string.stripe_paymentsheet_payment_method_alipay,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ internal object AlmaDefinition : PaymentMethodDefinition {
}

private object AlmaUiDefinitionFactory : UiDefinitionFactory.RequiresSharedDataSpec {
override fun createSupportedPaymentMethod(sharedDataSpec: SharedDataSpec) = SupportedPaymentMethod(
override fun createSupportedPaymentMethod(
metadata: PaymentMethodMetadata,
sharedDataSpec: SharedDataSpec,
) = SupportedPaymentMethod(
paymentMethodDefinition = AlmaDefinition,
sharedDataSpec = sharedDataSpec,
displayNameResource = R.string.stripe_paymentsheet_payment_method_alma,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ internal object AmazonPayDefinition : PaymentMethodDefinition {
}

private object AmazonPayUiDefinitionFactory : UiDefinitionFactory.RequiresSharedDataSpec {
override fun createSupportedPaymentMethod(sharedDataSpec: SharedDataSpec) = SupportedPaymentMethod(
override fun createSupportedPaymentMethod(
metadata: PaymentMethodMetadata,
sharedDataSpec: SharedDataSpec,
) = SupportedPaymentMethod(
paymentMethodDefinition = AmazonPayDefinition,
sharedDataSpec = sharedDataSpec,
displayNameResource = R.string.stripe_paymentsheet_payment_method_amazon_pay,
Expand All @@ -46,6 +49,7 @@ private object AmazonPayUiDefinitionFactory : UiDefinitionFactory.RequiresShared
emptyList()
}
return transformSpecToElements.transform(
metadata = metadata,
specs = sharedDataSpec.fields + localLayoutSpecs,
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ internal object AuBecsDebitDefinition : PaymentMethodDefinition {
}

private object AuBecsDebitUiDefinitionFactory : UiDefinitionFactory.RequiresSharedDataSpec {
override fun createSupportedPaymentMethod(sharedDataSpec: SharedDataSpec) = SupportedPaymentMethod(
override fun createSupportedPaymentMethod(
metadata: PaymentMethodMetadata,
sharedDataSpec: SharedDataSpec,
) = SupportedPaymentMethod(
paymentMethodDefinition = AuBecsDebitDefinition,
sharedDataSpec = sharedDataSpec,
displayNameResource = R.string.stripe_paymentsheet_payment_method_au_becs_debit,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ internal object BacsDebitDefinition : PaymentMethodDefinition {
}

private object BacsDebitUiDefinitionFactory : UiDefinitionFactory.RequiresSharedDataSpec {
override fun createSupportedPaymentMethod(sharedDataSpec: SharedDataSpec) = SupportedPaymentMethod(
override fun createSupportedPaymentMethod(
metadata: PaymentMethodMetadata,
sharedDataSpec: SharedDataSpec,
) = SupportedPaymentMethod(
paymentMethodDefinition = BacsDebitDefinition,
sharedDataSpec = sharedDataSpec,
displayNameResource = R.string.stripe_paymentsheet_payment_method_bacs_debit,
Expand Down Expand Up @@ -68,6 +71,7 @@ private object BacsDebitUiDefinitionFactory : UiDefinitionFactory.RequiresShared
)

return transformSpecToElements.transform(
metadata = metadata,
specs = sharedDataSpec.fields + localFields,
placeholderOverrideList = listOf(
IdentifierSpec.Name,
Expand Down
Loading
Loading