Skip to content

Commit 1f16579

Browse files
cttsai-stripecodex
andcommitted
Collect automatic-tax billing address for US bank accounts (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 ace5fd6 commit 1f16579

8 files changed

Lines changed: 240 additions & 30 deletions

File tree

paymentsheet/src/main/java/com/stripe/android/customersheet/CustomerSheetViewModel.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -903,6 +903,7 @@ internal class CustomerSheetViewModel(
903903
sellerBusinessName = null,
904904
forceSetupFutureUseBehavior = false,
905905
clientAttributionMetadata = clientAttributionMetadata,
906+
requiresBillingAddressForAutomaticTax = false,
906907
)
907908
}
908909

paymentsheet/src/main/java/com/stripe/android/paymentsheet/paymentdatacollection/ach/USBankAccountForm.kt

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ import androidx.compose.ui.text.style.TextOverflow
3131
import androidx.compose.ui.unit.dp
3232
import androidx.lifecycle.viewmodel.compose.viewModel
3333
import com.stripe.android.core.strings.resolvableString
34-
import com.stripe.android.paymentsheet.PaymentSheet.BillingDetailsCollectionConfiguration.AddressCollectionMode
3534
import com.stripe.android.paymentsheet.PaymentSheet.BillingDetailsCollectionConfiguration.CollectionMode
3635
import com.stripe.android.paymentsheet.R
3736
import com.stripe.android.paymentsheet.model.PaymentSelection.New
@@ -56,6 +55,7 @@ import com.stripe.android.uicore.elements.SameAsShippingElement
5655
import com.stripe.android.uicore.elements.SameAsShippingElementUI
5756
import com.stripe.android.uicore.elements.Section
5857
import com.stripe.android.uicore.elements.SectionCard
58+
import com.stripe.android.uicore.elements.SectionFieldValidationController
5959
import com.stripe.android.uicore.elements.TextField
6060
import com.stripe.android.uicore.elements.TextFieldController
6161
import com.stripe.android.uicore.elements.TextFieldSection
@@ -102,13 +102,15 @@ internal fun USBankAccountForm(
102102
sellerBusinessName = usBankAccountFormArgs.sellerBusinessName,
103103
forceSetupFutureUseBehavior = usBankAccountFormArgs.forceSetupFutureUseBehavior,
104104
clientAttributionMetadata = usBankAccountFormArgs.clientAttributionMetadata,
105+
requiresBillingAddressForAutomaticTax = usBankAccountFormArgs.requiresBillingAddressForAutomaticTax,
105106
)
106107
},
107108
)
108109

109110
val state by viewModel.currentScreenState.collectAsState()
110111
val lastTextFieldIdentifier by viewModel.lastTextFieldIdentifier.collectAsState()
111112
val addressController by viewModel.addressElement.addressController.collectAsState()
113+
val addressHiddenIdentifiers by viewModel.addressHiddenIdentifiers.collectAsState()
112114
val isEnabled = !state.isProcessing && enabled
113115

