diff --git a/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/PaymentSheetPlaygroundViewModel.kt b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/PaymentSheetPlaygroundViewModel.kt index b945f2459be..a4e7fabea2a 100644 --- a/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/PaymentSheetPlaygroundViewModel.kt +++ b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/PaymentSheetPlaygroundViewModel.kt @@ -417,6 +417,7 @@ internal class PaymentSheetPlaygroundViewModel( fun onEmbeddedResult(success: Boolean) { if (success) { setPlaygroundState(null) + status.value = StatusMessage(SUCCESS_RESULT) } } diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/flowcontroller/FlowControllerActivityResultCaller.kt b/paymentsheet/src/main/java/com/stripe/android/common/ui/PaymentElementActivityResultCaller.kt similarity index 80% rename from paymentsheet/src/main/java/com/stripe/android/paymentsheet/flowcontroller/FlowControllerActivityResultCaller.kt rename to paymentsheet/src/main/java/com/stripe/android/common/ui/PaymentElementActivityResultCaller.kt index 28fe39d0ca4..e193104e794 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/flowcontroller/FlowControllerActivityResultCaller.kt +++ b/paymentsheet/src/main/java/com/stripe/android/common/ui/PaymentElementActivityResultCaller.kt @@ -1,4 +1,4 @@ -package com.stripe.android.paymentsheet.flowcontroller +package com.stripe.android.common.ui import androidx.activity.result.ActivityResultCallback import androidx.activity.result.ActivityResultCaller @@ -7,7 +7,8 @@ import androidx.activity.result.ActivityResultRegistry import androidx.activity.result.ActivityResultRegistryOwner import androidx.activity.result.contract.ActivityResultContract -internal class FlowControllerActivityResultCaller( +internal class PaymentElementActivityResultCaller( + private val key: String, private val registryOwner: ActivityResultRegistryOwner ) : ActivityResultCaller { override fun registerForActivityResult( @@ -26,10 +27,6 @@ internal class FlowControllerActivityResultCaller( } private fun createKey(contract: ActivityResultContract): String { - return "${FLOW_CONTROLLER_KEY}_${contract::class.java.name}" - } - - private companion object { - const val FLOW_CONTROLLER_KEY = "FlowController" + return "${key}_${contract::class.java.name}" } } diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/EmbeddedPaymentElement.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/EmbeddedPaymentElement.kt index ee4f7e218a1..4475e434473 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentelement/EmbeddedPaymentElement.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/EmbeddedPaymentElement.kt @@ -2,11 +2,13 @@ package com.stripe.android.paymentelement import android.graphics.drawable.Drawable import android.os.Parcelable +import androidx.activity.result.ActivityResultCaller import androidx.annotation.RestrictTo import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.text.AnnotatedString +import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelStoreOwner import com.stripe.android.ExperimentalAllowsRemovalOfLastSavedPaymentMethodApi @@ -16,6 +18,7 @@ import com.stripe.android.common.ui.DelegateDrawable import com.stripe.android.model.CardBrand import com.stripe.android.model.PaymentIntent import com.stripe.android.model.SetupIntent +import com.stripe.android.paymentelement.embedded.EmbeddedConfirmationHelper import com.stripe.android.paymentelement.embedded.SharedPaymentElementViewModel import com.stripe.android.paymentsheet.ExternalPaymentMethodConfirmHandler import com.stripe.android.paymentsheet.PaymentSheet @@ -28,8 +31,8 @@ import kotlinx.parcelize.Parcelize @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) @ExperimentalEmbeddedPaymentElementApi class EmbeddedPaymentElement private constructor( + private val embeddedConfirmationHelper: EmbeddedConfirmationHelper, private val sharedViewModel: SharedPaymentElementViewModel, - private val resultCallback: ResultCallback, ) { /** * Contains information about the customer's selected payment option. @@ -67,7 +70,7 @@ class EmbeddedPaymentElement private constructor( * Results will be delivered to the [ResultCallback] supplied during initialization of [EmbeddedPaymentElement]. */ fun confirm() { - resultCallback.onResult(Result.Failed(NotImplementedError("Not yet implemented."))) + embeddedConfirmationHelper.confirm() } /** Configuration for [EmbeddedPaymentElement] **/ @@ -439,16 +442,25 @@ class EmbeddedPaymentElement private constructor( internal companion object { @ExperimentalEmbeddedPaymentElementApi fun create( + statusBarColor: Int?, + activityResultCaller: ActivityResultCaller, viewModelStoreOwner: ViewModelStoreOwner, + lifecycleOwner: LifecycleOwner, resultCallback: ResultCallback, ): EmbeddedPaymentElement { val sharedViewModel = ViewModelProvider( owner = viewModelStoreOwner, - factory = SharedPaymentElementViewModel.Factory() + factory = SharedPaymentElementViewModel.Factory(statusBarColor) )[SharedPaymentElementViewModel::class.java] return EmbeddedPaymentElement( + embeddedConfirmationHelper = EmbeddedConfirmationHelper( + confirmationHandler = sharedViewModel.confirmationHandler, + resultCallback = resultCallback, + activityResultCaller = activityResultCaller, + lifecycleOwner = lifecycleOwner, + confirmationStateSupplier = { sharedViewModel.confirmationState } + ), sharedViewModel = sharedViewModel, - resultCallback = resultCallback, ) } } diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/EmbeddedPaymentElementKtx.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/EmbeddedPaymentElementKtx.kt index aa33b90f5c6..48d82c5c7b3 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentelement/EmbeddedPaymentElementKtx.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/EmbeddedPaymentElementKtx.kt @@ -1,15 +1,19 @@ package com.stripe.android.paymentelement +import androidx.activity.compose.LocalActivityResultRegistryOwner import androidx.annotation.RestrictTo import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState +import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner +import com.stripe.android.common.ui.PaymentElementActivityResultCaller import com.stripe.android.common.ui.UpdateExternalPaymentMethodConfirmHandler import com.stripe.android.common.ui.UpdateIntentConfirmationInterceptor import com.stripe.android.paymentsheet.CreateIntentCallback import com.stripe.android.paymentsheet.ExternalPaymentMethodConfirmHandler +import com.stripe.android.utils.rememberActivity /** * Creates an [EmbeddedPaymentElement] that is remembered across compositions. @@ -35,10 +39,25 @@ fun rememberEmbeddedPaymentElement( UpdateExternalPaymentMethodConfirmHandler(externalPaymentMethodConfirmHandler) UpdateIntentConfirmationInterceptor(createIntentCallback) + val lifecycleOwner = LocalLifecycleOwner.current + val activityResultRegistryOwner = requireNotNull(LocalActivityResultRegistryOwner.current) { + "EmbeddedPaymentElement must have an ActivityResultRegistryOwner." + } + val onResult by rememberUpdatedState(newValue = resultCallback::onResult) + val activity = rememberActivity { + "EmbeddedPaymentElement must be created in the context of an Activity." + } + return remember { EmbeddedPaymentElement.create( + statusBarColor = activity.window?.statusBarColor, + activityResultCaller = PaymentElementActivityResultCaller( + key = "Embedded", + registryOwner = activityResultRegistryOwner, + ), + lifecycleOwner = lifecycleOwner, viewModelStoreOwner = viewModelStoreOwner, resultCallback = onResult, ) diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/confirmation/ConfirmationHandler.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/confirmation/ConfirmationHandler.kt index dad6247fa1c..10f09fee668 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentelement/confirmation/ConfirmationHandler.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/confirmation/ConfirmationHandler.kt @@ -52,7 +52,7 @@ internal interface ConfirmationHandler { * A factory for creating a [ConfirmationHandler] instance using a provided [CoroutineScope]. This scope is * used to launch confirmation tasks. */ - interface Factory { + fun interface Factory { fun create(scope: CoroutineScope): ConfirmationHandler } diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/EmbeddedConfirmationHelper.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/EmbeddedConfirmationHelper.kt new file mode 100644 index 00000000000..fcac7c4ad99 --- /dev/null +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/EmbeddedConfirmationHelper.kt @@ -0,0 +1,72 @@ +package com.stripe.android.paymentelement.embedded + +import androidx.activity.result.ActivityResultCaller +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import com.stripe.android.common.model.asCommonConfiguration +import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodMetadata +import com.stripe.android.paymentelement.EmbeddedPaymentElement +import com.stripe.android.paymentelement.ExperimentalEmbeddedPaymentElementApi +import com.stripe.android.paymentelement.confirmation.ConfirmationHandler +import com.stripe.android.paymentelement.confirmation.toConfirmationOption +import com.stripe.android.paymentsheet.model.PaymentSelection +import com.stripe.android.paymentsheet.state.PaymentElementLoader +import kotlinx.coroutines.launch + +@ExperimentalEmbeddedPaymentElementApi +internal class EmbeddedConfirmationHelper( + private val confirmationHandler: ConfirmationHandler, + private val resultCallback: EmbeddedPaymentElement.ResultCallback, + private val activityResultCaller: ActivityResultCaller, + private val lifecycleOwner: LifecycleOwner, + private val confirmationStateSupplier: () -> State?, +) { + init { + confirmationHandler.register( + activityResultCaller = activityResultCaller, + lifecycleOwner = lifecycleOwner + ) + + lifecycleOwner.lifecycleScope.launch { + confirmationHandler.state.collect { state -> + when (state) { + is ConfirmationHandler.State.Complete -> { + resultCallback.onResult(state.result.asEmbeddedResult()) + } + is ConfirmationHandler.State.Confirming, ConfirmationHandler.State.Idle -> Unit + } + } + } + } + + fun confirm() { + confirmationArgs()?.let { confirmationArgs -> + confirmationHandler.start(confirmationArgs) + } ?: run { + resultCallback.onResult( + EmbeddedPaymentElement.Result.Failed(IllegalStateException("Not in a state that's confirmable.")) + ) + } + } + + private fun confirmationArgs(): ConfirmationHandler.Args? { + val loadedState = confirmationStateSupplier() ?: return null + val confirmationOption = loadedState.selection?.toConfirmationOption( + initializationMode = loadedState.initializationMode, + configuration = loadedState.configuration.asCommonConfiguration(), + appearance = loadedState.configuration.appearance, + ) ?: return null + + return ConfirmationHandler.Args( + intent = loadedState.paymentMethodMetadata.stripeIntent, + confirmationOption = confirmationOption, + ) + } + + data class State( + val paymentMethodMetadata: PaymentMethodMetadata, + val selection: PaymentSelection?, + val initializationMode: PaymentElementLoader.InitializationMode, + val configuration: EmbeddedPaymentElement.Configuration, + ) +} diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/EmbeddedResultKtx.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/EmbeddedResultKtx.kt new file mode 100644 index 00000000000..472ba2dd438 --- /dev/null +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/EmbeddedResultKtx.kt @@ -0,0 +1,20 @@ +package com.stripe.android.paymentelement.embedded + +import com.stripe.android.paymentelement.EmbeddedPaymentElement +import com.stripe.android.paymentelement.ExperimentalEmbeddedPaymentElementApi +import com.stripe.android.paymentelement.confirmation.ConfirmationHandler + +@ExperimentalEmbeddedPaymentElementApi +internal fun ConfirmationHandler.Result.asEmbeddedResult(): EmbeddedPaymentElement.Result { + return when (this) { + is ConfirmationHandler.Result.Canceled -> { + EmbeddedPaymentElement.Result.Canceled() + } + is ConfirmationHandler.Result.Failed -> { + EmbeddedPaymentElement.Result.Failed(cause) + } + is ConfirmationHandler.Result.Succeeded -> { + EmbeddedPaymentElement.Result.Completed() + } + } +} 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 0900ca4690c..0e2280040c9 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 @@ -6,6 +6,7 @@ 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.PaymentConfiguration import com.stripe.android.core.injection.CoreCommonModule @@ -30,9 +31,13 @@ import com.stripe.android.paymentelement.EmbeddedPaymentElement import com.stripe.android.paymentelement.EmbeddedPaymentElement.ConfigureResult import com.stripe.android.paymentelement.EmbeddedPaymentElement.PaymentOptionDisplayData import com.stripe.android.paymentelement.ExperimentalEmbeddedPaymentElementApi +import com.stripe.android.paymentelement.confirmation.ALLOWS_MANUAL_CONFIRMATION +import com.stripe.android.paymentelement.confirmation.ConfirmationHandler +import com.stripe.android.paymentelement.confirmation.injection.PaymentElementConfirmationModule import com.stripe.android.payments.core.analytics.ErrorReporter import com.stripe.android.payments.core.analytics.RealErrorReporter import com.stripe.android.payments.core.injection.PRODUCT_USAGE +import com.stripe.android.payments.core.injection.STATUS_BAR_COLOR import com.stripe.android.payments.core.injection.StripeRepositoryModule import com.stripe.android.paymentsheet.BuildConfig import com.stripe.android.paymentsheet.DefaultPrefsRepository @@ -60,6 +65,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.plus import javax.inject.Inject import javax.inject.Named import javax.inject.Provider @@ -69,12 +75,19 @@ import kotlin.reflect.KClass @OptIn(ExperimentalEmbeddedPaymentElementApi::class) internal class SharedPaymentElementViewModel @Inject constructor( + confirmationHandlerFactory: ConfirmationHandler.Factory, + @IOContext ioContext: CoroutineContext, private val configurationHandler: EmbeddedConfigurationHandler, private val paymentOptionDisplayDataFactory: PaymentOptionDisplayDataFactory, ) : ViewModel() { private val _paymentOption: MutableStateFlow = MutableStateFlow(null) val paymentOption: StateFlow = _paymentOption.asStateFlow() + val confirmationHandler = confirmationHandlerFactory.create(viewModelScope + ioContext) + + @Volatile + var confirmationState: EmbeddedConfirmationHelper.State? = null + suspend fun configure( intentConfiguration: PaymentSheet.IntentConfiguration, configuration: EmbeddedPaymentElement.Configuration, @@ -84,6 +97,12 @@ internal class SharedPaymentElementViewModel @Inject constructor( configuration = configuration, ).fold( onSuccess = { state -> + confirmationState = EmbeddedConfirmationHelper.State( + paymentMethodMetadata = state.paymentMethodMetadata, + selection = state.paymentSelection, + initializationMode = PaymentElementLoader.InitializationMode.DeferredIntent(intentConfiguration), + configuration = configuration, + ) _paymentOption.value = paymentOptionDisplayDataFactory.create(state.paymentSelection) ConfigureResult.Succeeded() }, @@ -93,11 +112,12 @@ internal class SharedPaymentElementViewModel @Inject constructor( ) } - class Factory : ViewModelProvider.Factory { + class Factory(private val statusBarColor: Int?) : ViewModelProvider.Factory { override fun create(modelClass: KClass, extras: CreationExtras): T { val component = DaggerSharedPaymentElementViewModelComponent.builder() .savedStateHandle(extras.createSavedStateHandle()) .context(extras.requireApplication()) + .statusBarColor(statusBarColor) .build() @Suppress("UNCHECKED_CAST") return component.viewModel as T @@ -113,6 +133,7 @@ internal class SharedPaymentElementViewModel @Inject constructor( GooglePayLauncherModule::class, CoreCommonModule::class, StripeRepositoryModule::class, + PaymentElementConfirmationModule::class, ] ) internal interface SharedPaymentElementViewModelComponent { @@ -126,6 +147,9 @@ internal interface SharedPaymentElementViewModelComponent { @BindsInstance fun context(context: Context): Builder + @BindsInstance + fun statusBarColor(@Named(STATUS_BAR_COLOR) statusBarColor: Int?): Builder + fun build(): SharedPaymentElementViewModelComponent } } @@ -171,6 +195,11 @@ internal interface SharedPaymentElementViewModelModule { @Suppress("TooManyFunctions") companion object { + @Provides + @Singleton + @Named(ALLOWS_MANUAL_CONFIRMATION) + fun provideAllowsManualConfirmation() = true + @Provides fun provideEventReporterMode(): EventReporter.Mode { return EventReporter.Mode.Embedded diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/flowcontroller/FlowControllerFactory.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/flowcontroller/FlowControllerFactory.kt index c248df6ce77..5b5371e74e9 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/flowcontroller/FlowControllerFactory.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/flowcontroller/FlowControllerFactory.kt @@ -5,6 +5,7 @@ import androidx.activity.result.ActivityResultRegistryOwner import androidx.fragment.app.Fragment import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ViewModelStoreOwner +import com.stripe.android.common.ui.PaymentElementActivityResultCaller import com.stripe.android.paymentsheet.PaymentOptionCallback import com.stripe.android.paymentsheet.PaymentSheet import com.stripe.android.paymentsheet.PaymentSheetResultCallback @@ -48,7 +49,10 @@ internal class FlowControllerFactory( DefaultFlowController.getInstance( viewModelStoreOwner = viewModelStoreOwner, lifecycleOwner = lifecycleOwner, - activityResultCaller = FlowControllerActivityResultCaller(activityResultRegistryOwner), + activityResultCaller = PaymentElementActivityResultCaller( + key = "FlowController", + registryOwner = activityResultRegistryOwner, + ), statusBarColor = statusBarColor, paymentOptionCallback = paymentOptionCallback, paymentResultCallback = paymentResultCallback, diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/flowcontroller/FlowControllerActivityResultCallerTest.kt b/paymentsheet/src/test/java/com/stripe/android/common/ui/PaymentElementActivityResultCallerTest.kt similarity index 90% rename from paymentsheet/src/test/java/com/stripe/android/paymentsheet/flowcontroller/FlowControllerActivityResultCallerTest.kt rename to paymentsheet/src/test/java/com/stripe/android/common/ui/PaymentElementActivityResultCallerTest.kt index acb9ecc8f60..5312d13fc88 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/flowcontroller/FlowControllerActivityResultCallerTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/common/ui/PaymentElementActivityResultCallerTest.kt @@ -1,4 +1,4 @@ -package com.stripe.android.paymentsheet.flowcontroller +package com.stripe.android.common.ui import android.content.Context import android.content.Intent @@ -12,7 +12,7 @@ import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock import org.mockito.kotlin.verify -class FlowControllerActivityResultCallerTest { +class PaymentElementActivityResultCallerTest { @Test fun `on register callback, registry owner should register callback properly`() { val registry = mock { @@ -29,7 +29,7 @@ class FlowControllerActivityResultCallerTest { on { activityResultRegistry } doReturn registry } - val caller = FlowControllerActivityResultCaller(activityResultRegistryOwner) + val caller = PaymentElementActivityResultCaller("FlowController", activityResultRegistryOwner) val contract = TestActivityResultContract() val callback = ActivityResultCallback {} @@ -60,7 +60,7 @@ class FlowControllerActivityResultCallerTest { val activityResultRegistryOwner = mock() - val caller = FlowControllerActivityResultCaller(activityResultRegistryOwner) + val caller = PaymentElementActivityResultCaller("FlowController", activityResultRegistryOwner) val contract = TestActivityResultContract() val callback = ActivityResultCallback {} diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentelement/confirmation/FakeConfirmationHandler.kt b/paymentsheet/src/test/java/com/stripe/android/paymentelement/confirmation/FakeConfirmationHandler.kt new file mode 100644 index 00000000000..2e16b2d8143 --- /dev/null +++ b/paymentsheet/src/test/java/com/stripe/android/paymentelement/confirmation/FakeConfirmationHandler.kt @@ -0,0 +1,46 @@ +package com.stripe.android.paymentelement.confirmation + +import androidx.activity.result.ActivityResultCaller +import androidx.lifecycle.LifecycleOwner +import app.cash.turbine.Turbine +import kotlinx.coroutines.flow.MutableStateFlow + +internal class FakeConfirmationHandler( + override val hasReloadedFromProcessDeath: Boolean = false, + override val state: MutableStateFlow = MutableStateFlow(ConfirmationHandler.State.Idle) +) : ConfirmationHandler { + val registerTurbine: Turbine = Turbine() + val startTurbine: Turbine = Turbine() + val awaitResultTurbine: Turbine = Turbine(null) + + override fun register( + activityResultCaller: ActivityResultCaller, + lifecycleOwner: LifecycleOwner + ) { + registerTurbine.add( + RegisterCall( + activityResultCaller = activityResultCaller, + lifecycleOwner = lifecycleOwner, + ) + ) + } + + override fun start(arguments: ConfirmationHandler.Args) { + startTurbine.add(arguments) + } + + override suspend fun awaitResult(): ConfirmationHandler.Result? { + return awaitResultTurbine.awaitItem() + } + + fun validate() { + registerTurbine.ensureAllEventsConsumed() + startTurbine.ensureAllEventsConsumed() + awaitResultTurbine.ensureAllEventsConsumed() + } + + data class RegisterCall( + val activityResultCaller: ActivityResultCaller, + val lifecycleOwner: LifecycleOwner, + ) +} diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/EmbeddedConfirmationHelperTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/EmbeddedConfirmationHelperTest.kt new file mode 100644 index 00000000000..c25763c3ca8 --- /dev/null +++ b/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/EmbeddedConfirmationHelperTest.kt @@ -0,0 +1,128 @@ +package com.stripe.android.paymentelement.embedded + +import androidx.lifecycle.testing.TestLifecycleOwner +import app.cash.turbine.ReceiveTurbine +import app.cash.turbine.Turbine +import com.google.common.truth.Truth.assertThat +import com.stripe.android.core.strings.resolvableString +import com.stripe.android.isInstanceOf +import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodMetadataFactory +import com.stripe.android.paymentelement.EmbeddedPaymentElement +import com.stripe.android.paymentelement.ExperimentalEmbeddedPaymentElementApi +import com.stripe.android.paymentelement.confirmation.ConfirmationHandler +import com.stripe.android.paymentelement.confirmation.FakeConfirmationHandler +import com.stripe.android.paymentsheet.PaymentSheet +import com.stripe.android.paymentsheet.model.PaymentSelection +import com.stripe.android.paymentsheet.state.PaymentElementLoader +import com.stripe.android.testing.CoroutineTestRule +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.mockito.Mockito.mock +import kotlin.test.Test + +@ExperimentalEmbeddedPaymentElementApi +internal class EmbeddedConfirmationHelperTest { + @get:Rule + val coroutineTestRule = CoroutineTestRule() + + @Test + fun constructorRegistersWithConfirmationHandler() = testScenario { + assertThat(confirmationHandler.registerTurbine.awaitItem()).isNotNull() + } + + @Test + fun constructorWiresUpConfirmationHandlerToResultCallback() = testScenario { + assertThat(confirmationHandler.registerTurbine.awaitItem()).isNotNull() + + val exception = IllegalStateException("Test failure.") + confirmationHandler.state.value = ConfirmationHandler.State.Complete( + ConfirmationHandler.Result.Failed( + cause = exception, + message = "Error".resolvableString, + type = ConfirmationHandler.Result.Failed.ErrorType.Internal, + ) + ) + assertThat(resultCallbackTurbine.awaitItem()).isEqualTo( + EmbeddedPaymentElement.Result.Failed(exception) + ) + } + + @Test + fun confirmCallsResultCallbackWithFailureWhenLoadedStateIsNull() = testScenario( + loadedState = null, + ) { + assertThat(confirmationHandler.registerTurbine.awaitItem()).isNotNull() + confirmationHelper.confirm() + assertThat(resultCallbackTurbine.awaitItem()).isInstanceOf() + } + + @Test + fun confirmCallsResultCallbackWithFailureWhenNoSelection() = testScenario( + loadedState = defaultLoadedState().copy(selection = null), + ) { + assertThat(confirmationHandler.registerTurbine.awaitItem()).isNotNull() + confirmationHelper.confirm() + assertThat(resultCallbackTurbine.awaitItem()).isInstanceOf() + } + + @Test + fun confirmCallsConfirmationHandlerStart() = testScenario { + assertThat(confirmationHandler.registerTurbine.awaitItem()).isNotNull() + confirmationHelper.confirm() + assertThat(confirmationHandler.startTurbine.awaitItem()).isNotNull() + } + + private fun defaultLoadedState(): EmbeddedConfirmationHelper.State { + return EmbeddedConfirmationHelper.State( + paymentMethodMetadata = PaymentMethodMetadataFactory.create(), + selection = PaymentSelection.GooglePay, + initializationMode = PaymentElementLoader.InitializationMode.DeferredIntent( + PaymentSheet.IntentConfiguration( + mode = PaymentSheet.IntentConfiguration.Mode.Payment( + amount = 5000, + currency = "USD", + ), + ), + ), + configuration = EmbeddedPaymentElement.Configuration.Builder("Example, Inc") + .googlePay( + PaymentSheet.GooglePayConfiguration( + environment = PaymentSheet.GooglePayConfiguration.Environment.Test, + countryCode = "US", + ) + ) + .build() + ) + } + + private fun testScenario( + loadedState: EmbeddedConfirmationHelper.State? = defaultLoadedState(), + block: suspend Scenario.() -> Unit, + ) = runTest { + val resultCallbackTurbine = Turbine() + val confirmationHandler = FakeConfirmationHandler() + val confirmationHelper = EmbeddedConfirmationHelper( + confirmationHandler = confirmationHandler, + resultCallback = { + resultCallbackTurbine.add(it) + }, + activityResultCaller = mock(), + lifecycleOwner = TestLifecycleOwner(coroutineDispatcher = Dispatchers.Unconfined), + confirmationStateSupplier = { loadedState } + ) + Scenario( + confirmationHelper = confirmationHelper, + confirmationHandler = confirmationHandler, + resultCallbackTurbine = resultCallbackTurbine, + ).block() + resultCallbackTurbine.ensureAllEventsConsumed() + confirmationHandler.validate() + } + + private class Scenario( + val confirmationHelper: EmbeddedConfirmationHelper, + val confirmationHandler: FakeConfirmationHandler, + val resultCallbackTurbine: ReceiveTurbine, + ) +} 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 7f7571ff13a..251eb5b4ae9 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 @@ -10,6 +10,7 @@ import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodMetadataFact import com.stripe.android.model.PaymentIntentFixtures import com.stripe.android.paymentelement.EmbeddedPaymentElement import com.stripe.android.paymentelement.ExperimentalEmbeddedPaymentElementApi +import com.stripe.android.paymentelement.confirmation.FakeConfirmationHandler import com.stripe.android.paymentsheet.PaymentSheet import com.stripe.android.paymentsheet.model.PaymentSelection import com.stripe.android.paymentsheet.state.PaymentElementLoader @@ -24,10 +25,49 @@ import kotlin.test.Test @RunWith(RobolectricTestRunner::class) internal class SharedPaymentElementViewModelTest { + @Test + fun `configure sets initial confirmationState`() = testScenario { + val configuration = EmbeddedPaymentElement.Configuration.Builder("Example, Inc.").build() + configurationHandler.emit( + Result.success( + PaymentElementLoader.State( + config = configuration.asCommonConfiguration(), + customer = null, + linkState = null, + paymentSelection = null, + validationError = null, + paymentMethodMetadata = PaymentMethodMetadataFactory.create( + stripeIntent = PaymentIntentFixtures.PI_SUCCEEDED, + billingDetailsCollectionConfiguration = configuration + .billingDetailsCollectionConfiguration, + allowsDelayedPaymentMethods = configuration.allowsDelayedPaymentMethods, + allowsPaymentMethodsRequiringShippingAddress = configuration + .allowsPaymentMethodsRequiringShippingAddress, + isGooglePayReady = false, + cbcEligibility = CardBrandChoiceEligibility.Ineligible, + ), + ) + ) + ) + + assertThat(viewModel.confirmationState).isNull() + + assertThat( + viewModel.configure( + PaymentSheet.IntentConfiguration( + PaymentSheet.IntentConfiguration.Mode.Payment(5000, "USD"), + ), + configuration = configuration, + ) + ).isInstanceOf() + + assertThat(viewModel.confirmationState?.paymentMethodMetadata).isNotNull() + } + @Test fun `configure maps success result`() = testScenario { val configuration = EmbeddedPaymentElement.Configuration.Builder("Example, Inc.").build() - handler.emit( + configurationHandler.emit( Result.success( PaymentElementLoader.State( config = configuration.asCommonConfiguration(), @@ -65,7 +105,7 @@ internal class SharedPaymentElementViewModelTest { @Test fun `configure emits payment option`() = testScenario { val configuration = EmbeddedPaymentElement.Configuration.Builder("Example, Inc.").build() - handler.emit( + configurationHandler.emit( Result.success( PaymentElementLoader.State( config = configuration.asCommonConfiguration(), @@ -106,7 +146,7 @@ internal class SharedPaymentElementViewModelTest { @Test fun `configure maps failure result`() = testScenario { val exception = IllegalStateException("Hi") - handler.emit(Result.failure(exception)) + configurationHandler.emit(Result.failure(exception)) assertThat( viewModel.configure( PaymentSheet.IntentConfiguration( @@ -120,26 +160,33 @@ internal class SharedPaymentElementViewModelTest { private fun testScenario( block: suspend Scenario.() -> Unit, ) { + val confirmationHandler = FakeConfirmationHandler() val configurationHandler = FakeEmbeddedConfigurationHandler() val paymentOptionDisplayDataFactory = PaymentOptionDisplayDataFactory( iconLoader = mock(), context = ApplicationProvider.getApplicationContext(), ) - val viewModel = SharedPaymentElementViewModel( - configurationHandler = configurationHandler, - paymentOptionDisplayDataFactory = paymentOptionDisplayDataFactory, - ) - runTest { - Scenario(configurationHandler, viewModel).block() + val viewModel = SharedPaymentElementViewModel( + confirmationHandlerFactory = { confirmationHandler }, + ioContext = testScheduler, + configurationHandler = configurationHandler, + paymentOptionDisplayDataFactory = paymentOptionDisplayDataFactory, + ) + + Scenario( + configurationHandler = configurationHandler, + viewModel = viewModel, + ).block() } configurationHandler.turbine.ensureAllEventsConsumed() + confirmationHandler.validate() } private class Scenario( - val handler: FakeEmbeddedConfigurationHandler, + val configurationHandler: FakeEmbeddedConfigurationHandler, val viewModel: SharedPaymentElementViewModel, )