Skip to content

Commit 000dff8

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>
1 parent ec722da commit 000dff8

8 files changed

Lines changed: 239 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: 68 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,17 @@ 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.BillingAddressElement
4951
import com.stripe.android.ui.core.elements.SaveForFutureUseElement
5052
import com.stripe.android.ui.core.elements.SetAsDefaultPaymentMethodElement
5153
import com.stripe.android.uicore.elements.AddressElement
54+
import com.stripe.android.uicore.elements.AddressFieldsElement
5255
import com.stripe.android.uicore.elements.AutocompleteAddressElement
5356
import com.stripe.android.uicore.elements.AutocompleteAddressInteractor
57+
import com.stripe.android.uicore.elements.CountryConfig
58+
import com.stripe.android.uicore.elements.CountryElement
59+
import com.stripe.android.uicore.elements.DropdownFieldController
5460
import com.stripe.android.uicore.elements.EmailConfig
5561
import com.stripe.android.uicore.elements.IdentifierSpec
5662
import com.stripe.android.uicore.elements.NameConfig
@@ -81,8 +87,11 @@ internal class USBankAccountFormViewModel @Inject internal constructor(
8187
private val defaultBillingDetails = args.formArgs.billingDetails
8288
private val collectionConfiguration = args.formArgs.billingDetailsCollectionConfiguration
8389

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

8796
private val collectingPhone =
8897
args.formArgs.billingDetailsCollectionConfiguration.phone == CollectionMode.Always
@@ -171,6 +180,8 @@ internal class USBankAccountFormViewModel @Inject internal constructor(
171180
null
172181
}
173182

183+
private val defaultAddressValues = defaultAddress?.asFormFieldValues() ?: emptyMap()
184+
174185
val sameAsShippingElement = args.formArgs.shippingDetails
175186
?.toIdentifierMap(defaultBillingDetails)
176187
?.get(IdentifierSpec.SameAsShipping)
@@ -185,36 +196,70 @@ internal class USBankAccountFormViewModel @Inject internal constructor(
185196
private val autocompleteAddressElement = autocompleteAddressInteractorFactory?.let {
186197
AutocompleteAddressElement(
187198
identifier = IdentifierSpec.Generic("billing_details[address]"),
188-
initialValues = defaultAddress?.asFormFieldValues() ?: emptyMap(),
199+
initialValues = defaultAddressValues,
189200
countryCodes = collectionConfiguration.allowedBillingCountries,
190201
sameAsShippingElement = sameAsShippingElement,
191202
interactorFactory = it,
192203
shippingValuesMap = args.formArgs.shippingDetails?.toIdentifierMap(args.formArgs.billingDetails),
193204
)
194205
}
195206

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

204-
val address: StateFlow<Address?> = addressElement.getFormFieldValueFlow().mapAsStateFlow { formFieldValues ->
205-
formFieldValues.takeIf {
206-
it.all { value ->
207-
value.second.isComplete
245+
val address: StateFlow<Address?> = combineAsStateFlow(
246+
addressElement.getFormFieldValueFlow(),
247+
addressHiddenIdentifiers,
248+
) { formFieldValues, hiddenIdentifiers ->
249+
formFieldValues.filterNot { it.first in hiddenIdentifiers }
250+
.takeIf { visibleValues -> visibleValues.all { it.second.isComplete } }
251+
?.let { values ->
252+
val rawMap = values.associate { it.first to it.second.value }
253+
Address.fromFormFieldValues(rawMap)
208254
}
209-
}?.let { values ->
210-
val rawMap = values.associate { it.first to it.second.value }
211-
Address.fromFormFieldValues(rawMap)
212-
}
213255
}
214256

215257
val lastTextFieldIdentifier: StateFlow<IdentifierSpec?> = if (collectingAddress) {
216-
addressElement.getTextFieldIdentifiers().mapAsStateFlow {
217-
it.lastOrNull() ?: lastNonAddressTextFieldIdentifier
258+
combineAsStateFlow(
259+
addressElement.getTextFieldIdentifiers(),
260+
addressHiddenIdentifiers,
261+
) { identifiers, hiddenIdentifiers ->
262+
identifiers.lastOrNull { it !in hiddenIdentifiers } ?: lastNonAddressTextFieldIdentifier
218263
}
219264
} else {
220265
stateFlowOf(lastNonAddressTextFieldIdentifier)
@@ -279,9 +324,7 @@ internal class USBankAccountFormViewModel @Inject internal constructor(
279324
nameController.formFieldValue.mapAsStateFlow { it.isComplete },
280325
emailController.formFieldValue.mapAsStateFlow { it.isComplete },
281326
phoneController.formFieldValue.mapAsStateFlow { it.isComplete },
282-
addressElement.getFormFieldValueFlow().mapAsStateFlow { formFieldValues ->
283-
formFieldValues.all { it.second.isComplete }
284-
}
327+
address.mapAsStateFlow { it != null },
285328
) { validName, validEmail, validPhone, validAddress ->
286329
val validBaseInfo = if (args.instantDebits) {
287330
validEmail
@@ -290,7 +333,7 @@ internal class USBankAccountFormViewModel @Inject internal constructor(
290333
}
291334

292335
val validAddressInfo = (validPhone || collectionConfiguration.phone != CollectionMode.Always) &&
293-
(validAddress || collectionConfiguration.address != AddressCollectionMode.Full)
336+
(validAddress || collectingAddress.not())
294337

295338
validBaseInfo && validAddressInfo
296339
}
@@ -873,6 +916,7 @@ internal class USBankAccountFormViewModel @Inject internal constructor(
873916
val sellerBusinessName: String?,
874917
val forceSetupFutureUseBehavior: Boolean,
875918
val clientAttributionMetadata: ClientAttributionMetadata,
919+
val requiresBillingAddressForAutomaticTax: Boolean,
876920
)
877921

878922
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)