Skip to content

Commit e9de263

Browse files
Expose billingDetails on Checkout PaymentOptionDisplayData (#13656)
Toward iOS Checkout Elements API parity: iOS's `Session.paymentOption` (`PaymentOptionDisplayData`) exposes `billingDetails` so merchants building their own billing UI can read the collected billing details. Android's checkout `PaymentOptionDisplayData` was missing it. Add `billingDetails: PaymentSheet.BillingDetails?` and populate it in `DefaultCheckoutPaymentOptionDisplayDataFactory` reusing the same `selection.billingDetails?.toPaymentSheetBillingDetails()` derivation used by EmbeddedPaymentElement's PaymentOptionDisplayData. Committed-By-Agent: claude Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent f8d997e commit e9de263

4 files changed

Lines changed: 134 additions & 0 deletions

File tree

paymentsheet/src/main/java/com/stripe/android/checkout/CheckoutController.kt

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -635,6 +635,10 @@ class CheckoutController @Inject internal constructor(
635635
* A user facing string representing the payment method; e.g. "Google Pay" or "···· 4242" for a card.
636636
*/
637637
val label: String,
638+
/**
639+
* The billing details associated with the customer's selected payment method, if any were collected.
640+
*/
641+
val billingDetails: BillingDetails?,
638642
/**
639643
* A string representation of the customer's desired payment method:
640644
* - If this is a Stripe payment method, see
@@ -664,6 +668,64 @@ class CheckoutController @Inject internal constructor(
664668
}
665669
}
666670

671+
/**
672+
* The billing details collected for a payment method.
673+
*/
674+
@Poko
675+
@CheckoutSessionPreview
676+
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
677+
class BillingDetails internal constructor(
678+
/**
679+
* The customer's billing address.
680+
*/
681+
val address: Address?,
682+
/**
683+
* The customer's email address.
684+
*/
685+
val email: String?,
686+
/**
687+
* The customer's full name.
688+
*/
689+
val name: String?,
690+
/**
691+
* The customer's phone number, without formatting (e.g. 5551234567).
692+
*/
693+
val phone: String?,
694+
) {
695+
/**
696+
* A billing address.
697+
*/
698+
@Poko
699+
@CheckoutSessionPreview
700+
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
701+
class Address internal constructor(
702+
/**
703+
* City, district, suburb, town, or village.
704+
*/
705+
val city: String?,
706+
/**
707+
* Two-letter country code (ISO 3166-1 alpha-2).
708+
*/
709+
val country: String?,
710+
/**
711+
* Address line 1 (e.g., street, PO Box, or company name).
712+
*/
713+
val line1: String?,
714+
/**
715+
* Address line 2 (e.g., apartment, suite, unit, or building).
716+
*/
717+
val line2: String?,
718+
/**
719+
* ZIP or postal code.
720+
*/
721+
val postalCode: String?,
722+
/**
723+
* State, county, province, or region.
724+
*/
725+
val state: String?,
726+
)
727+
}
728+
667729
@CheckoutSessionPreview
668730
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
669731
class Builder(

paymentsheet/src/main/java/com/stripe/android/checkout/CheckoutPaymentOptionDisplayDataFactory.kt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ import androidx.compose.ui.text.AnnotatedString
55
import com.stripe.android.checkout.CheckoutController.Session.PaymentOptionDisplayData
66
import com.stripe.android.link.account.LinkAccountHolder
77
import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodMetadata
8+
import com.stripe.android.model.PaymentMethod
89
import com.stripe.android.paymentelement.CheckoutSessionPreview
910
import com.stripe.android.paymentelement.embedded.content.NullUiDefinitionFactoryHelper
1011
import com.stripe.android.paymentsheet.PaymentOptionCardArtDrawableLoader
1112
import com.stripe.android.paymentsheet.model.PaymentSelection
13+
import com.stripe.android.paymentsheet.model.billingDetails
1214
import com.stripe.android.paymentsheet.model.darkThemeIconUrl
1315
import com.stripe.android.paymentsheet.model.drawableResourceId
1416
import com.stripe.android.paymentsheet.model.drawableResourceIdNight
@@ -69,8 +71,28 @@ internal class DefaultCheckoutPaymentOptionDisplayDataFactory @Inject constructo
6971
linkAccountHolder.linkAccountInfo.value.account
7072
)
7173
).resolve(context),
74+
billingDetails = selection.billingDetails?.toCheckoutBillingDetails(),
7275
paymentMethodType = selection.paymentMethodType,
7376
mandateText = if (mandate == null) null else AnnotatedString(mandate.resolve(context)),
7477
)
7578
}
7679
}
80+
81+
@OptIn(CheckoutSessionPreview::class)
82+
private fun PaymentMethod.BillingDetails.toCheckoutBillingDetails(): CheckoutController.BillingDetails {
83+
return CheckoutController.BillingDetails(
84+
address = address?.let { modelAddress ->
85+
CheckoutController.BillingDetails.Address(
86+
city = modelAddress.city,
87+
country = modelAddress.country,
88+
line1 = modelAddress.line1,
89+
line2 = modelAddress.line2,
90+
postalCode = modelAddress.postalCode,
91+
state = modelAddress.state,
92+
)
93+
},
94+
email = email,
95+
name = name,
96+
phone = phone,
97+
)
98+
}

