Skip to content

Commit

Permalink
Move Link to confirmation definition (#9747)
Browse files Browse the repository at this point in the history
* Move `Link` to confirmation definition

* Add tests & test utils for `Link` confirmation definition

* Rebase on latest changes.

* Address PR comments
  • Loading branch information
samer-stripe authored Dec 6, 2024
1 parent c1afa5d commit a43b22d
Show file tree
Hide file tree
Showing 36 changed files with 1,188 additions and 317 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,8 @@ object PaymentMethodFactory {
cardJson.put("display_brand", card?.displayBrand)
cardJson.put("brand", card?.brand?.code)
cardJson.put("last4", card?.last4)
cardJson.put("exp_month", card?.expiryMonth)
cardJson.put("exp_year", card?.expiryYear)

val networks = paymentMethod.card?.networks
val networksJson = JSONObject()
Expand All @@ -175,10 +177,12 @@ object PaymentMethodFactory {
availableJson.put(it)
}

networksJson.put("available", availableJson)
networksJson.put("preferred", networks?.preferred)
if (availableJson.length() > 0) {
networksJson.put("available", availableJson)
networksJson.put("preferred", networks?.preferred)

cardJson.put("networks", networksJson)
cardJson.put("networks", networksJson)
}

paymentMethodJson.put("card", cardJson)

Expand Down
8 changes: 8 additions & 0 deletions paymentsheet/api/paymentsheet.api
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,14 @@ public final class com/stripe/android/paymentelement/confirmation/gpay/GooglePay
public synthetic fun newArray (I)[Ljava/lang/Object;
}

public final class com/stripe/android/paymentelement/confirmation/link/LinkConfirmationOption$Creator : android/os/Parcelable$Creator {
public fun <init> ()V
public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/paymentelement/confirmation/link/LinkConfirmationOption;
public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object;
public final fun newArray (I)[Lcom/stripe/android/paymentelement/confirmation/link/LinkConfirmationOption;
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: 1 addition & 1 deletion paymentsheet/detekt-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
<ID>LargeClass:SavedPaymentMethodMutatorTest.kt$SavedPaymentMethodMutatorTest</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( initializationMode: PaymentElementLoader.InitializationMode, configuration: CommonConfiguration, appearance: PaymentSheet.Appearance, ): ConfirmationHandler.Option?</ID>
<ID>LongMethod:ConfirmationOptionKtx.kt$internal fun PaymentSelection.toConfirmationOption( initializationMode: PaymentElementLoader.InitializationMode, configuration: CommonConfiguration, appearance: PaymentSheet.Appearance, 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$@Test fun `On lifecycle destroyed, should unregister all launchers`()</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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ internal interface ConfirmationDefinition<
onResult: (TLauncherResult) -> Unit,
): TLauncher

fun unregister(
launcher: TLauncher,
) {}

fun toResult(
confirmationOption: TConfirmationOption,
deferredIntentConfirmationType: DeferredIntentConfirmationType?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ internal class ConfirmationMediator<
}

fun unregister() {
launcher?.let {
definition.unregister(it)
}

launcher = null
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package com.stripe.android.paymentelement.confirmation

import com.stripe.android.common.model.CommonConfiguration
import com.stripe.android.link.LinkConfiguration
import com.stripe.android.lpmfoundations.paymentmethod.PaymentSheetCardBrandFilter
import com.stripe.android.model.PaymentMethod
import com.stripe.android.paymentelement.confirmation.bacs.BacsConfirmationOption
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.paymentsheet.PaymentSheet
import com.stripe.android.paymentsheet.model.PaymentSelection
import com.stripe.android.paymentsheet.state.PaymentElementLoader
Expand All @@ -14,6 +16,7 @@ internal fun PaymentSelection.toConfirmationOption(
initializationMode: PaymentElementLoader.InitializationMode,
configuration: CommonConfiguration,
appearance: PaymentSheet.Appearance,
linkConfiguration: LinkConfiguration?,
): ConfirmationHandler.Option? {
return when (this) {
is PaymentSelection.Saved -> PaymentMethodConfirmationOption.Saved(
Expand Down Expand Up @@ -81,6 +84,12 @@ internal fun PaymentSelection.toConfirmationOption(
)
)
}
is PaymentSelection.Link -> null
is PaymentSelection.Link -> linkConfiguration?.let {
LinkConfirmationOption(
initializationMode = initializationMode,
shippingDetails = configuration.shippingDetails,
configuration = linkConfiguration,
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.stripe.android.paymentelement.confirmation.injection
import com.stripe.android.paymentelement.confirmation.bacs.BacsConfirmationModule
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 dagger.Module

@Module(
Expand All @@ -11,6 +12,7 @@ import dagger.Module
BacsConfirmationModule::class,
ExternalPaymentMethodConfirmationModule::class,
GooglePayConfirmationModule::class,
LinkConfirmationModule::class,
]
)
internal interface PaymentElementConfirmationModule
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package com.stripe.android.paymentelement.confirmation.link

import androidx.activity.result.ActivityResultCaller
import com.stripe.android.common.exception.stripeErrorMessage
import com.stripe.android.link.LinkActivityResult
import com.stripe.android.link.LinkPaymentLauncher
import com.stripe.android.link.account.LinkStore
import com.stripe.android.model.StripeIntent
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

internal class LinkConfirmationDefinition(
private val linkPaymentLauncher: LinkPaymentLauncher,
private val linkStore: LinkStore,
) : ConfirmationDefinition<LinkConfirmationOption, LinkPaymentLauncher, Unit, LinkActivityResult> {
override val key: String = "Link"

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

override fun createLauncher(
activityResultCaller: ActivityResultCaller,
onResult: (LinkActivityResult) -> Unit
): LinkPaymentLauncher {
return linkPaymentLauncher.apply {
register(activityResultCaller, onResult)
}
}

override fun unregister(launcher: LinkPaymentLauncher) {
launcher.unregister()
}

override suspend fun action(
confirmationOption: LinkConfirmationOption,
intent: StripeIntent
): ConfirmationDefinition.Action<Unit> {
return ConfirmationDefinition.Action.Launch(
launcherArguments = Unit,
receivesResultInProcess = false,
deferredIntentConfirmationType = null,
)
}

override fun launch(
launcher: LinkPaymentLauncher,
arguments: Unit,
confirmationOption: LinkConfirmationOption,
intent: StripeIntent
) {
launcher.present(confirmationOption.configuration)
}

override fun toResult(
confirmationOption: LinkConfirmationOption,
deferredIntentConfirmationType: DeferredIntentConfirmationType?,
intent: StripeIntent,
result: LinkActivityResult
): ConfirmationDefinition.Result {
if (
result !is LinkActivityResult.Canceled ||
result.reason != LinkActivityResult.Canceled.Reason.BackPressed
) {
linkStore.markLinkAsUsed()
}

return when (result) {
is LinkActivityResult.Completed -> ConfirmationDefinition.Result.NextStep(
confirmationOption = PaymentMethodConfirmationOption.Saved(
initializationMode = confirmationOption.initializationMode,
shippingDetails = confirmationOption.shippingDetails,
paymentMethod = result.paymentMethod,
optionsParams = null,
),
intent = intent,
)
is LinkActivityResult.Failed -> ConfirmationDefinition.Result.Failed(
cause = result.error,
message = result.error.stripeErrorMessage(),
type = ConfirmationHandler.Result.Failed.ErrorType.Payment,
)
is LinkActivityResult.Canceled -> ConfirmationDefinition.Result.Canceled(
action = ConfirmationHandler.Result.Canceled.Action.InformCancellation,
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.stripe.android.paymentelement.confirmation.link

import com.stripe.android.link.LinkPaymentLauncher
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 LinkConfirmationModule {
@JvmSuppressWildcards
@Provides
@IntoSet
fun providesLinkConfirmationDefinition(
linkStore: LinkStore,
linkPaymentLauncher: LinkPaymentLauncher,
): ConfirmationDefinition<*, *, *, *> {
return LinkConfirmationDefinition(
linkStore = linkStore,
linkPaymentLauncher = linkPaymentLauncher,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.stripe.android.paymentelement.confirmation.link

import com.stripe.android.link.LinkConfiguration
import com.stripe.android.paymentelement.confirmation.ConfirmationHandler
import com.stripe.android.paymentsheet.addresselement.AddressDetails
import com.stripe.android.paymentsheet.state.PaymentElementLoader
import kotlinx.parcelize.Parcelize

@Parcelize
internal data class LinkConfirmationOption(
val initializationMode: PaymentElementLoader.InitializationMode,
val shippingDetails: AddressDetails?,
val configuration: LinkConfiguration,
) : ConfirmationHandler.Option
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ internal class EmbeddedConfirmationHelper(
initializationMode = loadedState.initializationMode,
configuration = loadedState.configuration.asCommonConfiguration(),
appearance = loadedState.configuration.appearance,
linkConfiguration = null,
) ?: return null

return ConfirmationHandler.Args(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
package com.stripe.android.paymentsheet

import androidx.activity.result.ActivityResultCaller
import androidx.lifecycle.SavedStateHandle
import com.stripe.android.link.LinkActivityResult
import com.stripe.android.link.LinkConfiguration
import com.stripe.android.link.LinkConfigurationCoordinator
import com.stripe.android.link.LinkPaymentDetails
import com.stripe.android.link.LinkPaymentLauncher
import com.stripe.android.link.account.LinkStore
import com.stripe.android.link.analytics.LinkAnalyticsHelper
import com.stripe.android.link.injection.LinkAnalyticsComponent
Expand All @@ -17,7 +14,6 @@ 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.payments.paymentlauncher.PaymentResult
import com.stripe.android.paymentsheet.model.PaymentSelection
import com.stripe.android.paymentsheet.state.LinkState
import com.stripe.android.paymentsheet.viewmodels.BaseSheetViewModel.Companion.SAVE_PROCESSING
Expand All @@ -33,7 +29,6 @@ import kotlinx.coroutines.launch
import javax.inject.Inject

internal class LinkHandler @Inject constructor(
private val linkLauncher: LinkPaymentLauncher,
val linkConfigurationCoordinator: LinkConfigurationCoordinator,
private val savedStateHandle: SavedStateHandle,
private val linkStore: LinkStore,
Expand All @@ -42,22 +37,12 @@ internal class LinkHandler @Inject constructor(
sealed class ProcessingState {
data object Ready : ProcessingState()

data object Launched : ProcessingState()

data object Started : ProcessingState()

data class PaymentDetailsCollected(
val paymentSelection: PaymentSelection?
) : ProcessingState()

data class Error(val message: String?) : ProcessingState()

data object Cancelled : ProcessingState()

data class PaymentMethodCollected(val paymentMethod: PaymentMethod) : ProcessingState()

data class CompletedWithPaymentResult(val result: PaymentResult) : ProcessingState()

data object CompleteWithoutLink : ProcessingState()
}

Expand All @@ -69,23 +54,12 @@ internal class LinkHandler @Inject constructor(
val isLinkEnabled: StateFlow<Boolean?> = _isLinkEnabled

private val _linkConfiguration = MutableStateFlow<LinkConfiguration?>(null)
private val linkConfiguration: StateFlow<LinkConfiguration?> = _linkConfiguration.asStateFlow()
val linkConfiguration: StateFlow<LinkConfiguration?> = _linkConfiguration.asStateFlow()

private val linkAnalyticsHelper: LinkAnalyticsHelper by lazy {
linkAnalyticsComponentBuilder.build().linkAnalyticsHelper
}

fun registerFromActivity(activityResultCaller: ActivityResultCaller) {
linkLauncher.register(
activityResultCaller,
::onLinkActivityResult,
)
}

fun unregisterFromActivity() {
linkLauncher.unregister()
}

fun setupLink(state: LinkState?) {
_isLinkEnabled.value = state != null

Expand Down Expand Up @@ -198,38 +172,6 @@ internal class LinkHandler @Inject constructor(
}
}

fun launchLink() {
val config = _linkConfiguration.value ?: return

linkLauncher.present(
config,
)

_processingState.tryEmit(ProcessingState.Launched)
}

/**
* Method called with the result of launching the Link UI to collect a payment.
*/
fun onLinkActivityResult(result: LinkActivityResult) {
val paymentMethod = (result as? LinkActivityResult.Completed)?.paymentMethod
val cancelPaymentFlow = result is LinkActivityResult.Canceled &&
result.reason == LinkActivityResult.Canceled.Reason.BackPressed

if (paymentMethod != null) {
// If payment was completed inside the Link UI, dismiss immediately.
_processingState.tryEmit(ProcessingState.PaymentMethodCollected(paymentMethod))
linkStore.markLinkAsUsed()
} else if (cancelPaymentFlow) {
// We launched the user straight into Link, but they decided to exit out of it.
_processingState.tryEmit(ProcessingState.Cancelled)
} else {
val paymentResult = result.convertToPaymentResult()
_processingState.tryEmit(ProcessingState.CompletedWithPaymentResult(paymentResult))
linkStore.markLinkAsUsed()
}
}

@OptIn(DelicateCoroutinesApi::class)
fun logOut() {
val configuration = linkConfiguration.value ?: return
Expand All @@ -239,11 +181,4 @@ internal class LinkHandler @Inject constructor(
linkConfigurationCoordinator.logOut(configuration = configuration)
}
}

private fun LinkActivityResult.convertToPaymentResult() =
when (this) {
is LinkActivityResult.Completed -> PaymentResult.Completed
is LinkActivityResult.Canceled -> PaymentResult.Canceled
is LinkActivityResult.Failed -> PaymentResult.Failed(error)
}
}
Loading

0 comments on commit a43b22d

Please sign in to comment.