Skip to content

Commit

Permalink
Move USBankAccountForm into BankFormElement
Browse files Browse the repository at this point in the history
  • Loading branch information
tillh-stripe committed Oct 11, 2024
1 parent 8aac691 commit d5c640f
Show file tree
Hide file tree
Showing 34 changed files with 570 additions and 336 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,13 @@ import com.stripe.android.customersheet.injection.DaggerCustomerSheetViewModelCo
import com.stripe.android.customersheet.util.CustomerSheetHacks
import com.stripe.android.customersheet.util.isUnverifiedUSBankAccount
import com.stripe.android.customersheet.util.sortPaymentMethods
import com.stripe.android.financialconnections.model.BankAccount
import com.stripe.android.lpmfoundations.luxe.SupportedPaymentMethod
import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodMetadata
import com.stripe.android.lpmfoundations.paymentmethod.UiDefinitionFactory
import com.stripe.android.model.CardBrand
import com.stripe.android.model.PaymentMethod
import com.stripe.android.model.PaymentMethod.Type.USBankAccount
import com.stripe.android.model.PaymentMethodCode
import com.stripe.android.model.PaymentMethodCreateParams
import com.stripe.android.model.PaymentMethodUpdateParams
Expand All @@ -60,6 +62,7 @@ import com.stripe.android.paymentsheet.model.SavedSelection
import com.stripe.android.paymentsheet.model.toSavedSelection
import com.stripe.android.paymentsheet.parseAppearance
import com.stripe.android.paymentsheet.paymentdatacollection.ach.USBankAccountFormArguments
import com.stripe.android.paymentsheet.paymentdatacollection.ach.USBankAccountTextBuilder
import com.stripe.android.paymentsheet.ui.EditPaymentMethodViewInteractor
import com.stripe.android.paymentsheet.ui.ModifiableEditPaymentMethodViewInteractor
import com.stripe.android.paymentsheet.ui.PaymentMethodRemovalDelayMillis
Expand Down Expand Up @@ -456,6 +459,11 @@ internal class CustomerSheetViewModel(
* `CustomerSheet` does not implement `Link` so we don't need a coordinator or callback.
*/
linkConfigurationCoordinator = null,
usBankAccountFormArguments = createDefaultUsBankArguments(paymentMethodMetadata.stripeIntent),
formArguments = FormArgumentsFactory.create(
paymentMethodCode = paymentMethod.code,
metadata = paymentMethodMetadata,
),
onLinkInlineSignupStateChanged = {
throw IllegalStateException(
"`CustomerSheet` does not implement `Link` and should not " +
Expand All @@ -465,7 +473,7 @@ internal class CustomerSheetViewModel(
),
) ?: listOf(),
primaryButtonLabel = if (
paymentMethod.code == PaymentMethod.Type.USBankAccount.code &&
paymentMethod.code == USBankAccount.code &&
it.bankAccountResult !is CollectBankAccountResultInternal.Completed
) {
UiCoreR.string.stripe_continue_button_label.resolvableString
Expand Down Expand Up @@ -802,8 +810,13 @@ internal class CustomerSheetViewModel(
"`CustomerSheet` does not implement `Link` and should not " +
"receive `InlineSignUpViewState` updates"
)
}
)
},
usBankAccountFormArguments = createDefaultUsBankArguments(stripeIntent),
formArguments = FormArgumentsFactory.create(
paymentMethodCode = paymentMethodCode,
metadata = paymentMethodMetadata,
),
),
) ?: emptyList()

