Skip to content

Commit

Permalink
Add LinkInlineSignup confirmation definition
Browse files Browse the repository at this point in the history
  • Loading branch information
samer-stripe committed Jan 23, 2025
1 parent f237b9d commit 5729503
Show file tree
Hide file tree
Showing 35 changed files with 1,382 additions and 877 deletions.
16 changes: 16 additions & 0 deletions paymentsheet/api/paymentsheet.api
Original file line number Diff line number Diff line change
Expand Up @@ -661,6 +661,22 @@ public final class com/stripe/android/paymentelement/confirmation/link/LinkConfi
public synthetic fun newArray (I)[Ljava/lang/Object;
}

public final class com/stripe/android/paymentelement/confirmation/linkinline/LinkInlineSignupConfirmationDefinition$Result$Creator : android/os/Parcelable$Creator {
public fun <init> ()V
public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/paymentelement/confirmation/linkinline/LinkInlineSignupConfirmationDefinition$Result;
public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object;
public final fun newArray (I)[Lcom/stripe/android/paymentelement/confirmation/linkinline/LinkInlineSignupConfirmationDefinition$Result;
public synthetic fun newArray (I)[Ljava/lang/Object;
}

public final class com/stripe/android/paymentelement/confirmation/linkinline/LinkInlineSignupConfirmationOption$Creator : android/os/Parcelable$Creator {
public fun <init> ()V
public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/paymentelement/confirmation/linkinline/LinkInlineSignupConfirmationOption;
public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object;
public final fun newArray (I)[Lcom/stripe/android/paymentelement/confirmation/linkinline/LinkInlineSignupConfirmationOption;
public synthetic fun newArray (I)[Ljava/lang/Object;
}

public final class com/stripe/android/paymentelement/embedded/DefaultEmbeddedConfigurationHandler$Arguments$Creator : android/os/Parcelable$Creator {
public fun <init> ()V
public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/paymentelement/embedded/DefaultEmbeddedConfigurationHandler$Arguments;
Expand Down
2 changes: 2 additions & 0 deletions paymentsheet/detekt-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>ConstructorParameterNaming:BankFormScreenState.kt$BankFormScreenState$private val _isProcessing: Boolean = false</ID>
<ID>CyclomaticComplexMethod:ConfirmationOptionKtx.kt$internal fun PaymentSelection.toConfirmationOption( configuration: CommonConfiguration, linkConfiguration: LinkConfiguration?, ): ConfirmationHandler.Option?</ID>
<ID>CyclomaticComplexMethod:CustomerSheetViewModel.kt$CustomerSheetViewModel$fun handleViewAction(viewAction: CustomerSheetViewAction)</ID>
<ID>CyclomaticComplexMethod:PlaceholderHelper.kt$PlaceholderHelper$@VisibleForTesting internal fun specForPlaceholderField( field: PlaceholderField, placeholderOverrideList: List&lt;IdentifierSpec>, requiresMandate: Boolean, configuration: PaymentSheet.BillingDetailsCollectionConfiguration, )</ID>
<ID>CyclomaticComplexMethod:TransformSpecToElements.kt$TransformSpecToElements$fun transform( specs: List&lt;FormItemSpec>, placeholderOverrideList: List&lt;IdentifierSpec> = emptyList(), ): List&lt;FormElement></ID>
Expand All @@ -25,6 +26,7 @@
<ID>LargeClass:PaymentSheetViewModelTest.kt$PaymentSheetViewModelTest</ID>
<ID>LargeClass:USBankAccountFormViewModelTest.kt$USBankAccountFormViewModelTest</ID>
<ID>LongMethod:AutocompleteScreen.kt$@Composable internal fun AutocompleteScreenUI(viewModel: AutocompleteViewModel)</ID>
<ID>LongMethod:ConfirmationOptionKtx.kt$internal fun PaymentSelection.toConfirmationOption( configuration: CommonConfiguration, linkConfiguration: LinkConfiguration?, ): ConfirmationHandler.Option?</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, ), 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, walletsState: StateFlow&lt;WalletsState?>, ): PaymentMethodVerticalLayoutInteractor</ID>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,13 @@ internal class LinkTest {
response.testBodyFromFile("payment-intent-confirm.json")
}

networkRule.enqueue(
method("POST"),
path("/v1/consumers/sessions/log_out"),
) { response ->
response.testBodyFromFile("consumer-session-logout-success.json")
}

page.clickPrimaryButton()
}

Expand Down Expand Up @@ -721,6 +728,13 @@ internal class LinkTest {
response.testBodyFromFile("payment-intent-confirm.json")
}

networkRule.enqueue(
method("POST"),
path("/v1/consumers/sessions/log_out"),
) { response ->
response.testBodyFromFile("consumer-session-logout-success.json")
}

page.clickPrimaryButton()
}

Expand Down Expand Up @@ -784,6 +798,13 @@ internal class LinkTest {
response.testBodyFromFile("payment-intent-confirm.json")
}

networkRule.enqueue(
method("POST"),
path("/v1/consumers/sessions/log_out"),
) { response ->
response.testBodyFromFile("consumer-session-logout-success.json")
}

page.clickPrimaryButton()
}

