Skip to content

Commit bbdafcb

Browse files
cttsai-stripecodex
andcommitted
Collect automatic-tax billing address for LPM forms (MOBILESDK-4667)
Committed-By-Agent: codex Co-authored-by: codex <noreply@openai.com> Committed-By-Agent: codex Co-authored-by: codex <noreply@openai.com> Committed-By-Agent: codex Co-authored-by: codex <noreply@openai.com> Committed-By-Agent: codex Co-authored-by: codex <noreply@openai.com> Committed-By-Agent: codex Co-authored-by: codex <noreply@openai.com>
1 parent 8b2e4ec commit bbdafcb

5 files changed

Lines changed: 552 additions & 5 deletions

File tree

payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/BillingAddressElement.kt

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,4 +242,30 @@ class BillingAddressElement(
242242
override fun onValidationStateChanged(isValidating: Boolean) {
243243
addressElement.onValidationStateChanged(isValidating)
244244
}
245+
246+
fun withAdditionalFields(
247+
additionalFieldsByCountry: Map<String, Set<IdentifierSpec>>,
248+
sameAsShippingElement: SameAsShippingElement?,
249+
): BillingAddressElement {
250+
val widenedCollectionMode = when (val mode = configuration.addressCollectionMode) {
251+
BillingAddressCollectionMode.Never -> mode
252+
BillingAddressCollectionMode.Full -> mode
253+
is BillingAddressCollectionMode.Country -> BillingAddressCollectionMode.Country(
254+
additionalFieldsByCountry = buildMap {
255+
putAll(mode.additionalFieldsByCountry)
256+
additionalFieldsByCountry.forEach { (countryCode, additionalFields) ->
257+
merge(countryCode, additionalFields) { existing, additional -> existing + additional }
258+
}
259+
}
260+
)
261+
}
262+
263+
return BillingAddressElement(
264+
configuration = configuration.copy(
265+
addressCollectionMode = widenedCollectionMode,
266+
),
267+
countryDropdownFieldController = countryDropdownFieldController,
268+
sameAsShippingElement = sameAsShippingElement,
269+
)
270+
}
245271
}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
package com.stripe.android.lpmfoundations.paymentmethod
2+
3+
import com.stripe.android.core.strings.resolvableString
4+
import com.stripe.android.model.PaymentMethod
5+
import com.stripe.android.paymentsheet.PaymentSheet
6+
import com.stripe.android.ui.core.BillingDetailsCollectionConfiguration
7+
import com.stripe.android.ui.core.R
8+
import com.stripe.android.ui.core.elements.BillingAddressCollectionMode
9+
import com.stripe.android.ui.core.elements.BillingAddressElement
10+
import com.stripe.android.ui.core.elements.additionalAutomaticTaxFieldsByCountry
11+
import com.stripe.android.uicore.elements.AddressFieldsElement
12+
import com.stripe.android.uicore.elements.CountryConfig
13+
import com.stripe.android.uicore.elements.DropdownFieldController
14+
import com.stripe.android.uicore.elements.FormElement
15+
import com.stripe.android.uicore.elements.IdentifierSpec
16+
import com.stripe.android.uicore.elements.SameAsShippingController
17+
import com.stripe.android.uicore.elements.SameAsShippingElement
18+
import com.stripe.android.uicore.elements.SectionController
19+
import com.stripe.android.uicore.elements.SectionElement
20+
21+
/**
22+
* Widens an existing shared address for automatic tax, or appends a country-first billing
23+
* address when the payment method does not already collect one.
24+
*/
25+
internal fun List<FormElement>.withAutomaticTaxBillingAddressIfNecessary(
26+
paymentMethodCode: String,
27+
arguments: UiDefinitionFactory.Arguments,
28+
): List<FormElement> {
29+
val shouldCollectTaxAddress = arguments.requiresBillingAddressForAutomaticTax &&
30+
arguments.billingDetailsCollectionConfiguration.address ==
31+
PaymentSheet.BillingDetailsCollectionConfiguration.AddressCollectionMode.Automatic
32+
if (!shouldCollectTaxAddress || paymentMethodCode in separatelyRenderedPaymentMethodCodes) {
33+
return this
34+
}
35+
36+
val addressField = filterIsInstance<SectionElement>()
37+
.flatMap { it.fields }
38+
.filterIsInstance<AddressFieldsElement>()
39+
.firstOrNull()
40+
if (addressField != null && addressField !is BillingAddressElement) {
41+
return this
42+
}
43+
44+
val existingSameAsShippingElement = filterIsInstance<SameAsShippingElement>().firstOrNull()
45+
val sameAsShippingElement = existingSameAsShippingElement ?: createSameAsShippingElement(arguments)
46+
47+
if (addressField is BillingAddressElement) {
48+
return widenBillingAddress(
49+
addressField = addressField,
50+
sameAsShippingElement = sameAsShippingElement,
51+
appendSameAsShippingElement = existingSameAsShippingElement == null,
52+
)
53+
}
54+
55+
val addressElement = createTaxBillingAddressElement(
56+
arguments = arguments,
57+
countryDropdownFieldController = DropdownFieldController(
58+
config = CountryConfig(arguments.billingDetailsCollectionConfiguration.allowedBillingCountries),
59+
initialValue = arguments.initialValues[IdentifierSpec.Country],
60+
),
61+
sameAsShippingElement = sameAsShippingElement,
62+
)
63+
64+
return this + listOfNotNull(
65+
SectionElement.wrap(addressElement, R.string.stripe_billing_details.resolvableString),
66+
sameAsShippingElement,
67+
)
68+
}
69+
70+
private fun List<FormElement>.widenBillingAddress(
71+
addressField: BillingAddressElement,
72+
sameAsShippingElement: SameAsShippingElement?,
73+
appendSameAsShippingElement: Boolean,
74+
): List<FormElement> {
75+
val addressSectionIndex = indexOfFirst { element ->
76+
element is SectionElement && addressField in element.fields
77+
}
78+
val addressSection = this[addressSectionIndex] as SectionElement
79+
val widenedFields = addressSection.fields.map { field ->
80+
if (field === addressField) {
81+
addressField.withAdditionalFields(
82+
additionalFieldsByCountry = additionalAutomaticTaxFieldsByCountry,
83+
sameAsShippingElement = sameAsShippingElement,
84+
)
85+
} else {
86+
field
87+
}
88+
}
89+
val widenedSection = SectionElement(
90+
identifier = addressSection.identifier,
91+
fields = widenedFields,
92+
controller = SectionController(
93+
label = addressSection.controller.label,
94+
sectionFieldValidationControllers = widenedFields.map { it.sectionFieldErrorController() },
95+
),
96+
)
97+
return flatMapIndexed { index, element ->
98+
if (index == addressSectionIndex) {
99+
listOfNotNull(
100+
widenedSection,
101+
sameAsShippingElement.takeIf { appendSameAsShippingElement },
102+
)
103+
} else {
104+
listOf(element)
105+
}
106+
}
107+
}
108+
109+
private fun createTaxBillingAddressElement(
110+
arguments: UiDefinitionFactory.Arguments,
111+
countryDropdownFieldController: DropdownFieldController,
112+
sameAsShippingElement: SameAsShippingElement?,
113+
): BillingAddressElement {
114+
return BillingAddressElement(
115+
configuration = BillingAddressElement.Configuration(
116+
identifier = IdentifierSpec.Generic("billing_details[address]"),
117+
initialValues = arguments.initialValues,
118+
countryCodes = arguments.billingDetailsCollectionConfiguration.allowedBillingCountries,
119+
countryElementIdentifier = IdentifierSpec.Country,
120+
autocompleteAddressInteractorFactory = null,
121+
shippingValues = arguments.shippingValues,
122+
addressCollectionMode = BillingAddressCollectionMode.Country(
123+
additionalFieldsByCountry = additionalAutomaticTaxFieldsByCountry,
124+
),
125+
collectionConfiguration = BillingDetailsCollectionConfiguration(
126+
collectName = false,
127+
collectEmail = false,
128+
collectPhone = false,
129+
address = BillingDetailsCollectionConfiguration.AddressCollectionMode.Automatic,
130+
allowedCountries = arguments.billingDetailsCollectionConfiguration.allowedBillingCountries,
131+
),
132+
shouldHideCountryOnNoAddressCollection = false,
133+
),
134+
countryDropdownFieldController = countryDropdownFieldController,
135+
sameAsShippingElement = sameAsShippingElement,
136+
)
137+
}
138+
139+
private fun createSameAsShippingElement(
140+
arguments: UiDefinitionFactory.Arguments,
141+
): SameAsShippingElement? {
142+
return arguments.shippingValues
143+
?.get(IdentifierSpec.SameAsShipping)
144+
?.toBooleanStrictOrNull()
145+
?.let { isSameAsShipping ->
146+
SameAsShippingElement(
147+
identifier = IdentifierSpec.SameAsShipping,
148+
controller = SameAsShippingController(isSameAsShipping),
149+
)
150+
}
151+
}
152+
153+
private val separatelyRenderedPaymentMethodCodes = setOf(
154+
PaymentMethod.Type.USBankAccount.code,
155+
PaymentMethod.Type.Link.code,
156+
)

