diff --git a/paymentsheet/api/paymentsheet.api b/paymentsheet/api/paymentsheet.api index cb023ed5956..7e065da03e5 100644 --- a/paymentsheet/api/paymentsheet.api +++ b/paymentsheet/api/paymentsheet.api @@ -679,6 +679,30 @@ public final class com/stripe/android/paymentelement/embedded/EmbeddedConfirmati public synthetic fun newArray (I)[Ljava/lang/Object; } +public final class com/stripe/android/paymentelement/embedded/FormContract$Args$Creator : android/os/Parcelable$Creator { + public fun ()V + public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/paymentelement/embedded/FormContract$Args; + public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; + public final fun newArray (I)[Lcom/stripe/android/paymentelement/embedded/FormContract$Args; + public synthetic fun newArray (I)[Ljava/lang/Object; +} + +public final class com/stripe/android/paymentelement/embedded/FormResult$Cancelled$Creator : android/os/Parcelable$Creator { + public fun ()V + public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/paymentelement/embedded/FormResult$Cancelled; + public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; + public final fun newArray (I)[Lcom/stripe/android/paymentelement/embedded/FormResult$Cancelled; + public synthetic fun newArray (I)[Ljava/lang/Object; +} + +public final class com/stripe/android/paymentelement/embedded/FormResult$Complete$Creator : android/os/Parcelable$Creator { + public fun ()V + public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/paymentelement/embedded/FormResult$Complete; + public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; + public final fun newArray (I)[Lcom/stripe/android/paymentelement/embedded/FormResult$Complete; + public synthetic fun newArray (I)[Ljava/lang/Object; +} + public final class com/stripe/android/paymentsheet/BuildConfig { public static final field BUILD_TYPE Ljava/lang/String; public static final field DEBUG Z diff --git a/paymentsheet/src/main/AndroidManifest.xml b/paymentsheet/src/main/AndroidManifest.xml index c23e33c7ee1..7587190cac3 100644 --- a/paymentsheet/src/main/AndroidManifest.xml +++ b/paymentsheet/src/main/AndroidManifest.xml @@ -37,6 +37,9 @@ + Unit)? +} + +internal class DefaultEmbeddedActivityLauncher( + private val activityResultCaller: ActivityResultCaller, + private val lifecycleOwner: LifecycleOwner, + private val selectionHolder: EmbeddedSelectionHolder +) : EmbeddedActivityLauncher { + + private var formActivityLauncher: ActivityResultLauncher = + activityResultCaller.registerForActivityResult(FormContract()) { result -> + if (result is FormResult.Complete) { + selectionHolder.set(result.selection) + } + } + + override var formLauncher: ((code: String, paymentMethodMetadata: PaymentMethodMetadata?) -> Unit)? = + { code, metadata -> + formActivityLauncher.launch(FormContract.Args(code, metadata)) + } + + init { + lifecycleOwner.lifecycle.addObserver( + object : DefaultLifecycleObserver { + override fun onDestroy(owner: LifecycleOwner) { + formActivityLauncher.unregister() + formLauncher = null + super.onDestroy(owner) + } + } + ) + } +} 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 index 799361f59c5..b155eee9a5a 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/EmbeddedContentHelper.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/EmbeddedContentHelper.kt @@ -46,6 +46,8 @@ internal interface EmbeddedContentHelper { rowStyle: Embedded.RowStyle, embeddedViewDisplaysMandateText: Boolean, ) + + fun setFormLauncher(formLauncher: ((code: String, paymentMethodMetaData: PaymentMethodMetadata?) -> Unit)?) } internal fun interface EmbeddedContentHelperFactory { @@ -80,6 +82,8 @@ internal class DefaultEmbeddedContentHelper @AssistedInject constructor( private val _embeddedContent = MutableStateFlow(null) override val embeddedContent: StateFlow = _embeddedContent.asStateFlow() + private var formLauncher: ((code: String, paymentMethodMetaData: PaymentMethodMetadata?) -> Unit)? = null + init { coroutineScope.launch { state.collect { state -> @@ -119,6 +123,12 @@ internal class DefaultEmbeddedContentHelper @AssistedInject constructor( ) } + override fun setFormLauncher( + formLauncher: ((code: String, paymentMethodMetaData: PaymentMethodMetadata?) -> Unit)? + ) { + this.formLauncher = formLauncher + } + private fun createInteractor( coroutineScope: CoroutineScope, paymentMethodMetadata: PaymentMethodMetadata, @@ -161,6 +171,7 @@ internal class DefaultEmbeddedContentHelper @AssistedInject constructor( transitionToManageScreen = { }, transitionToFormScreen = { + formLauncher?.invoke(it, state.value?.paymentMethodMetadata) }, paymentMethods = customerStateHolder.paymentMethods, mostRecentlySelectedSavedPaymentMethod = customerStateHolder.mostRecentlySelectedSavedPaymentMethod, diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/FormActivity.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/FormActivity.kt new file mode 100644 index 00000000000..402fd2370e0 --- /dev/null +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/FormActivity.kt @@ -0,0 +1,76 @@ +package com.stripe.android.paymentelement.embedded + +import android.app.Activity +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.material.ExperimentalMaterialApi +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 + +internal class FormActivity : AppCompatActivity() { + private val formArgs: FormContract.Args? by lazy { + FormContract.Args.fromIntent(intent) + } + + private val viewModel: FormActivityViewModel by viewModels { + FormActivityViewModel.Factory() + } + + @OptIn(ExperimentalMaterialApi::class) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + if (formArgs == null) { + setFormResult(FormResult.Cancelled) + finish() + return + } + + formArgs?.let { args -> + args.paymentMethodMetadata?. let { metadata -> + viewModel.initializeFormInteractor( + metadata, + args.selectedPaymentMethodCode, + ) + } + } + + setContent { + StripeTheme { + val bottomSheetState = rememberStripeBottomSheetState() + ElementsBottomSheetLayout( + state = bottomSheetState, + onDismissed = { + setResult( + Activity.RESULT_OK, + FormResult.toIntent(intent, FormResult.Cancelled) + ) + finish() + } + ) { + VerticalModeFormUI( + viewModel.formInteractor, + false, + ) + } + } + } + } + + override fun finish() { + super.finish() + fadeOut() + } + + private fun setFormResult(result: FormResult) { + setResult( + Activity.RESULT_OK, + FormResult.toIntent(intent, result) + ) + } +} diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/FormActivityViewModel.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/FormActivityViewModel.kt new file mode 100644 index 00000000000..6b4f41464b9 --- /dev/null +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/FormActivityViewModel.kt @@ -0,0 +1,189 @@ +package com.stripe.android.paymentelement.embedded + +import android.content.Context +import androidx.lifecycle.SavedStateHandle +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.injection.CoreCommonModule +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.ExperimentalEmbeddedPaymentElementApi +import com.stripe.android.payments.bankaccount.CollectBankAccountLauncher.Companion.HOSTED_SURFACE_PAYMENT_ELEMENT +import com.stripe.android.payments.core.injection.StripeRepositoryModule +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 dagger.BindsInstance +import dagger.Component +import javax.inject.Inject +import javax.inject.Singleton + +internal class FormActivityViewModel @Inject constructor( + private val cardAccountRangeRepositoryFactory: CardAccountRangeRepository.Factory, + private val selectionHolder: EmbeddedSelectionHolder, + private val linkConfigurationCoordinator: LinkConfigurationCoordinator, +) : ViewModel() { + + lateinit var formInteractor: DefaultVerticalModeFormInteractor + + fun initializeFormInteractor( + paymentMethodMetadata: PaymentMethodMetadata, + selectedPaymentMethodCode: PaymentMethodCode, + ) { + val formHelper = createFormHelper(paymentMethodMetadata = paymentMethodMetadata) + + val paymentMethodIncentiveInteractor = PaymentMethodIncentiveInteractor( + paymentMethodMetadata.paymentMethodIncentive + ) + + formInteractor = DefaultVerticalModeFormInteractor( + selectedPaymentMethodCode = selectedPaymentMethodCode, + formArguments = formHelper.createFormArguments(selectedPaymentMethodCode), + formElements = formHelper.formElementsForCode(selectedPaymentMethodCode), + onFormFieldValuesChanged = formHelper::onFormFieldValuesChanged, + usBankAccountArguments = createUsBankAccountFormArguments( + paymentMethodMetadata, + selectedPaymentMethodCode + ), + reportFieldInteraction = { + }, + headerInformation = null, + canGoBackDelegate = { false }, + isLiveMode = paymentMethodMetadata.stripeIntent.isLiveMode, + processing = stateFlowOf(false), + paymentMethodIncentive = paymentMethodIncentiveInteractor.displayedIncentive, + coroutineScope = viewModelScope, + ) + } + + private fun createFormHelper(paymentMethodMetadata: PaymentMethodMetadata): FormHelper { + val linkInlineHandler = createLinkInlineHandler() + 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 = { + }, + linkConfigurationCoordinator = linkConfigurationCoordinator, + onLinkInlineSignupStateChanged = linkInlineHandler::onStateUpdated, + ) + } + + private fun createLinkInlineHandler(): LinkInlineHandler { + return LinkInlineHandler( + coroutineScope = viewModelScope, + payWithLink = { _, _, _ -> + }, + selection = selectionHolder.selection, + updateLinkPrimaryButtonUiState = { + }, + primaryButtonLabel = stateFlowOf(null), + shouldCompleteLinkFlowInline = false, + ) + } + + 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 && + // Instant Debits does not support saving for future use + 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, + ) + } + + internal class Factory : ViewModelProvider.Factory { + override fun create(modelClass: Class, extras: CreationExtras): T { + val component = DaggerFormActivityViewModelComponent.builder() + .savedStateHandle(extras.createSavedStateHandle()) + .context(extras.requireApplication()) + .build() + @Suppress("UNCHECKED_CAST") + return component.viewModel as T + } + } +} + +@OptIn(ExperimentalEmbeddedPaymentElementApi::class) +@Singleton +@Component( + modules = [ + SharedPaymentElementViewModelModule::class, + CoreCommonModule::class, + StripeRepositoryModule::class, + ] +) +internal interface FormActivityViewModelComponent { + val viewModel: FormActivityViewModel + + @Component.Builder + interface Builder { + @BindsInstance + fun savedStateHandle(savedStateHandle: SavedStateHandle): Builder + + @BindsInstance + fun context(context: Context): Builder + + fun build(): FormActivityViewModelComponent + } +} diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/FormContract.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/FormContract.kt new file mode 100644 index 00000000000..4c66fc8a5fd --- /dev/null +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/FormContract.kt @@ -0,0 +1,65 @@ +package com.stripe.android.paymentelement.embedded + +import android.content.Context +import android.content.Intent +import android.os.Parcelable +import androidx.activity.result.contract.ActivityResultContract +import androidx.core.os.BundleCompat +import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodMetadata +import com.stripe.android.paymentsheet.model.PaymentSelection +import com.stripe.android.view.ActivityStarter +import kotlinx.parcelize.Parcelize + +internal sealed interface FormResult : Parcelable { + + @Parcelize + data class Complete(val selection: PaymentSelection) : FormResult + + @Parcelize + object Cancelled : FormResult + + companion object { + internal const val EXTRA_RESULT = ActivityStarter.Result.EXTRA + + fun toIntent(intent: Intent, result: FormResult): Intent { + return intent.putExtra(EXTRA_RESULT, result) + } + + fun fromIntent(intent: Intent?): FormResult { + val result = intent?.extras?.let { bundle -> + BundleCompat.getParcelable(bundle, EXTRA_RESULT, FormResult::class.java) + } + + return result ?: Cancelled + } + } +} + +internal class FormContract : ActivityResultContract() { + override fun createIntent(context: Context, input: Args): Intent { + return Intent(context, FormActivity::class.java) + .putExtra(EXTRA_ARGS, input) + } + + override fun parseResult(resultCode: Int, intent: Intent?): FormResult { + return FormResult.fromIntent(intent) + } + + @Parcelize + internal data class Args( + val selectedPaymentMethodCode: String, + val paymentMethodMetadata: PaymentMethodMetadata?, + ) : Parcelable { + companion object { + fun fromIntent(intent: Intent): Args? { + return intent.extras?.let { bundle -> + BundleCompat.getParcelable(bundle, EXTRA_ARGS, Args::class.java) + } + } + } + } + + internal companion object { + internal const val EXTRA_ARGS: String = "extra_activity_args" + } +} 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 6c324519681..7a2611a4ace 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 @@ -2,6 +2,8 @@ package com.stripe.android.paymentelement.embedded import android.content.Context import android.content.res.Resources +import androidx.activity.result.ActivityResultCaller +import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider @@ -29,6 +31,7 @@ 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.paymentelement.EmbeddedPaymentElement import com.stripe.android.paymentelement.EmbeddedPaymentElement.ConfigureResult import com.stripe.android.paymentelement.EmbeddedPaymentElement.PaymentOptionDisplayData @@ -110,6 +113,15 @@ internal class SharedPaymentElementViewModel @Inject constructor( } } + fun initEmbeddedActivityLauncher(activityResultCaller: ActivityResultCaller, lifecycleOwner: LifecycleOwner) { + val launcher = DefaultEmbeddedActivityLauncher(activityResultCaller, lifecycleOwner, selectionHolder) + setFormLauncher(launcher.formLauncher) + } + + private fun setFormLauncher(launch: ((code: String, paymentMethodMetadata: PaymentMethodMetadata?) -> Unit)?) { + embeddedContentHelper.setFormLauncher(launch) + } + suspend fun configure( intentConfiguration: PaymentSheet.IntentConfiguration, configuration: EmbeddedPaymentElement.Configuration, diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/EmbeddedActivityLauncherTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/EmbeddedActivityLauncherTest.kt new file mode 100644 index 00000000000..24711f264d5 --- /dev/null +++ b/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/EmbeddedActivityLauncherTest.kt @@ -0,0 +1,86 @@ +import androidx.activity.result.ActivityResultCallback +import androidx.activity.result.ActivityResultCaller +import androidx.activity.result.ActivityResultLauncher +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.testing.TestLifecycleOwner +import com.stripe.android.model.PaymentMethodFixtures +import com.stripe.android.paymentelement.embedded.DefaultEmbeddedActivityLauncher +import com.stripe.android.paymentelement.embedded.EmbeddedSelectionHolder +import com.stripe.android.paymentelement.embedded.FormContract +import com.stripe.android.paymentelement.embedded.FormResult +import junit.framework.TestCase.assertNull +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.Captor +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.any +import org.mockito.kotlin.capture +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +internal class EmbeddedActivityLauncherTest { + + private lateinit var activityResultCaller: ActivityResultCaller + private lateinit var lifecycleOwner: TestLifecycleOwner + private lateinit var selectionHolder: EmbeddedSelectionHolder + private lateinit var launcher: DefaultEmbeddedActivityLauncher + private lateinit var formActivityLauncher: ActivityResultLauncher + + @Captor + private lateinit var contractCallbackCaptor: ArgumentCaptor> + + @Before + fun setUp() { + MockitoAnnotations.openMocks(this) + activityResultCaller = mock() + lifecycleOwner = TestLifecycleOwner() + selectionHolder = mock() + + formActivityLauncher = mock() + + whenever( + activityResultCaller.registerForActivityResult( + any(), + capture(contractCallbackCaptor) + ) + ).thenReturn(formActivityLauncher) + + launcher = DefaultEmbeddedActivityLauncher(activityResultCaller, lifecycleOwner, selectionHolder) + } + + @Test + fun `formLauncher launches activity with correct parameters`() { + val code = "test_code" + val expectedArgs = FormContract.Args(code, null) + launcher.formLauncher?.invoke(code, null) + verify(formActivityLauncher).launch(expectedArgs) + } + + @Test + fun `formActivityLauncher callback updates selection holder on complete result`() { + val selection = PaymentMethodFixtures.CARD_PAYMENT_SELECTION + val result = FormResult.Complete(PaymentMethodFixtures.CARD_PAYMENT_SELECTION) + contractCallbackCaptor.value.onActivityResult(result) + verify(selectionHolder).set(selection) + } + + @Test + fun `formActivityLauncher callback does not update selection holder on non-complete result`() { + val result = FormResult.Cancelled + contractCallbackCaptor.value.onActivityResult(result) + verify(selectionHolder, never()).set(any()) + } + + @Test + fun `cleanup happens on lifecycle destroy`() { + lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY) + verify(formActivityLauncher).unregister() + assertNull(launcher.formLauncher) + } +} 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 index 96bd337665e..9b7a45e61fe 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/FakeEmbeddedContentHelper.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/FakeEmbeddedContentHelper.kt @@ -28,6 +28,12 @@ internal class FakeEmbeddedContentHelper( ) } + override fun setFormLauncher( + formLauncher: ((code: String, paymentMethodMetaData: PaymentMethodMetadata?) -> Unit)? + ) { + // NO-OP + } + fun validate() { dataLoadedTurbine.ensureAllEventsConsumed() } diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/FormActivityTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/FormActivityTest.kt new file mode 100644 index 00000000000..88794c98288 --- /dev/null +++ b/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/FormActivityTest.kt @@ -0,0 +1,44 @@ +package com.stripe.android.paymentelement.embedded + +import android.content.Context +import android.os.Bundle +import androidx.lifecycle.Lifecycle +import androidx.test.core.app.ActivityScenario +import androidx.test.core.app.ApplicationProvider +import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +internal class FormActivityTest { + + private lateinit var scenario: ActivityScenario + private lateinit var context: Context + + @Before + fun setUp() { + context = ApplicationProvider.getApplicationContext() + } + + @After + fun tearDown() { + if (::scenario.isInitialized) { + scenario.close() + } + } + + @Test + fun `when launched without args should finish with cancelled result`() { + ActivityScenario.launchActivityForResult( + FormActivity::class.java, + Bundle.EMPTY + ).use { activityScenario -> + assertThat(activityScenario.state).isEqualTo(Lifecycle.State.DESTROYED) + val result = FormContract().parseResult(0, activityScenario.result.resultData) + assertThat(result).isInstanceOf(FormResult.Cancelled::class.java) + } + } +} diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/FormActivityViewModelTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/FormActivityViewModelTest.kt new file mode 100644 index 00000000000..641ef83f085 --- /dev/null +++ b/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/FormActivityViewModelTest.kt @@ -0,0 +1,61 @@ +package com.stripe.android.paymentelement.embedded + +import androidx.lifecycle.SavedStateHandle +import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodMetadataFactory +import com.stripe.android.model.PaymentIntentFixtures +import com.stripe.android.paymentsheet.PaymentSheet +import com.stripe.android.ui.core.cbc.CardBrandChoiceEligibility +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 `initializeFormInteractor sets up form interactor correctly`() = testScenario { + val paymentMethodMetadata = PaymentMethodMetadataFactory.create( + stripeIntent = PaymentIntentFixtures.PI_SUCCEEDED, + billingDetailsCollectionConfiguration = PaymentSheet.BillingDetailsCollectionConfiguration(), + allowsDelayedPaymentMethods = true, + allowsPaymentMethodsRequiringShippingAddress = true, + isGooglePayReady = true, + cbcEligibility = CardBrandChoiceEligibility.Ineligible, + ) + + val selectedPaymentMethodCode = "card" + + viewModel.initializeFormInteractor( + paymentMethodMetadata, + selectedPaymentMethodCode + ) + + assert(viewModel.formInteractor.state.value.formArguments.paymentMethodCode == "card") + } + + private fun testScenario( + block: suspend Scenario.() -> Unit, + ) = runTest { + val savedStateHandle = SavedStateHandle() + val selectionHolder = EmbeddedSelectionHolder(savedStateHandle) + + val viewModel = FormActivityViewModel( + cardAccountRangeRepositoryFactory = NullCardAccountRangeRepositoryFactory, + selectionHolder = EmbeddedSelectionHolder(savedStateHandle), + linkConfigurationCoordinator = FakeLinkConfigurationCoordinator() + ) + + Scenario( + viewModel = viewModel, + selectionHolder = selectionHolder, + ).block() + } + + private class Scenario( + val viewModel: FormActivityViewModel, + val selectionHolder: EmbeddedSelectionHolder, + ) +}