Skip to content

Commit 223dbf9

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>
1 parent 354f22e commit 223dbf9

4 files changed

Lines changed: 496 additions & 14 deletions

File tree

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
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.BillingAddressElement
9+
import com.stripe.android.uicore.elements.AddressFieldsElement
10+
import com.stripe.android.uicore.elements.CountryConfig
11+
import com.stripe.android.uicore.elements.CountryElement
12+
import com.stripe.android.uicore.elements.DropdownFieldController
13+
import com.stripe.android.uicore.elements.FormElement
14+
import com.stripe.android.uicore.elements.IdentifierSpec
15+
import com.stripe.android.uicore.elements.SameAsShippingController
16+
import com.stripe.android.uicore.elements.SameAsShippingElement
17+
import com.stripe.android.uicore.elements.SectionController
18+
import com.stripe.android.uicore.elements.SectionElement
19+
20+
/**
21+
* Widens an existing country section for automatic tax, or appends a country-first billing
22+
* address when the payment method does not already collect one.
23+
*/
24+
internal fun List<FormElement>.withAutomaticTaxBillingAddressIfNecessary(
25+
paymentMethodCode: String,
26+
arguments: UiDefinitionFactory.Arguments,
27+
): List<FormElement> {
28+
val shouldCollectTaxAddress = arguments.requiresBillingAddressForAutomaticTax &&
29+
arguments.billingDetailsCollectionConfiguration.address ==
30+
PaymentSheet.BillingDetailsCollectionConfiguration.AddressCollectionMode.Automatic
31+
if (!shouldCollectTaxAddress || paymentMethodCode in separatelyRenderedPaymentMethodCodes) {
32+
return this
33+
}
34+
35+
val sections = filterIsInstance<SectionElement>()
36+
if (sections.any { section -> section.fields.any { it is AddressFieldsElement } }) {
37+
return this
38+
}
39+
40+
val sameAsShippingElement = createSameAsShippingElement(arguments)
41+
42+
val countrySectionIndex = indexOfFirst { element ->
43+
element is SectionElement && element.fields.any { it is CountryElement }
44+
}
45+
if (countrySectionIndex >= 0) {
46+
val countrySection = this[countrySectionIndex] as SectionElement
47+
val widenedFields = countrySection.fields.map { field ->
48+
if (field is CountryElement) {
49+
createTaxBillingAddressElement(
50+
arguments = arguments,
51+
countryDropdownFieldController = field.controller,
52+
sameAsShippingElement = sameAsShippingElement,
53+
)
54+
} else {
55+
field
56+
}
57+
}
58+
val widenedSection = SectionElement(
59+
identifier = countrySection.identifier,
60+
fields = widenedFields,
61+
controller = SectionController(
62+
label = countrySection.controller.label,
63+
sectionFieldValidationControllers = widenedFields.map { it.sectionFieldErrorController() },
64+
),
65+
)
66+
return flatMapIndexed { index, element ->
67+
if (index == countrySectionIndex) {
68+
listOfNotNull(widenedSection, sameAsShippingElement)
69+
} else {
70+
listOf(element)
71+
}
72+
}
73+
}
74+
75+
val addressElement = createTaxBillingAddressElement(
76+
arguments = arguments,
77+
countryDropdownFieldController = DropdownFieldController(
78+
config = CountryConfig(arguments.billingDetailsCollectionConfiguration.allowedBillingCountries),
79+
initialValue = arguments.initialValues[IdentifierSpec.Country],
80+
),
81+
sameAsShippingElement = sameAsShippingElement,
82+
)
83+
84+
return this + listOfNotNull(
85+
SectionElement.wrap(addressElement, R.string.stripe_billing_details.resolvableString),
86+
sameAsShippingElement,
87+
)
88+
}
89+
90+
private fun createTaxBillingAddressElement(
91+
arguments: UiDefinitionFactory.Arguments,
92+
countryDropdownFieldController: DropdownFieldController,
93+
sameAsShippingElement: SameAsShippingElement?,
94+
): BillingAddressElement {
95+
return BillingAddressElement(
96+
identifier = IdentifierSpec.Generic("billing_details[address]"),
97+
rawValuesMap = arguments.initialValues,
98+
countryCodes = arguments.billingDetailsCollectionConfiguration.allowedBillingCountries,
99+
countryDropdownFieldController = countryDropdownFieldController,
100+
autocompleteAddressInteractorFactory = null,
101+
sameAsShippingElement = sameAsShippingElement,
102+
shippingValuesMap = arguments.shippingValues,
103+
collectionConfiguration = BillingDetailsCollectionConfiguration(
104+
collectName = false,
105+
collectEmail = false,
106+
collectPhone = false,
107+
address = BillingDetailsCollectionConfiguration.AddressCollectionMode.Automatic,
108+
allowedCountries = arguments.billingDetailsCollectionConfiguration.allowedBillingCountries,
109+
),
110+
shouldHideCountryOnNoAddressCollection = false,
111+
requiresBillingAddressForAutomaticTax = true,
112+
)
113+
}
114+
115+
private fun createSameAsShippingElement(
116+
arguments: UiDefinitionFactory.Arguments,
117+
): SameAsShippingElement? {
118+
return arguments.shippingValues
119+
?.get(IdentifierSpec.SameAsShipping)
120+
?.toBooleanStrictOrNull()
121+
?.let { isSameAsShipping ->
122+
SameAsShippingElement(
123+
identifier = IdentifierSpec.SameAsShipping,
124+
controller = SameAsShippingController(isSameAsShipping),
125+
)
126+
}
127+
}
128+
129+
private val separatelyRenderedPaymentMethodCodes = setOf(
130+
PaymentMethod.Type.USBankAccount.code,
131+
PaymentMethod.Type.Link.code,
132+
)

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

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -330,29 +330,37 @@ internal data class PaymentMethodMetadata(
330330
code: String,
331331
uiDefinitionFactoryArgumentsFactory: UiDefinitionFactory.Arguments.Factory,
332332
): List<FormElement>? {
333-
return if (isExternalPaymentMethod(code)) {
333+
val (formElements, arguments) = if (isExternalPaymentMethod(code)) {
334+
val arguments = uiDefinitionFactoryArgumentsFactory.create(this, requiresMandate = false)
334335
getUiDefinitionFactoryForExternalPaymentMethod(code)?.createFormElements(
335336
metadata = this,
336-
arguments = uiDefinitionFactoryArgumentsFactory.create(this, requiresMandate = false)
337-
)
337+
arguments = arguments,
338+
) to arguments
338339
} else if (isCustomPaymentMethod(code)) {
340+
val arguments = uiDefinitionFactoryArgumentsFactory.create(this, requiresMandate = false)
339341
getUiDefinitionFactoryForCustomPaymentMethod(code)?.createFormElements(
340342
metadata = this,
341-
arguments = uiDefinitionFactoryArgumentsFactory.create(this, requiresMandate = false)
342-
)
343+
arguments = arguments,
344+
) to arguments
343345
} else {
344346
val definition = supportedPaymentMethodDefinitions().firstOrNull { it.type.code == code } ?: return null
347+
val arguments = uiDefinitionFactoryArgumentsFactory.create(
348+
metadata = this,
349+
requiresMandate = definition.requiresMandate(this),
350+
)
345351

346352
definition.uiDefinitionFactory(this).formElements(
347353
metadata = this,
348354
definition = definition,
349355
sharedDataSpecs = sharedDataSpecs,
350-
arguments = uiDefinitionFactoryArgumentsFactory.create(
351-
metadata = this,
352-
requiresMandate = definition.requiresMandate(this),
353-
),
354-
)
356+
arguments = arguments,
357+
) to arguments
355358
}
359+
360+
return formElements?.withAutomaticTaxBillingAddressIfNecessary(
361+
paymentMethodCode = code,
362+
arguments = arguments,
363+
)
356364
}
357365

358366
fun allowRedisplay(

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)