Skip to content

Commit

Permalink
Add CVC recollection to confirmation definitions (#9890)
Browse files Browse the repository at this point in the history
* Add CVC recollection to confirmation definitions

* Add tests for CVC recollection confirmation definition
  • Loading branch information
samer-stripe authored Jan 13, 2025
1 parent b67e9c7 commit 7d51a49
Show file tree
Hide file tree
Showing 26 changed files with 1,141 additions and 123 deletions.
4 changes: 1 addition & 3 deletions paymentsheet/detekt-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@
<ID>LargeClass:USBankAccountFormViewModelTest.kt$USBankAccountFormViewModelTest</ID>
<ID>LongMethod:AutocompleteScreen.kt$@Composable internal fun AutocompleteScreenUI(viewModel: AutocompleteViewModel)</ID>
<ID>LongMethod:CustomerSheetScreen.kt$@Composable internal fun SelectPaymentMethod( viewState: CustomerSheetViewState.SelectPaymentMethod, viewActionHandler: (CustomerSheetViewAction) -> Unit, paymentMethodNameProvider: (PaymentMethodCode?) -> ResolvableString, modifier: Modifier = Modifier, )</ID>
<ID>LongMethod:DefaultConfirmationHandlerTest.kt$DefaultConfirmationHandlerTest$private fun test( someDefinitionAction: ConfirmationDefinition.Action&lt;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&lt;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 )</ID>
<ID>LongMethod:EditPaymentMethod.kt$@Composable internal fun EditPaymentMethodUi( viewState: EditPaymentMethodViewState, viewActionHandler: (action: EditPaymentMethodViewAction) -> Unit, modifier: Modifier = Modifier )</ID>
<ID>LongMethod:DefaultConfirmationHandlerTest.kt$DefaultConfirmationHandlerTest$private fun test( someDefinitionAction: ConfirmationDefinition.Action&lt;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, ), someDefinitionIsConfirmable: Boolean = true, someOtherDefinitionAction: ConfirmationDefinition.Action&lt;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 )</ID>
<ID>LongMethod:EmbeddedContentHelper.kt$DefaultEmbeddedContentHelper$private fun createInteractor( coroutineScope: CoroutineScope, paymentMethodMetadata: PaymentMethodMetadata, ): PaymentMethodVerticalLayoutInteractor</ID>
<ID>LongMethod:FormViewModelTest.kt$FormViewModelTest$@Test fun `Verify params are set when element address fields are complete`()</ID>
<ID>LongMethod:FormViewModelTest.kt$FormViewModelTest$@Test fun `Verify params are set when required address fields are complete`()</ID>
Expand Down Expand Up @@ -77,7 +76,6 @@
<ID>TooManyFunctions:CustomerSheetEventReporter.kt$CustomerSheetEventReporter</ID>
<ID>TooManyFunctions:DefaultCustomerSheetEventReporter.kt$DefaultCustomerSheetEventReporter : CustomerSheetEventReporter</ID>
<ID>TooManyFunctions:DefaultEventReporter.kt$DefaultEventReporter : EventReporter</ID>
<ID>TooManyFunctions:DefaultFlowController.kt$DefaultFlowController : FlowController</ID>
<ID>TooManyFunctions:DefaultLinkEventsReporter.kt$DefaultLinkEventsReporter : LinkEventsReporter</ID>
<ID>TooManyFunctions:DelegateDrawable.kt$DelegateDrawable : Drawable</ID>
<ID>TooManyFunctions:EventReporter.kt$EventReporter</ID>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ internal interface ConfirmationDefinition<
confirmationOption: ConfirmationHandler.Option,
): TConfirmationOption?

fun canConfirm(
confirmationOption: TConfirmationOption,
confirmationParameters: Parameters,
): Boolean = true

