Skip to content

Commit 3aa243d

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 a35942b commit 3aa243d

5 files changed

Lines changed: 565 additions & 10 deletions

File tree

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

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -73,14 +73,14 @@ fun cardBillingAddressCollectionMode(
7373
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
7474
class BillingAddressElement(
7575
override val identifier: IdentifierSpec,
76-
rawValuesMap: Map<IdentifierSpec, String?> = emptyMap(),
77-
countryCodes: Set<String> = emptySet(),
78-
countryDropdownFieldController: DropdownFieldController = DropdownFieldController(
76+
private val rawValuesMap: Map<IdentifierSpec, String?> = emptyMap(),
77+
private val countryCodes: Set<String> = emptySet(),
78+
private val countryDropdownFieldController: DropdownFieldController = DropdownFieldController(
7979
CountryConfig(countryCodes),
8080
rawValuesMap[IdentifierSpec.Country]
8181
),
82-
countryElementIdentifier: IdentifierSpec,
83-
autocompleteAddressInteractorFactory: AutocompleteAddressInteractor.Factory?,
82+
private val countryElementIdentifier: IdentifierSpec,
83+
private val autocompleteAddressInteractorFactory: AutocompleteAddressInteractor.Factory?,
8484
sameAsShippingElement: SameAsShippingElement?,
8585
shippingValuesMap: Map<IdentifierSpec, String?>?,
8686
private val addressCollectionMode: BillingAddressCollectionMode,
@@ -239,4 +239,37 @@ class BillingAddressElement(
239239
override fun onValidationStateChanged(isValidating: Boolean) {
240240
addressElement.onValidationStateChanged(isValidating)
241241
}
242+
243+
fun withAdditionalFields(
244+
additionalFieldsByCountry: Map<String, Set<IdentifierSpec>>,
245+
sameAsShippingElement: SameAsShippingElement?,
246+
shippingValuesMap: Map<IdentifierSpec, String?>?,
247+
): BillingAddressElement {
248+
val widenedCollectionMode = when (val mode = addressCollectionMode) {
249+
BillingAddressCollectionMode.Never -> mode
250+
BillingAddressCollectionMode.Full -> mode
251+
is BillingAddressCollectionMode.Country -> BillingAddressCollectionMode.Country(
252+
additionalFieldsByCountry = buildMap {
253+
putAll(mode.additionalFieldsByCountry)
254+
additionalFieldsByCountry.forEach { (countryCode, additionalFields) ->
255+
merge(countryCode, additionalFields) { existing, additional -> existing + additional }
256+
}
257+
}
258+
)
259+
}
260+
261+
return BillingAddressElement(
262+
identifier = identifier,
263+
rawValuesMap = rawValuesMap,
264+
countryCodes = countryCodes,
265+
countryDropdownFieldController = countryDropdownFieldController,
266+
countryElementIdentifier = countryElementIdentifier,
267+
autocompleteAddressInteractorFactory = autocompleteAddressInteractorFactory,
268+
sameAsShippingElement = sameAsShippingElement,
269+
shippingValuesMap = shippingValuesMap,
270+
addressCollectionMode = widenedCollectionMode,
271+
collectionConfiguration = collectionConfiguration,
272+
shouldHideCountryOnNoAddressCollection = shouldHideCountryOnNoAddressCollection,
273+
)
274+
}
242275
}
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
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+
arguments = arguments,
51+
sameAsShippingElement = sameAsShippingElement,
52+
appendSameAsShippingElement = existingSameAsShippingElement == null,
53+
)
54+
}
55+
56+
val addressElement = createTaxBillingAddressElement(
57+
arguments = arguments,
58+
countryDropdownFieldController = DropdownFieldController(
59+
config = CountryConfig(arguments.billingDetailsCollectionConfiguration.allowedBillingCountries),
60+
initialValue = arguments.initialValues[IdentifierSpec.Country],
61+
),
62+
sameAsShippingElement = sameAsShippingElement,
63+
)
64+
65+
return this + listOfNotNull(
66+
SectionElement.wrap(addressElement, R.string.stripe_billing_details.resolvableString),
67+
sameAsShippingElement,
68+
)
69+
}
70+
71+
private fun List<FormElement>.widenBillingAddress(
72+
addressField: BillingAddressElement,
73+
arguments: UiDefinitionFactory.Arguments,
74+
sameAsShippingElement: SameAsShippingElement?,
75+
appendSameAsShippingElement: Boolean,
76+
): List<FormElement> {
77+
val addressSectionIndex = indexOfFirst { element ->
78+
element is SectionElement && addressField in element.fields
79+
}
80+
val addressSection = this[addressSectionIndex] as SectionElement
81+
val widenedFields = addressSection.fields.map { field ->
82+
if (field === addressField) {
83+
addressField.withAdditionalFields(
84+
additionalFieldsByCountry = additionalAutomaticTaxFieldsByCountry,
85+
sameAsShippingElement = sameAsShippingElement,
86+
shippingValuesMap = arguments.shippingValues,
87+
)
88+
} else {
89+
field
90+
}
91+
}
92+
val widenedSection = SectionElement(
93+
identifier = addressSection.identifier,
94+
fields = widenedFields,
95+
controller = SectionController(
96+
label = addressSection.controller.label,
97+
sectionFieldValidationControllers = widenedFields.map { it.sectionFieldErrorController() },
98+
),
99+
)
100+
return flatMapIndexed { index, element ->
101+
if (index == addressSectionIndex) {
102+
listOfNotNull(
103+
widenedSection,
104+
sameAsShippingElement.takeIf { appendSameAsShippingElement },
105+
)
106+
} else {
107+
listOf(element)
108+
}
109+
}
110+
}
111+
112+
private fun createTaxBillingAddressElement(
113+
arguments: UiDefinitionFactory.Arguments,
114+
countryDropdownFieldController: DropdownFieldController,
115+
sameAsShippingElement: SameAsShippingElement?,
116+
): BillingAddressElement {
117+
return BillingAddressElement(
118+
identifier = IdentifierSpec.Generic("billing_details[address]"),
119+
rawValuesMap = arguments.initialValues,
120+
countryCodes = arguments.billingDetailsCollectionConfiguration.allowedBillingCountries,
121+
countryDropdownFieldController = countryDropdownFieldController,
122+
countryElementIdentifier = IdentifierSpec.Country,
123+
autocompleteAddressInteractorFactory = null,
124+
sameAsShippingElement = sameAsShippingElement,
125+
shippingValuesMap = arguments.shippingValues,
126+
addressCollectionMode = BillingAddressCollectionMode.Country(
127+
additionalFieldsByCountry = additionalAutomaticTaxFieldsByCountry,
128+
),
129+
collectionConfiguration = BillingDetailsCollectionConfiguration(
130+
collectName = false,
131+
collectEmail = false,
132+
collectPhone = false,
133+
address = BillingDetailsCollectionConfiguration.AddressCollectionMode.Automatic,
134+
allowedCountries = arguments.billingDetailsCollectionConfiguration.allowedBillingCountries,
135+
),
136+
shouldHideCountryOnNoAddressCollection = false,
137+
)
138+
}
139+
140+
private fun createSameAsShippingElement(
141+
arguments: UiDefinitionFactory.Arguments,
142+
): SameAsShippingElement? {
143+
return arguments.shippingValues
144+
?.get(IdentifierSpec.SameAsShipping)
145+
?.toBooleanStrictOrNull()
146+
?.let { isSameAsShipping ->
147+
SameAsShippingElement(
148+
identifier = IdentifierSpec.SameAsShipping,
149+
controller = SameAsShippingController(isSameAsShipping),
150+
)
151+
}
152+
}
153+
154+
private val separatelyRenderedPaymentMethodCodes = setOf(
155+
PaymentMethod.Type.USBankAccount.code,
156+
PaymentMethod.Type.Link.code,
157+
)

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
createFormElements(
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)