Skip to content

Simplify LinkHandler & source of truth for Link enablement #10858

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package com.stripe.android.paymentelement.embedded.content

import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodMetadata
import com.stripe.android.lpmfoundations.paymentmethod.WalletType
import com.stripe.android.model.SetupIntent
import com.stripe.android.paymentsheet.LinkHandler
import com.stripe.android.paymentsheet.model.GooglePayButtonType
import com.stripe.android.paymentsheet.state.WalletsState
import com.stripe.android.uicore.utils.combineAsStateFlow
import com.stripe.android.uicore.utils.mapAsStateFlow
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Inject

Expand All @@ -19,14 +20,11 @@ internal class DefaultEmbeddedWalletsHelper @Inject constructor(
override fun walletsState(paymentMethodMetadata: PaymentMethodMetadata): StateFlow<WalletsState?> {
linkHandler.setupLink(paymentMethodMetadata.linkState)

return combineAsStateFlow(
linkHandler.isLinkEnabled,
linkHandler.linkConfigurationCoordinator.emailFlow,
) { isLinkAvailable, linkEmail ->
return linkHandler.linkConfigurationCoordinator.emailFlow.mapAsStateFlow { linkEmail ->
WalletsState.create(
isLinkAvailable = isLinkAvailable,
isLinkAvailable = paymentMethodMetadata.availableWallets.contains(WalletType.Link),
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has the same condition as isLinkEnabled would after setupLink (linkState != null)

linkEmail = linkEmail,
isGooglePayReady = paymentMethodMetadata.isGooglePayReady == true,
isGooglePayReady = paymentMethodMetadata.isGooglePayReady,
buttonsEnabled = true,
paymentMethodTypes = paymentMethodMetadata.supportedPaymentMethodTypes(),
googlePayLauncherConfig = null, // This isn't used for embedded.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import com.stripe.android.paymentsheet.state.LinkState
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
import javax.inject.Singleton
Expand All @@ -17,18 +15,12 @@ import javax.inject.Singleton
internal class LinkHandler @Inject constructor(
val linkConfigurationCoordinator: LinkConfigurationCoordinator,
) {
private val _isLinkEnabled = MutableStateFlow<Boolean?>(null)
val isLinkEnabled: StateFlow<Boolean?> = _isLinkEnabled

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

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

if (state == null) return

_linkConfiguration.value = state.configuration
linkConfiguration.value = state.configuration
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By doing this, we effectively making this handler a logout manager outside of setting up for eager launch. I think there's an opportunity to move Link setup into PaymentElementLoader. From there, we indicate to the individual products if a session is eligible for eager launch through PaymentMethodMetadata then each individual product can decide if they want to launch into it (in this case only PaymentSheet for now)


suspend fun setupLinkWithEagerLaunch(state: LinkState?): Boolean {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import com.stripe.android.link.LinkLaunchMode
import com.stripe.android.link.LinkPaymentLauncher
import com.stripe.android.link.domain.LinkProminenceFeatureProvider
import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodMetadata
import com.stripe.android.lpmfoundations.paymentmethod.WalletType
import com.stripe.android.model.PaymentIntent
import com.stripe.android.model.SetupIntent
import com.stripe.android.paymentsheet.analytics.EventReporter
Expand Down Expand Up @@ -92,13 +93,12 @@ internal class PaymentOptionsViewModel @Inject constructor(
override val walletsProcessingState: StateFlow<WalletsProcessingState?> = MutableStateFlow(null).asStateFlow()

override val walletsState: StateFlow<WalletsState?> = combineAsStateFlow(
linkHandler.isLinkEnabled,
linkHandler.linkConfigurationCoordinator.emailFlow,
buttonsEnabled,
) { isLinkAvailable, linkEmail, buttonsEnabled ->
) { linkEmail, buttonsEnabled ->
val paymentMethodMetadata = args.state.paymentMethodMetadata
WalletsState.create(
isLinkAvailable = isLinkAvailable,
isLinkAvailable = paymentMethodMetadata.availableWallets.contains(WalletType.Link),
linkEmail = linkEmail,
isGooglePayReady = paymentMethodMetadata.isGooglePayReady,
buttonsEnabled = buttonsEnabled,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import com.stripe.android.core.utils.requireApplication
import com.stripe.android.googlepaylauncher.GooglePayEnvironment
import com.stripe.android.googlepaylauncher.GooglePayPaymentMethodLauncher
import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodMetadata
import com.stripe.android.lpmfoundations.paymentmethod.WalletType
import com.stripe.android.model.PaymentMethodOptionsParams
import com.stripe.android.model.SetupIntent
import com.stripe.android.model.StripeIntent
Expand Down Expand Up @@ -169,13 +170,12 @@ internal class PaymentSheetViewModel @Inject internal constructor(
override val error: StateFlow<ResolvableString?> = buyButtonState.mapAsStateFlow { it?.errorMessage?.message }

override val walletsState: StateFlow<WalletsState?> = combineAsStateFlow(
linkHandler.isLinkEnabled,
linkHandler.linkConfigurationCoordinator.emailFlow,
buttonsEnabled,
paymentMethodMetadata,
) { isLinkAvailable, linkEmail, buttonsEnabled, paymentMethodMetadata ->
) { linkEmail, buttonsEnabled, paymentMethodMetadata ->
WalletsState.create(
isLinkAvailable = isLinkAvailable,
isLinkAvailable = paymentMethodMetadata?.availableWallets?.contains(WalletType.Link),
linkEmail = linkEmail,
isGooglePayReady = paymentMethodMetadata?.isGooglePayReady == true,
buttonsEnabled = buttonsEnabled,
Expand Down Expand Up @@ -454,7 +454,7 @@ internal class PaymentSheetViewModel @Inject internal constructor(
paymentSelectionWithCvcIfEnabled(paymentSelection)
?.toConfirmationOption(
configuration = config.asCommonConfiguration(),
linkConfiguration = linkHandler.linkConfiguration.value,
linkConfiguration = paymentMethodMetadata.value?.linkState?.configuration,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.stripe.android.core.strings.ResolvableString
import com.stripe.android.core.strings.orEmpty
import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodMetadata
import com.stripe.android.lpmfoundations.paymentmethod.PaymentSheetCardBrandFilter
import com.stripe.android.lpmfoundations.paymentmethod.WalletType
import com.stripe.android.model.PaymentMethod
import com.stripe.android.model.PaymentMethodUpdateParams
import com.stripe.android.paymentsheet.analytics.EventReporter
Expand Down Expand Up @@ -450,7 +451,9 @@ internal class SavedPaymentMethodMutator(
setDefaultPaymentMethodExecutor = setDefaultPaymentMethodExecutor,
)
},
isLinkEnabled = viewModel.linkHandler.isLinkEnabled,
isLinkEnabled = viewModel.paymentMethodMetadata.mapAsStateFlow {
it?.availableWallets?.contains(WalletType.Link)
},
isNotPaymentFlow = !viewModel.isCompleteFlow,
).apply {
viewModel.viewModelScope.launch {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import com.stripe.android.paymentsheet.state.LinkState
import com.stripe.android.paymentsheet.viewmodels.BaseSheetViewModel.Companion.SAVE_SELECTION
import com.stripe.android.testing.PaymentIntentFactory
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Test
Expand All @@ -42,7 +41,6 @@ class LinkHandlerTest {
signupMode = LinkSignupMode.InsteadOfSaveForFutureUse
)
)
assertThat(handler.isLinkEnabled.first()).isTrue()
assertThat(savedStateHandle.get<PaymentSelection>(SAVE_SELECTION)).isNull()
}

Expand All @@ -55,7 +53,6 @@ class LinkHandlerTest {
signupMode = LinkSignupMode.AlongsideSaveForFutureUse
)
)
assertThat(handler.isLinkEnabled.first()).isTrue()
assertThat(savedStateHandle.get<PaymentSelection>(SAVE_SELECTION)).isNull()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import com.stripe.android.link.ui.inline.LinkSignupMode
import com.stripe.android.link.ui.inline.SignUpConsentAction
import com.stripe.android.link.ui.inline.UserInput
import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodMetadataFactory
import com.stripe.android.lpmfoundations.paymentmethod.WalletType
import com.stripe.android.model.CardBrand
import com.stripe.android.model.PaymentIntentFixtures
import com.stripe.android.model.PaymentMethod
Expand Down Expand Up @@ -321,15 +322,18 @@ internal class PaymentOptionsViewModelTest {
@Test
fun `Does not select Link when user is logged out of their Link account`() = runTest {
val viewModel = createViewModel(
linkState = LinkState(
configuration = mock(),
signupMode = null,
loginState = LinkState.LoginState.LoggedOut,
args = PAYMENT_OPTION_CONTRACT_ARGS.updateState(
availableWallets = listOf(WalletType.Link),
linkState = LinkState(
configuration = mock(),
signupMode = null,
loginState = LinkState.LoginState.LoggedOut,
)
),
)

assertThat(viewModel.selection.value).isNotEqualTo(PaymentSelection.Link())
assertThat(viewModel.linkHandler.isLinkEnabled.value).isTrue()
assertThat(viewModel.paymentMethodMetadata.value?.availableWallets).contains(WalletType.Link)
}

@Test
Expand All @@ -339,7 +343,7 @@ internal class PaymentOptionsViewModelTest {
)

assertThat(viewModel.selection.value).isNotEqualTo(PaymentSelection.Link())
assertThat(viewModel.linkHandler.isLinkEnabled.value).isFalse()
assertThat(viewModel.paymentMethodMetadata.value?.availableWallets).doesNotContain(WalletType.Link)
}

@Test
Expand Down Expand Up @@ -830,11 +834,7 @@ internal class PaymentOptionsViewModelTest {
@Test
fun `Displays Link wallet button if customer has not saved PMs and Google Pay is not available`() = runTest {
val args = PAYMENT_OPTION_CONTRACT_ARGS.updateState(
linkState = LinkState(
configuration = mock(),
signupMode = null,
loginState = LinkState.LoginState.NeedsVerification,
),
availableWallets = listOf(WalletType.Link),
isGooglePayReady = false,
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import com.stripe.android.link.LinkActivityResult
import com.stripe.android.link.LinkPaymentLauncher
import com.stripe.android.link.model.AccountStatus
import com.stripe.android.link.ui.LinkButtonTestTag
import com.stripe.android.lpmfoundations.paymentmethod.WalletType
import com.stripe.android.model.CardBrand
import com.stripe.android.model.PaymentIntent
import com.stripe.android.model.PaymentIntentFixtures
Expand Down Expand Up @@ -222,7 +223,10 @@ internal class PaymentSheetActivityTest {

@Test
fun `link button should not be enabled when editing`() {
val viewModel = createViewModel(isLinkAvailable = true)
val viewModel = createViewModel(
availableWallets = listOf(WalletType.Link),
isLinkAvailable = true,
)
val scenario = activityScenario(viewModel)

scenario.launch(intent).onActivity {
Expand All @@ -240,7 +244,10 @@ internal class PaymentSheetActivityTest {

@Test
fun `link button should not be enabled when processing`() {
val viewModel = createViewModel(isLinkAvailable = true)
val viewModel = createViewModel(
availableWallets = listOf(WalletType.Link),
isLinkAvailable = true
)
val scenario = activityScenario(viewModel)

scenario.launch(intent).onActivity { activity ->
Expand Down Expand Up @@ -312,7 +319,10 @@ internal class PaymentSheetActivityTest {

@Test
fun `Errors are cleared when checking out with Link`() {
val viewModel = createViewModel(isLinkAvailable = true)
val viewModel = createViewModel(
availableWallets = listOf(WalletType.Link),
isLinkAvailable = true,
)
val scenario = activityScenario(viewModel)

scenario.launch(intent).onActivity {
Expand Down Expand Up @@ -731,7 +741,11 @@ internal class PaymentSheetActivityTest {
@Test
fun `link flow updates the payment sheet before and after`() = runTest {
RecordingLinkPaymentLauncher.test {
val viewModel = createViewModel(isLinkAvailable = true, linkPaymentLauncher = launcher)
val viewModel = createViewModel(
availableWallets = listOf(WalletType.Link),
isLinkAvailable = true,
linkPaymentLauncher = launcher
)
val scenario = activityScenario(viewModel)

scenario.launch(intent).onActivity {
Expand Down Expand Up @@ -1213,6 +1227,7 @@ internal class PaymentSheetActivityTest {
paymentIntent: PaymentIntent = PAYMENT_INTENT,
paymentMethods: List<PaymentMethod> = PAYMENT_METHODS,
loadDelay: Duration = Duration.ZERO,
availableWallets: List<WalletType> = emptyList(),
isGooglePayAvailable: Boolean = false,
isLinkAvailable: Boolean = false,
linkPaymentLauncher: LinkPaymentLauncher = RecordingLinkPaymentLauncher.noOp(),
Expand All @@ -1236,6 +1251,7 @@ internal class PaymentSheetActivityTest {
stripeIntent = paymentIntent,
customer = PaymentSheetFixtures.EMPTY_CUSTOMER_STATE.copy(paymentMethods = paymentMethods),
isGooglePayAvailable = isGooglePayAvailable,
availableWallets = availableWallets,
linkState = LinkState(
configuration = mock(),
loginState = LinkState.LoginState.LoggedOut,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.stripe.android.common.model.asCommonConfiguration
import com.stripe.android.core.strings.resolvableString
import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodMetadataFactory
import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodSaveConsentBehavior
import com.stripe.android.lpmfoundations.paymentmethod.WalletType
import com.stripe.android.model.PaymentMethod
import com.stripe.android.model.PaymentMethodFixtures.BILLING_DETAILS
import com.stripe.android.model.StripeIntent
Expand Down Expand Up @@ -176,6 +177,7 @@ internal object PaymentSheetFixtures {
config: PaymentSheet.Configuration = configuration,
paymentSelection: PaymentSelection? = state.paymentSelection,
linkState: LinkState? = state.paymentMethodMetadata.linkState,
availableWallets: List<WalletType> = state.paymentMethodMetadata.availableWallets,
): PaymentOptionContract.Args {
return copy(
state = state.copy(
Expand All @@ -189,6 +191,7 @@ internal object PaymentSheetFixtures {
config = config.asCommonConfiguration(),
paymentSelection = paymentSelection,
paymentMethodMetadata = PaymentMethodMetadataFactory.create(
availableWallets = availableWallets,
stripeIntent = stripeIntent,
isGooglePayReady = isGooglePayReady,
linkState = linkState,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import com.stripe.android.link.ui.inline.SignUpConsentAction
import com.stripe.android.link.ui.inline.UserInput
import com.stripe.android.link.utils.errorMessage
import com.stripe.android.lpmfoundations.luxe.LpmRepositoryTestHelpers
import com.stripe.android.lpmfoundations.paymentmethod.WalletType
import com.stripe.android.lpmfoundations.paymentmethod.definitions.CardDefinition
import com.stripe.android.model.Address
import com.stripe.android.model.CardBrand
Expand Down Expand Up @@ -635,14 +636,15 @@ internal class PaymentSheetViewModelTest {
@Test
fun `Enables Link when user is logged out of their Link account`() = runTest {
val viewModel = createViewModel(
availableWallets = listOf(WalletType.Link),
linkState = LinkState(
configuration = mock(),
loginState = LinkState.LoginState.LoggedOut,
signupMode = null,
),
)

assertThat(viewModel.linkHandler.isLinkEnabled.value).isTrue()
assertThat(viewModel.paymentMethodMetadata.value?.availableWallets).contains(WalletType.Link)
}

@Test
Expand All @@ -651,7 +653,7 @@ internal class PaymentSheetViewModelTest {
linkState = null,
)

assertThat(viewModel.linkHandler.isLinkEnabled.value).isFalse()
assertThat(viewModel.paymentMethodMetadata.value?.availableWallets).doesNotContain(WalletType.Link)
}

@Test
Expand Down Expand Up @@ -1943,6 +1945,7 @@ internal class PaymentSheetViewModelTest {
@Test
fun `Hides Google Pay wallet button if Google Pay is not available`() = runTest {
val viewModel = createViewModel(
availableWallets = listOf(WalletType.Link),
isGooglePayReady = false,
linkState = LinkState(
configuration = mock(),
Expand All @@ -1961,6 +1964,7 @@ internal class PaymentSheetViewModelTest {
@Test
fun `Shows Link wallet button if Link is available`() = runTest {
val viewModel = createViewModel(
availableWallets = listOf(WalletType.Link),
linkState = LinkState(
configuration = mock(),
loginState = LinkState.LoginState.LoggedOut,
Expand Down Expand Up @@ -3514,6 +3518,7 @@ internal class PaymentSheetViewModelTest {
linkConfigurationCoordinator: LinkConfigurationCoordinator = this.linkConfigurationCoordinator,
customerRepository: CustomerRepository = FakeCustomerRepository(customer?.paymentMethods ?: emptyList()),
shouldFailLoad: Boolean = false,
availableWallets: List<WalletType> = emptyList(),
linkState: LinkState? = null,
isGooglePayReady: Boolean = false,
delay: Duration = Duration.ZERO,
Expand All @@ -3526,6 +3531,7 @@ internal class PaymentSheetViewModelTest {
paymentElementLoader: PaymentElementLoader = FakePaymentElementLoader(
stripeIntent = stripeIntent,
shouldFail = shouldFailLoad,
availableWallets = availableWallets,
linkState = linkState,
customer = customer,
delay = delay,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -484,7 +484,10 @@ internal class DefaultFlowControllerTest {
config = PaymentSheet.Configuration("com.stripe.android.paymentsheet.test").asCommonConfiguration(),
paymentSelection = null,
validationError = null,
paymentMethodMetadata = PaymentMethodMetadataFactory.create(allowsDelayedPaymentMethods = false),
paymentMethodMetadata = PaymentMethodMetadataFactory.create(
allowsDelayedPaymentMethods = false,
availableWallets = emptyList(),
),
),
configuration = PaymentSheet.Configuration("com.stripe.android.paymentsheet.test"),
enableLogging = ENABLE_LOGGING,
Expand Down
Loading
Loading