114116
USBankAccountEmitters(
@@ -127,6 +129,9 @@ internal fun USBankAccountForm(
127129
emailController = viewModel.emailController,
128130
phoneController = viewModel.phoneController,
129131
addressController = addressController,
132+
addressValidationController = viewModel.addressElement.sectionFieldErrorController(),
133+
addressHiddenIdentifiers = addressHiddenIdentifiers,
134+
showAddress = viewModel.collectingAddress,
130135
lastTextFieldIdentifier = lastTextFieldIdentifier,
131136
sameAsShippingElement = viewModel.sameAsShippingElement,
132137
saveForFutureUseElement = viewModel.saveForFutureUseElement,
@@ -148,6 +153,9 @@ internal fun BankAccountForm(
148153
emailController: TextFieldController,
149154
phoneController: PhoneNumberController,
150155
addressController: AddressController,
156+
addressValidationController: SectionFieldValidationController,
157+
addressHiddenIdentifiers: Set<IdentifierSpec>,
158+
showAddress: Boolean,
151159
lastTextFieldIdentifier: IdentifierSpec?,
152160
sameAsShippingElement: SameAsShippingElement?,
153161
saveForFutureUseElement: SaveForFutureUseElement,
@@ -165,6 +173,9 @@ internal fun BankAccountForm(
165173
emailController = emailController,
166174
phoneController = phoneController,
167175
addressController = addressController,
176+
addressValidationController = addressValidationController,
177+
addressHiddenIdentifiers = addressHiddenIdentifiers,
178+
showAddress = showAddress,
168179
lastTextFieldIdentifier = lastTextFieldIdentifier,
169180
sameAsShippingElement = sameAsShippingElement,
170181
enabled = enabled
@@ -215,6 +226,9 @@ private fun BillingDetailsForm(
215226
emailController: TextFieldController,
216227
phoneController: PhoneNumberController,
217228
addressController: AddressController,
229+
addressValidationController: SectionFieldValidationController,
230+
addressHiddenIdentifiers: Set<IdentifierSpec>,
231+
showAddress: Boolean,
218232
lastTextFieldIdentifier: IdentifierSpec?,
219233
sameAsShippingElement: SameAsShippingElement?,
220234
) {
@@ -292,10 +306,12 @@ private fun BillingDetailsForm(
292306
modifier = Modifier.padding(top = 16.dp)
293307
)
294308
}
295-
if (formArgs.billingDetailsCollectionConfiguration.address == AddressCollectionMode.Full) {
309+
if (showAddress) {
296310
AddressSection(
297311
enabled = enabled,
298312
addressController = addressController,
313+
addressValidationController = addressValidationController,
314+
hiddenIdentifiers = addressHiddenIdentifiers,
299315
lastTextFieldIdentifier = lastTextFieldIdentifier,
300316
sameAsShippingElement = sameAsShippingElement,
301317
modifier = Modifier.padding(top = 16.dp)
@@ -337,11 +353,13 @@ private fun PhoneSection(
337353
private fun AddressSection(
338354
enabled: Boolean,
339355
addressController: AddressController,
356+
addressValidationController: SectionFieldValidationController,
357+
hiddenIdentifiers: Set<IdentifierSpec>,
340358
lastTextFieldIdentifier: IdentifierSpec?,
341359
sameAsShippingElement: SameAsShippingElement?,
342360
modifier: Modifier = Modifier,
343361
) {
344-
val validationMessage by addressController.validationMessage.collectAsState()
362+
val validationMessage by addressValidationController.validationMessage.collectAsState()
345363

346364
Box(
347365
modifier = Modifier
@@ -357,7 +375,7 @@ private fun AddressSection(
357375
AddressElementUI(
358376
enabled = enabled,
359377
controller = addressController,
360-
hiddenIdentifiers = emptySet(),
378+
hiddenIdentifiers = hiddenIdentifiers,
361379
lastTextFieldIdentifier = lastTextFieldIdentifier,
362380
)
363381
}

paymentsheet/src/main/java/com/stripe/android/paymentsheet/paymentdatacollection/ach/USBankAccountFormArguments.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ internal class USBankAccountFormArguments(
7070
val sellerBusinessName: String?,
7171
val forceSetupFutureUseBehavior: Boolean,
7272
val clientAttributionMetadata: ClientAttributionMetadata,
73+
val requiresBillingAddressForAutomaticTax: Boolean,
7374
) {
7475
companion object {
7576
fun create(
@@ -121,6 +122,8 @@ internal class USBankAccountFormArguments(
121122
sellerBusinessName = paymentMethodMetadata.sellerBusinessName,
122123
forceSetupFutureUseBehavior = paymentMethodMetadata.forceSetupFutureUseBehaviorAndNewMandate,
123124
clientAttributionMetadata = paymentMethodMetadata.clientAttributionMetadata,
125+
requiresBillingAddressForAutomaticTax = paymentMethodMetadata.requiresBillingAddressForAutomaticTax &&
126+
instantDebits.not(),
124127
)
125128
}
126129

@@ -182,6 +185,8 @@ internal class USBankAccountFormArguments(
182185
sellerBusinessName = paymentMethodMetadata.sellerBusinessName,
183186
forceSetupFutureUseBehavior = paymentMethodMetadata.forceSetupFutureUseBehaviorAndNewMandate,
184187
clientAttributionMetadata = paymentMethodMetadata.clientAttributionMetadata,
188+
requiresBillingAddressForAutomaticTax = paymentMethodMetadata.requiresBillingAddressForAutomaticTax &&
189+
instantDebits.not(),
185190
)
186191
}
187192
}

paymentsheet/src/main/java/com/stripe/android/paymentsheet/paymentdatacollection/ach/USBankAccountFormViewModel.kt

Lines changed: 69 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,18 @@ import com.stripe.android.paymentsheet.paymentdatacollection.ach.BankFormScreenS
4646
import com.stripe.android.paymentsheet.paymentdatacollection.ach.USBankAccountFormViewModel.AnalyticsEvent.Finished
4747
import com.stripe.android.paymentsheet.paymentdatacollection.ach.di.DaggerUSBankAccountFormComponent
4848
import com.stripe.android.paymentsheet.utils.getSetAsDefaultPaymentMethodFromPaymentSelection
49+
import com.stripe.android.ui.core.BillingDetailsCollectionConfiguration
50+
import com.stripe.android.ui.core.elements.BillingAddressCollectionMode
51+
import com.stripe.android.ui.core.elements.BillingAddressElement
4952
import com.stripe.android.ui.core.elements.SaveForFutureUseElement
5053
import com.stripe.android.ui.core.elements.SetAsDefaultPaymentMethodElement
54+
import com.stripe.android.ui.core.elements.additionalAutomaticTaxFieldsByCountry
5155
import com.stripe.android.uicore.elements.AddressElement
56+
import com.stripe.android.uicore.elements.AddressFieldsElement
5257
import com.stripe.android.uicore.elements.AutocompleteAddressElement
5358
import com.stripe.android.uicore.elements.AutocompleteAddressInteractor
59+
import com.stripe.android.uicore.elements.CountryConfig
60+
import com.stripe.android.uicore.elements.DropdownFieldController
5461
import com.stripe.android.uicore.elements.EmailConfig
5562
import com.stripe.android.uicore.elements.IdentifierSpec
5663
import com.stripe.android.uicore.elements.NameConfig
@@ -81,8 +88,11 @@ internal class USBankAccountFormViewModel @Inject internal constructor(
8188
private val defaultBillingDetails = args.formArgs.billingDetails
8289
private val collectionConfiguration = args.formArgs.billingDetailsCollectionConfiguration
8390

84-
private val collectingAddress =
85-
args.formArgs.billingDetailsCollectionConfiguration.address == AddressCollectionMode.Full
91+
private val collectingTaxAddress = args.requiresBillingAddressForAutomaticTax &&
92+
collectionConfiguration.address == AddressCollectionMode.Automatic
93+
94+
val collectingAddress = collectionConfiguration.address == AddressCollectionMode.Full ||
95+
collectingTaxAddress
8696

8797
private val collectingPhone =
8898
args.formArgs.billingDetailsCollectionConfiguration.phone == CollectionMode.Always
@@ -171,6 +181,8 @@ internal class USBankAccountFormViewModel @Inject internal constructor(
171181
null
172182
}
173183

184+
private val defaultAddressValues = defaultAddress?.asFormFieldValues() ?: emptyMap()
185+
174186
val sameAsShippingElement = args.formArgs.shippingDetails
175187
?.toIdentifierMap(defaultBillingDetails)
176188
?.get(IdentifierSpec.SameAsShipping)
@@ -185,7 +197,7 @@ internal class USBankAccountFormViewModel @Inject internal constructor(
185197
private val autocompleteAddressElement = autocompleteAddressInteractorFactory?.let {
186198
AutocompleteAddressElement(
187199
identifier = IdentifierSpec.Generic("billing_details[address]"),
188-
initialValues = defaultAddress?.asFormFieldValues() ?: emptyMap(),
200+
initialValues = defaultAddressValues,
189201
countryCodes = collectionConfiguration.allowedBillingCountries,
190202
countryElementIdentifier = IdentifierSpec.Country,
191203
sameAsShippingElement = sameAsShippingElement,
@@ -194,28 +206,62 @@ internal class USBankAccountFormViewModel @Inject internal constructor(
194206
)
195207
}
196208

197-
val addressElement = autocompleteAddressElement ?: AddressElement(
198-
_identifier = IdentifierSpec.Generic("billing_details[address]"),
199-
rawValuesMap = defaultAddress?.asFormFieldValues() ?: emptyMap(),
200-
countryCodes = collectionConfiguration.allowedBillingCountries,
201-
sameAsShippingElement = sameAsShippingElement,
202-
shippingValuesMap = args.formArgs.shippingDetails?.toIdentifierMap(args.formArgs.billingDetails),
203-
)
209+
val addressElement: AddressFieldsElement = if (collectingTaxAddress) {
210+
BillingAddressElement(
211+
identifier = IdentifierSpec.Generic("billing_details[address]"),
212+
rawValuesMap = defaultAddressValues,
213+
countryCodes = collectionConfiguration.allowedBillingCountries,
214+
countryDropdownFieldController = DropdownFieldController(
215+
config = CountryConfig(collectionConfiguration.allowedBillingCountries),
216+
initialValue = defaultAddressValues[IdentifierSpec.Country],
217+
),
218+
countryElementIdentifier = IdentifierSpec.Country,
219+
autocompleteAddressInteractorFactory = null,
220+
sameAsShippingElement = sameAsShippingElement,
221+
shippingValuesMap = args.formArgs.shippingDetails?.toIdentifierMap(args.formArgs.billingDetails),
222+
addressCollectionMode = BillingAddressCollectionMode.Country(
223+
additionalFieldsByCountry = additionalAutomaticTaxFieldsByCountry,
224+
),
225+
collectionConfiguration = BillingDetailsCollectionConfiguration(
226+
collectName = false,
227+
collectEmail = false,
228+
collectPhone = false,
229+
address = BillingDetailsCollectionConfiguration.AddressCollectionMode.Automatic,
230+
allowedCountries = collectionConfiguration.allowedBillingCountries,
231+
),
232+
shouldHideCountryOnNoAddressCollection = false,
233+
)
234+
} else {
235+
autocompleteAddressElement ?: AddressElement(
236+
_identifier = IdentifierSpec.Generic("billing_details[address]"),
237+
rawValuesMap = defaultAddressValues,
238+
countryCodes = collectionConfiguration.allowedBillingCountries,
239+
sameAsShippingElement = sameAsShippingElement,
240+
shippingValuesMap = args.formArgs.shippingDetails?.toIdentifierMap(args.formArgs.billingDetails),
241+
)
242+
}
243+
244+
val addressHiddenIdentifiers: StateFlow<Set<IdentifierSpec>> =
245+
(addressElement as? BillingAddressElement)?.hiddenIdentifiers ?: stateFlowOf(emptySet())
204246

205-
val address: StateFlow<Address?> = addressElement.getFormFieldValueFlow().mapAsStateFlow { formFieldValues ->
206-
formFieldValues.takeIf {
207-
it.all { value ->
208-
value.second.isComplete
247+
val address: StateFlow<Address?> = combineAsStateFlow(
248+
addressElement.getFormFieldValueFlow(),
249+
addressHiddenIdentifiers,
250+
) { formFieldValues, hiddenIdentifiers ->
251+
formFieldValues.filterNot { it.first in hiddenIdentifiers }
252+
.takeIf { visibleValues -> visibleValues.all { it.second.isComplete } }
253+
?.let { values ->
254+
val rawMap = values.associate { it.first to it.second.value }
255+
Address.fromFormFieldValues(rawMap)
209256
}
210-
}?.let { values ->
211-
val rawMap = values.associate { it.first to it.second.value }
212-
Address.fromFormFieldValues(rawMap)
213-
}
214257
}
215258

216259
val lastTextFieldIdentifier: StateFlow<IdentifierSpec?> = if (collectingAddress) {
217-
addressElement.getTextFieldIdentifiers().mapAsStateFlow {
218-
it.lastOrNull() ?: lastNonAddressTextFieldIdentifier
260+
combineAsStateFlow(
261+
addressElement.getTextFieldIdentifiers(),
262+
addressHiddenIdentifiers,
263+
) { identifiers, hiddenIdentifiers ->
264+
identifiers.lastOrNull { it !in hiddenIdentifiers } ?: lastNonAddressTextFieldIdentifier
219265
}
220266
} else {
221267
stateFlowOf(lastNonAddressTextFieldIdentifier)
@@ -280,9 +326,7 @@ internal class USBankAccountFormViewModel @Inject internal constructor(
280326
nameController.formFieldValue.mapAsStateFlow { it.isComplete },
281327
emailController.formFieldValue.mapAsStateFlow { it.isComplete },
282328
phoneController.formFieldValue.mapAsStateFlow { it.isComplete },
283-
addressElement.getFormFieldValueFlow().mapAsStateFlow { formFieldValues ->
284-
formFieldValues.all { it.second.isComplete }
285-
}
329+
address.mapAsStateFlow { it != null },
286330
) { validName, validEmail, validPhone, validAddress ->
287331
val validBaseInfo = if (args.instantDebits) {
288332
validEmail
@@ -291,7 +335,7 @@ internal class USBankAccountFormViewModel @Inject internal constructor(
291335
}
292336

293337
val validAddressInfo = (validPhone || collectionConfiguration.phone != CollectionMode.Always) &&
294-
(validAddress || collectionConfiguration.address != AddressCollectionMode.Full)
338+
(validAddress || collectingAddress.not())
295339

296340
validBaseInfo && validAddressInfo
297341
}
@@ -874,6 +918,7 @@ internal class USBankAccountFormViewModel @Inject internal constructor(
874918
val sellerBusinessName: String?,
875919
val forceSetupFutureUseBehavior: Boolean,
876920
val clientAttributionMetadata: ClientAttributionMetadata,
921+
val requiresBillingAddressForAutomaticTax: Boolean,
877922
)
878923

879924
private companion object {

paymentsheet/src/test/java/com/stripe/android/customersheet/CustomerSheetScreenshotTest.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ internal class CustomerSheetScreenshotTest {
9999
sellerBusinessName = null,
100100
forceSetupFutureUseBehavior = false,
101101
clientAttributionMetadata = PaymentMethodMetadataFixtures.CLIENT_ATTRIBUTION_METADATA,
102+
requiresBillingAddressForAutomaticTax = false,
102103
)
103104

104105
private val selectPaymentMethodViewState = CustomerSheetViewState.SelectPaymentMethod(

paymentsheet/src/test/java/com/stripe/android/paymentsheet/paymentdatacollection/ach/AccountPreviewScreenshotTest.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ internal class AccountPreviewScreenshotTest {
205205
showCheckboxes: Boolean = false,
206206
) {
207207
paparazzi.snapshot {
208+
val addressController = createAddressController(fillAddress = fillAddress)
208209
BankAccountForm(
209210
state = state,
210211
instantDebits = instantDebits,
@@ -213,7 +214,11 @@ internal class AccountPreviewScreenshotTest {
213214
nameController = createNameController(),
214215
emailController = createEmailController(),
215216
phoneController = createPhoneNumberController(),
216-
addressController = createAddressController(fillAddress = fillAddress),
217+
addressController = addressController,
218+
addressValidationController = addressController,
219+
addressHiddenIdentifiers = emptySet(),
220+
showAddress = formArguments.billingDetailsCollectionConfiguration.address ==
221+
PaymentSheet.BillingDetailsCollectionConfiguration.AddressCollectionMode.Full,
217222
sameAsShippingElement = sameAsShippingElement,
218223
saveForFutureUseElement = saveForFutureUseElement,
219224
setAsDefaultPaymentMethodElement = setAsDefaultPaymentMethodElement,

paymentsheet/src/test/java/com/stripe/android/paymentsheet/paymentdatacollection/ach/BillingDetailsCollectionScreenshotTest.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ internal class BillingDetailsCollectionScreenshotTest {
147147
enabled: Boolean = true,
148148
) {
149149
paparazzi.snapshot {
150+
val addressController = createAddressController(fillAddress)
150151
BankAccountForm(
151152
state = bankFormScreenState,
152153
formArgs = formArgs,
@@ -155,7 +156,11 @@ internal class BillingDetailsCollectionScreenshotTest {
155156
nameController = createNameController(nameControllerInitialValue),
156157
emailController = createEmailController(emailControllerInitialValue),
157158
phoneController = createPhoneNumberController(phoneControllerInitialValue),
158-
addressController = createAddressController(fillAddress),
159+
addressController = addressController,
160+
addressValidationController = addressController,
161+
addressHiddenIdentifiers = emptySet(),
162+
showAddress = formArgs.billingDetailsCollectionConfiguration.address ==
163+
PaymentSheet.BillingDetailsCollectionConfiguration.AddressCollectionMode.Full,
159164
sameAsShippingElement = null,
160165
saveForFutureUseElement = saveForFutureUseElement,
161166
setAsDefaultPaymentMethodElement = setAsDefaultPaymentMethodElement,

0 commit comments

Comments
 (0)