Skip to content

Commit

Permalink
Refactor validation errors from Payment- & CustomerSheetLoader
Browse files Browse the repository at this point in the history
  • Loading branch information
tillh-stripe committed Feb 5, 2024
1 parent 4ca235c commit 2ce55b3
Show file tree
Hide file tree
Showing 19 changed files with 79 additions and 52 deletions.
5 changes: 1 addition & 4 deletions paymentsheet/detekt-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>CyclomaticComplexMethod:CustomerSheetViewModel.kt$CustomerSheetViewModel$fun handleViewAction(viewAction: CustomerSheetViewAction)</ID>
<ID>CyclomaticComplexMethod:LpmRepository.kt$LpmRepository$private fun convertToSupportedPaymentMethod( stripeIntent: StripeIntent, sharedDataSpec: SharedDataSpec, billingDetailsCollectionConfiguration: BillingDetailsCollectionConfiguration, isDeferred: Boolean = false, )</ID>
<ID>CyclomaticComplexMethod:PlaceholderHelper.kt$PlaceholderHelper$@VisibleForTesting internal fun specForPlaceholderField( field: PlaceholderField, placeholderOverrideList: List&lt;IdentifierSpec>, requiresMandate: Boolean, configuration: PaymentSheet.BillingDetailsCollectionConfiguration, )</ID>
<ID>EmptyFunctionBlock:PrimaryButtonAnimator.kt$PrimaryButtonAnimator.&lt;no name provided>${ }</ID>
<ID>ForbiddenComment:PaymentOptionFactory.kt$PaymentOptionFactory$// TODO: Should use labelResource paymentMethodCreateParams or extension function</ID>
Expand Down Expand Up @@ -31,11 +30,10 @@
<ID>LongMethod:EditPaymentMethod.kt$@Composable internal fun EditPaymentMethodUi( viewState: EditPaymentMethodViewState, viewActionHandler: (action: EditPaymentMethodViewAction) -> Unit, modifier: Modifier = Modifier )</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>
<ID>LongMethod:LpmRepository.kt$LpmRepository$private fun convertToSupportedPaymentMethod( stripeIntent: StripeIntent, sharedDataSpec: SharedDataSpec, billingDetailsCollectionConfiguration: BillingDetailsCollectionConfiguration, isDeferred: Boolean = false, )</ID>
<ID>LongMethod:PaymentElement.kt$@Composable internal fun PaymentElement( formViewModelSubComponentBuilderProvider: Provider&lt;FormViewModelSubcomponent.Builder>, enabled: Boolean, supportedPaymentMethods: List&lt;SupportedPaymentMethod>, selectedItem: SupportedPaymentMethod, linkSignupMode: LinkSignupMode?, linkConfigurationCoordinator: LinkConfigurationCoordinator?, showCheckboxFlow: Flow&lt;Boolean>, onItemSelectedListener: (SupportedPaymentMethod) -> Unit, onLinkSignupStateChanged: (LinkConfiguration, InlineSignupViewState) -> Unit, formArguments: FormArguments, usBankAccountFormArguments: USBankAccountFormArguments, onFormFieldValuesChanged: (FormFieldValues?) -> Unit, )</ID>
<ID>LongMethod:PaymentOptionFactory.kt$PaymentOptionFactory$fun create(selection: PaymentSelection): PaymentOption</ID>
<ID>LongMethod:PaymentSheetConfigurationKtx.kt$internal fun PaymentSheet.Appearance.parseAppearance()</ID>
<ID>LongMethod:PaymentSheetLoader.kt$DefaultPaymentSheetLoader$private suspend fun create( elementsSession: ElementsSession, config: PaymentSheet.Configuration, isGooglePayReady: Boolean, ): PaymentSheetState.Full</ID>
<ID>LongMethod:PaymentSheetLoader.kt$DefaultPaymentSheetLoader$private suspend fun create( elementsSession: ElementsSession, config: PaymentSheet.Configuration, ): PaymentSheetState.Full</ID>
<ID>LongMethod:PaymentSheetScreen.kt$@Composable internal fun PaymentSheetScreenContent( viewModel: BaseSheetViewModel, type: PaymentSheetFlowType, modifier: Modifier = Modifier, )</ID>
<ID>LongMethod:PlaceholderHelperTest.kt$PlaceholderHelperTest$@Test fun `Test correct placeholder is removed for placeholder spec`()</ID>
<ID>LongMethod:USBankAccountEmitters.kt$@Composable internal fun USBankAccountEmitters( viewModel: USBankAccountFormViewModel, usBankAccountFormArgs: USBankAccountFormArguments, )</ID>
Expand Down Expand Up @@ -69,7 +67,6 @@
<ID>MaxLineLength:FlowControllerConfigurationHandlerTest.kt$FlowControllerConfigurationHandlerTest$.</ID>
<ID>MaxLineLength:InjectableActivityScenario.kt$InjectableActivityScenario$delegate ?: throw IllegalStateException("Cannot move to state $newState since the activity hasn't been launched.")</ID>
<ID>MaxLineLength:InjectableActivityScenario.kt$InjectableActivityScenario$val d = delegate ?: throw IllegalStateException("Cannot run onActivity since the activity hasn't been launched.")</ID>
<ID>MaxLineLength:LinkHandlerTest.kt$whenever(linkConfigurationCoordinator.attachNewCardToAccount(eq(linkConfiguration), any())).thenReturn(attachNewCardToAccountResult)</ID>
<ID>MaxLineLength:LpmRepositoryTest.kt$LpmRepositoryTest$fun</ID>
<ID>MaxLineLength:PaymentSheet.kt$PaymentSheet.Address$*</ID>
<ID>MaxLineLength:PaymentSheet.kt$PaymentSheet.BillingDetails$*</ID>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import com.stripe.android.model.PaymentMethod
import com.stripe.android.payments.financialconnections.IsFinancialConnectionsAvailable
import com.stripe.android.paymentsheet.PaymentSheet
import com.stripe.android.paymentsheet.model.PaymentSelection
import com.stripe.android.paymentsheet.model.requireValidOrThrow
import com.stripe.android.paymentsheet.model.validate
import com.stripe.android.paymentsheet.repositories.ElementsSessionRepository
import com.stripe.android.paymentsheet.state.toInternal
import com.stripe.android.ui.core.cbc.CardBrandChoiceEligibility
Expand Down Expand Up @@ -60,7 +60,7 @@ internal class DefaultCustomerSheetLoader @Inject constructor(
mode = PaymentSheet.IntentConfiguration.Mode.Setup(),
)
)
return elementsSessionRepository.get(initializationMode).mapCatching { elementsSession ->
return elementsSessionRepository.get(initializationMode).onSuccess { elementsSession ->
val billingDetailsCollectionConfig = configuration?.billingDetailsCollectionConfiguration.toInternal()
val metadata = PaymentMethodMetadata(
stripeIntent = elementsSession.stripeIntent,
Expand All @@ -73,8 +73,6 @@ internal class DefaultCustomerSheetLoader @Inject constructor(
metadata = metadata,
serverLpmSpecs = elementsSession.paymentMethodSpecs,
)

elementsSession.requireValidOrThrow()
}
}

Expand Down Expand Up @@ -154,6 +152,7 @@ internal class DefaultCustomerSheetLoader @Inject constructor(
} else {
CardBrandChoiceEligibility.Ineligible
},
loadingError = elementsSession?.stripeIntent?.validate(),
)
)
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@ internal sealed interface CustomerSheetState {
val isGooglePayReady: Boolean,
val paymentSelection: PaymentSelection?,
val cbcEligibility: CardBrandChoiceEligibility,
val loadingError: Throwable?,
) : CustomerSheetState
}
Original file line number Diff line number Diff line change
Expand Up @@ -271,18 +271,24 @@ internal class CustomerSheetViewModel @Inject constructor(

result.fold(
onSuccess = { state ->
supportedPaymentMethods.clear()
supportedPaymentMethods.addAll(state.supportedPaymentMethods)
if (state.loadingError != null) {
_result.update {
InternalCustomerSheetResult.Error(state.loadingError)
}
} else {
supportedPaymentMethods.clear()
supportedPaymentMethods.addAll(state.supportedPaymentMethods)

originalPaymentSelection = state.paymentSelection
isGooglePayReadyAndEnabled = state.isGooglePayReady
stripeIntent = state.stripeIntent
originalPaymentSelection = state.paymentSelection
isGooglePayReadyAndEnabled = state.isGooglePayReady
stripeIntent = state.stripeIntent

transitionToInitialScreen(
paymentMethods = state.customerPaymentMethods,
paymentSelection = state.paymentSelection,
cbcEligibility = state.cbcEligibility,
)
transitionToInitialScreen(
paymentMethods = state.customerPaymentMethods,
paymentSelection = state.paymentSelection,
cbcEligibility = state.cbcEligibility,
)
}
},
onFailure = { cause ->
_result.update {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ import com.stripe.android.paymentsheet.state.GooglePayState
import com.stripe.android.paymentsheet.state.PaymentSheetLoader
import com.stripe.android.paymentsheet.state.PaymentSheetState
import com.stripe.android.paymentsheet.state.WalletsState
import com.stripe.android.paymentsheet.state.asPaymentSheetLoadingException
import com.stripe.android.paymentsheet.ui.HeaderTextFactory
import com.stripe.android.paymentsheet.ui.ModifiableEditPaymentMethodViewInteractor
import com.stripe.android.paymentsheet.ui.PrimaryButton
Expand Down Expand Up @@ -335,21 +334,39 @@ internal class PaymentSheetViewModel @Inject internal constructor(
)
}

private suspend fun handlePaymentSheetStateLoadFailure(error: Throwable) {
private fun handlePaymentSheetStateLoadFailure(error: Throwable) {
setStripeIntent(null)
onFatal(error)
}

private suspend fun handlePaymentSheetStateLoaded(state: PaymentSheetState.Full) {
if (state.loadingError != null) {
handlePaymentSheetStateLoadedWithInvalidIntent(
stripeIntent = state.stripeIntent,
error = state.loadingError,
)
} else {
initializeWithState(state)
}
}

private suspend fun handlePaymentSheetStateLoadedWithInvalidIntent(
stripeIntent: StripeIntent,
error: Throwable,
) {
val pendingResult = awaitPaymentResult()

if (pendingResult is InternalPaymentResult.Completed) {
// If we just received a transaction result after process death, we don't error. Instead, we dismiss
// PaymentSheet and return a `Completed` result to the caller.
val usedPaymentMethod = error.asPaymentSheetLoadingException.usedPaymentMethod
val usedPaymentMethod = stripeIntent.paymentMethod
handlePaymentCompleted(usedPaymentMethod, finishImmediately = true)
} else {
setStripeIntent(null)
onFatal(error)
handlePaymentSheetStateLoadFailure(error)
}
}

private suspend fun handlePaymentSheetStateLoaded(state: PaymentSheetState.Full) {
private suspend fun initializeWithState(state: PaymentSheetState.Full) {
cbcEligibility = when (state.isEligibleForCardBrandChoice) {
true -> CardBrandChoiceEligibility.Eligible(
preferredNetworks = state.config.preferredNetworks
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,13 @@ internal class FlowControllerConfigurationHandler @Inject constructor(

paymentSheetLoader.load(initializationMode, configuration).fold(
onSuccess = { state ->
viewModel.previousConfigureRequest = configureRequest
onInitSuccess(state, configureRequest)
onConfigured()
if (state.loadingError != null) {
onConfigured(state.loadingError)
} else {
viewModel.previousConfigureRequest = configureRequest
onInitSuccess(state, configureRequest)
onConfigured()
}
},
onFailure = { error ->
onConfigured(error = error)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.stripe.android.paymentsheet.model

import com.stripe.android.model.ElementsSession
import com.stripe.android.model.PaymentIntent
import com.stripe.android.model.PaymentIntent.ConfirmationMethod.Automatic
import com.stripe.android.model.SetupIntent
Expand All @@ -9,10 +8,10 @@ import com.stripe.android.model.StripeIntent.Status.Canceled
import com.stripe.android.model.StripeIntent.Status.RequiresCapture
import com.stripe.android.model.StripeIntent.Status.Succeeded
import com.stripe.android.paymentsheet.state.PaymentSheetLoadingException
import com.stripe.android.paymentsheet.state.asPaymentSheetLoadingException

internal fun ElementsSession.requireValidOrThrow(): ElementsSession {
StripeIntentValidator.requireValid(stripeIntent)
return this
internal fun StripeIntent.validate(): PaymentSheetLoadingException? {
return runCatching { StripeIntentValidator.requireValid(this) }.exceptionOrNull()?.asPaymentSheetLoadingException
}

/**
Expand All @@ -23,20 +22,18 @@ internal object StripeIntentValidator {
fun requireValid(
stripeIntent: StripeIntent
): StripeIntent {
val paymentMethod = stripeIntent.paymentMethod

val exception = when {
stripeIntent is PaymentIntent && stripeIntent.confirmationMethod != Automatic -> {
PaymentSheetLoadingException.InvalidConfirmationMethod(stripeIntent.confirmationMethod)
}
stripeIntent is PaymentIntent && stripeIntent.isInTerminalState -> {
PaymentSheetLoadingException.PaymentIntentInTerminalState(paymentMethod, stripeIntent.status)
PaymentSheetLoadingException.PaymentIntentInTerminalState(stripeIntent.status)
}
stripeIntent is PaymentIntent && (stripeIntent.amount == null || stripeIntent.currency == null) -> {
PaymentSheetLoadingException.MissingAmountOrCurrency
}
stripeIntent is SetupIntent && stripeIntent.isInTerminalState -> {
PaymentSheetLoadingException.SetupIntentInTerminalState(paymentMethod, stripeIntent.status)
PaymentSheetLoadingException.SetupIntentInTerminalState(stripeIntent.status)
}
else -> {
// valid
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import com.stripe.android.paymentsheet.model.currency
import com.stripe.android.paymentsheet.model.getPMAddForm
import com.stripe.android.paymentsheet.model.getPMsToAdd
import com.stripe.android.paymentsheet.model.getSupportedSavedCustomerPMs
import com.stripe.android.paymentsheet.model.requireValidOrThrow
import com.stripe.android.paymentsheet.model.validate
import com.stripe.android.paymentsheet.repositories.CustomerRepository
import com.stripe.android.paymentsheet.repositories.ElementsSessionRepository
import com.stripe.android.ui.core.BillingDetailsCollectionConfiguration
Expand Down Expand Up @@ -173,6 +173,7 @@ internal class DefaultPaymentSheetLoader @Inject constructor(
linkState = linkState.await(),
isEligibleForCardBrandChoice = elementsSession.isEligibleForCardBrandChoice,
paymentSelection = initialPaymentSelection.await(),
loadingError = stripeIntent.validate(),
)
} else {
val requested = stripeIntent.paymentMethodTypes.joinToString(separator = ", ")
Expand Down Expand Up @@ -212,7 +213,7 @@ internal class DefaultPaymentSheetLoader @Inject constructor(
initializationMode: PaymentSheet.InitializationMode,
configuration: PaymentSheet.Configuration,
): Result<ElementsSession> {
return elementsSessionRepository.get(initializationMode).mapCatching { elementsSession ->
return elementsSessionRepository.get(initializationMode).onSuccess { elementsSession ->
val billingDetailsCollectionConfig =
configuration.billingDetailsCollectionConfiguration.toInternal()

Expand All @@ -230,8 +231,6 @@ internal class DefaultPaymentSheetLoader @Inject constructor(
if (!didParseServerResponse) {
eventReporter.onLpmSpecFailure()
}

elementsSession.requireValidOrThrow()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,17 @@ package com.stripe.android.paymentsheet.state

import com.stripe.android.core.exception.StripeException
import com.stripe.android.model.PaymentIntent
import com.stripe.android.model.PaymentMethod
import com.stripe.android.model.StripeIntent
import com.stripe.android.paymentsheet.state.PaymentSheetLoadingException.Unknown

internal sealed class PaymentSheetLoadingException : Throwable() {

abstract val type: String
abstract val usedPaymentMethod: PaymentMethod?

data class InvalidConfirmationMethod(
private val confirmationMethod: PaymentIntent.ConfirmationMethod,
) : PaymentSheetLoadingException() {

override val usedPaymentMethod: PaymentMethod? = null

override val type: String = "invalidConfirmationMethod"

override val message: String = """
Expand All @@ -31,8 +27,6 @@ internal sealed class PaymentSheetLoadingException : Throwable() {
private val supported: String,
) : PaymentSheetLoadingException() {

override val usedPaymentMethod: PaymentMethod? = null

override val type: String = "noPaymentMethodTypesAvailable"

override val message: String
Expand All @@ -41,7 +35,6 @@ internal sealed class PaymentSheetLoadingException : Throwable() {
}

data class PaymentIntentInTerminalState(
override val usedPaymentMethod: PaymentMethod?,
private val status: StripeIntent.Status?,
) : PaymentSheetLoadingException() {

Expand All @@ -55,7 +48,6 @@ internal sealed class PaymentSheetLoadingException : Throwable() {
}

data class SetupIntentInTerminalState(
override val usedPaymentMethod: PaymentMethod?,
private val status: StripeIntent.Status?,
) : PaymentSheetLoadingException() {

Expand All @@ -69,7 +61,6 @@ internal sealed class PaymentSheetLoadingException : Throwable() {
}

object MissingAmountOrCurrency : PaymentSheetLoadingException() {
override val usedPaymentMethod: PaymentMethod? = null
override val type: String = "missingAmountOrCurrency"
override val message: String = "PaymentIntent must contain amount and currency."
}
Expand All @@ -82,8 +73,6 @@ internal sealed class PaymentSheetLoadingException : Throwable() {
get() = StripeException.create(cause).analyticsValue()

override val message: String? = cause.message

override val usedPaymentMethod: PaymentMethod? = null
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ internal sealed interface PaymentSheetState : Parcelable {
val linkState: LinkState?,
val isEligibleForCardBrandChoice: Boolean,
val paymentSelection: PaymentSelection?,
val loadingError: PaymentSheetLoadingException?,
) : PaymentSheetState {

val showSavedPaymentMethods: Boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ class DefaultCustomerSheetLoaderTest {
paymentMethod = PaymentMethodFixtures.CARD_PAYMENT_METHOD,
),
cbcEligibility = CardBrandChoiceEligibility.Ineligible,
loadingError = null,
)
)

Expand Down Expand Up @@ -196,6 +197,7 @@ class DefaultCustomerSheetLoaderTest {
isGooglePayReady = false,
paymentSelection = null,
cbcEligibility = CardBrandChoiceEligibility.Ineligible,
loadingError = null,
)
)
}
Expand Down Expand Up @@ -238,6 +240,7 @@ class DefaultCustomerSheetLoaderTest {
PaymentMethodFixtures.CARD_PAYMENT_METHOD.copy(id = "pm_3")
),
cbcEligibility = CardBrandChoiceEligibility.Ineligible,
loadingError = null,
)
)
}
Expand Down Expand Up @@ -276,6 +279,7 @@ class DefaultCustomerSheetLoaderTest {
isGooglePayReady = false,
paymentSelection = null,
cbcEligibility = CardBrandChoiceEligibility.Ineligible,
loadingError = null,
)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ internal class FakeCustomerSheetLoader(
isGooglePayReady = isGooglePayAvailable,
paymentSelection = paymentSelection,
cbcEligibility = cbcEligibility,
loadingError = null,
)
)
}
Expand Down
Loading

0 comments on commit 2ce55b3

Please sign in to comment.