Skip to content

Commit 594cbd0

Browse files
authored
Fix empty form in vertical mode when there are no form elements. (#11582)
* Fix empty form in vertical mode when there are no form elements. * Move logic for deciding header information into state * Update `VerticalModeInitialScreenFactoryTest`
1 parent 74504ab commit 594cbd0

File tree

12 files changed

+189
-26
lines changed

12 files changed

+189
-26
lines changed

paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/form/EmbeddedFormInteractorFactory.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import com.stripe.android.paymentsheet.ui.transformToPaymentSelection
1414
import com.stripe.android.paymentsheet.verticalmode.DefaultVerticalModeFormInteractor
1515
import com.stripe.android.paymentsheet.verticalmode.PaymentMethodIncentiveInteractor
1616
import com.stripe.android.uicore.utils.mapAsStateFlow
17+
import com.stripe.android.uicore.utils.stateFlowOf
1718
import kotlinx.coroutines.CoroutineScope
1819
import kotlinx.coroutines.Dispatchers
1920
import kotlinx.coroutines.flow.MutableSharedFlow
@@ -81,6 +82,8 @@ internal class EmbeddedFormInteractorFactory @Inject constructor(
8182
),
8283
isLiveMode = paymentMethodMetadata.stripeIntent.isLiveMode,
8384
processing = formActivityStateHelper.state.mapAsStateFlow { it.isProcessing },
85+
// Should never show wallets header in Embedded form
86+
showsWalletHeader = stateFlowOf(false),
8487
paymentMethodIncentive = PaymentMethodIncentiveInteractor(
8588
paymentMethodMetadata.paymentMethodIncentive
8689
).displayedIncentive,

paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/form/FormActivityUI.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,6 @@ internal fun FormActivityUI(
6161
content = {
6262
VerticalModeFormUI(
6363
interactor = interactor,
64-
showsWalletHeader = false
6564
)
6665
USBankAccountMandate(state)
6766
FormActivityError(state)

paymentsheet/src/main/java/com/stripe/android/paymentsheet/navigation/PaymentSheetScreen.kt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,6 @@ internal sealed interface PaymentSheetScreen {
328328

329329
class VerticalModeForm(
330330
private val interactor: VerticalModeFormInteractor,
331-
private val showsWalletHeader: Boolean = false,
332331
) : PaymentSheetScreen, Closeable {
333332

334333
override val buyButtonState = stateFlowOf(
@@ -354,12 +353,12 @@ internal sealed interface PaymentSheetScreen {
354353
}
355354

356355
override fun showsWalletsHeader(isCompleteFlow: Boolean): StateFlow<Boolean> {
357-
return stateFlowOf(showsWalletHeader)
356+
return interactor.state.mapAsStateFlow { it.showsWalletHeader }
358357
}
359358

360359
@Composable
361360
override fun Content(modifier: Modifier) {
362-
VerticalModeFormUI(interactor, showsWalletHeader, modifier)
361+
VerticalModeFormUI(interactor, modifier)
363362
}
364363

365364
override fun close() {

paymentsheet/src/main/java/com/stripe/android/paymentsheet/verticalmode/VerticalModeFormInteractor.kt

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import com.stripe.android.paymentsheet.paymentdatacollection.ach.USBankAccountFo
1212
import com.stripe.android.paymentsheet.viewmodels.BaseSheetViewModel
1313
import com.stripe.android.uicore.elements.FormElement
1414
import com.stripe.android.uicore.utils.combineAsStateFlow
15+
import com.stripe.android.uicore.utils.mapAsStateFlow
1516
import kotlinx.coroutines.CoroutineScope
1617
import kotlinx.coroutines.Dispatchers
1718
import kotlinx.coroutines.SupervisorJob
@@ -38,9 +39,15 @@ internal interface VerticalModeFormInteractor {
3839
private val isValidating: Boolean,
3940
val usBankAccountFormArguments: USBankAccountFormArguments,
4041
val formArguments: FormArguments,
42+
val showsWalletHeader: Boolean,
4143
private val formElements: List<FormElement>,
42-
val headerInformation: FormHeaderInformation?,
44+
private val paymentMethodIncentive: PaymentMethodIncentive?,
45+
private val headerInformation: FormHeaderInformation?,
4346
) {
47+
val formHeader = headerInformation?.copy(
48+
promoBadge = paymentMethodIncentive?.takeIfMatches(selectedPaymentMethodCode)?.displayText,
49+
)?.takeIf { !showsWalletHeader }
50+
4451
val formUiElements = formElements.onEach { element ->
4552
element.onValidationStateChanged(isValidating)
4653
}
@@ -59,6 +66,7 @@ internal class DefaultVerticalModeFormInteractor(
5966
private val onFormFieldValuesChanged: (formValues: FormFieldValues?, selectedPaymentMethodCode: String) -> Unit,
6067
private val usBankAccountArguments: USBankAccountFormArguments,
6168
private val reportFieldInteraction: (String) -> Unit,
69+
private val showsWalletHeader: StateFlow<Boolean>,
6270
private val headerInformation: FormHeaderInformation?,
6371
override val isLiveMode: Boolean,
6472
processing: StateFlow<Boolean>,
@@ -73,17 +81,18 @@ internal class DefaultVerticalModeFormInteractor(
7381
processing,
7482
paymentMethodIncentive,
7583
isValidating,
76-
) { isProcessing, paymentMethodIncentive, isValidating ->
84+
showsWalletHeader,
85+
) { isProcessing, paymentMethodIncentive, isValidating, showsWalletHeader ->
7786
VerticalModeFormInteractor.State(
7887
selectedPaymentMethodCode = selectedPaymentMethodCode,
7988
isProcessing = isProcessing,
8089
usBankAccountFormArguments = usBankAccountArguments,
8190
formArguments = formArguments,
8291
formElements = formElements,
8392
isValidating = isValidating,
84-
headerInformation = headerInformation?.copy(
85-
promoBadge = paymentMethodIncentive?.takeIfMatches(selectedPaymentMethodCode)?.displayText,
86-
),
93+
showsWalletHeader = showsWalletHeader,
94+
paymentMethodIncentive = paymentMethodIncentive,
95+
headerInformation = headerInformation,
8796
)
8897
}
8998

@@ -138,6 +147,7 @@ internal class DefaultVerticalModeFormInteractor(
138147
selectedPaymentMethodCode = selectedPaymentMethodCode,
139148
bankFormInteractor = bankFormInteractor,
140149
),
150+
showsWalletHeader = viewModel.walletsState.mapAsStateFlow { it != null },
141151
headerInformation = paymentMethodMetadata.formHeaderInformationForCode(
142152
selectedPaymentMethodCode,
143153
customerHasSavedPaymentMethods = customerStateHolder.paymentMethods.value.any {

paymentsheet/src/main/java/com/stripe/android/paymentsheet/verticalmode/VerticalModeFormUI.kt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ const val TEST_TAG_HEADER_TITLE = "TEST_TAG_HEADER_TITLE"
3434
@Composable
3535
internal fun VerticalModeFormUI(
3636
interactor: VerticalModeFormInteractor,
37-
showsWalletHeader: Boolean,
3837
modifier: Modifier = Modifier
3938
) {
4039
val horizontalPadding = StripeTheme.getOuterFormInsets()
@@ -43,9 +42,9 @@ internal fun VerticalModeFormUI(
4342
val state by interactor.state.collectAsState()
4443

4544
Column(modifier) {
46-
val headerInformation = state.headerInformation
45+
val headerInformation = state.formHeader
4746
val enabled = !state.isProcessing
48-
if (headerInformation != null && !showsWalletHeader) {
47+
if (headerInformation != null) {
4948
VerticalModeFormHeaderUI(isEnabled = enabled, formHeaderInformation = headerInformation)
5049
}
5150

paymentsheet/src/main/java/com/stripe/android/paymentsheet/verticalmode/VerticalModeInitialScreenFactory.kt

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,13 @@ internal object VerticalModeInitialScreenFactory {
1616
): List<PaymentSheetScreen> {
1717
val supportedPaymentMethodTypes = paymentMethodMetadata.supportedPaymentMethodTypes()
1818
val bankFormInteractor = BankFormInteractor.create(viewModel)
19+
val formHelper = DefaultFormHelper.create(viewModel, paymentMethodMetadata)
1920

20-
if (supportedPaymentMethodTypes.size == 1 && customerStateHolder.paymentMethods.value.isEmpty()) {
21+
if (
22+
supportedPaymentMethodTypes.size == 1 &&
23+
customerStateHolder.paymentMethods.value.isEmpty() &&
24+
formHelper.formTypeForCode(supportedPaymentMethodTypes[0]) == FormHelper.FormType.UserInteractionRequired
25+
) {
2126
return listOf(
2227
PaymentSheetScreen.VerticalModeForm(
2328
interactor = DefaultVerticalModeFormInteractor.create(
@@ -27,7 +32,6 @@ internal object VerticalModeInitialScreenFactory {
2732
customerStateHolder = customerStateHolder,
2833
bankFormInteractor = bankFormInteractor,
2934
),
30-
showsWalletHeader = true,
3135
)
3236
)
3337
}
@@ -45,8 +49,6 @@ internal object VerticalModeInitialScreenFactory {
4549
(viewModel.selection.value as? PaymentSelection.New?)?.let { newPaymentSelection ->
4650
val paymentMethodCode = newPaymentSelection.paymentMethodCreateParams.typeCode
4751

48-
val formHelper = DefaultFormHelper.create(viewModel, paymentMethodMetadata)
49-
5052
if (formHelper.formTypeForCode(paymentMethodCode) == FormHelper.FormType.UserInteractionRequired) {
5153
add(
5254
PaymentSheetScreen.VerticalModeForm(

paymentsheet/src/test/java/com/stripe/android/paymentsheet/navigation/PaymentSheetScreenVerticalModeFormTest.kt

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package com.stripe.android.paymentsheet.navigation
22

33
import app.cash.turbine.test
44
import com.google.common.truth.Truth.assertThat
5+
import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodMetadataFactory
6+
import com.stripe.android.paymentsheet.forms.FormArgumentsFactory
57
import com.stripe.android.paymentsheet.verticalmode.VerticalModeFormInteractor
68
import com.stripe.android.uicore.utils.stateFlowOf
79
import kotlinx.coroutines.flow.StateFlow
@@ -43,6 +45,58 @@ internal class PaymentSheetScreenVerticalModeFormTest {
4345
assertThat(hasCalledOnClose).isTrue()
4446
}
4547

48+
@Test
49+
fun `showWalletsHeader is true when interactor state is true`() = runTest {
50+
val interactor = FakeVerticalModeFormInteractor(
51+
state = stateFlowOf(
52+
VerticalModeFormInteractor.State(
53+
selectedPaymentMethodCode = "card",
54+
isProcessing = false,
55+
isValidating = false,
56+
usBankAccountFormArguments = mock(),
57+
formArguments = FormArgumentsFactory.create(
58+
paymentMethodCode = "card",
59+
metadata = PaymentMethodMetadataFactory.create(),
60+
),
61+
formElements = emptyList(),
62+
showsWalletHeader = false,
63+
paymentMethodIncentive = null,
64+
headerInformation = null,
65+
)
66+
)
67+
)
68+
69+
PaymentSheetScreen.VerticalModeForm(interactor).showsWalletsHeader(isCompleteFlow = true).test {
70+
assertThat(awaitItem()).isFalse()
71+
}
72+
}
73+
74+
@Test
75+
fun `showWalletsHeader is false when interactor state is false`() = runTest {
76+
val interactor = FakeVerticalModeFormInteractor(
77+
state = stateFlowOf(
78+
VerticalModeFormInteractor.State(
79+
selectedPaymentMethodCode = "card",
80+
isProcessing = false,
81+
isValidating = false,
82+
usBankAccountFormArguments = mock(),
83+
formArguments = FormArgumentsFactory.create(
84+
paymentMethodCode = "card",
85+
metadata = PaymentMethodMetadataFactory.create(),
86+
),
87+
formElements = emptyList(),
88+
showsWalletHeader = true,
89+
paymentMethodIncentive = null,
90+
headerInformation = null,
91+
)
92+
)
93+
)
94+
95+
PaymentSheetScreen.VerticalModeForm(interactor).showsWalletsHeader(isCompleteFlow = true).test {
96+
assertThat(awaitItem()).isTrue()
97+
}
98+
}
99+
46100
private class FakeVerticalModeFormInteractor(
47101
override val state: StateFlow<VerticalModeFormInteractor.State> = stateFlowOf(mock()),
48102
override val isLiveMode: Boolean = false,

paymentsheet/src/test/java/com/stripe/android/paymentsheet/verticalmode/DefaultVerticalModeFormInteractorTest.kt

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import com.google.common.truth.Truth.assertThat
88
import com.stripe.android.core.strings.resolvableString
99
import com.stripe.android.core.utils.FeatureFlags
1010
import com.stripe.android.isInstanceOf
11+
import com.stripe.android.lpmfoundations.FormHeaderInformation
1112
import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodMetadataFactory
1213
import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodSaveConsentBehavior
1314
import com.stripe.android.model.PaymentMethodFixtures
@@ -234,6 +235,62 @@ internal class DefaultVerticalModeFormInteractorTest {
234235
FeatureFlags.cardScanGooglePayMigration.setEnabled(false)
235236
}
236237

238+
@Test
239+
fun `'headerInformation' is not null if 'showWalletsHeaders' is false`() {
240+
val headerInformation = FormHeaderInformation(
241+
displayName = "Card".resolvableString,
242+
shouldShowIcon = false,
243+
iconResource = 0,
244+
iconResourceNight = null,
245+
lightThemeIconUrl = null,
246+
darkThemeIconUrl = null,
247+
iconRequiresTinting = false,
248+
promoBadge = null,
249+
)
250+
251+
runScenario(
252+
selectedPaymentMethodCode = "card",
253+
headerInformation = headerInformation,
254+
) {
255+
showWalletsHeaderSource.emit(false)
256+
257+
interactor.state.test {
258+
val state = awaitItem()
259+
260+
assertThat(state.showsWalletHeader).isFalse()
261+
assertThat(state.formHeader).isEqualTo(headerInformation)
262+
}
263+
}
264+
}
265+
266+
@Test
267+
fun `'headerInformation' is null if 'showWalletsHeaders' is true`() {
268+
val headerInformation = FormHeaderInformation(
269+
displayName = "Card".resolvableString,
270+
shouldShowIcon = false,
271+
iconResource = 0,
272+
iconResourceNight = null,
273+
lightThemeIconUrl = null,
274+
darkThemeIconUrl = null,
275+
iconRequiresTinting = false,
276+
promoBadge = null,
277+
)
278+
279+
runScenario(
280+
selectedPaymentMethodCode = "card",
281+
headerInformation = headerInformation,
282+
) {
283+
showWalletsHeaderSource.emit(true)
284+
285+
interactor.state.test {
286+
val state = awaitItem()
287+
288+
assertThat(state.showsWalletHeader).isTrue()
289+
assertThat(state.formHeader).isNull()
290+
}
291+
}
292+
}
293+
237294
private fun testSetAsDefaultElements(
238295
hasSavedPaymentMethods: Boolean,
239296
block: (SaveForFutureUseElement?, SetAsDefaultPaymentMethodElement?) -> Unit
@@ -328,11 +385,13 @@ internal class DefaultVerticalModeFormInteractorTest {
328385
private fun runScenario(
329386
selectedPaymentMethodCode: String,
330387
formElements: List<FormElement> = emptyList(),
388+
headerInformation: FormHeaderInformation? = null,
331389
testBlock: suspend TestParams.() -> Unit,
332390
) {
333391
val formArguments = mock<FormArguments>()
334392
val usBankAccountArguments = mock<USBankAccountFormArguments>()
335393
val processing: MutableStateFlow<Boolean> = MutableStateFlow(false)
394+
val showWalletsHeader: MutableStateFlow<Boolean> = MutableStateFlow(false)
336395
val validationRequested = MutableSharedFlow<Unit>()
337396

338397
val onFormFieldValuesChangedTurbine = Turbine<Pair<FormFieldValues?, String>>()
@@ -349,18 +408,20 @@ internal class DefaultVerticalModeFormInteractorTest {
349408
reportFieldInteraction = {
350409
reportFieldInteractionTurbine.add(it)
351410
},
352-
headerInformation = null,
411+
headerInformation = headerInformation,
353412
isLiveMode = true,
354413
processing = processing,
355414
validationRequested = validationRequested,
356415
coroutineScope = CoroutineScope(UnconfinedTestDispatcher()),
357416
paymentMethodIncentive = stateFlowOf(null),
417+
showsWalletHeader = showWalletsHeader,
358418
uiContext = UnconfinedTestDispatcher(),
359419
)
360420

361421
TestParams(
362422
interactor = interactor,
363423
processingSource = processing,
424+
showWalletsHeaderSource = showWalletsHeader,
364425
validationRequestedSource = validationRequested,
365426
onFormFieldValuesChangedTurbine = onFormFieldValuesChangedTurbine,
366427
reportFieldInteractionTurbine = reportFieldInteractionTurbine,
@@ -378,6 +439,7 @@ internal class DefaultVerticalModeFormInteractorTest {
378439
private class TestParams(
379440
val interactor: DefaultVerticalModeFormInteractor,
380441
val processingSource: MutableStateFlow<Boolean>,
442+
val showWalletsHeaderSource: MutableStateFlow<Boolean>,
381443
val validationRequestedSource: MutableSharedFlow<Unit>,
382444
val onFormFieldValuesChangedTurbine: ReceiveTurbine<Pair<FormFieldValues?, String>>,
383445
val reportFieldInteractionTurbine: ReceiveTurbine<String>,

paymentsheet/src/test/java/com/stripe/android/paymentsheet/verticalmode/FakeVerticalModeFormInteractor.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ internal class FakeVerticalModeFormInteractor private constructor(
2929
metadata: PaymentMethodMetadata,
3030
isProcessing: Boolean = false,
3131
isValidating: Boolean = false,
32+
showsWalletHeader: Boolean = false,
3233
): VerticalModeFormInteractor {
3334
val formArguments = FormArgumentsFactory.create(
3435
paymentMethodCode = paymentMethodCode,
@@ -53,6 +54,8 @@ internal class FakeVerticalModeFormInteractor private constructor(
5354
uiDefinitionFactoryArgumentsFactory = uiDefinitionArgumentsFactory,
5455
)!!,
5556
isValidating = isValidating,
57+
showsWalletHeader = showsWalletHeader,
58+
paymentMethodIncentive = null,
5659
headerInformation = metadata.formHeaderInformationForCode(
5760
code = paymentMethodCode,
5861
customerHasSavedPaymentMethods = false,

paymentsheet/src/test/java/com/stripe/android/paymentsheet/verticalmode/VerticalModeFormUIScreenshotTest.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -368,8 +368,8 @@ internal class VerticalModeFormUIScreenshotTest {
368368
FakeVerticalModeFormInteractor.create(
369369
paymentMethodCode = "card",
370370
metadata = metadata,
371+
showsWalletHeader = true,
371372
),
372-
showsWalletHeader = true,
373373
)
374374
val viewModel = FakeBaseSheetViewModel.create(metadata, initialScreen, canGoBack = false)
375375
viewModel.walletsStateSource.value = WalletsState(
@@ -464,8 +464,8 @@ internal class VerticalModeFormUIScreenshotTest {
464464
paymentMethodCode = paymentMethodCode,
465465
metadata = metadata,
466466
isProcessing = isProcessing,
467+
showsWalletHeader = showsWalletHeader,
467468
),
468-
showsWalletHeader = showsWalletHeader,
469469
)
470470
}
471471
}

0 commit comments

Comments
 (0)