diff --git a/paymentsheet/detekt-baseline.xml b/paymentsheet/detekt-baseline.xml index e30e641f156..5a138f76faa 100644 --- a/paymentsheet/detekt-baseline.xml +++ b/paymentsheet/detekt-baseline.xml @@ -26,11 +26,10 @@ LargeClass:SavedPaymentMethodMutatorTest.kt$SavedPaymentMethodMutatorTest LargeClass:USBankAccountFormViewModelTest.kt$USBankAccountFormViewModelTest LongMethod:AutocompleteScreen.kt$@Composable internal fun AutocompleteScreenUI(viewModel: AutocompleteViewModel) - LongMethod:ConfirmationOptionKtx.kt$internal fun PaymentSelection.toConfirmationOption( initializationMode: PaymentElementLoader.InitializationMode, configuration: CommonConfiguration, appearance: PaymentSheet.Appearance, linkConfiguration: LinkConfiguration?, ): ConfirmationHandler.Option? LongMethod:CustomerSheetScreen.kt$@Composable internal fun SelectPaymentMethod( viewState: CustomerSheetViewState.SelectPaymentMethod, viewActionHandler: (CustomerSheetViewAction) -> Unit, paymentMethodNameProvider: (PaymentMethodCode?) -> ResolvableString, modifier: Modifier = Modifier, ) - LongMethod:DefaultConfirmationHandlerTest.kt$DefaultConfirmationHandlerTest$@Test fun `On lifecycle destroyed, should unregister all launchers`() LongMethod:DefaultConfirmationHandlerTest.kt$DefaultConfirmationHandlerTest$private fun test( someDefinitionAction: ConfirmationDefinition.Action<SomeConfirmationDefinition.LauncherArgs> = ConfirmationDefinition.Action.Fail( cause = IllegalStateException("Failed!"), message = R.string.stripe_something_went_wrong.resolvableString, errorType = ConfirmationHandler.Result.Failed.ErrorType.Internal, ), someDefinitionResult: ConfirmationDefinition.Result = ConfirmationDefinition.Result.Failed( cause = IllegalStateException("Failed!"), message = R.string.stripe_something_went_wrong.resolvableString, type = ConfirmationHandler.Result.Failed.ErrorType.Internal, ), someOtherDefinitionAction: ConfirmationDefinition.Action<SomeOtherConfirmationDefinition.LauncherArgs> = ConfirmationDefinition.Action.Fail( cause = IllegalStateException("Failed!"), message = R.string.stripe_something_went_wrong.resolvableString, errorType = ConfirmationHandler.Result.Failed.ErrorType.Internal, ), someOtherDefinitionResult: ConfirmationDefinition.Result = ConfirmationDefinition.Result.Failed( cause = IllegalStateException("Failed!"), message = R.string.stripe_something_went_wrong.resolvableString, type = ConfirmationHandler.Result.Failed.ErrorType.Internal, ), shouldRegister: Boolean = true, savedStateHandle: SavedStateHandle = SavedStateHandle(), dispatcher: CoroutineDispatcher = UnconfinedTestDispatcher(), scenarioTest: suspend Scenario.() -> Unit ) LongMethod:EditPaymentMethod.kt$@Composable internal fun EditPaymentMethodUi( viewState: EditPaymentMethodViewState, viewActionHandler: (action: EditPaymentMethodViewAction) -> Unit, modifier: Modifier = Modifier ) + LongMethod:EmbeddedContentHelper.kt$DefaultEmbeddedContentHelper$private fun createInteractor( coroutineScope: CoroutineScope, paymentMethodMetadata: PaymentMethodMetadata, ): PaymentMethodVerticalLayoutInteractor LongMethod:FormViewModelTest.kt$FormViewModelTest$@Test fun `Verify params are set when element address fields are complete`() LongMethod:FormViewModelTest.kt$FormViewModelTest$@Test fun `Verify params are set when required address fields are complete`() LongMethod:PaymentMethodMetadataTest.kt$PaymentMethodMetadataTest$@OptIn(ExperimentalCardBrandFilteringApi::class) @Test fun `should create metadata properly with elements session response, payment sheet config, and data specs`() @@ -49,7 +48,6 @@ MagicNumber:PrimaryButton.kt$PrimaryButton$0.5f MagicNumber:USBankAccountForm.kt$0.5f MaxLineLength:CardDefinition.kt$internal - MaxLineLength:CommonConfiguration.kt$CommonConfiguration$"secret. See CustomerSession API: https://docs.stripe.com/api/customer_sessions/create" MaxLineLength:CustomerRepositoryTest.kt$CustomerRepositoryTest$fun MaxLineLength:CustomerSheetViewModelTest.kt$CustomerSheetViewModelTest$fun MaxLineLength:CustomerSheetViewModelTest.kt$CustomerSheetViewModelTest$publishableKey = "pk_test_51HvTI7Lu5o3livep6t5AgBSkMvWoTtA0nyA7pVYDqpfLkRtWun7qZTYCOHCReprfLM464yaBeF72UFfB7cY9WG4a00ZnDtiC2C" @@ -72,8 +70,6 @@ MaxLineLength:SupportedPaymentMethod.kt$SupportedPaymentMethod$/** This describes the image in the LPM selector. These can be found internally [here](https://www.figma.com/file/2b9r3CJbyeVAmKi1VHV2h9/Mobile-Payment-Element?node-id=1128%3A0) */ MaxLineLength:USBankAccountFormViewModelTest.kt$USBankAccountFormViewModelTest$fun MaximumLineLength:CardDefinition.kt$internal - MaximumLineLength:CommonConfiguration.kt$CommonConfiguration$ - ThrowsCount:CommonConfiguration.kt$CommonConfiguration$fun validate() TooManyFunctions:CustomerSheetEventReporter.kt$CustomerSheetEventReporter TooManyFunctions:DefaultCustomerSheetEventReporter.kt$DefaultCustomerSheetEventReporter : CustomerSheetEventReporter TooManyFunctions:DefaultEventReporter.kt$DefaultEventReporter : EventReporter @@ -81,6 +77,7 @@ TooManyFunctions:DelegateDrawable.kt$DelegateDrawable : Drawable TooManyFunctions:EventReporter.kt$EventReporter TooManyFunctions:PaymentMethodMetadata.kt$PaymentMethodMetadata : Parcelable + TooManyFunctions:SharedPaymentElementViewModel.kt$SharedPaymentElementViewModelModule UnusedPrivateClass:PaymentOptionsViewModelTest.kt$PaymentOptionsViewModelTest$MyHostActivity : AppCompatActivity diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/EmbeddedContent.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/EmbeddedContent.kt index 8e2f51eb9e2..a2efc3c33bf 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/EmbeddedContent.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/EmbeddedContent.kt @@ -1,5 +1,6 @@ package com.stripe.android.paymentelement.embedded +import androidx.compose.animation.animateContentSize import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable @@ -16,13 +17,14 @@ import com.stripe.android.uicore.strings.resolve @Immutable internal data class EmbeddedContent( private val interactor: PaymentMethodVerticalLayoutInteractor, - private val mandate: ResolvableString? = null, + val mandate: ResolvableString? = null, ) { @Composable fun Content() { Column( modifier = Modifier .padding(top = 8.dp) + .animateContentSize() ) { EmbeddedVerticalList() EmbeddedMandate() diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/EmbeddedContentHelper.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/EmbeddedContentHelper.kt new file mode 100644 index 00000000000..24a710a3821 --- /dev/null +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/EmbeddedContentHelper.kt @@ -0,0 +1,258 @@ +package com.stripe.android.paymentelement.embedded + +import androidx.lifecycle.SavedStateHandle +import com.stripe.android.cards.CardAccountRangeRepository +import com.stripe.android.core.injection.IOContext +import com.stripe.android.core.strings.ResolvableString +import com.stripe.android.core.strings.orEmpty +import com.stripe.android.link.LinkConfigurationCoordinator +import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodMetadata +import com.stripe.android.paymentsheet.CustomerStateHolder +import com.stripe.android.paymentsheet.FormHelper +import com.stripe.android.paymentsheet.LinkInlineHandler +import com.stripe.android.paymentsheet.NewOrExternalPaymentSelection +import com.stripe.android.paymentsheet.SavedPaymentMethodMutator +import com.stripe.android.paymentsheet.analytics.EventReporter +import com.stripe.android.paymentsheet.model.PaymentSelection +import com.stripe.android.paymentsheet.repositories.CustomerRepository +import com.stripe.android.paymentsheet.verticalmode.DefaultPaymentMethodVerticalLayoutInteractor +import com.stripe.android.paymentsheet.verticalmode.DefaultPaymentMethodVerticalLayoutInteractor.FormType +import com.stripe.android.paymentsheet.verticalmode.PaymentMethodIncentiveInteractor +import com.stripe.android.paymentsheet.verticalmode.PaymentMethodVerticalLayoutInteractor +import com.stripe.android.ui.core.cbc.CardBrandChoiceEligibility +import com.stripe.android.uicore.utils.stateFlowOf +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlin.coroutines.CoroutineContext + +internal interface EmbeddedContentHelper { + val embeddedContent: StateFlow + + fun dataLoaded(paymentMethodMetadata: PaymentMethodMetadata) +} + +internal fun interface EmbeddedContentHelperFactory { + fun create(coroutineScope: CoroutineScope): EmbeddedContentHelper +} + +@AssistedFactory +internal interface DefaultEmbeddedContentHelperFactory : EmbeddedContentHelperFactory { + override fun create(coroutineScope: CoroutineScope): DefaultEmbeddedContentHelper +} + +internal class DefaultEmbeddedContentHelper @AssistedInject constructor( + @Assisted private val coroutineScope: CoroutineScope, + private val cardAccountRangeRepositoryFactory: CardAccountRangeRepository.Factory, + private val savedStateHandle: SavedStateHandle, + private val eventReporter: EventReporter, + private val linkConfigurationCoordinator: LinkConfigurationCoordinator, + @IOContext private val workContext: CoroutineContext, + private val customerRepository: CustomerRepository, + private val selectionHolder: EmbeddedSelectionHolder, +) : EmbeddedContentHelper { + + private val paymentMethodMetadata: StateFlow = savedStateHandle.getStateFlow( + key = PAYMENT_METHOD_METADATA_KEY_EMBEDDED_CONTENT, + initialValue = null, + ) + private val mandate: StateFlow = savedStateHandle.getStateFlow( + key = MANDATE_KEY_EMBEDDED_CONTENT, + initialValue = null, + ) + private val _embeddedContent = MutableStateFlow(null) + override val embeddedContent: StateFlow = _embeddedContent.asStateFlow() + + init { + coroutineScope.launch { + paymentMethodMetadata.collect { paymentMethodMetadata -> + _embeddedContent.value = if (paymentMethodMetadata == null) { + null + } else { + EmbeddedContent( + interactor = createInteractor( + coroutineScope = coroutineScope, + paymentMethodMetadata = paymentMethodMetadata, + ) + ) + } + } + } + coroutineScope.launch { + mandate.collect { mandate -> + _embeddedContent.update { originalEmbeddedContent -> + originalEmbeddedContent?.copy(mandate = mandate) + } + } + } + } + + override fun dataLoaded(paymentMethodMetadata: PaymentMethodMetadata) { + savedStateHandle[PAYMENT_METHOD_METADATA_KEY_EMBEDDED_CONTENT] = paymentMethodMetadata + } + + private fun createInteractor( + coroutineScope: CoroutineScope, + paymentMethodMetadata: PaymentMethodMetadata, + ): PaymentMethodVerticalLayoutInteractor { + val paymentMethodIncentiveInteractor = PaymentMethodIncentiveInteractor( + incentive = paymentMethodMetadata.paymentMethodIncentive, + ) + val customerStateHolder: CustomerStateHolder = CustomerStateHolder( + savedStateHandle = savedStateHandle, + selection = selectionHolder.selection, + ) + val formHelper = createFormHelper( + coroutineScope = coroutineScope, + paymentMethodMetadata = paymentMethodMetadata, + ) + val savedPaymentMethodMutator = createSavedPaymentMethodMutator( + coroutineScope = coroutineScope, + paymentMethodMetadata = paymentMethodMetadata, + customerStateHolder = customerStateHolder, + ) + + return DefaultPaymentMethodVerticalLayoutInteractor( + paymentMethodMetadata = paymentMethodMetadata, + processing = stateFlowOf(false), + selection = selectionHolder.selection, + paymentMethodIncentiveInteractor = paymentMethodIncentiveInteractor, + formTypeForCode = { code -> + if (formHelper.requiresFormScreen(code)) { + FormType.UserInteractionRequired + } else { + val mandate = formHelper.formElementsForCode(code).firstNotNullOfOrNull { it.mandateText } + if (mandate == null) { + FormType.Empty + } else { + FormType.MandateOnly(mandate) + } + } + }, + onFormFieldValuesChanged = formHelper::onFormFieldValuesChanged, + transitionToManageScreen = { + }, + transitionToManageOneSavedPaymentMethodScreen = { + }, + transitionToFormScreen = { + }, + paymentMethods = customerStateHolder.paymentMethods, + mostRecentlySelectedSavedPaymentMethod = customerStateHolder.mostRecentlySelectedSavedPaymentMethod, + providePaymentMethodName = savedPaymentMethodMutator.providePaymentMethodName, + canRemove = customerStateHolder.canRemove, + onEditPaymentMethod = { + }, + onSelectSavedPaymentMethod = { + setSelection(PaymentSelection.Saved(it)) + }, + walletsState = stateFlowOf(null), + canShowWalletsInline = true, + onMandateTextUpdated = { updatedMandate -> + savedStateHandle[MANDATE_KEY_EMBEDDED_CONTENT] = updatedMandate + }, + updateSelection = { updatedSelection -> + setSelection(updatedSelection) + }, + isCurrentScreen = stateFlowOf(false), + reportPaymentMethodTypeSelected = eventReporter::onSelectPaymentMethod, + reportFormShown = eventReporter::onPaymentMethodFormShown, + onUpdatePaymentMethod = savedPaymentMethodMutator::updatePaymentMethod, + isLiveMode = paymentMethodMetadata.stripeIntent.isLiveMode, + ) + } + + private fun createSavedPaymentMethodMutator( + coroutineScope: CoroutineScope, + paymentMethodMetadata: PaymentMethodMetadata, + customerStateHolder: CustomerStateHolder, + ): SavedPaymentMethodMutator { + return SavedPaymentMethodMutator( + eventReporter = eventReporter, + coroutineScope = coroutineScope, + workContext = workContext, + customerRepository = customerRepository, + selection = selectionHolder.selection, + providePaymentMethodName = { code -> + code?.let { + paymentMethodMetadata.supportedPaymentMethodForCode(code) + }?.displayName.orEmpty() + }, + clearSelection = { + setSelection(null) + }, + customerStateHolder = customerStateHolder, + onPaymentMethodRemoved = { + }, + onModifyPaymentMethod = { _, _, _, _, _ -> + }, + onUpdatePaymentMethod = { _, _, _, _ -> + }, + navigationPop = { + }, + isCbcEligible = { + paymentMethodMetadata.cbcEligibility is CardBrandChoiceEligibility.Eligible + }, + isGooglePayReady = stateFlowOf(false), + isLinkEnabled = stateFlowOf(false), + isNotPaymentFlow = false, + ) + } + + private fun createFormHelper( + coroutineScope: CoroutineScope, + paymentMethodMetadata: PaymentMethodMetadata, + ): FormHelper { + val linkInlineHandler = createLinkInlineHandler(coroutineScope) + return FormHelper( + 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 = { + setSelection(it) + }, + linkConfigurationCoordinator = linkConfigurationCoordinator, + onLinkInlineSignupStateChanged = linkInlineHandler::onStateUpdated, + ) + } + + private fun createLinkInlineHandler( + coroutineScope: CoroutineScope, + ): LinkInlineHandler { + return LinkInlineHandler( + coroutineScope = coroutineScope, + payWithLink = { _, _, _ -> + }, + selection = selectionHolder.selection, + updateLinkPrimaryButtonUiState = { + }, + primaryButtonLabel = stateFlowOf(null), + shouldCompleteLinkFlowInline = false, + ) + } + + private fun setSelection(paymentSelection: PaymentSelection?) { + savedStateHandle[MANDATE_KEY_EMBEDDED_CONTENT] = null + selectionHolder.set(paymentSelection) + } + + companion object { + const val PAYMENT_METHOD_METADATA_KEY_EMBEDDED_CONTENT = "PAYMENT_METHOD_METADATA_KEY_EMBEDDED_CONTENT" + const val MANDATE_KEY_EMBEDDED_CONTENT = "MANDATE_KEY_EMBEDDED_CONTENT" + } +} diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/SharedPaymentElementViewModel.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/SharedPaymentElementViewModel.kt index e9af5e52b8c..1118d5f4130 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/SharedPaymentElementViewModel.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/SharedPaymentElementViewModel.kt @@ -9,6 +9,8 @@ import androidx.lifecycle.createSavedStateHandle import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.CreationExtras import com.stripe.android.PaymentConfiguration +import com.stripe.android.cards.CardAccountRangeRepository +import com.stripe.android.cards.DefaultCardAccountRangeRepositoryFactory import com.stripe.android.core.injection.CoreCommonModule import com.stripe.android.core.injection.ENABLE_LOGGING import com.stripe.android.core.injection.IOContext @@ -83,6 +85,7 @@ internal class SharedPaymentElementViewModel @Inject constructor( private val configurationHandler: EmbeddedConfigurationHandler, private val paymentOptionDisplayDataFactory: PaymentOptionDisplayDataFactory, private val selectionHolder: EmbeddedSelectionHolder, + embeddedContentHelperFactory: EmbeddedContentHelperFactory, ) : ViewModel() { private val _paymentOption: MutableStateFlow = MutableStateFlow(null) val paymentOption: StateFlow = _paymentOption.asStateFlow() @@ -90,8 +93,8 @@ internal class SharedPaymentElementViewModel @Inject constructor( val confirmationStateHolder = confirmationStateHolderFactory.create(viewModelScope) val confirmationHandler = confirmationHandlerFactory.create(viewModelScope + ioContext) - private val _embeddedContent = MutableStateFlow(null) - val embeddedContent: StateFlow = _embeddedContent.asStateFlow() + private val embeddedContentHelper = embeddedContentHelperFactory.create(viewModelScope) + val embeddedContent: StateFlow = embeddedContentHelper.embeddedContent init { viewModelScope.launch { @@ -117,6 +120,9 @@ internal class SharedPaymentElementViewModel @Inject constructor( configuration = configuration, ) selectionHolder.set(state.paymentSelection) + embeddedContentHelper.dataLoaded( + paymentMethodMetadata = state.paymentMethodMetadata, + ) ConfigureResult.Succeeded() }, onFailure = { error -> @@ -179,6 +185,16 @@ internal interface SharedPaymentElementViewModelComponent { ], ) internal interface SharedPaymentElementViewModelModule { + @Binds + fun bindsEmbeddedContentHelperFactory( + factory: DefaultEmbeddedContentHelperFactory + ): EmbeddedContentHelperFactory + + @Binds + fun bindsCardAccountRangeRepositoryFactory( + defaultCardAccountRangeRepositoryFactory: DefaultCardAccountRangeRepositoryFactory + ): CardAccountRangeRepository.Factory + @Binds fun bindsConfigurationHandler( handler: DefaultEmbeddedConfigurationHandler diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/DefaultEmbeddedContentHelperTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/DefaultEmbeddedContentHelperTest.kt new file mode 100644 index 00000000000..fa482262a3f --- /dev/null +++ b/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/DefaultEmbeddedContentHelperTest.kt @@ -0,0 +1,109 @@ +package com.stripe.android.paymentelement.embedded + +import androidx.lifecycle.SavedStateHandle +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import com.stripe.android.core.strings.resolvableString +import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodMetadata +import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodMetadataFactory +import com.stripe.android.paymentelement.embedded.DefaultEmbeddedContentHelper.Companion.MANDATE_KEY_EMBEDDED_CONTENT +import com.stripe.android.paymentelement.embedded.DefaultEmbeddedContentHelper.Companion.PAYMENT_METHOD_METADATA_KEY_EMBEDDED_CONTENT +import com.stripe.android.utils.FakeCustomerRepository +import com.stripe.android.utils.FakeLinkConfigurationCoordinator +import com.stripe.android.utils.NullCardAccountRangeRepositoryFactory +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.test.runTest +import org.mockito.Mockito.mock +import kotlin.test.Test + +internal class DefaultEmbeddedContentHelperTest { + @Test + fun `dataLoaded updates savedStateHandle with paymentMethodMetadata`() = testScenario { + assertThat(savedStateHandle.get(PAYMENT_METHOD_METADATA_KEY_EMBEDDED_CONTENT)) + .isNull() + val paymentMethodMetadata = PaymentMethodMetadataFactory.create() + embeddedContentHelper.dataLoaded(paymentMethodMetadata) + assertThat(savedStateHandle.get(PAYMENT_METHOD_METADATA_KEY_EMBEDDED_CONTENT)) + .isEqualTo(paymentMethodMetadata) + } + + @Test + fun `dataLoaded emits embeddedContent event`() = testScenario { + embeddedContentHelper.embeddedContent.test { + assertThat(awaitItem()).isNull() + embeddedContentHelper.dataLoaded(PaymentMethodMetadataFactory.create()) + assertThat(awaitItem()).isNotNull() + } + } + + @Test + fun `setting mandate emits embeddedContent event`() = testScenario { + embeddedContentHelper.embeddedContent.test { + assertThat(awaitItem()).isNull() + embeddedContentHelper.dataLoaded(PaymentMethodMetadataFactory.create()) + awaitItem().run { + assertThat(this).isNotNull() + assertThat(this?.mandate).isNull() + } + savedStateHandle[MANDATE_KEY_EMBEDDED_CONTENT] = "Hi".resolvableString + awaitItem().run { + assertThat(this).isNotNull() + assertThat(this?.mandate).isEqualTo("Hi".resolvableString) + } + } + } + + @Test + fun `initializing embeddedContentHelper with paymentMethodMetadata emits correct initial event`() = testScenario( + setup = { + set(PAYMENT_METHOD_METADATA_KEY_EMBEDDED_CONTENT, PaymentMethodMetadataFactory.create()) + } + ) { + embeddedContentHelper.embeddedContent.test { + assertThat(awaitItem()).isNotNull() + } + } + + @Test + fun `initializing embeddedContentHelper with mandate emits correct initial event`() = testScenario( + setup = { + set(PAYMENT_METHOD_METADATA_KEY_EMBEDDED_CONTENT, PaymentMethodMetadataFactory.create()) + set(MANDATE_KEY_EMBEDDED_CONTENT, "Hi".resolvableString) + } + ) { + embeddedContentHelper.embeddedContent.test { + awaitItem().run { + assertThat(this).isNotNull() + assertThat(this?.mandate).isEqualTo("Hi".resolvableString) + } + } + } + + private class Scenario( + val embeddedContentHelper: DefaultEmbeddedContentHelper, + val savedStateHandle: SavedStateHandle, + ) + + private fun testScenario( + setup: SavedStateHandle.() -> Unit = {}, + block: suspend Scenario.() -> Unit, + ) = runTest { + val savedStateHandle = SavedStateHandle() + savedStateHandle.setup() + val embeddedContentHelper = DefaultEmbeddedContentHelper( + coroutineScope = CoroutineScope(Dispatchers.Unconfined), + cardAccountRangeRepositoryFactory = NullCardAccountRangeRepositoryFactory, + savedStateHandle = savedStateHandle, + eventReporter = mock(), + linkConfigurationCoordinator = FakeLinkConfigurationCoordinator(), + workContext = Dispatchers.Unconfined, + customerRepository = FakeCustomerRepository(), + selectionHolder = EmbeddedSelectionHolder(savedStateHandle), + ) + Scenario( + embeddedContentHelper = embeddedContentHelper, + savedStateHandle = savedStateHandle, + ).block() + } +} diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/FakeEmbeddedContentHelper.kt b/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/FakeEmbeddedContentHelper.kt new file mode 100644 index 00000000000..b8915ccc4a5 --- /dev/null +++ b/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/FakeEmbeddedContentHelper.kt @@ -0,0 +1,21 @@ +package com.stripe.android.paymentelement.embedded + +import app.cash.turbine.ReceiveTurbine +import app.cash.turbine.Turbine +import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodMetadata +import kotlinx.coroutines.flow.MutableStateFlow + +internal class FakeEmbeddedContentHelper( + override val embeddedContent: MutableStateFlow = MutableStateFlow(null) +) : EmbeddedContentHelper { + private val _dataLoadedTurbine = Turbine() + val dataLoadedTurbine: ReceiveTurbine = _dataLoadedTurbine + + override fun dataLoaded(paymentMethodMetadata: PaymentMethodMetadata) { + _dataLoadedTurbine.add(paymentMethodMetadata) + } + + fun validate() { + dataLoadedTurbine.ensureAllEventsConsumed() + } +} diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/SharedPaymentElementViewModelTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/SharedPaymentElementViewModelTest.kt index ab65e1d087d..4a02d6cb8e5 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/SharedPaymentElementViewModelTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/SharedPaymentElementViewModelTest.kt @@ -64,6 +64,7 @@ internal class SharedPaymentElementViewModelTest { configuration = configuration, ) ).isInstanceOf() + assertThat(embeddedContentHelper.dataLoadedTurbine.awaitItem()).isNotNull() assertThat(viewModel.confirmationStateHolder.state?.paymentMethodMetadata).isNotNull() } @@ -103,6 +104,7 @@ internal class SharedPaymentElementViewModelTest { configuration = configuration, ) ).isInstanceOf() + assertThat(embeddedContentHelper.dataLoadedTurbine.awaitItem()).isNotNull() assertThat(viewModel.confirmationStateHolder.state?.selection?.paymentMethodType).isEqualTo("google_pay") selectionHolder.set(null) @@ -145,6 +147,7 @@ internal class SharedPaymentElementViewModelTest { viewModel.paymentOption.test { assertThat(awaitItem()).isNull() } + assertThat(embeddedContentHelper.dataLoadedTurbine.awaitItem()).isNotNull() } @Test @@ -188,6 +191,7 @@ internal class SharedPaymentElementViewModelTest { assertThat(awaitItem()?.paymentMethodType).isEqualTo("google_pay") } assertThat(selectionHolder.selection.value?.paymentMethodType).isEqualTo("google_pay") + assertThat(embeddedContentHelper.dataLoadedTurbine.awaitItem()).isNotNull() } @Test @@ -248,52 +252,58 @@ internal class SharedPaymentElementViewModelTest { assertThat(selectionHolder.selection.value?.paymentMethodType).isNull() assertThat(awaitItem()).isNull() } + assertThat(embeddedContentHelper.dataLoadedTurbine.awaitItem()).isNotNull() } private fun testScenario( block: suspend Scenario.() -> Unit, - ) { - runTest { - val confirmationHandler = FakeConfirmationHandler() - val configurationHandler = FakeEmbeddedConfigurationHandler() - val paymentOptionDisplayDataFactory = PaymentOptionDisplayDataFactory( - iconLoader = mock(), - context = ApplicationProvider.getApplicationContext(), - ) - val savedStateHandle = SavedStateHandle() - val selectionHolder = EmbeddedSelectionHolder(savedStateHandle) - val confirmationStateHolder = EmbeddedConfirmationStateHolder( - savedStateHandle = savedStateHandle, - selectionHolder = selectionHolder, - coroutineScope = CoroutineScope(UnconfinedTestDispatcher()), - ) + ) = runTest { + val confirmationHandler = FakeConfirmationHandler() + val configurationHandler = FakeEmbeddedConfigurationHandler() + val paymentOptionDisplayDataFactory = PaymentOptionDisplayDataFactory( + iconLoader = mock(), + context = ApplicationProvider.getApplicationContext(), + ) + val savedStateHandle = SavedStateHandle() + val selectionHolder = EmbeddedSelectionHolder(savedStateHandle) + val confirmationStateHolder = EmbeddedConfirmationStateHolder( + savedStateHandle = savedStateHandle, + selectionHolder = selectionHolder, + coroutineScope = CoroutineScope(UnconfinedTestDispatcher()), + ) + val embeddedContentHelper = FakeEmbeddedContentHelper() - val viewModel = SharedPaymentElementViewModel( - confirmationStateHolderFactory = EmbeddedConfirmationStateHolderFactory { - confirmationStateHolder - }, - confirmationHandlerFactory = { confirmationHandler }, - ioContext = testScheduler, - configurationHandler = configurationHandler, - paymentOptionDisplayDataFactory = paymentOptionDisplayDataFactory, - selectionHolder = selectionHolder, - ) + val viewModel = SharedPaymentElementViewModel( + confirmationStateHolderFactory = EmbeddedConfirmationStateHolderFactory { + confirmationStateHolder + }, + confirmationHandlerFactory = { confirmationHandler }, + ioContext = testScheduler, + configurationHandler = configurationHandler, + paymentOptionDisplayDataFactory = paymentOptionDisplayDataFactory, + selectionHolder = selectionHolder, + embeddedContentHelperFactory = EmbeddedContentHelperFactory { + embeddedContentHelper + } + ) - Scenario( - configurationHandler = configurationHandler, - viewModel = viewModel, - selectionHolder = selectionHolder, - ).block() + Scenario( + configurationHandler = configurationHandler, + viewModel = viewModel, + selectionHolder = selectionHolder, + embeddedContentHelper = embeddedContentHelper, + ).block() - configurationHandler.turbine.ensureAllEventsConsumed() - confirmationHandler.validate() - } + configurationHandler.turbine.ensureAllEventsConsumed() + confirmationHandler.validate() + embeddedContentHelper.validate() } private class Scenario( val configurationHandler: FakeEmbeddedConfigurationHandler, val viewModel: SharedPaymentElementViewModel, val selectionHolder: EmbeddedSelectionHolder, + val embeddedContentHelper: FakeEmbeddedContentHelper, ) private class FakeEmbeddedConfigurationHandler : EmbeddedConfigurationHandler {