Expand Down Expand Up @@ -832,6 +853,13 @@ internal class LinkTest {
response.testBodyFromFile("payment-intent-confirm.json")
}

networkRule.enqueue(
method("POST"),
path("/v1/consumers/sessions/log_out"),
) { response ->
response.testBodyFromFile("consumer-session-logout-success.json")
}

page.clickPrimaryButton()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,19 @@ import kotlinx.parcelize.Parcelize
/**
* Valid user input into the inline sign up view.
*/
@Parcelize
internal sealed class UserInput : Parcelable {
/**
* Represents an input that is valid for signing in to a link account.
*/
@Parcelize
data class SignIn(
val email: String
) : UserInput()

/**
* Represents an input that is valid for signing up to a link account.
*/
@Parcelize
data class SignUp(
val email: String,
val phone: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.stripe.android.paymentelement.confirmation.bacs.BacsConfirmationOptio
import com.stripe.android.paymentelement.confirmation.epms.ExternalPaymentMethodConfirmationOption
import com.stripe.android.paymentelement.confirmation.gpay.GooglePayConfirmationOption
import com.stripe.android.paymentelement.confirmation.link.LinkConfirmationOption
import com.stripe.android.paymentelement.confirmation.linkinline.LinkInlineSignupConfirmationOption
import com.stripe.android.paymentsheet.model.PaymentSelection

internal fun PaymentSelection.toConfirmationOption(
Expand Down Expand Up @@ -39,6 +40,22 @@ internal fun PaymentSelection.toConfirmationOption(
)
}
}
is PaymentSelection.New.LinkInline -> linkConfiguration?.let {
LinkInlineSignupConfirmationOption(
createParams = paymentMethodCreateParams,
optionsParams = paymentMethodOptionsParams,
userInput = input,
linkConfiguration = linkConfiguration,
saveOption = when (customerRequestedSave) {
PaymentSelection.CustomerRequestedSave.RequestReuse ->
LinkInlineSignupConfirmationOption.PaymentMethodSaveOption.RequestedReuse
PaymentSelection.CustomerRequestedSave.RequestNoReuse ->
LinkInlineSignupConfirmationOption.PaymentMethodSaveOption.RequestedNoReuse
PaymentSelection.CustomerRequestedSave.NoRequest ->
LinkInlineSignupConfirmationOption.PaymentMethodSaveOption.NoRequest
}
)
}
is PaymentSelection.New -> {
if (paymentMethodCreateParams.typeCode == PaymentMethod.Type.BacsDebit.code) {
BacsConfirmationOption(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.stripe.android.paymentelement.confirmation.bacs.BacsConfirmationModul
import com.stripe.android.paymentelement.confirmation.epms.ExternalPaymentMethodConfirmationModule
import com.stripe.android.paymentelement.confirmation.gpay.GooglePayConfirmationModule
import com.stripe.android.paymentelement.confirmation.link.LinkConfirmationModule
import com.stripe.android.paymentelement.confirmation.linkinline.LinkInlineSignupConfirmationModule
import dagger.Module

@Module(
Expand All @@ -13,6 +14,7 @@ import dagger.Module
ExternalPaymentMethodConfirmationModule::class,
GooglePayConfirmationModule::class,
LinkConfirmationModule::class,
LinkInlineSignupConfirmationModule::class,
]
)
internal interface PaymentElementConfirmationModule
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
package com.stripe.android.paymentelement.confirmation.linkinline

import android.os.Parcelable
import androidx.activity.result.ActivityResultCaller
import com.stripe.android.link.LinkConfigurationCoordinator
import com.stripe.android.link.LinkPaymentDetails
import com.stripe.android.link.account.LinkStore
import com.stripe.android.link.analytics.LinkAnalyticsHelper
import com.stripe.android.link.model.AccountStatus
import com.stripe.android.model.ConfirmPaymentIntentParams
import com.stripe.android.model.PaymentMethod
import com.stripe.android.model.PaymentMethodCreateParams
import com.stripe.android.model.PaymentMethodOptionsParams
import com.stripe.android.model.wallets.Wallet
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 kotlinx.coroutines.flow.first
import kotlinx.parcelize.Parcelize

internal class LinkInlineSignupConfirmationDefinition(
private val linkConfigurationCoordinator: LinkConfigurationCoordinator,
private val linkAnalyticsHelper: LinkAnalyticsHelper,
private val linkStore: LinkStore,
) : ConfirmationDefinition<
LinkInlineSignupConfirmationOption,
LinkInlineSignupConfirmationDefinition.Launcher,
LinkInlineSignupConfirmationDefinition.LauncherArguments,
LinkInlineSignupConfirmationDefinition.Result,
> {
override val key: String = "LinkInlineSignup"

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

override suspend fun action(
confirmationOption: LinkInlineSignupConfirmationOption,
confirmationParameters: ConfirmationDefinition.Parameters
): ConfirmationDefinition.Action<LauncherArguments> {
val nextConfirmationOption = createPaymentMethodConfirmationOption(confirmationOption)

return ConfirmationDefinition.Action.Launch(
launcherArguments = LauncherArguments(nextConfirmationOption),
receivesResultInProcess = true,
deferredIntentConfirmationType = null,
)
}

override fun createLauncher(
activityResultCaller: ActivityResultCaller,
onResult: (Result) -> Unit
): Launcher {
return Launcher(onResult)
}

override fun launch(
launcher: Launcher,
arguments: LauncherArguments,
confirmationOption: LinkInlineSignupConfirmationOption,
confirmationParameters: ConfirmationDefinition.Parameters,
) {
launcher.onResult(Result(arguments.nextConfirmationOption))
}

override fun toResult(
confirmationOption: LinkInlineSignupConfirmationOption,
confirmationParameters: ConfirmationDefinition.Parameters,
deferredIntentConfirmationType: DeferredIntentConfirmationType?,
result: Result,
): ConfirmationDefinition.Result {
return ConfirmationDefinition.Result.NextStep(
confirmationOption = result.nextConfirmationOption,
parameters = confirmationParameters,
)
}

private suspend fun createPaymentMethodConfirmationOption(
linkInlineSignupConfirmationOption: LinkInlineSignupConfirmationOption,
): PaymentMethodConfirmationOption {
val configuration = linkInlineSignupConfirmationOption.linkConfiguration
val userInput = linkInlineSignupConfirmationOption.userInput

return when (linkConfigurationCoordinator.getAccountStatusFlow(configuration).first()) {
AccountStatus.Verified -> createOptionAfterAttachingToLink(linkInlineSignupConfirmationOption)
AccountStatus.VerificationStarted,
AccountStatus.NeedsVerification -> {
linkAnalyticsHelper.onLinkPopupSkipped()

linkInlineSignupConfirmationOption.toNewOption()
}
AccountStatus.SignedOut,
AccountStatus.Error -> {
linkConfigurationCoordinator.signInWithUserInput(configuration, userInput).fold(
onSuccess = {
// If successful, the account was fetched or created, so try again
createPaymentMethodConfirmationOption(linkInlineSignupConfirmationOption)
},
onFailure = {
linkInlineSignupConfirmationOption.toNewOption()
}
)
}
}
}

private suspend fun createOptionAfterAttachingToLink(
linkInlineSignupConfirmationOption: LinkInlineSignupConfirmationOption,
): PaymentMethodConfirmationOption {
val createParams = linkInlineSignupConfirmationOption.createParams
val saveOption = linkInlineSignupConfirmationOption.saveOption

val linkPaymentDetails = linkConfigurationCoordinator.attachNewCardToAccount(
linkInlineSignupConfirmationOption.linkConfiguration,
createParams,
).getOrNull()

return when (linkPaymentDetails) {
is LinkPaymentDetails.New -> {
linkStore.markLinkAsUsed()

linkPaymentDetails.toNewOption(saveOption)
}
is LinkPaymentDetails.Saved -> {
linkStore.markLinkAsUsed()

linkPaymentDetails.toSavedOption(createParams, saveOption)
}
null -> linkInlineSignupConfirmationOption.toNewOption()
}
}

private fun LinkPaymentDetails.Saved.toSavedOption(
createParams: PaymentMethodCreateParams,
saveOption: LinkInlineSignupConfirmationOption.PaymentMethodSaveOption,
): PaymentMethodConfirmationOption.Saved {
val last4 = paymentDetails.last4

return PaymentMethodConfirmationOption.Saved(
paymentMethod = PaymentMethod.Builder()
.setId(paymentDetails.id)
.setCode(createParams.typeCode)
.setCard(
PaymentMethod.Card(
last4 = last4,
wallet = Wallet.LinkWallet(last4),
)
)
.setType(PaymentMethod.Type.Card)
.build(),
optionsParams = PaymentMethodOptionsParams.Card(
setupFutureUsage = ConfirmPaymentIntentParams.SetupFutureUsage.OffSession?.takeIf {
saveOption.shouldSave()
} ?: ConfirmPaymentIntentParams.SetupFutureUsage.Blank
),
)
}

private fun LinkPaymentDetails.New.toNewOption(
saveOption: LinkInlineSignupConfirmationOption.PaymentMethodSaveOption
): PaymentMethodConfirmationOption.New {
return PaymentMethodConfirmationOption.New(
createParams = paymentMethodCreateParams,
optionsParams = PaymentMethodOptionsParams.Card(
setupFutureUsage = saveOption.setupFutureUsage,
),
shouldSave = saveOption.shouldSave(),
)
}

private fun LinkInlineSignupConfirmationOption.toNewOption(): PaymentMethodConfirmationOption.New {
return PaymentMethodConfirmationOption.New(
createParams = createParams,
optionsParams = optionsParams,
shouldSave = saveOption.shouldSave(),
)
}

private fun LinkInlineSignupConfirmationOption.PaymentMethodSaveOption.shouldSave(): Boolean {
return this == LinkInlineSignupConfirmationOption.PaymentMethodSaveOption.RequestedReuse
}

@Parcelize
data class Result(
val nextConfirmationOption: PaymentMethodConfirmationOption,
) : Parcelable

data class LauncherArguments(
val nextConfirmationOption: PaymentMethodConfirmationOption,
)

class Launcher(
val onResult: (Result) -> Unit,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.stripe.android.paymentelement.confirmation.linkinline

import com.stripe.android.link.LinkConfigurationCoordinator
import com.stripe.android.link.account.LinkStore
import com.stripe.android.link.injection.LinkAnalyticsComponent
import com.stripe.android.paymentelement.confirmation.ConfirmationDefinition
import dagger.Module
import dagger.Provides
import dagger.multibindings.IntoSet

@Module(
subcomponents = [
LinkAnalyticsComponent::class,
]
)
internal object LinkInlineSignupConfirmationModule {
@JvmSuppressWildcards
@Provides
@IntoSet
fun providesLinkConfirmationDefinition(
linkStore: LinkStore,
linkConfigurationCoordinator: LinkConfigurationCoordinator,
linkAnalyticsComponentBuilder: LinkAnalyticsComponent.Builder,
): ConfirmationDefinition<*, *, *, *> {
return LinkInlineSignupConfirmationDefinition(
linkStore = linkStore,
linkConfigurationCoordinator = linkConfigurationCoordinator,
linkAnalyticsHelper = linkAnalyticsComponentBuilder.build().linkAnalyticsHelper,
)
}
}
Loading

0 comments on commit 5729503

Please sign in to comment.