transition(
Expand All @@ -813,7 +826,6 @@ internal class CustomerSheetViewModel(
formFieldValues = null,
formElements = formElements,
formArguments = formArguments,
usBankAccountFormArguments = createDefaultUsBankArguments(stripeIntent),
draftPaymentSelection = null,
enabled = true,
isLiveMode = isLiveModeProvider(),
Expand Down Expand Up @@ -854,7 +866,6 @@ internal class CustomerSheetViewModel(
handleViewAction(CustomerSheetViewAction.OnUpdateCustomButtonUIState(it))
},
hostedSurface = CollectBankAccountLauncher.HOSTED_SURFACE_CUSTOMER_SHEET,
onUpdatePrimaryButtonState = { /* no-op, CustomerSheetScreen does not use PrimaryButton.State */ },
onError = { error ->
handleViewAction(CustomerSheetViewAction.OnFormError(error))
}
Expand Down Expand Up @@ -887,19 +898,38 @@ internal class CustomerSheetViewModel(
}
}

private fun onCollectUSBankAccountResult(bankAccountResult: CollectBankAccountResultInternal) {
private fun onCollectUSBankAccountResult(bankAccountResult: CollectBankAccountResultInternal?) {
updateViewState<CustomerSheetViewState.AddPaymentMethod> {
val isCompleted = bankAccountResult is CollectBankAccountResultInternal.Completed

// TODO: Needed?
val mandateText = USBankAccountTextBuilder.buildMandateAndMicrodepositsText(
merchantName = configuration.merchantDisplayName,
isVerifyingMicrodeposits = bankAccountResult?.usesMicrodeposits ?: false,
isSaveForFutureUseSelected = true,
isSetupFlow = true,
isInstantDebits = false,
)

it.copy(
bankAccountResult = bankAccountResult,
primaryButtonLabel = if (bankAccountResult is CollectBankAccountResultInternal.Completed) {
primaryButtonLabel = if (isCompleted) {
R.string.stripe_paymentsheet_save.resolvableString
} else {
UiCoreR.string.stripe_continue_button_label.resolvableString
},
mandateText = mandateText,
)
}
}

private val CollectBankAccountResultInternal.usesMicrodeposits: Boolean
get() {
val completedResult = this as? CollectBankAccountResultInternal.Completed
val session = completedResult?.response?.usBankAccountData?.financialConnectionsSession
return session?.paymentAccount is BankAccount
}

private fun onConfirmUSBankAccount(usBankAccount: PaymentSelection.New.USBankAccount) {
createAndAttach(usBankAccount.paymentMethodCreateParams)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import com.stripe.android.paymentsheet.R
import com.stripe.android.paymentsheet.forms.FormFieldValues
import com.stripe.android.paymentsheet.model.PaymentSelection
import com.stripe.android.paymentsheet.paymentdatacollection.FormArguments
import com.stripe.android.paymentsheet.paymentdatacollection.ach.USBankAccountFormArguments
import com.stripe.android.paymentsheet.ui.ModifiableEditPaymentMethodViewInteractor
import com.stripe.android.paymentsheet.ui.PaymentSheetTopBarState
import com.stripe.android.paymentsheet.ui.PaymentSheetTopBarStateFactory
Expand Down Expand Up @@ -99,7 +98,6 @@ internal sealed class CustomerSheetViewState(
val formFieldValues: FormFieldValues?,
val formElements: List<FormElement>,
val formArguments: FormArguments,
val usBankAccountFormArguments: USBankAccountFormArguments,
val draftPaymentSelection: PaymentSelection?,
val enabled: Boolean,
override val isLiveMode: Boolean,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,6 @@ internal fun AddPaymentMethod(
viewActionHandler(CustomerSheetViewAction.OnAddPaymentMethodItemChanged(it))
},
formArguments = viewState.formArguments,
usBankAccountFormArguments = viewState.usBankAccountFormArguments,
onFormFieldValuesChanged = {
// This only gets emitted if form field values are complete
viewActionHandler(CustomerSheetViewAction.OnFormFieldValuesCompleted(it))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.stripe.android.lpmfoundations.luxe

import com.stripe.android.lpmfoundations.paymentmethod.UiDefinitionFactory
import com.stripe.android.paymentsheet.PaymentSheet
import com.stripe.android.paymentsheet.forms.PlaceholderHelper.specsForConfiguration
import com.stripe.android.ui.core.elements.AddressSpec
import com.stripe.android.ui.core.elements.AffirmTextSpec
Expand Down Expand Up @@ -39,7 +40,7 @@ import com.stripe.android.uicore.elements.IdentifierSpec
*
*/
internal class TransformSpecToElements(
private val arguments: UiDefinitionFactory.Arguments,
private val arguments: Arguments,
) {
fun transform(
specs: List<FormItemSpec>,
Expand Down Expand Up @@ -82,4 +83,24 @@ internal class TransformSpecToElements(
}
}
}

data class Arguments(
val initialValues: Map<IdentifierSpec, String?>,
val shippingValues: Map<IdentifierSpec, String?>?,
val merchantName: String,
val billingDetailsCollectionConfiguration: PaymentSheet.BillingDetailsCollectionConfiguration,
val requiresMandate: Boolean,
)
}

internal fun TransformSpecToElements(arguments: UiDefinitionFactory.Arguments): TransformSpecToElements {
return TransformSpecToElements(
arguments = TransformSpecToElements.Arguments(
initialValues = arguments.initialValues,
shippingValues = arguments.shippingValues,
merchantName = arguments.merchantName,
billingDetailsCollectionConfiguration = arguments.billingDetailsCollectionConfiguration,
requiresMandate = arguments.requiresMandate,
)
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import com.stripe.android.model.PaymentMethodCreateParams
import com.stripe.android.model.PaymentMethodExtraParams
import com.stripe.android.paymentsheet.PaymentSheet
import com.stripe.android.paymentsheet.addresselement.toIdentifierMap
import com.stripe.android.paymentsheet.paymentdatacollection.FormArguments
import com.stripe.android.paymentsheet.paymentdatacollection.ach.USBankAccountFormArguments
import com.stripe.android.ui.core.cbc.CardBrandChoiceEligibility
import com.stripe.android.ui.core.elements.SharedDataSpec
import com.stripe.android.uicore.elements.FormElement
Expand All @@ -27,8 +29,11 @@ internal sealed interface UiDefinitionFactory {
val cbcEligibility: CardBrandChoiceEligibility,
val billingDetailsCollectionConfiguration: PaymentSheet.BillingDetailsCollectionConfiguration,
val requiresMandate: Boolean,
val usBankAccountFormArguments: USBankAccountFormArguments,
val formArguments: FormArguments,
val onLinkInlineSignupStateChanged: (InlineSignupViewState) -> Unit,
) {

interface Factory {
fun create(
metadata: PaymentMethodMetadata,
Expand All @@ -41,6 +46,8 @@ internal sealed interface UiDefinitionFactory {
private val onLinkInlineSignupStateChanged: (InlineSignupViewState) -> Unit,
private val paymentMethodCreateParams: PaymentMethodCreateParams? = null,
private val paymentMethodExtraParams: PaymentMethodExtraParams? = null,
private val usBankAccountFormArguments: USBankAccountFormArguments,
private val formArguments: FormArguments,
) : Factory {
override fun create(
metadata: PaymentMethodMetadata,
Expand All @@ -60,6 +67,8 @@ internal sealed interface UiDefinitionFactory {
saveForFutureUseInitialValue = false,
billingDetailsCollectionConfiguration = metadata.billingDetailsCollectionConfiguration,
requiresMandate = requiresMandate,
usBankAccountFormArguments = usBankAccountFormArguments,
formArguments = formArguments,
onLinkInlineSignupStateChanged = onLinkInlineSignupStateChanged,
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package com.stripe.android.lpmfoundations.paymentmethod.bank

import androidx.activity.compose.LocalActivityResultRegistryOwner
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.lifecycle.viewmodel.compose.viewModel
import com.stripe.android.core.strings.ResolvableString
import com.stripe.android.paymentsheet.model.PaymentSelection.New
import com.stripe.android.paymentsheet.paymentdatacollection.FormArguments
import com.stripe.android.paymentsheet.paymentdatacollection.ach.USBankAccountForm
import com.stripe.android.paymentsheet.paymentdatacollection.ach.USBankAccountFormArguments
import com.stripe.android.paymentsheet.paymentdatacollection.ach.USBankAccountFormViewModel
import com.stripe.android.paymentsheet.ui.PrimaryButton
import com.stripe.android.ui.core.elements.RenderableFormElement
import com.stripe.android.uicore.elements.IdentifierSpec
import com.stripe.android.uicore.forms.FormFieldEntry
import com.stripe.android.uicore.utils.collectAsState
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.onEach

internal class BankFormElement(
private val formArguments: FormArguments,
private val usBankAccountFormArgs: USBankAccountFormArguments,
) : RenderableFormElement(
allowsUserInteraction = true,
identifier = IdentifierSpec.Generic("bank_form")
) {

private val formFieldValues = MutableStateFlow<List<Pair<IdentifierSpec, FormFieldEntry>>>(emptyList())

override fun getFormFieldValueFlow(): StateFlow<List<Pair<IdentifierSpec, FormFieldEntry>>> {
return formFieldValues.asStateFlow()
}

@Composable
override fun ComposeUI(enabled: Boolean) {
val activityResultRegistryOwner = LocalActivityResultRegistryOwner.current

val viewModel = viewModel<USBankAccountFormViewModel>(
factory = USBankAccountFormViewModel.Factory {
makeViewModelArgs()
},
)

LaunchedEffect(viewModel) {
viewModel
.formFieldValues
.onEach { formFieldValues ->
val state = viewModel.currentScreenState.value

handleFormFieldValuesChanged(
formFieldValues = formFieldValues,
label = state.primaryButtonText,
mandateText = state.mandateText,
collectBankAccount = viewModel::collectBankAccount,
)
}
.collect()
}

val screenState by viewModel.currentScreenState.collectAsState()
val hasRequiredFields by viewModel.requiredFields.collectAsState()

LaunchedEffect(screenState, hasRequiredFields) {
usBankAccountFormArgs.onError(screenState.error)
// TODO Get rid of this
// usBankAccountFormArgs.onMandateTextChanged(screenState.mandateText, false)
}

DisposableEffect(Unit) {
viewModel.register(activityResultRegistryOwner!!)

onDispose {
usBankAccountFormArgs.onUpdatePrimaryButtonUIState { null }
}
}

USBankAccountForm(
viewModel = viewModel,
formArgs = formArguments,
usBankAccountFormArgs = usBankAccountFormArgs,
)
}

private fun makeViewModelArgs(): USBankAccountFormViewModel.Args {
return USBankAccountFormViewModel.Args(
instantDebits = usBankAccountFormArgs.instantDebits,
linkMode = usBankAccountFormArgs.linkMode,
formArgs = formArguments,
hostedSurface = usBankAccountFormArgs.hostedSurface,
showCheckbox = usBankAccountFormArgs.showCheckbox,
isCompleteFlow = usBankAccountFormArgs.isCompleteFlow,
isPaymentFlow = usBankAccountFormArgs.isPaymentFlow,
stripeIntentId = usBankAccountFormArgs.stripeIntentId,
clientSecret = usBankAccountFormArgs.clientSecret,
onBehalfOf = usBankAccountFormArgs.onBehalfOf,
savedPaymentMethod = usBankAccountFormArgs.draftPaymentSelection as? New.USBankAccount,
shippingDetails = usBankAccountFormArgs.shippingDetails,
)
}

private fun handleFormFieldValuesChanged(
formFieldValues: List<Pair<IdentifierSpec, FormFieldEntry>>,
mandateText: ResolvableString?,
label: ResolvableString,
collectBankAccount: () -> Unit,
) {
val isComplete = formFieldValues.all { it.second.isComplete }

val identifiers = formFieldValues.toMap().keys

val hasResult = identifiers.contains(IdentifierSpec.LinkAccountSessionId) ||
identifiers.contains(IdentifierSpec.LinkPaymentMethodId)

if (hasResult) {
// PaymentSheet will take over
usBankAccountFormArgs.onUpdatePrimaryButtonUIState { null }
} else {
usBankAccountFormArgs.onUpdatePrimaryButtonUIState {
PrimaryButton.UIState(
label = label,
onClick = collectBankAccount,
enabled = isComplete,
lockVisible = usBankAccountFormArgs.isCompleteFlow,
)
}
}

usBankAccountFormArgs.onMandateTextChanged(mandateText, false)

this.formFieldValues.value = formFieldValues
}
}
Loading

0 comments on commit d5c640f

Please sign in to comment.