Skip to content

Commit 4ac1fce

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>
1 parent ec9398c commit 4ac1fce

4 files changed

Lines changed: 445 additions & 16 deletions

File tree

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
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+
countryElement = field,
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+
countryElement = CountryElement(
78+
identifier = IdentifierSpec.Country,
79+
controller = DropdownFieldController(
80+
config = CountryConfig(arguments.billingDetailsCollectionConfiguration.allowedBillingCountries),
81+
initialValue = arguments.initialValues[IdentifierSpec.Country],
82+
),
83+
),
84+
sameAsShippingElement = sameAsShippingElement,
85+
)
86+
87+
return this + listOfNotNull(
88+
SectionElement.wrap(addressElement, R.string.stripe_billing_details.resolvableString),
89+
sameAsShippingElement,
90+
)
91+
}
92+
93+
private fun createTaxBillingAddressElement(
94+
arguments: UiDefinitionFactory.Arguments,
95+
countryElement: CountryElement,
96+
sameAsShippingElement: SameAsShippingElement?,
97+
): BillingAddressElement {
98+
return BillingAddressElement(
99+
identifier = IdentifierSpec.Generic("billing_details[address]"),
100+
rawValuesMap = arguments.initialValues,
101+
countryCodes = arguments.billingDetailsCollectionConfiguration.allowedBillingCountries,
102+
countryElement = countryElement,
103+
autocompleteAddressInteractorFactory = null,
104+
sameAsShippingElement = sameAsShippingElement,
105+
shippingValuesMap = arguments.shippingValues,
106+
collectionConfiguration = BillingDetailsCollectionConfiguration(
107+
collectName = false,
108+
collectEmail = false,
109+
collectPhone = false,
110+
address = BillingDetailsCollectionConfiguration.AddressCollectionMode.Automatic,
111+
allowedCountries = arguments.billingDetailsCollectionConfiguration.allowedBillingCountries,
112+
),
113+
shouldHideCountryOnNoAddressCollection = false,
114+
requiresBillingAddressForAutomaticTax = true,
115+
)
116+
}
117+
118+
private fun createSameAsShippingElement(
119+
arguments: UiDefinitionFactory.Arguments,
120+
): SameAsShippingElement? {
121+
return arguments.shippingValues
122+
?.get(IdentifierSpec.SameAsShipping)
123+
?.toBooleanStrictOrNull()
124+
?.let { isSameAsShipping ->
125+
SameAsShippingElement(
126+
identifier = IdentifierSpec.SameAsShipping,
127+
controller = SameAsShippingController(isSameAsShipping),
128+
)
129+
}
130+
}
131+
132+
private val separatelyRenderedPaymentMethodCodes = setOf(
133+
PaymentMethod.Type.USBankAccount.code,
134+
PaymentMethod.Type.Link.code,
135+
)

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: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import androidx.lifecycle.viewModelScope
77
import com.stripe.android.paymentsheet.forms.PlaceholderHelper.connectBillingDetailsFields
88
import com.stripe.android.paymentsheet.model.PaymentSelection
99
import com.stripe.android.paymentsheet.paymentdatacollection.FormArguments
10-
import com.stripe.android.ui.core.elements.CardBillingAddressElement
10+
import com.stripe.android.ui.core.elements.BillingAddressElement
1111
import com.stripe.android.uicore.elements.FormElement
1212
import com.stripe.android.uicore.elements.IdentifierSpec
1313
import com.stripe.android.uicore.elements.SectionElement
@@ -58,9 +58,9 @@ internal class FormViewModel(
5858
}
5959
}
6060

61-
private val cardBillingElement = elements.filterIsInstance<SectionElement>()
61+
private val billingAddressElement = elements.filterIsInstance<SectionElement>()
6262
.flatMap { it.fields }
63-
.filterIsInstance<CardBillingAddressElement>()
63+
.filterIsInstance<BillingAddressElement>()
6464
.firstOrNull()
6565

6666
private var externalHiddenIdentifiers = MutableStateFlow(emptySet<IdentifierSpec>())
@@ -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)