paymentsheet/src/test/java/com/stripe/android/checkout/CheckoutControllerStateHolderTest.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ internal class CheckoutControllerStateHolderTest {
3535
val expectedOption = PaymentOptionDisplayData(
3636
imageLoader = { error("not needed for this test") },
3737
label = "Google Pay",
38+
billingDetails = null,
3839
paymentMethodType = "google_pay",
3940
mandateText = null,
4041
)

paymentsheet/src/test/java/com/stripe/android/checkout/DefaultCheckoutPaymentOptionFactoryTest.kt

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import com.stripe.android.link.model.LinkAccount
1313
import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodMetadata
1414
import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodMetadataFactory
1515
import com.stripe.android.model.LinkBrand
16+
import com.stripe.android.model.PaymentMethod
1617
import com.stripe.android.model.PaymentMethodFixtures
1718
import com.stripe.android.model.SetupIntentFixtures
1819
import com.stripe.android.paymentelement.CheckoutSessionPreview
@@ -80,6 +81,54 @@ internal class DefaultCheckoutPaymentOptionFactoryTest {
8081
assertThat(option?.mandateText).isNull()
8182
}
8283

84+
@Test
85+
fun `create populates billing details from a saved payment method`() = runScenario {
86+
val option = factory.create(
87+
selection = PaymentSelection.Saved(PaymentMethodFixtures.CARD_PAYMENT_METHOD),
88+
paymentMethodMetadata = metadata,
89+
)
90+
91+
val billingDetails = requireNotNull(option?.billingDetails)
92+
assertThat(billingDetails.name).isEqualTo("Jenny Rosen")
93+
assertThat(billingDetails.email).isEqualTo("jenny.rosen@example.com")
94+
assertThat(billingDetails.phone).isEqualTo("123-456-7890")
95+
assertThat(billingDetails.address?.line1).isEqualTo("1234 Main Street")
96+
assertThat(billingDetails.address?.city).isEqualTo("San Francisco")
97+
assertThat(billingDetails.address?.state).isEqualTo("CA")
98+
assertThat(billingDetails.address?.postalCode).isEqualTo("94111")
99+
assertThat(billingDetails.address?.country).isEqualTo("US")
100+
}
101+
102+
@Test
103+
fun `create leaves billing details null for Google Pay`() = runScenario {
104+
val option = factory.create(selection = PaymentSelection.GooglePay, paymentMethodMetadata = metadata)
105+
106+
assertThat(option?.billingDetails).isNull()
107+
}
108+
109+
@Test
110+
fun `create leaves address null when the payment method has billing details but no address`() = runScenario {
111+
val option = factory.create(
112+
selection = PaymentSelection.Saved(
113+
PaymentMethodFixtures.CARD_PAYMENT_METHOD.copy(
114+
billingDetails = PaymentMethod.BillingDetails(
115+
name = "Jenny Rosen",
116+
email = "jenny.rosen@example.com",
117+
phone = "123-456-7890",
118+
address = null,
119+
),
120+
),
121+
),
122+
paymentMethodMetadata = metadata,
123+
)
124+
125+
val billingDetails = requireNotNull(option?.billingDetails)
126+
assertThat(billingDetails.name).isEqualTo("Jenny Rosen")
127+
assertThat(billingDetails.email).isEqualTo("jenny.rosen@example.com")
128+
assertThat(billingDetails.phone).isEqualTo("123-456-7890")
129+
assertThat(billingDetails.address).isNull()
130+
}
131+
83132
@Test
84133
fun `imageLoader returns the card art when the card art loader provides one`() = runScenario(
85134
cardArt = ColorDrawable(),

0 commit comments

Comments
 (0)