Skip to content

Commit 62de350

Browse files
authored
Fix issue where "Billing address same as shipping" is broken if initial billing and shipping addresses are the same. (#11292)
1 parent df377cf commit 62de350

File tree

3 files changed

+191
-2
lines changed

3 files changed

+191
-2
lines changed

paymentsheet/detekt-baseline.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
<ID>LargeClass:DefaultFlowControllerTest.kt$DefaultFlowControllerTest</ID>
2626
<ID>LargeClass:DefaultIntentConfirmationInterceptorTest.kt$DefaultIntentConfirmationInterceptorTest</ID>
2727
<ID>LargeClass:DefaultPaymentElementLoaderTest.kt$DefaultPaymentElementLoaderTest</ID>
28+
<ID>LargeClass:InputAddressViewModelTest.kt$InputAddressViewModelTest</ID>
2829
<ID>LargeClass:PaymentMethodMetadataTest.kt$PaymentMethodMetadataTest</ID>
2930
<ID>LargeClass:PaymentOptionsViewModelTest.kt$PaymentOptionsViewModelTest</ID>
3031
<ID>LargeClass:PaymentSheetActivityTest.kt$PaymentSheetActivityTest</ID>
@@ -46,6 +47,8 @@
4647
<ID>LongMethod:InputAddressScreen.kt$@Composable internal fun InputAddressScreen( inputAddressViewModelSubcomponentBuilderProvider: Provider&lt;InputAddressViewModelSubcomponent.Builder> )</ID>
4748
<ID>LongMethod:InputAddressViewModelTest.kt$InputAddressViewModelTest$@OptIn(AddressElementSameAsBillingPreview::class) @Test fun `'Shipping same as billing' should work as expected when only billing provided`()</ID>
4849
<ID>LongMethod:InputAddressViewModelTest.kt$InputAddressViewModelTest$@OptIn(AddressElementSameAsBillingPreview::class) @Test fun `'Shipping same as billing' should work as expected with both billing &amp; shipping`()</ID>
50+
<ID>LongMethod:InputAddressViewModelTest.kt$InputAddressViewModelTest$@OptIn(AddressElementSameAsBillingPreview::class) @Test fun `'Shipping same as billing' should work as expected with same billing &amp; shipping &amp; empty values`()</ID>
51+
<ID>LongMethod:InputAddressViewModelTest.kt$InputAddressViewModelTest$@OptIn(AddressElementSameAsBillingPreview::class) @Test fun `'Shipping same as billing' should work as expected with same billing &amp; shipping`()</ID>
4952
<ID>LongMethod:LinkInlineSignupConfirmationDefinitionTest.kt$LinkInlineSignupConfirmationDefinitionTest$private fun testSuccessfulSignupWithNewCard( saveOption: LinkInlineSignupConfirmationOption.PaymentMethodSaveOption, expectedSetupForFutureUsage: ConfirmPaymentIntentParams.SetupFutureUsage?, expectedShouldSave: Boolean, )</ID>
5053
<ID>LongMethod:LinkInlineSignupFields.kt$@Composable internal fun LinkInlineSignupFields( sectionError: Int?, emailController: TextFieldController, phoneNumberController: PhoneNumberController, nameController: TextFieldController, signUpState: SignUpState, enabled: Boolean, isShowingPhoneFirst: Boolean, requiresNameCollection: Boolean, allowsDefaultOptIn: Boolean, errorMessage: String?, didShowAllFields: Boolean, onShowingAllFields: () -> Unit, modifier: Modifier = Modifier, emailFocusRequester: FocusRequester = remember { FocusRequester() }, phoneFocusRequester: FocusRequester = remember { FocusRequester() }, nameFocusRequester: FocusRequester = remember { FocusRequester() }, )</ID>
5154
<ID>LongMethod:PaymentDetails.kt$@Preview(showBackground = true) @Composable private fun PaymentDetailsListItemPreview()</ID>

paymentsheet/src/main/java/com/stripe/android/paymentsheet/addresselement/InputAddressViewModel.kt

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,20 +29,28 @@ internal class InputAddressViewModel @Inject constructor(
2929
private val initialBillingAddress = args.config?.billingAddress?.toAddressDetails()
3030
private val initialShippingAddress = args.config?.address
3131

32+
private val initialInputsAreTheSame = addressesAreTheSame(initialBillingAddress, initialShippingAddress)
33+
3234
private val unparsedBillingAddress = initialBillingAddress?.toIdentifierMap()
3335
private var parsedBillingAddress: Map<IdentifierSpec, String?>? = null
3436

3537
private val _shippingSameAsBillingState = MutableStateFlow(
3638
if (canUseShippingSameAsBilling()) {
3739
ShippingSameAsBillingState.Show(
38-
isChecked = initialBillingAddress != null && initialShippingAddress == null
40+
isChecked = (initialBillingAddress != null && initialShippingAddress == null) ||
41+
initialInputsAreTheSame
3942
)
4043
} else {
4144
ShippingSameAsBillingState.Hide
4245
}
4346
)
4447

45-
private var previousUserInput: Map<IdentifierSpec, String?>? = initialShippingAddress?.toIdentifierMap()
48+
private var previousUserInput: Map<IdentifierSpec, String?>? = if (initialInputsAreTheSame) {
49+
null
50+
} else {
51+
initialShippingAddress?.toIdentifierMap()
52+
}
53+
4654
private var setShippingSameAsShippingAtLeastOnce: Boolean = _shippingSameAsBillingState.value.run {
4755
this is ShippingSameAsBillingState.Show && isChecked
4856
}
@@ -241,6 +249,28 @@ internal class InputAddressViewModel @Inject constructor(
241249
} ?: false
242250
}
243251

252+
private fun addressesAreTheSame(
253+
addressOne: AddressDetails?,
254+
addressTwo: AddressDetails?
255+
): Boolean {
256+
if (addressOne == null || addressTwo == null) {
257+
return false
258+
}
259+
260+
return addressOne.name softEquals addressTwo.name &&
261+
addressOne.phoneNumber softEquals addressTwo.phoneNumber &&
262+
addressOne.address?.line1 softEquals addressTwo.address?.line1 &&
263+
addressOne.address?.line2 softEquals addressTwo.address?.line2 &&
264+
addressOne.address?.city softEquals addressTwo.address?.city &&
265+
addressOne.address?.state softEquals addressTwo.address?.state &&
266+
addressOne.address?.country softEquals addressTwo.address?.country &&
267+
addressOne.address?.postalCode softEquals addressTwo.address?.postalCode
268+
}
269+
270+
private infix fun String?.softEquals(other: String?): Boolean {
271+
return this == other || (isNullOrEmpty() && other.isNullOrEmpty())
272+
}
273+
244274
private fun PaymentSheet.BillingDetails.toAddressDetails(): AddressDetails {
245275
return AddressDetails(
246276
name = name,

paymentsheet/src/test/java/com/stripe/android/paymentsheet/addresselement/InputAddressViewModelTest.kt

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,162 @@ class InputAddressViewModelTest {
535535
}
536536
}
537537

538+
@OptIn(AddressElementSameAsBillingPreview::class)
539+
@Test
540+
fun `'Shipping same as billing' should work as expected with same billing & shipping`() = runTest {
541+
val viewModel = createViewModel(
542+
config = AddressLauncher.Configuration.Builder()
543+
.allowedCountries(setOf("US"))
544+
.address(
545+
AddressDetails(
546+
name = "John Doe",
547+
address = PaymentSheet.Address(
548+
line1 = "123 Apple Street",
549+
city = "San Francisco",
550+
country = "US",
551+
state = "CA",
552+
postalCode = "99999"
553+
),
554+
)
555+
)
556+
.billingAddress(
557+
PaymentSheet.BillingDetails(
558+
name = "John Doe",
559+
address = PaymentSheet.Address(
560+
line1 = "123 Apple Street",
561+
city = "San Francisco",
562+
country = "US",
563+
state = "CA",
564+
postalCode = "99999"
565+
),
566+
)
567+
)
568+
.additionalFields(
569+
AddressLauncher.AdditionalFieldsConfiguration(
570+
phone = AddressLauncher.AdditionalFieldsConfiguration.FieldConfiguration.HIDDEN,
571+
)
572+
)
573+
.build()
574+
)
575+
576+
turbineScope {
577+
val shippingSameAsBillingStateTurbine = viewModel.shippingSameAsBillingState.testIn(scope = this)
578+
val formValuesTurbine = viewModel.addressFormController.uncompletedFormValues.testIn(scope = this)
579+
580+
// Should be checked
581+
assertThat(shippingSameAsBillingStateTurbine.awaitItem()).isEqualTo(createShowState(isChecked = true))
582+
assertThat(formValuesTurbine.awaitItem()).containsExactlyEntriesIn(
583+
mapOf(
584+
IdentifierSpec.Name to FormFieldEntry(value = "John Doe", isComplete = true),
585+
IdentifierSpec.Country to FormFieldEntry(value = "US", isComplete = true),
586+
IdentifierSpec.State to FormFieldEntry(value = "CA", isComplete = true),
587+
IdentifierSpec.Line1 to FormFieldEntry(value = "123 Apple Street", isComplete = true),
588+
IdentifierSpec.Line2 to FormFieldEntry(value = "", isComplete = true),
589+
IdentifierSpec.City to FormFieldEntry(value = "San Francisco", isComplete = true),
590+
IdentifierSpec.PostalCode to FormFieldEntry(value = "99999", isComplete = true)
591+
)
592+
)
593+
594+
viewModel.clickBillingSameAsShipping(newValue = false)
595+
596+
// Should be unchecked and empty
597+
assertThat(shippingSameAsBillingStateTurbine.awaitItem()).isEqualTo(createShowState(isChecked = false))
598+
assertThat(formValuesTurbine.awaitItem()).containsExactlyEntriesIn(
599+
mapOf(
600+
IdentifierSpec.Name to FormFieldEntry(value = "", isComplete = false),
601+
IdentifierSpec.Country to FormFieldEntry(value = "US", isComplete = true),
602+
IdentifierSpec.State to FormFieldEntry(value = null, isComplete = false),
603+
IdentifierSpec.Line1 to FormFieldEntry(value = "", isComplete = false),
604+
IdentifierSpec.Line2 to FormFieldEntry(value = "", isComplete = true),
605+
IdentifierSpec.City to FormFieldEntry(value = "", isComplete = false),
606+
IdentifierSpec.PostalCode to FormFieldEntry(value = "", isComplete = false)
607+
)
608+
)
609+
610+
shippingSameAsBillingStateTurbine.cancel()
611+
formValuesTurbine.cancel()
612+
}
613+
}
614+
615+
@OptIn(AddressElementSameAsBillingPreview::class)
616+
@Test
617+
fun `'Shipping same as billing' should work as expected with same billing & shipping & empty values`() = runTest {
618+
val viewModel = createViewModel(
619+
config = AddressLauncher.Configuration.Builder()
620+
.allowedCountries(setOf("US"))
621+
.address(
622+
AddressDetails(
623+
name = "John Doe",
624+
address = PaymentSheet.Address(
625+
line1 = "123 Apple Street",
626+
line2 = "",
627+
city = "San Francisco",
628+
country = "US",
629+
state = "CA",
630+
postalCode = "99999"
631+
),
632+
)
633+
)
634+
.billingAddress(
635+
PaymentSheet.BillingDetails(
636+
name = "John Doe",
637+
address = PaymentSheet.Address(
638+
line1 = "123 Apple Street",
639+
line2 = null,
640+
city = "San Francisco",
641+
country = "US",
642+
state = "CA",
643+
postalCode = "99999"
644+
),
645+
)
646+
)
647+
.additionalFields(
648+
AddressLauncher.AdditionalFieldsConfiguration(
649+
phone = AddressLauncher.AdditionalFieldsConfiguration.FieldConfiguration.HIDDEN,
650+
)
651+
)
652+
.build()
653+
)
654+
655+
turbineScope {
656+
val shippingSameAsBillingStateTurbine = viewModel.shippingSameAsBillingState.testIn(scope = this)
657+
val formValuesTurbine = viewModel.addressFormController.uncompletedFormValues.testIn(scope = this)
658+
659+
// Should be checked
660+
assertThat(shippingSameAsBillingStateTurbine.awaitItem()).isEqualTo(createShowState(isChecked = true))
661+
assertThat(formValuesTurbine.awaitItem()).containsExactlyEntriesIn(
662+
mapOf(
663+
IdentifierSpec.Name to FormFieldEntry(value = "John Doe", isComplete = true),
664+
IdentifierSpec.Country to FormFieldEntry(value = "US", isComplete = true),
665+
IdentifierSpec.State to FormFieldEntry(value = "CA", isComplete = true),
666+
IdentifierSpec.Line1 to FormFieldEntry(value = "123 Apple Street", isComplete = true),
667+
IdentifierSpec.Line2 to FormFieldEntry(value = "", isComplete = true),
668+
IdentifierSpec.City to FormFieldEntry(value = "San Francisco", isComplete = true),
669+
IdentifierSpec.PostalCode to FormFieldEntry(value = "99999", isComplete = true)
670+
)
671+
)
672+
673+
viewModel.clickBillingSameAsShipping(newValue = false)
674+
675+
// Should be unchecked and empty
676+
assertThat(shippingSameAsBillingStateTurbine.awaitItem()).isEqualTo(createShowState(isChecked = false))
677+
assertThat(formValuesTurbine.awaitItem()).containsExactlyEntriesIn(
678+
mapOf(
679+
IdentifierSpec.Name to FormFieldEntry(value = "", isComplete = false),
680+
IdentifierSpec.Country to FormFieldEntry(value = "US", isComplete = true),
681+
IdentifierSpec.State to FormFieldEntry(value = null, isComplete = false),
682+
IdentifierSpec.Line1 to FormFieldEntry(value = "", isComplete = false),
683+
IdentifierSpec.Line2 to FormFieldEntry(value = "", isComplete = true),
684+
IdentifierSpec.City to FormFieldEntry(value = "", isComplete = false),
685+
IdentifierSpec.PostalCode to FormFieldEntry(value = "", isComplete = false)
686+
)
687+
)
688+
689+
shippingSameAsBillingStateTurbine.cancel()
690+
formValuesTurbine.cancel()
691+
}
692+
}
693+
538694
@OptIn(AddressElementSameAsBillingPreview::class)
539695
private fun billingSameAsShippingInitialValueTest(
540696
address: AddressDetails?,

0 commit comments

Comments
 (0)