diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/form/FormActivity.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/form/FormActivity.kt index f46394d766d..2f79a36eed7 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/form/FormActivity.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/form/FormActivity.kt @@ -6,8 +6,8 @@ import androidx.activity.compose.setContent import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.Text import com.stripe.android.common.ui.ElementsBottomSheetLayout +import com.stripe.android.paymentsheet.verticalmode.VerticalModeFormUI import com.stripe.android.uicore.StripeTheme import com.stripe.android.uicore.elements.bottomsheet.rememberStripeBottomSheetState import com.stripe.android.uicore.utils.fadeOut @@ -47,7 +47,10 @@ internal class FormActivity : AppCompatActivity() { finish() } ) { - Text(viewModel.selectedPaymentMethodCode) + VerticalModeFormUI( + interactor = viewModel.formInteractor, + showsWalletHeader = false + ) } } } diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/form/FormActivityComponent.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/form/FormActivityComponent.kt index e24507e5a17..2631c644e2c 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/form/FormActivityComponent.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/form/FormActivityComponent.kt @@ -1,12 +1,28 @@ package com.stripe.android.paymentelement.embedded.form +import android.content.Context +import androidx.lifecycle.SavedStateHandle +import com.stripe.android.cards.CardAccountRangeRepository +import com.stripe.android.cards.DefaultCardAccountRangeRepositoryFactory +import com.stripe.android.link.LinkConfigurationCoordinator +import com.stripe.android.link.RealLinkConfigurationCoordinator +import com.stripe.android.link.injection.LinkAnalyticsComponent +import com.stripe.android.link.injection.LinkComponent import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodMetadata import com.stripe.android.model.PaymentMethodCode +import com.stripe.android.paymentelement.embedded.EmbeddedCommonModule +import dagger.Binds import dagger.BindsInstance import dagger.Component +import dagger.Module import javax.inject.Singleton -@Component +@Component( + modules = [ + EmbeddedCommonModule::class, + FormActivityModule::class + ] +) @Singleton internal interface FormActivityComponent { @@ -20,6 +36,28 @@ internal interface FormActivityComponent { @BindsInstance fun selectedPaymentMethodCode(selectedPaymentMethodCode: PaymentMethodCode): Builder + @BindsInstance + fun context(context: Context): Builder + + @BindsInstance + fun savedStateHandle(savedStateHandle: SavedStateHandle): Builder + fun build(): FormActivityComponent } } + +@Module( + subcomponents = [ + LinkAnalyticsComponent::class, + LinkComponent::class, + ] +) +internal interface FormActivityModule { + @Binds + fun bindsCardAccountRangeRepositoryFactory( + defaultCardAccountRangeRepositoryFactory: DefaultCardAccountRangeRepositoryFactory + ): CardAccountRangeRepository.Factory + + @Binds + fun bindsLinkConfigurationCoordinator(impl: RealLinkConfigurationCoordinator): LinkConfigurationCoordinator +} diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/form/FormActivityViewModel.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/form/FormActivityViewModel.kt index 6495036738b..78de65bdc60 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/form/FormActivityViewModel.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/form/FormActivityViewModel.kt @@ -2,23 +2,145 @@ package com.stripe.android.paymentelement.embedded.form import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.createSavedStateHandle +import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.CreationExtras +import com.stripe.android.cards.CardAccountRangeRepository +import com.stripe.android.core.utils.requireApplication +import com.stripe.android.link.LinkConfigurationCoordinator +import com.stripe.android.lpmfoundations.luxe.isSaveForFutureUseValueChangeable import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodMetadata +import com.stripe.android.model.PaymentIntent +import com.stripe.android.model.PaymentMethod import com.stripe.android.model.PaymentMethodCode +import com.stripe.android.paymentelement.embedded.EmbeddedSelectionHolder +import com.stripe.android.payments.bankaccount.CollectBankAccountLauncher.Companion.HOSTED_SURFACE_PAYMENT_ELEMENT +import com.stripe.android.paymentsheet.DefaultFormHelper +import com.stripe.android.paymentsheet.FormHelper +import com.stripe.android.paymentsheet.LinkInlineHandler +import com.stripe.android.paymentsheet.NewOrExternalPaymentSelection +import com.stripe.android.paymentsheet.model.PaymentSelection +import com.stripe.android.paymentsheet.paymentdatacollection.ach.USBankAccountFormArguments +import com.stripe.android.paymentsheet.verticalmode.BankFormInteractor +import com.stripe.android.paymentsheet.verticalmode.DefaultVerticalModeFormInteractor +import com.stripe.android.paymentsheet.verticalmode.PaymentMethodIncentiveInteractor +import com.stripe.android.uicore.utils.stateFlowOf import javax.inject.Inject internal class FormActivityViewModel @Inject constructor( - val paymentMethodMetadata: PaymentMethodMetadata, - val selectedPaymentMethodCode: PaymentMethodCode + paymentMethodMetadata: PaymentMethodMetadata, + selectedPaymentMethodCode: PaymentMethodCode, + private val cardAccountRangeRepositoryFactory: CardAccountRangeRepository.Factory, + private val selectionHolder: EmbeddedSelectionHolder, + private val linkConfigurationCoordinator: LinkConfigurationCoordinator, ) : ViewModel() { + + val formInteractor = initializeFormInteractor(paymentMethodMetadata, selectedPaymentMethodCode) + + private fun initializeFormInteractor( + paymentMethodMetadata: PaymentMethodMetadata, + selectedPaymentMethodCode: PaymentMethodCode, + ): DefaultVerticalModeFormInteractor { + val formHelper = createFormHelper(paymentMethodMetadata = paymentMethodMetadata) + + return DefaultVerticalModeFormInteractor( + selectedPaymentMethodCode = selectedPaymentMethodCode, + formArguments = formHelper.createFormArguments(selectedPaymentMethodCode), + formElements = formHelper.formElementsForCode(selectedPaymentMethodCode), + onFormFieldValuesChanged = formHelper::onFormFieldValuesChanged, + usBankAccountArguments = createUsBankAccountFormArguments( + paymentMethodMetadata, + selectedPaymentMethodCode + ), + reportFieldInteraction = { + }, + headerInformation = null, + isLiveMode = paymentMethodMetadata.stripeIntent.isLiveMode, + processing = stateFlowOf(false), + paymentMethodIncentive = PaymentMethodIncentiveInteractor( + paymentMethodMetadata.paymentMethodIncentive + ).displayedIncentive, + coroutineScope = viewModelScope, + ) + } + + private fun createFormHelper(paymentMethodMetadata: PaymentMethodMetadata): FormHelper { + return DefaultFormHelper( + cardAccountRangeRepositoryFactory = cardAccountRangeRepositoryFactory, + paymentMethodMetadata = paymentMethodMetadata, + newPaymentSelectionProvider = { + when (val currentSelection = selectionHolder.selection.value) { + is PaymentSelection.ExternalPaymentMethod -> { + NewOrExternalPaymentSelection.External(currentSelection) + } + is PaymentSelection.New -> { + NewOrExternalPaymentSelection.New(currentSelection) + } + else -> null + } + }, + selectionUpdater = { selection -> + selectionHolder.set(selection) + }, + linkConfigurationCoordinator = linkConfigurationCoordinator, + linkInlineHandler = LinkInlineHandler.create(), + coroutineScope = viewModelScope + ) + } + + private fun createUsBankAccountFormArguments( + paymentMethodMetadata: PaymentMethodMetadata, + selectedPaymentMethodCode: PaymentMethodCode, + ): USBankAccountFormArguments { + val isSaveForFutureUseValueChangeable = isSaveForFutureUseValueChangeable( + code = selectedPaymentMethodCode, + intent = paymentMethodMetadata.stripeIntent, + paymentMethodSaveConsentBehavior = paymentMethodMetadata.paymentMethodSaveConsentBehavior, + hasCustomerConfiguration = paymentMethodMetadata.hasCustomerConfiguration, + ) + val instantDebits = selectedPaymentMethodCode == PaymentMethod.Type.Link.code + val bankFormInteractor = BankFormInteractor( + updateSelection = { selectionHolder.set(it) }, + paymentMethodIncentiveInteractor = PaymentMethodIncentiveInteractor( + paymentMethodMetadata.paymentMethodIncentive + ) + ) + return USBankAccountFormArguments( + showCheckbox = isSaveForFutureUseValueChangeable && instantDebits.not(), + hostedSurface = HOSTED_SURFACE_PAYMENT_ELEMENT, + instantDebits = instantDebits, + linkMode = paymentMethodMetadata.linkMode, + onBehalfOf = null, + isCompleteFlow = false, + isPaymentFlow = paymentMethodMetadata.stripeIntent is PaymentIntent, + stripeIntentId = paymentMethodMetadata.stripeIntent.id, + clientSecret = paymentMethodMetadata.stripeIntent.clientSecret, + shippingDetails = paymentMethodMetadata.shippingDetails, + draftPaymentSelection = null, + onMandateTextChanged = { _, _ -> + }, + onLinkedBankAccountChanged = bankFormInteractor::handleLinkedBankAccountChanged, + onUpdatePrimaryButtonUIState = { + }, + onUpdatePrimaryButtonState = { + }, + onError = { + }, + incentive = paymentMethodMetadata.paymentMethodIncentive, + ) + } + class Factory( private val argSupplier: () -> FormContract.Args ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T { + override fun create(modelClass: Class, extras: CreationExtras): T { val args = argSupplier() val component = DaggerFormActivityComponent.builder() .paymentMethodMetadata(args.paymentMethodMetadata) .selectedPaymentMethodCode(args.selectedPaymentMethodCode) + .context(extras.requireApplication()) + .savedStateHandle(extras.createSavedStateHandle()) .build() return component.viewModel as T diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/form/FormActivityViewModelTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/form/FormActivityViewModelTest.kt new file mode 100644 index 00000000000..fe8faca462a --- /dev/null +++ b/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/form/FormActivityViewModelTest.kt @@ -0,0 +1,45 @@ +package com.stripe.android.paymentelement.embedded.form + +import androidx.lifecycle.SavedStateHandle +import com.google.common.truth.Truth.assertThat +import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodMetadata +import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodMetadataFactory +import com.stripe.android.model.PaymentMethodCode +import com.stripe.android.paymentelement.embedded.EmbeddedSelectionHolder +import com.stripe.android.utils.FakeLinkConfigurationCoordinator +import com.stripe.android.utils.NullCardAccountRangeRepositoryFactory +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +internal class FormActivityViewModelTest { + + @Test + fun `viewmodel initializes interactor correctly`() = runTest { + val paymentMethodMetadata = PaymentMethodMetadataFactory.create() + + val selectedPaymentMethodCode = "klarna" + + val viewModel = createViewModel(paymentMethodMetadata, selectedPaymentMethodCode) + + assertThat(viewModel.formInteractor.state.value.formArguments.paymentMethodCode).isEqualTo("klarna") + } + + private fun createViewModel( + paymentMethodMetadata: PaymentMethodMetadata, + paymentMethodCode: PaymentMethodCode + ): FormActivityViewModel { + val savedStateHandle = SavedStateHandle() + val selectionHolder = EmbeddedSelectionHolder(savedStateHandle) + + return FormActivityViewModel( + paymentMethodMetadata = paymentMethodMetadata, + selectedPaymentMethodCode = paymentMethodCode, + cardAccountRangeRepositoryFactory = NullCardAccountRangeRepositoryFactory, + selectionHolder = selectionHolder, + linkConfigurationCoordinator = FakeLinkConfigurationCoordinator() + ) + } +}