Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -908,7 +908,14 @@ constructor(
val displayBrand: String? = null,
@JvmField
@field:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
val cardArt: CardArt? = null
val cardArt: CardArt? = null,
/**
* Indicates whether this payment method was created from a card_present payment method, indicate that a
* customer uses a Terminal device or application to save their card.
*/
@JvmField
@field:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
val createdFromCardPresent: Boolean = false,
) : TypeData() {
override val type: Type get() = Type.Card

Expand All @@ -926,7 +933,8 @@ constructor(
wallet: Wallet? = this.wallet,
networks: Networks? = this.networks,
displayBrand: String? = this.displayBrand,
cardArt: CardArt? = this.cardArt
cardArt: CardArt? = this.cardArt,
createdFromCardPresent: Boolean = this.createdFromCardPresent
): Card {
return Card(
brand = brand,
Expand All @@ -941,7 +949,8 @@ constructor(
wallet = wallet,
networks = networks,
displayBrand = displayBrand,
cardArt = cardArt
cardArt = cardArt,
createdFromCardPresent = createdFromCardPresent
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,8 @@ class PaymentMethodJsonParser : ModelJsonParser<PaymentMethod> {
displayBrand = StripeJsonUtils.optString(json, FIELD_DISPLAY_BRAND),
cardArt = json.optJSONObject(FIELD_CARD_ART)?.let {
CardArtJsonParser().parse(it)
}
},
createdFromCardPresent = json.has(FIELD_GENERATED_FROM),
)
}

Expand Down Expand Up @@ -357,6 +358,7 @@ class PaymentMethodJsonParser : ModelJsonParser<PaymentMethod> {
private const val FIELD_CUSTOMER = "customer"
private const val FIELD_LIVEMODE = "livemode"
private const val FIELD_ALLOW_REDISPLAY = "allow_redisplay"
private const val FIELD_GENERATED_FROM = "generated_from"
private const val FIELD_TYPE = "type"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.google.common.truth.Truth.assertThat
import com.stripe.android.model.Address
import com.stripe.android.model.PaymentMethod
import com.stripe.android.model.PaymentMethodFixtures
import org.json.JSONObject
import kotlin.test.Test

class PaymentMethodJsonParserTest {
Expand Down Expand Up @@ -194,4 +195,23 @@ class PaymentMethodJsonParserTest {

assertThat(paymentMethod.card?.cardArt).isNull()
}

@Test
fun `parse card JSON without generated_from sets createdFromCardPresent to false`() {
val paymentMethod = PaymentMethodJsonParser().parse(PaymentMethodFixtures.CARD_JSON)

assertThat(paymentMethod.card?.createdFromCardPresent).isFalse()
}

@Test
fun `parse card JSON with generated_from key sets createdFromCardPresent to true`() {
val json = JSONObject(PaymentMethodFixtures.CARD_JSON.toString())

json.getJSONObject("card")
.put("generated_from", JSONObject())

val paymentMethod = PaymentMethodJsonParser().parse(json)

assertThat(paymentMethod.card?.createdFromCardPresent).isTrue()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ internal data class EditCardPayload(
internal data class CardEditConfiguration(
val cardBrandFilter: CardBrandFilter,
val isCbcModifiable: Boolean,
// Flag that indicates if mandatory input of missing country & ZIP code
// are required for billing details in automatic mode.
val isAutomaticAddressInputMandatory: Boolean = true,
// Local flag for whether expiry date and address can be edited.
// This flag has no effect on Card Brand Choice.
// It will be removed before release.
Expand Down Expand Up @@ -228,11 +231,19 @@ internal class DefaultEditCardDetailsInteractor(
): CardUpdateParams? {
val hasChanges = hasCardDetailsChanged(cardDetailsEntry) ||
hasBillingDetailsChanged(billingDetailsEntry)
val isComplete = (cardDetailsEntry?.isComplete() != false) &&
billingDetailsEntry?.isComplete(billingDetailsCollectionConfiguration) != false

val cardDetailsAreComplete = cardDetailsEntry?.isComplete() != false
val billingDetailsAreComplete = billingDetailsEntry
?.isComplete(billingDetailsCollectionConfiguration) != false

val addressInputNotRequired =
billingDetailsCollectionConfiguration.address == AddressCollectionMode.Automatic &&
!(cardEditConfiguration?.isAutomaticAddressInputMandatory ?: true)

val isComplete = cardDetailsAreComplete && (billingDetailsAreComplete || addressInputNotRequired)

return if ((hasChanges || requiresModification.not()) && isComplete) {
toUpdateParams(cardDetailsEntry, billingDetailsEntry)
toUpdateParams(cardDetailsEntry, billingDetailsEntry.takeIf { billingDetailsAreComplete })
} else {
null
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ internal class DefaultUpdatePaymentMethodInteractor(
val payload = EditCardPayload.create(savedPaymentMethodCard.card, savedPaymentMethodCard.billingDetails)
val cardEditConfiguration = CardEditConfiguration(
cardBrandFilter = cardBrandFilter,
isAutomaticAddressInputMandatory = !savedPaymentMethodCard.card.createdFromCardPresent,
isCbcModifiable = isModifiable && displayableSavedPaymentMethod.canChangeCbc(),
areExpiryDateAndAddressModificationSupported = isModifiable && canUpdateFullPaymentMethodDetails,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,71 @@ internal class DefaultEditCardDetailsInteractorTest {
assertThat(state.cardDetailsState?.expiryDateState?.enabled).isFalse()
}

@Test
fun `optional automatic address with complete billing includes address on card update params`() {
var cardUpdateParams: CardUpdateParams? = null
val handler = handler(
isAutomaticAddressInputMandatory = false,
onCardUpdateParamsChanged = { cardUpdateParams = it },
)
handler.updateBillingDetails(
PaymentSheetFixtures.billingDetailsFormState(
postalCode = FormFieldEntry("10001", isComplete = true),
country = FormFieldEntry("US", isComplete = true),
)
)
assertThat(cardUpdateParams).isNotNull()
assertThat(cardUpdateParams?.billingDetails?.address?.postalCode).isEqualTo("10001")
assertThat(cardUpdateParams?.billingDetails?.address?.country).isEqualTo("US")
}

@Test
fun `optional automatic address with incomplete billing has no billing in card update params`() {
var capturedCardUpdateParams: CardUpdateParams? = null
val handler = handler(
isAutomaticAddressInputMandatory = false,
onCardUpdateParamsChanged = {
capturedCardUpdateParams = it
}
)

handler.handleViewAction(
EditCardDetailsInteractor.ViewAction.BillingDetailsChanged(
PaymentSheetFixtures.billingDetailsFormState(
line1 = null,
line2 = null,
city = null,
state = null,
postalCode = FormFieldEntry("", isComplete = false),
country = FormFieldEntry("", isComplete = false),
)
)
)

handler.updateCardBrand(CardBrand.Visa)

assertThat(capturedCardUpdateParams).isNotNull()
assertThat(capturedCardUpdateParams?.cardBrand).isEqualTo(CardBrand.Visa)
// Billing is not included when automatic fields are incomplete; the card update API call omits it.
assertThat(capturedCardUpdateParams?.billingDetails).isNull()
}

@Test
fun `full address mode puts line1 on card update params when line1 is changed`() {
var cardUpdateParams: CardUpdateParams? = null
val handler = handler(
addressCollectionMode = AddressCollectionMode.Full,
onCardUpdateParamsChanged = { cardUpdateParams = it },
)
handler.updateBillingDetails(
PaymentSheetFixtures.billingDetailsFormState(
line1 = FormFieldEntry("9 New Street", isComplete = true),
)
)
assertThat(cardUpdateParams).isNotNull()
assertThat(cardUpdateParams?.billingDetails?.address?.line1).isEqualTo("9 New Street")
}

@Test
fun stateIsUpdateWhenNewCardBrandIsSelected() {
val handler = handler()
Expand Down Expand Up @@ -634,6 +699,7 @@ internal class DefaultEditCardDetailsInteractorTest {
card: PaymentMethod.Card = PaymentMethodFixtures.CARD_WITH_NETWORKS,
cardBrandFilter: CardBrandFilter = DefaultCardBrandFilter,
isCbcModifiable: Boolean = true,
isAutomaticAddressInputMandatory: Boolean = true,
areExpiryDateAndAddressModificationSupported: Boolean = true,
addressCollectionMode: AddressCollectionMode = AddressCollectionMode.Automatic,
nameCollection: CollectionMode = CollectionMode.Automatic,
Expand All @@ -648,6 +714,7 @@ internal class DefaultEditCardDetailsInteractorTest {
cardEditConfiguration = CardEditConfiguration(
cardBrandFilter = cardBrandFilter,
isCbcModifiable = isCbcModifiable,
isAutomaticAddressInputMandatory = isAutomaticAddressInputMandatory,
areExpiryDateAndAddressModificationSupported = areExpiryDateAndAddressModificationSupported,
),
requiresModification = requiresModification,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,35 @@ class DefaultUpdatePaymentMethodInteractorTest {
}
}

@Test
fun `when card was created from card present then automatic address is not mandatory in card edit config`() {
val editFactory = FakeEditCardDetailsInteractorFactory()
val displayable = PaymentMethodFixtures.CARD_WITH_NETWORKS_PAYMENT_METHOD.copy(
card = PaymentMethodFixtures.CARD_WITH_NETWORKS.copy(createdFromCardPresent = true)
).toDisplayableSavedPaymentMethod()
runScenario(
displayableSavedPaymentMethod = displayable,
editCardDetailsInteractorFactory = editFactory,
) {
assertThat(interactor.editCardDetailsInteractor).isNotNull()
assertThat(editFactory.cardEditConfiguration?.isAutomaticAddressInputMandatory)
.isFalse()
}
}

@Test
fun `when card was not created from card present then automatic address is mandatory in card edit config`() {
val editFactory = FakeEditCardDetailsInteractorFactory()
runScenario(
displayableSavedPaymentMethod = PaymentMethodFixtures.displayableCard(),
editCardDetailsInteractorFactory = editFactory,
) {
assertThat(interactor.editCardDetailsInteractor).isNotNull()
assertThat(editFactory.cardEditConfiguration?.isAutomaticAddressInputMandatory)
.isTrue()
}
}

@Test
fun cbcEligibleCard_isModifiablePaymentMethod() {
runScenario(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ internal class FakeEditCardDetailsInteractorFactory : EditCardDetailsInteractor.
var billingDetailsCollectionConfiguration: PaymentSheet.BillingDetailsCollectionConfiguration? = null
private set

var cardEditConfiguration: CardEditConfiguration? = null
private set

var onCardUpdateParamsChanged: CardUpdateParamsCallback? = null
private set

Expand All @@ -19,6 +22,7 @@ internal class FakeEditCardDetailsInteractorFactory : EditCardDetailsInteractor.
onBrandChoiceChanged: CardBrandCallback,
onCardUpdateParamsChanged: CardUpdateParamsCallback
): EditCardDetailsInteractor {
this.cardEditConfiguration = cardEditConfiguration
this.onCardUpdateParamsChanged = onCardUpdateParamsChanged
this.billingDetailsCollectionConfiguration = billingDetailsCollectionConfiguration
return FakeEditCardDetailsInteractor(
Expand Down
Loading