Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
Original file line number Diff line number Diff line change
Expand Up @@ -1231,5 +1231,8 @@ internal fun LinkController.PaymentMethodType.toDisplayType(): PaymentMethodDisp

LinkController.PaymentMethodType.BankAccount ->
PaymentMethodDisplayData.Type.BankAccount

LinkController.PaymentMethodType.Generic ->
PaymentMethodDisplayData.Type.Card

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Added to this ticket to update when settlement times are server-driven

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,14 @@ sealed interface LinkPaymentDetails : Parcelable {
val bankName: String?,
override val last4: String,
) : LinkPaymentDetails

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@Parcelize
data class Generic(
val nickname: String?,
val label: String,
val sublabel: String?,
val icon: ConsumerPaymentDetails.Display.Icon?,
override val last4: String
) : LinkPaymentDetails
}
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,63 @@ object ConsumerFixtures {
""".trimIndent()
)

val CONSUMER_SINGLE_GENERIC_PAYMENT_DETAILS_JSON = JSONObject(
"""
{
"redacted_payment_details": [
{
"id": "csmrpd_126",
"is_default": false,
"type": "CRYPTO",
"display": {
"type": "0x••••22Dd",
"label": "Crypto",
"sublabel": "0x••••22Dd",
"last4": "0x••••22Dd",
"icon": {
"default": "https://example.com/crypto-icon.png"
}
},
"next_action_types": ["redirect_to_url"]
}
]
}
""".trimIndent()
)

val CONSUMER_GENERIC_PAYMENT_DETAILS_NO_SUBLABEL_ICON_JSON = JSONObject(
"""
{
"redacted_payment_details": [
{
"id": "csmrpd_126",
"is_default": false,
"type": "CRYPTO",
"display": {
"type": "0x••••22Dd",
"label": "Crypto"
},
"next_action_types": []
}
]
}
""".trimIndent()
)

val CONSUMER_GENERIC_PAYMENT_DETAILS_NO_DISPLAY_JSON = JSONObject(
"""
{
"redacted_payment_details": [
{
"id": "csmrpd_126",
"is_default": false,
"type": "CRYPTO"
}
]
}
""".trimIndent()
)

val PAYMENT_DETAILS_SHARE_JSON = JSONObject(
"""
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,72 @@ class ConsumerPaymentDetailsJsonParserTest {
}

@Test
fun `parse card with unrecognized funding defaults to Unknown`() {
fun `parse generic payment type`() {
assertEquals(
ConsumerPaymentDetails(
listOf(
ConsumerPaymentDetails.Generic(
id = "csmrpd_126",
last4 = "0x••••22Dd",
isDefault = false,
nickname = null,
billingAddress = null,
billingEmailAddress = null,
rawType = "CRYPTO",
display = ConsumerPaymentDetails.Display(
label = "Crypto",
sublabel = "0x••••22Dd",
icon = ConsumerPaymentDetails.Display.Icon(
defaultUrl = "https://example.com/crypto-icon.png"
)
),
nextActionTypes = listOf("redirect_to_url")
)
)
),
ConsumerPaymentDetailsJsonParser
.parse(ConsumerFixtures.CONSUMER_SINGLE_GENERIC_PAYMENT_DETAILS_JSON),
)
}

@Test
fun `parse generic payment type without sublabel or icon`() {
assertEquals(
ConsumerPaymentDetails(
listOf(
ConsumerPaymentDetails.Generic(
id = "csmrpd_126",
last4 = "",
isDefault = false,
nickname = null,
billingAddress = null,
billingEmailAddress = null,
rawType = "CRYPTO",
display = ConsumerPaymentDetails.Display(
label = "Crypto",
sublabel = null,
icon = null
),
nextActionTypes = emptyList()
)
)
),
ConsumerPaymentDetailsJsonParser
.parse(ConsumerFixtures.CONSUMER_GENERIC_PAYMENT_DETAILS_NO_SUBLABEL_ICON_JSON),
)
}

@Test
fun `parse generic payment type without display is dropped`() {
assertEquals(
ConsumerPaymentDetails(emptyList()),
ConsumerPaymentDetailsJsonParser
.parse(ConsumerFixtures.CONSUMER_GENERIC_PAYMENT_DETAILS_NO_DISPLAY_JSON),
)
}

@Test
fun `parse card with unrecognized funding defaults to Generic`() {
val json = createCardJsonWithFunding("INVALID")
val expected = createExpectedCardWithFunding(ConsumerPaymentDetails.Card.Funding.Unknown)
assertEquals(expected, ConsumerPaymentDetailsJsonParser.parse(json))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ data class ConsumerPaymentDetails(
open val nickname: String?,
open val billingAddress: BillingAddress?,
open val billingEmailAddress: String?,
open val display: Display? = null
) : Parcelable {

abstract val last4: String
Expand Down Expand Up @@ -60,7 +61,7 @@ data class ConsumerPaymentDetails(
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
companion object {
fun fromCode(code: String?): Funding {
return Funding.entries.firstOrNull { it.code == code }
return entries.firstOrNull { it.code == code }
?: Unknown
}
}
Expand Down Expand Up @@ -132,6 +133,27 @@ data class ConsumerPaymentDetails(
}
}

@Parcelize
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
data class Generic(
override val id: String,
override val last4: String,
override val isDefault: Boolean,
override val nickname: String?,
override val billingAddress: BillingAddress?,
override val billingEmailAddress: String?,
val rawType: String,
override val display: Display,
val nextActionTypes: List<String>,
) : PaymentDetails(
id = id,
type = rawType,
isDefault = isDefault,
nickname = nickname,
billingAddress = billingAddress,
billingEmailAddress = billingEmailAddress
)

@Parcelize
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
data class BillingAddress(
Expand All @@ -143,4 +165,16 @@ data class ConsumerPaymentDetails(
val postalCode: String?,
val countryCode: CountryCode?,
) : Parcelable

@Parcelize
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
data class Display(
val label: String,
val sublabel: String?,
val icon: Icon?
) : Parcelable {
@Parcelize
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
data class Icon(val defaultUrl: String) : Parcelable
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ private const val FIELD_CARD_DETAILS = "card_details"
private const val FIELD_CARD_LAST_4 = "last4"
private const val FIELD_BANK_ACCOUNT_DETAILS = "bank_account_details"
private const val FIELD_BANK_ACCOUNT_LAST_4 = "last4"
private const val FIELD_UNKNOWN_LAST_4 = "last4"
private const val FIELD_BANK_ACCOUNT_BANK_ACCOUNT_NAME = "bank_account_name"

private const val FIELD_BILLING_ADDRESS = "billing_address"
Expand All @@ -41,6 +42,12 @@ private const val FIELD_BANK_ACCOUNT_BANK_ICON_CODE = "bank_icon_code"
private const val FIELD_IS_DEFAULT = "is_default"
private const val FIELD_NICKNAME = "nickname"
private const val FIELD_FUNDING = "funding"
private const val FIELD_DISPLAY = "display"
private const val FIELD_LABEL = "label"
private const val FIELD_SUBLABEL = "sublabel"
private const val FIELD_ICON = "icon"
private const val FIELD_DEFAULT_ICON = "default"
private const val FIELD_NEXT_ACTION_TYPES = "next_action_types"

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
object ConsumerPaymentDetailsJsonParser : ModelJsonParser<ConsumerPaymentDetails> {
Expand All @@ -63,7 +70,6 @@ object ConsumerPaymentDetailsJsonParser : ModelJsonParser<ConsumerPaymentDetails
val id = json.getString(FIELD_ID)
val isDefault = json.optBoolean(FIELD_IS_DEFAULT)
val nickname = optString(json, FIELD_NICKNAME)?.takeIf { it.isNotBlank() }

when (type.lowercase()) {
"card" -> {
val cardDetails = json.getJSONObject(FIELD_CARD_DETAILS)
Expand Down Expand Up @@ -100,10 +106,32 @@ object ConsumerPaymentDetailsJsonParser : ModelJsonParser<ConsumerPaymentDetails
billingEmailAddress = optString(json, FIELD_BILLING_EMAIL_ADDRESS)
)
}
else -> null
else -> {
val display = json.optJSONObject(FIELD_DISPLAY)
display?.let {
ConsumerPaymentDetails.Generic(
id = id,
last4 = display.optString(FIELD_UNKNOWN_LAST_4),
isDefault = isDefault,
nickname = nickname,
billingAddress = parseBillingAddress(json),
billingEmailAddress = optString(json, FIELD_BILLING_EMAIL_ADDRESS),
rawType = type,
display = parseDisplay(display),
nextActionTypes = jsonArrayToList(json.getJSONArray(FIELD_NEXT_ACTION_TYPES))
)
}
}
}
}

private fun parseDisplay(display: JSONObject) = ConsumerPaymentDetails.Display(
label = display.getString(FIELD_LABEL),
sublabel = display.optString(FIELD_SUBLABEL).takeIf { it.isNotBlank() },
icon = display.optJSONObject(FIELD_ICON)
?.let { ConsumerPaymentDetails.Display.Icon(defaultUrl = it.getString(FIELD_DEFAULT_ICON)) }
)

private fun parseBillingAddress(json: JSONObject) =
json.optJSONObject(FIELD_BILLING_ADDRESS)?.let { address ->
ConsumerPaymentDetails.BillingAddress(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ interface ConsumersApiService {
suspend fun sharePaymentDetails(
consumerSessionClientSecret: String,
paymentDetailsId: String,
expectedPaymentMethodType: String,
expectedPaymentMethodType: String?,
billingPhone: String?,
requestSurface: String,
requestOptions: ApiRequest.Options,
Expand Down Expand Up @@ -481,7 +481,7 @@ class ConsumersApiServiceImpl(
override suspend fun sharePaymentDetails(
consumerSessionClientSecret: String,
paymentDetailsId: String,
expectedPaymentMethodType: String,
expectedPaymentMethodType: String?,
billingPhone: String?,
requestSurface: String,
requestOptions: ApiRequest.Options,
Expand Down
11 changes: 11 additions & 0 deletions paymentsheet/api/paymentsheet.api
Original file line number Diff line number Diff line change
Expand Up @@ -362,13 +362,24 @@ public final class com/stripe/android/link/ui/verification/VerificationScreenKt
public static final fun VerificationPreview (Landroidx/compose/runtime/Composer;I)V
}

public final class com/stripe/android/link/ui/wallet/ComposableSingletons$IconKt {
public static final field INSTANCE Lcom/stripe/android/link/ui/wallet/ComposableSingletons$IconKt;
public fun <init> ()V
public final fun getLambda$-2139801056$paymentsheet_release ()Lkotlin/jvm/functions/Function3;
}

public final class com/stripe/android/link/ui/wallet/ComposableSingletons$PaymentDetailsKt {
public static final field INSTANCE Lcom/stripe/android/link/ui/wallet/ComposableSingletons$PaymentDetailsKt;
public fun <init> ()V
public final fun getLambda$-1132714699$paymentsheet_release ()Lkotlin/jvm/functions/Function3;
public final fun getLambda$1381508991$paymentsheet_release ()Lkotlin/jvm/functions/Function3;
public final fun getLambda$629401301$paymentsheet_release ()Lkotlin/jvm/functions/Function2;
}

public final class com/stripe/android/link/ui/wallet/IconKt {
public static final fun Icon (Ljava/lang/String;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V
}

public abstract interface annotation class com/stripe/android/paymentelement/AddressAutocompletePreview : java/lang/annotation/Annotation {
}

Expand Down
4 changes: 4 additions & 0 deletions paymentsheet/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,8 @@ to be saved and used in future checkout sessions. -->
<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 on manage card screen when card details are not editable. -->
<string name="stripe_paymentsheet_unknown_details_cannot_be_changed">Payment method details cannot be changed.</string>
<!-- Text displayed below a credit card entry form when the card will be saved to make future payments. -->
<string name="stripe_paymentsheet_card_mandate">By providing your card information, you allow %s to charge your card for future payments in accordance with their terms.</string>
<!-- Text displayed below a credit card entry form when the card will be saved with the merchant. -->
Expand Down Expand Up @@ -173,6 +175,8 @@ to be saved and used in future checkout sessions. -->
<string name="stripe_paymentsheet_manage_bank_account">Manage US bank account</string>
<!-- Title shown above a view containing a customer's card payment methods that they can delete or update -->
<string name="stripe_paymentsheet_manage_card">Manage card</string>
<!-- Title shown above a view containing a customer's unknown payment methods that they can delete or update -->
<string name="stripe_paymentsheet_manage_payment_method">Manage payment method</string>
<!-- Title shown above a view containing the customer's cards that they can delete or update -->
<string name="stripe_paymentsheet_manage_cards">Manage cards</string>
<!-- Title shown above a view containing the customer's bank account that they can delete -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ internal fun ConsumerPaymentDetails.PaymentDetails.isSupportedWithBillingConfig(
allowedCountries.contains(it.value.uppercase())
} ?: false
is ConsumerPaymentDetails.BankAccount -> allowedCountries.contains(CountryCode.US.value)
is ConsumerPaymentDetails.Generic -> billingAddress?.countryCode?.let {
allowedCountries.contains(it.value.uppercase())
} ?: false
is ConsumerPaymentDetails.Passthrough -> false
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -757,7 +757,8 @@ class LinkController @Inject internal constructor(
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
enum class PaymentMethodType {
Card,
BankAccount
BankAccount,
Generic
}

/**
Expand Down
Loading
Loading