suspend fun action(
confirmationOption: TConfirmationOption,
confirmationParameters: Parameters,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,13 @@ internal class ConfirmationMediator<

val key = definition.key

fun canConfirm(confirmationOption: ConfirmationHandler.Option): Boolean {
return definition.option(confirmationOption) != null
fun canConfirm(
confirmationOption: ConfirmationHandler.Option,
confirmationParameters: ConfirmationDefinition.Parameters,
): Boolean {
return definition.option(confirmationOption)?.let {
definition.canConfirm(it, confirmationParameters)
} ?: false
}

fun register(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,10 @@ internal class DefaultConfirmationHandler(

_state.value = ConfirmationHandler.State.Confirming(confirmationOption)

val parameters = arguments.toParameters()

val mediator = mediators.find { mediator ->
mediator.canConfirm(confirmationOption)
mediator.canConfirm(confirmationOption, parameters)
} ?: run {
errorReporter.report(
errorEvent = ErrorReporter
Expand All @@ -139,7 +141,7 @@ internal class DefaultConfirmationHandler(
return
}

when (val action = mediator.action(confirmationOption, arguments.toParameters())) {
when (val action = mediator.action(confirmationOption, parameters)) {
is ConfirmationMediator.Action.Launch -> {
storeIsAwaitingForResult(
key = mediator.key,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package com.stripe.android.paymentelement.confirmation.cvc

import androidx.activity.result.ActivityResultCaller
import com.stripe.android.model.PaymentMethodOptionsParams
import com.stripe.android.paymentelement.confirmation.ConfirmationDefinition
import com.stripe.android.paymentelement.confirmation.ConfirmationHandler
import com.stripe.android.paymentelement.confirmation.PaymentMethodConfirmationOption
import com.stripe.android.paymentelement.confirmation.intent.DeferredIntentConfirmationType
import com.stripe.android.paymentsheet.cvcrecollection.CvcRecollectionHandler
import com.stripe.android.paymentsheet.paymentdatacollection.cvcrecollection.CvcRecollectionContract
import com.stripe.android.paymentsheet.paymentdatacollection.cvcrecollection.CvcRecollectionLauncher
import com.stripe.android.paymentsheet.paymentdatacollection.cvcrecollection.CvcRecollectionLauncherFactory
import com.stripe.android.paymentsheet.paymentdatacollection.cvcrecollection.CvcRecollectionResult

internal class CvcRecollectionConfirmationDefinition(
private val handler: CvcRecollectionHandler,
private val factory: CvcRecollectionLauncherFactory,
) : ConfirmationDefinition<
PaymentMethodConfirmationOption.Saved,
CvcRecollectionLauncher,
Unit,
CvcRecollectionResult,
> {
override val key: String = "CvcRecollection"

override fun option(confirmationOption: ConfirmationHandler.Option): PaymentMethodConfirmationOption.Saved? {
return confirmationOption as? PaymentMethodConfirmationOption.Saved
}

override fun canConfirm(
confirmationOption: PaymentMethodConfirmationOption.Saved,
confirmationParameters: ConfirmationDefinition.Parameters,
): Boolean {
return handler.requiresCVCRecollection(
stripeIntent = confirmationParameters.intent,
initializationMode = confirmationParameters.initializationMode,
paymentMethod = confirmationOption.paymentMethod,
optionsParams = confirmationOption.optionsParams,
)
}

override suspend fun action(
confirmationOption: PaymentMethodConfirmationOption.Saved,
confirmationParameters: ConfirmationDefinition.Parameters
): ConfirmationDefinition.Action<Unit> {
return ConfirmationDefinition.Action.Launch(
launcherArguments = Unit,
receivesResultInProcess = true,
deferredIntentConfirmationType = null,
)
}

override fun createLauncher(
activityResultCaller: ActivityResultCaller,
onResult: (CvcRecollectionResult) -> Unit
): CvcRecollectionLauncher {
return factory.create(
activityResultLauncher = activityResultCaller.registerForActivityResult(
CvcRecollectionContract(),
onResult,
),
)
}

override fun launch(
launcher: CvcRecollectionLauncher,
arguments: Unit,
confirmationOption: PaymentMethodConfirmationOption.Saved,
confirmationParameters: ConfirmationDefinition.Parameters
) {
handler.launch(confirmationOption.paymentMethod) { recollectionData ->
launcher.launch(
data = recollectionData,
appearance = confirmationParameters.appearance,
isLiveMode = confirmationParameters.intent.isLiveMode,
)
}
}

override fun toResult(
confirmationOption: PaymentMethodConfirmationOption.Saved,
confirmationParameters: ConfirmationDefinition.Parameters,
deferredIntentConfirmationType: DeferredIntentConfirmationType?,
result: CvcRecollectionResult
): ConfirmationDefinition.Result {
return when (result) {
is CvcRecollectionResult.Confirmed -> ConfirmationDefinition.Result.NextStep(
confirmationOption = confirmationOption.copy(
optionsParams = when (val params = confirmationOption.optionsParams) {
is PaymentMethodOptionsParams.Card -> params.copy(cvc = result.cvc)
else -> PaymentMethodOptionsParams.Card(cvc = result.cvc)
}
),
parameters = confirmationParameters,
)
is CvcRecollectionResult.Cancelled -> ConfirmationDefinition.Result.Canceled(
action = ConfirmationHandler.Result.Canceled.Action.None,
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.stripe.android.paymentelement.confirmation.cvc

import com.stripe.android.paymentelement.confirmation.ConfirmationDefinition
import com.stripe.android.paymentsheet.cvcrecollection.CvcRecollectionHandler
import com.stripe.android.paymentsheet.cvcrecollection.CvcRecollectionHandlerImpl
import com.stripe.android.paymentsheet.paymentdatacollection.cvcrecollection.CvcRecollectionLauncherFactory
import com.stripe.android.paymentsheet.paymentdatacollection.cvcrecollection.DefaultCvcRecollectionLauncherFactory
import dagger.Module
import dagger.Provides
import dagger.multibindings.IntoSet

@Module
internal class CvcRecollectionConfirmationModule {
@Provides
fun provideCvcRecollectionLauncherFactory(): CvcRecollectionLauncherFactory {
return DefaultCvcRecollectionLauncherFactory
}

@Provides
fun provideCvcRecollectionHandler(): CvcRecollectionHandler {
return CvcRecollectionHandlerImpl()
}

@JvmSuppressWildcards
@Provides
@IntoSet
fun providesCvcConfirmationDefinition(
cvcRecollectionLauncherFactory: CvcRecollectionLauncherFactory,
cvcRecollectionHandler: CvcRecollectionHandler,
): ConfirmationDefinition<*, *, *, *> {
return CvcRecollectionConfirmationDefinition(cvcRecollectionHandler, cvcRecollectionLauncherFactory)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.stripe.android.paymentelement.confirmation.injection

import com.stripe.android.paymentelement.confirmation.cvc.CvcRecollectionConfirmationModule
import dagger.Module

@Module(
includes = [
CvcRecollectionConfirmationModule::class,
PaymentElementConfirmationModule::class,
]
)
internal interface ExtendedPaymentElementConfirmationModule
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import com.stripe.android.paymentelement.EmbeddedPaymentElement.PaymentOptionDis
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.paymentelement.confirmation.injection.ExtendedPaymentElementConfirmationModule
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
Expand All @@ -47,8 +47,6 @@ import com.stripe.android.paymentsheet.PaymentSheet
import com.stripe.android.paymentsheet.PrefsRepository
import com.stripe.android.paymentsheet.analytics.DefaultEventReporter
import com.stripe.android.paymentsheet.analytics.EventReporter
import com.stripe.android.paymentsheet.cvcrecollection.CvcRecollectionHandler
import com.stripe.android.paymentsheet.cvcrecollection.CvcRecollectionHandlerImpl
import com.stripe.android.paymentsheet.repositories.CustomerApiRepository
import com.stripe.android.paymentsheet.repositories.CustomerRepository
import com.stripe.android.paymentsheet.repositories.ElementsSessionRepository
Expand Down Expand Up @@ -165,7 +163,7 @@ internal class SharedPaymentElementViewModel @Inject constructor(
GooglePayLauncherModule::class,
CoreCommonModule::class,
StripeRepositoryModule::class,
PaymentElementConfirmationModule::class,
ExtendedPaymentElementConfirmationModule::class,
]
)
internal interface SharedPaymentElementViewModelComponent {
Expand Down Expand Up @@ -292,11 +290,6 @@ internal interface SharedPaymentElementViewModelModule {
)
}

@Provides
fun provideCVCRecollectionHandler(): CvcRecollectionHandler {
return CvcRecollectionHandlerImpl()
}

@Provides
fun provideDurationProvider(): DurationProvider {
return DefaultDurationProvider.instance
Expand Down
Loading

0 comments on commit 7d51a49

Please sign in to comment.