paymentsheet/src/main/java/com/stripe/android/lpmfoundations/paymentmethod/UiDefinitionFactory.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,10 @@ internal sealed interface UiDefinitionFactory {
185185

186186
buildFormElements(metadata, arguments, builder)
187187

188-
return builder.build()
188+
return builder.build().withAutomaticTaxBillingAddressIfNecessary(
189+
paymentMethodCode = createSupportedPaymentMethod(metadata).code,
190+
arguments = arguments,
191+
)
189192
}
190193

191194
protected open fun buildFormElements(
@@ -302,6 +305,9 @@ internal sealed interface UiDefinitionFactory {
302305
createBaseFormElements(
303306
metadata = metadata,
304307
arguments = arguments,
308+
).withAutomaticTaxBillingAddressIfNecessary(
309+
paymentMethodCode = definition.type.code,
310+
arguments = arguments,
305311
)
306312
}
307313

@@ -313,6 +319,9 @@ internal sealed interface UiDefinitionFactory {
313319
sharedDataSpec = sharedDataSpec,
314320
transformSpecToElements = TransformSpecToElements(arguments),
315321
arguments = arguments,
322+
).withAutomaticTaxBillingAddressIfNecessary(
323+
paymentMethodCode = definition.type.code,
324+
arguments = arguments,
316325
)
317326
} else {
318327
null

paymentsheet/src/main/java/com/stripe/android/paymentsheet/forms/FormViewModel.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ internal class FormViewModel(
5858
}
5959
}
6060

61-
private val cardBillingElement = elements.filterIsInstance<SectionElement>()
61+
private val billingAddressElement = elements.filterIsInstance<SectionElement>()
6262
.flatMap { it.fields }
6363
.filterIsInstance<BillingAddressElement>()
6464
.firstOrNull()
@@ -77,10 +77,10 @@ internal class FormViewModel(
7777
}
7878

7979
internal val hiddenIdentifiers = combineAsStateFlow(
80-
cardBillingElement?.hiddenIdentifiers ?: stateFlowOf(emptySet()),
80+
billingAddressElement?.hiddenIdentifiers ?: stateFlowOf(emptySet()),
8181
externalHiddenIdentifiers
82-
) { cardBillingIdentifiers, externalHiddenIdentifiers ->
83-
externalHiddenIdentifiers.plus(cardBillingIdentifiers)
82+
) { billingAddressIdentifiers, externalHiddenIdentifiers ->
83+
externalHiddenIdentifiers.plus(billingAddressIdentifiers)
8484
}
8585

8686
// This will convert the save for future use value into a CustomerRequestedSave operation

0 commit comments

Comments
 (0)