Skip to content
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

Tolu/link confirm payment #9802

Closed
wants to merge 9 commits into from
Closed
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
@@ -48,6 +48,11 @@ internal class LinkActivity : ComponentActivity() {
}

val vm = viewModel ?: return
vm.registerFromActivity(
activityResultCaller = this,
lifecycleOwner = this,
)

setContent {
var bottomSheetContent by remember { mutableStateOf<BottomSheetContent?>(null) }
val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.stripe.android.link

import android.app.Application
import androidx.activity.result.ActivityResultCaller
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.SavedStateHandle
@@ -23,6 +24,7 @@ import com.stripe.android.paymentelement.confirmation.ConfirmationHandler
import com.stripe.android.paymentsheet.R
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import javax.inject.Inject
@@ -49,6 +51,27 @@ internal class LinkActivityViewModel @Inject constructor(
var navController: NavHostController? = null
var dismissWithResult: ((LinkActivityResult) -> Unit)? = null

init {
viewModelScope.launch {
listenToConfirmationState()
}
}

private suspend fun listenToConfirmationState() {
// confirmationHandler.state
// .filterIsInstance<ConfirmationHandler.State.Complete>()
// .collect {
// dismissWithResult?.invoke(LinkActivityResult.Completed)
// }
}

fun registerFromActivity(
activityResultCaller: ActivityResultCaller,
lifecycleOwner: LifecycleOwner,
) {
confirmationHandler.register(activityResultCaller, lifecycleOwner)
}

fun handleViewAction(action: LinkAction) {
when (action) {
LinkAction.BackPressed -> handleBackPressed()
@@ -117,6 +140,7 @@ internal class LinkActivityViewModel @Inject constructor(
.stripeAccountIdProvider { args.stripeAccountId }
.savedStateHandle(handle)
.context(app)
.savedStateHandle(handle)
.build()
.viewModel
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.stripe.android.link.injection

import com.stripe.android.link.LinkActivityViewModel
import com.stripe.android.link.account.LinkAccountManager
import com.stripe.android.paymentelement.confirmation.DefaultConfirmationHandler
import dagger.Module
import dagger.Provides

@Module
internal object LinkViewModelModule {
@Provides
@NativeLinkScope
fun provideLinkActivityViewModel(
component: NativeLinkComponent,
defaultConfirmationHandlerFactory: DefaultConfirmationHandler.Factory,
linkAccountManager: LinkAccountManager
): LinkActivityViewModel {
return LinkActivityViewModel(
component,
defaultConfirmationHandlerFactory,
linkAccountManager
)
}
}
Original file line number Diff line number Diff line change
@@ -27,6 +27,9 @@ internal annotation class NativeLinkScope
modules = [
NativeLinkModule::class,
DefaultConfirmationModule::class,
// ViewModelModule::class,
LinkViewModelModule::class,
DefaultConfirmationModule::class,
]
)
internal interface NativeLinkComponent {
Original file line number Diff line number Diff line change
@@ -161,6 +161,8 @@ internal interface NativeLinkModule {
factory: DefaultLinkConfirmationHandler.Factory
): LinkConfirmationHandler.Factory = factory

@Provides
@NativeLinkScope
fun provideEventReporterMode(): EventReporter.Mode = EventReporter.Mode.Custom
}
}
Original file line number Diff line number Diff line change
@@ -41,6 +41,7 @@ import com.stripe.android.link.theme.HorizontalPadding
import com.stripe.android.link.theme.linkColors
import com.stripe.android.link.theme.linkShapes
import com.stripe.android.link.ui.BottomSheetContent
import com.stripe.android.link.ui.ErrorText
import com.stripe.android.link.ui.PrimaryButton
import com.stripe.android.link.ui.SecondaryButton
import com.stripe.android.model.ConsumerPaymentDetails
@@ -91,17 +92,11 @@ internal fun WalletBody(
hideBottomSheetContent: () -> Unit
) {
if (state.paymentDetailsList.isEmpty()) {
Box(
modifier = Modifier
.fillMaxSize()
.testTag(WALLET_LOADER_TAG),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
Loader()
return
}

val context = LocalContext.current
val focusManager = LocalFocusManager.current

LaunchedEffect(state.isProcessing) {
@@ -133,6 +128,15 @@ internal fun WalletBody(
BankAccountTerms()
}

AnimatedVisibility(visible = state.errorMessage != null) {
ErrorText(
text = state.errorMessage?.resolve(context).orEmpty(),
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp)
)
}

Spacer(modifier = Modifier.height(16.dp))

PrimaryButton(
@@ -154,6 +158,18 @@ internal fun WalletBody(
}
}

@Composable
private fun Loader() {
Box(
modifier = Modifier
.fillMaxSize()
.testTag(WALLET_LOADER_TAG),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
}

@Composable
private fun PaymentMethodSection(
state: WalletUiState,
Original file line number Diff line number Diff line change
@@ -5,8 +5,8 @@ import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import com.stripe.android.common.exception.stripeErrorMessage
import com.stripe.android.core.Logger
import com.stripe.android.core.strings.resolvableString
import com.stripe.android.link.LinkActivityResult
import com.stripe.android.link.LinkConfiguration
import com.stripe.android.link.LinkScreen
@@ -21,6 +21,10 @@ import com.stripe.android.model.ConsumerPaymentDetails
import com.stripe.android.model.ConsumerPaymentDetailsUpdateParams
import com.stripe.android.model.PaymentMethod
import com.stripe.android.model.PaymentMethodCreateParams
import com.stripe.android.paymentelement.confirmation.ConfirmationHandler
import com.stripe.android.paymentelement.confirmation.PaymentMethodConfirmationOption
import com.stripe.android.paymentsheet.PaymentSheet
import com.stripe.android.paymentsheet.state.PaymentElementLoader
import com.stripe.android.ui.core.FieldValuesToParamsMapConverter
import com.stripe.android.ui.core.elements.CardDetailsUtil.createExpiryDateFormFieldValues
import com.stripe.android.ui.core.elements.CvcController
@@ -34,11 +38,7 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
import kotlin.Result
import kotlin.String
import kotlin.Throwable
import kotlin.Unit
import kotlin.fold
import kotlin.takeIf
import com.stripe.android.link.confirmation.Result as LinkConfirmationResult

internal class WalletViewModel @Inject constructor(
@@ -48,6 +48,7 @@ internal class WalletViewModel @Inject constructor(
private val linkConfirmationHandler: LinkConfirmationHandler,
private val logger: Logger,
private val navigate: (route: LinkScreen) -> Unit,
private val confirmationHandler: ConfirmationHandler,
private val navigateAndClearStack: (route: LinkScreen) -> Unit,
private val dismissWithResult: (LinkActivityResult) -> Unit
) : ViewModel() {
@@ -59,7 +60,8 @@ internal class WalletViewModel @Inject constructor(
selectedItem = null,
isProcessing = false,
hasCompleted = false,
primaryButtonLabel = completePaymentButtonLabel(configuration.stripeIntent)
primaryButtonLabel = completePaymentButtonLabel(configuration.stripeIntent),
errorMessage = null
)
)

@@ -151,55 +153,65 @@ internal class WalletViewModel @Inject constructor(
val card = selectedPaymentDetails as? ConsumerPaymentDetails.Card
val isExpired = card != null && card.isExpired

if (isExpired) {
performPaymentDetailsUpdate(selectedPaymentDetails).fold(
onSuccess = { result ->
val updatedPaymentDetails = result.paymentDetails.single {
it.id == selectedPaymentDetails.id
}
performPaymentConfirmation(updatedPaymentDetails)
},
onFailure = { error ->
performPaymentConfirmationWithCvc(
selectedPaymentDetails = selectedPaymentDetails,
cvc = cvcController.formFieldValue.value.takeIf { it.isComplete }?.value
)
}

private suspend fun performPaymentConfirmationWithCvc(
selectedPaymentDetails: ConsumerPaymentDetails.PaymentDetails,
cvc: String?
) {
viewModelScope.launch {
confirmationHandler.start(
arguments = ConfirmationHandler.Args(
intent = configuration.stripeIntent,
confirmationOption = PaymentMethodConfirmationOption.New(
createParams = createPaymentMethodCreateParams(
selectedPaymentDetails = selectedPaymentDetails,
linkAccount = linkAccount
),
optionsParams = null,
shouldSave = false
),
appearance = PaymentSheet.Appearance(),
initializationMode = PaymentElementLoader.InitializationMode.PaymentIntent(
clientSecret = configuration.stripeIntent.clientSecret ?: ""
),
shippingDetails = null
)
)
val result = confirmationHandler.awaitResult()
when (result) {
is ConfirmationHandler.Result.Succeeded -> {
dismissWithResult(LinkActivityResult.Completed)
}
is ConfirmationHandler.Result.Canceled -> {
dismissWithResult(LinkActivityResult.Canceled(LinkActivityResult.Canceled.Reason.BackPressed))
}
is ConfirmationHandler.Result.Failed -> {
_uiState.update {
it.copy(
alertMessage = error.stripeErrorMessage(),
errorMessage = result.message,
isProcessing = false
)
}
}
)
} else {
// Confirm payment with LinkConfirmationHandler
performPaymentConfirmationWithCvc(
selectedPaymentDetails = selectedPaymentDetails,
cvc = cvcController.formFieldValue.value.takeIf { it.isComplete }?.value
)
null -> Unit
}
}
}

private suspend fun performPaymentConfirmationWithCvc(
private fun createPaymentMethodCreateParams(
selectedPaymentDetails: ConsumerPaymentDetails.PaymentDetails,
cvc: String?
) {
val result = linkConfirmationHandler.confirm(
paymentDetails = selectedPaymentDetails,
linkAccount = linkAccount,
cvc = cvc
linkAccount: LinkAccount,
): PaymentMethodCreateParams {
return PaymentMethodCreateParams.createLink(
paymentDetailsId = selectedPaymentDetails.id,
consumerSessionClientSecret = linkAccount.clientSecret,
extraParams = emptyMap(),
)
when (result) {
LinkConfirmationResult.Canceled -> Unit
is LinkConfirmationResult.Failed -> {
_uiState.update {
it.copy(
errorMessage = result.message,
isProcessing = false
)
}
}
LinkConfirmationResult.Succeeded -> {
dismissWithResult(LinkActivityResult.Completed)
}
}
}

private suspend fun performPaymentDetailsUpdate(
@@ -248,6 +260,7 @@ internal class WalletViewModel @Inject constructor(
logger = parentComponent.logger,
linkAccount = linkAccount,
navigate = navigate,
confirmationHandler = parentComponent.viewModel.confirmationHandler,
navigateAndClearStack = navigateAndClearStack,
dismissWithResult = dismissWithResult
)
Original file line number Diff line number Diff line change
@@ -17,6 +17,7 @@ import com.stripe.android.model.PaymentMethod
import com.stripe.android.model.PaymentMethodCreateParams
import com.stripe.android.paymentsheet.PaymentSheet
import com.stripe.android.paymentsheet.paymentdatacollection.FormArguments
import com.stripe.android.paymentsheet.addresselement.AddressDetails
import com.stripe.android.ui.core.Amount
import com.stripe.android.ui.core.cbc.CardBrandChoiceEligibility
import org.mockito.kotlin.mock
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
package com.stripe.android.link.confirmation

import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth
import com.stripe.android.core.Logger
import com.stripe.android.core.strings.resolvableString
import com.stripe.android.link.LinkConfiguration
import com.stripe.android.link.TestFactory
import com.stripe.android.link.model.LinkAccount
import com.stripe.android.model.ConsumerPaymentDetails
import com.stripe.android.model.PaymentIntentFixtures
import com.stripe.android.model.PaymentMethodCreateParams
import com.stripe.android.model.SetupIntentFixtures
import com.stripe.android.paymentelement.confirmation.ConfirmationHandler
import com.stripe.android.paymentelement.confirmation.FakeConfirmationHandler
import com.stripe.android.paymentelement.confirmation.PaymentMethodConfirmationOption
import com.stripe.android.paymentsheet.PaymentSheet
import com.stripe.android.paymentsheet.R
import com.stripe.android.paymentsheet.state.PaymentElementLoader
import com.stripe.android.testing.FakeLogger
@@ -28,44 +26,7 @@ internal class DefaultLinkConfirmationHandlerTest {
val configuration = TestFactory.LINK_CONFIGURATION
val confirmationHandler = FakeConfirmationHandler()
val handler = createHandler(
confirmationHandler = confirmationHandler,
configuration = configuration
)

confirmationHandler.awaitResultTurbine.add(
item = ConfirmationHandler.Result.Succeeded(
intent = configuration.stripeIntent,
deferredIntentConfirmationType = null
)
)

val result = handler.confirm(
paymentDetails = TestFactory.CONSUMER_PAYMENT_DETAILS_CARD,
linkAccount = TestFactory.LINK_ACCOUNT,
cvc = CVC
)

assertThat(result).isEqualTo(Result.Succeeded)
confirmationHandler.startTurbine.awaitItem().assertConfirmationArgs(
configuration = configuration,
linkAccount = TestFactory.LINK_ACCOUNT,
paymentDetails = TestFactory.CONSUMER_PAYMENT_DETAILS_CARD,
cvc = CVC,
initMode = PaymentElementLoader.InitializationMode.PaymentIntent(
clientSecret = configuration.stripeIntent.clientSecret.orEmpty()
)
)
}

@Test
fun `successful confirmation yields success result with setup intent`() = runTest(dispatcher) {
val configuration = TestFactory.LINK_CONFIGURATION.copy(
stripeIntent = SetupIntentFixtures.SI_SUCCEEDED
)
val confirmationHandler = FakeConfirmationHandler()
val handler = createHandler(
confirmationHandler = confirmationHandler,
configuration = configuration
confirmationHandler = confirmationHandler
)

confirmationHandler.awaitResultTurbine.add(
@@ -80,16 +41,27 @@ internal class DefaultLinkConfirmationHandlerTest {
linkAccount = TestFactory.LINK_ACCOUNT
)

assertThat(result).isEqualTo(Result.Succeeded)
confirmationHandler.startTurbine.awaitItem().assertConfirmationArgs(
configuration = configuration,
linkAccount = TestFactory.LINK_ACCOUNT,
paymentDetails = TestFactory.CONSUMER_PAYMENT_DETAILS_CARD,
cvc = null,
initMode = PaymentElementLoader.InitializationMode.SetupIntent(
clientSecret = configuration.stripeIntent.clientSecret.orEmpty()
Truth.assertThat(result).isEqualTo(Result.Succeeded)
Truth.assertThat(confirmationHandler.startTurbine.awaitItem())
.isEqualTo(
ConfirmationHandler.Args(
intent = configuration.stripeIntent,
confirmationOption = PaymentMethodConfirmationOption.New(
createParams = PaymentMethodCreateParams.createLink(
paymentDetailsId = TestFactory.CONSUMER_PAYMENT_DETAILS_CARD.id,
consumerSessionClientSecret = TestFactory.LINK_ACCOUNT.clientSecret,
extraParams = emptyMap(),
),
optionsParams = null,
shouldSave = false
),
appearance = PaymentSheet.Appearance(),
initializationMode = PaymentElementLoader.InitializationMode.PaymentIntent(
clientSecret = configuration.stripeIntent.clientSecret ?: ""
),
shippingDetails = configuration.shippingDetails
)
)
)
}

@Test
@@ -117,8 +89,8 @@ internal class DefaultLinkConfirmationHandlerTest {
linkAccount = TestFactory.LINK_ACCOUNT
)

assertThat(result).isEqualTo(Result.Failed(errorMessage))
assertThat(logger.errorLogs)
Truth.assertThat(result).isEqualTo(Result.Failed(errorMessage))
Truth.assertThat(logger.errorLogs)
.containsExactly("DefaultLinkConfirmationHandler: Failed to confirm payment" to error)
}

@@ -138,11 +110,11 @@ internal class DefaultLinkConfirmationHandlerTest {
linkAccount = TestFactory.LINK_ACCOUNT
)

assertThat(result).isEqualTo(Result.Canceled)
Truth.assertThat(result).isEqualTo(Result.Canceled)
}

@Test
fun `null confirmation yields failed result`() = runTest(dispatcher) {
fun `null confirmation yields canceled result`() = runTest(dispatcher) {
val confirmationHandler = FakeConfirmationHandler()
val logger = FakeLogger()
val handler = createHandler(
@@ -157,8 +129,8 @@ internal class DefaultLinkConfirmationHandlerTest {
linkAccount = TestFactory.LINK_ACCOUNT
)

assertThat(result).isEqualTo(Result.Failed(R.string.stripe_something_went_wrong.resolvableString))
assertThat(logger.errorLogs)
Truth.assertThat(result).isEqualTo(Result.Failed(R.string.stripe_something_went_wrong.resolvableString))
Truth.assertThat(logger.errorLogs)
.containsExactly("DefaultLinkConfirmationHandler: Payment confirmation returned null" to null)
}

@@ -183,32 +155,10 @@ internal class DefaultLinkConfirmationHandlerTest {
linkAccount = TestFactory.LINK_ACCOUNT
)

assertThat(result).isEqualTo(Result.Failed(R.string.stripe_something_went_wrong.resolvableString))
assertThat(logger.errorLogs)
.containsExactly(
"DefaultLinkConfirmationHandler: Failed to confirm payment"
to DefaultLinkConfirmationHandler.NO_CLIENT_SECRET_FOUND
)
}

private fun ConfirmationHandler.Args.assertConfirmationArgs(
configuration: LinkConfiguration,
paymentDetails: ConsumerPaymentDetails.PaymentDetails,
linkAccount: LinkAccount,
cvc: String?,
initMode: PaymentElementLoader.InitializationMode
) {
assertThat(intent).isEqualTo(configuration.stripeIntent)
val option = confirmationOption as PaymentMethodConfirmationOption.New
assertThat(option.createParams).isEqualTo(
PaymentMethodCreateParams.createLink(
paymentDetailsId = paymentDetails.id,
consumerSessionClientSecret = linkAccount.clientSecret,
extraParams = cvc?.let { mapOf("card" to mapOf("cvc" to cvc)) },
)
)
assertThat(shippingDetails).isEqualTo(configuration.shippingDetails)
assertThat(initializationMode).isEqualTo(initMode)
Truth.assertThat(result).isEqualTo(Result.Failed(R.string.stripe_something_went_wrong.resolvableString))
Truth.assertThat(logger.errorLogs)
.containsExactly("DefaultLinkConfirmationHandler: Failed to confirm payment"
to DefaultLinkConfirmationHandler.NO_CLIENT_SECRET_FOUND)
}

private fun createHandler(
@@ -224,8 +174,4 @@ internal class DefaultLinkConfirmationHandlerTest {
confirmationHandler.validate()
return handler
}

companion object {
private const val CVC = "333"
}
}
Original file line number Diff line number Diff line change
@@ -24,7 +24,8 @@ internal class WalletScreenScreenshotTest {
selectedItem = null,
isProcessing = false,
hasCompleted = false,
primaryButtonLabel = primaryButtonLabel
primaryButtonLabel = primaryButtonLabel,
errorMessage = null
)
)
}
@@ -37,7 +38,8 @@ internal class WalletScreenScreenshotTest {
selectedItem = TestFactory.CONSUMER_PAYMENT_DETAILS.paymentDetails.firstOrNull(),
isProcessing = false,
hasCompleted = false,
primaryButtonLabel = primaryButtonLabel
primaryButtonLabel = primaryButtonLabel,
errorMessage = null
)
)
}
@@ -50,7 +52,8 @@ internal class WalletScreenScreenshotTest {
selectedItem = TestFactory.CONSUMER_PAYMENT_DETAILS.paymentDetails.firstOrNull(),
isProcessing = false,
hasCompleted = false,
primaryButtonLabel = primaryButtonLabel
primaryButtonLabel = primaryButtonLabel,
errorMessage = null
),
isExpanded = true
)
@@ -64,7 +67,8 @@ internal class WalletScreenScreenshotTest {
selectedItem = TestFactory.CONSUMER_PAYMENT_DETAILS.paymentDetails.firstOrNull(),
isProcessing = false,
hasCompleted = false,
primaryButtonLabel = primaryButtonLabel
primaryButtonLabel = primaryButtonLabel,
errorMessage = null
),
isExpanded = true
)
@@ -78,7 +82,8 @@ internal class WalletScreenScreenshotTest {
selectedItem = TestFactory.CONSUMER_PAYMENT_DETAILS.paymentDetails.firstOrNull(),
isProcessing = true,
hasCompleted = false,
primaryButtonLabel = primaryButtonLabel
primaryButtonLabel = primaryButtonLabel,
errorMessage = null
),
isExpanded = true
)
@@ -99,7 +104,8 @@ internal class WalletScreenScreenshotTest {
selectedItem = paymentDetailsList.firstOrNull(),
isProcessing = false,
hasCompleted = false,
primaryButtonLabel = primaryButtonLabel
primaryButtonLabel = primaryButtonLabel,
errorMessage = null
),
isExpanded = true
)
@@ -120,7 +126,8 @@ internal class WalletScreenScreenshotTest {
selectedItem = paymentDetailsList.firstOrNull(),
isProcessing = false,
hasCompleted = false,
primaryButtonLabel = primaryButtonLabel
primaryButtonLabel = primaryButtonLabel,
errorMessage = null
),
isExpanded = true
)
@@ -136,7 +143,8 @@ internal class WalletScreenScreenshotTest {
},
isProcessing = false,
hasCompleted = false,
primaryButtonLabel = primaryButtonLabel
primaryButtonLabel = primaryButtonLabel,
errorMessage = null
),
isExpanded = true
)
Original file line number Diff line number Diff line change
@@ -1,39 +1,28 @@
package com.stripe.android.link.ui.wallet

import androidx.activity.ComponentActivity
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.assertCountEquals
import androidx.compose.ui.test.assertHasClickAction
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsEnabled
import androidx.compose.ui.test.assertIsNotEnabled
import androidx.compose.ui.test.hasTestTag
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onLast
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
import com.stripe.android.core.strings.resolvableString
import com.stripe.android.link.TestFactory
import com.stripe.android.link.account.FakeLinkAccountManager
import com.stripe.android.link.account.LinkAccountManager
import com.stripe.android.link.confirmation.FakeLinkConfirmationHandler
import com.stripe.android.link.confirmation.LinkConfirmationHandler
import com.stripe.android.link.ui.BottomSheetContent
import com.stripe.android.link.ui.PrimaryButtonTag
import com.stripe.android.model.ConsumerPaymentDetails
import com.stripe.android.testing.CoroutineTestRule
import com.stripe.android.testing.FakeLogger
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -45,8 +34,10 @@ internal class WalletScreenTest {
@get:Rule
val composeTestRule = createAndroidComposeRule<ComponentActivity>()

@get:Rule
val coroutineTestRule = CoroutineTestRule(dispatcher)
@Before
fun setup() {
Dispatchers.setMain(dispatcher)
}

@Test
fun `wallet list is collapsed on start`() = runTest(dispatcher) {
@@ -61,11 +52,7 @@ internal class WalletScreenTest {
)
val viewModel = createViewModel(linkAccountManager)
composeTestRule.setContent {
WalletScreen(
viewModel = viewModel,
showBottomSheetContent = {},
hideBottomSheetContent = {}
)
WalletScreen(viewModel)
}
composeTestRule.waitForIdle()

@@ -92,11 +79,7 @@ internal class WalletScreenTest {
)
val viewModel = createViewModel(linkAccountManager)
composeTestRule.setContent {
WalletScreen(
viewModel = viewModel,
showBottomSheetContent = {},
hideBottomSheetContent = {}
)
WalletScreen(viewModel)
}
composeTestRule.waitForIdle()

@@ -121,11 +104,7 @@ internal class WalletScreenTest {
)
val viewModel = createViewModel(linkAccountManager)
composeTestRule.setContent {
WalletScreen(
viewModel = viewModel,
showBottomSheetContent = {},
hideBottomSheetContent = {}
)
WalletScreen(viewModel)
}

composeTestRule.waitForIdle()
@@ -156,11 +135,7 @@ internal class WalletScreenTest {
)
val viewModel = createViewModel(linkAccountManager)
composeTestRule.setContent {
WalletScreen(
viewModel = viewModel,
showBottomSheetContent = {},
hideBottomSheetContent = {}
)
WalletScreen(viewModel)
}

composeTestRule.waitForIdle()
@@ -183,11 +158,7 @@ internal class WalletScreenTest {

val viewModel = createViewModel(linkAccountManager)
composeTestRule.setContent {
WalletScreen(
viewModel = viewModel,
showBottomSheetContent = {},
hideBottomSheetContent = {}
)
WalletScreen(viewModel)
}

composeTestRule.waitForIdle()
@@ -196,171 +167,15 @@ internal class WalletScreenTest {
onPaymentMethodList().assertCountEquals(0)
}

@Test
fun `wallet menu is displayed on payment method menu clicked`() = runTest(dispatcher) {
val viewModel = createViewModel()
composeTestRule.setContent {
var sheetContent by remember { mutableStateOf<BottomSheetContent?>(null) }
Box {
WalletScreen(
viewModel = viewModel,
showBottomSheetContent = {
sheetContent = it
},
hideBottomSheetContent = {
sheetContent = null
}
)

sheetContent?.let {
Column { it() }
}
}
}

composeTestRule.waitForIdle()

onCollapsedWalletRow().performClick()

composeTestRule.waitForIdle()

onWalletPaymentMethodMenu().assertDoesNotExist()
onWalletPaymentMethodRowMenuButton().onLast().performClick()

composeTestRule.waitForIdle()

onWalletPaymentMethodMenu().assertIsDisplayed()
}

@Test
fun `wallet menu is dismissed on cancel clicked`() = runTest(dispatcher) {
testMenu(
nodeTag = onWalletPaymentMethodMenuCancelTag()
)
}

@Test
fun `wallet menu is dismissed on remove clicked`() = runTest(dispatcher) {
testMenu(
nodeTag = onWalletPaymentMethodMenuRemoveTag(),
expectedRemovedCounter = 1
)
}

@Test
fun `wallet menu is dismissed on edit clicked`() = runTest(dispatcher) {
testMenu(
nodeTag = onWalletPaymentMethodMenuUpdateTag(),
expectedEditPaymentMethodCounter = 1
)
}

@Test
fun `wallet menu is dismissed on setAsDefault clicked`() = runTest(dispatcher) {
testMenu(
nodeTag = onWalletPaymentMethodMenuSetAsDefaultTag(),
expectedSetAsDefaultCounter = 1
)
}

private fun testMenu(
nodeTag: SemanticsNodeInteraction,
expectedRemovedCounter: Int = 0,
expectedSetAsDefaultCounter: Int = 0,
expectedEditPaymentMethodCounter: Int = 0
) {
var onSetDefaultCounter = 0
var onRemoveClickedCounter = 0
var onEditPaymentMethodClickedCounter = 0
composeTestRule.setContent {
var sheetContent by remember { mutableStateOf<BottomSheetContent?>(null) }
Box {
TestWalletBody(
onSetDefaultClicked = {
onSetDefaultCounter += 1
},
onRemoveClicked = {
onRemoveClickedCounter += 1
},
onEditPaymentMethodClicked = {
onEditPaymentMethodClickedCounter += 1
},
showBottomSheetContent = {
sheetContent = it
},
hideBottomSheetContent = {
sheetContent = null
}
)

sheetContent?.let {
Column { it() }
}
}
}

composeTestRule.waitForIdle()

onWalletPaymentMethodRowMenuButton().onLast().performClick()

composeTestRule.waitForIdle()

onWalletPaymentMethodMenu().assertIsDisplayed()

nodeTag.performClick()

composeTestRule.waitForIdle()

onWalletPaymentMethodMenu().assertDoesNotExist()
assertThat(onSetDefaultCounter).isEqualTo(expectedSetAsDefaultCounter)
assertThat(onRemoveClickedCounter).isEqualTo(expectedRemovedCounter)
assertThat(onEditPaymentMethodClickedCounter).isEqualTo(expectedEditPaymentMethodCounter)
}

@Composable
private fun TestWalletBody(
onRemoveClicked: (ConsumerPaymentDetails.PaymentDetails) -> Unit = {},
onSetDefaultClicked: (ConsumerPaymentDetails.PaymentDetails) -> Unit = {},
onEditPaymentMethodClicked: (ConsumerPaymentDetails.PaymentDetails) -> Unit = {},
showBottomSheetContent: (BottomSheetContent?) -> Unit,
hideBottomSheetContent: () -> Unit
) {
val paymentDetails = TestFactory.CONSUMER_PAYMENT_DETAILS.paymentDetails
.filterIsInstance<ConsumerPaymentDetails.Card>()
.map { it.copy(isDefault = false) }
WalletBody(
state = WalletUiState(
paymentDetailsList = paymentDetails,
selectedItem = paymentDetails.firstOrNull(),
isProcessing = false,
hasCompleted = false,
primaryButtonLabel = "Buy".resolvableString
),
isExpanded = true,
onItemSelected = {},
onExpandedChanged = {},
onPrimaryButtonClick = {},
onPayAnotherWayClicked = {},
onRemoveClicked = onRemoveClicked,
onSetDefaultClicked = onSetDefaultClicked,
onEditPaymentMethodClicked = onEditPaymentMethodClicked,
showBottomSheetContent = showBottomSheetContent,
hideBottomSheetContent = hideBottomSheetContent,
onAddNewPaymentMethodClicked = {}
)
}

private fun createViewModel(
linkAccountManager: LinkAccountManager = FakeLinkAccountManager(),
linkConfirmationHandler: LinkConfirmationHandler = FakeLinkConfirmationHandler()
linkAccountManager: LinkAccountManager = FakeLinkAccountManager()
): WalletViewModel {
return WalletViewModel(
configuration = TestFactory.LINK_CONFIGURATION,
linkAccount = TestFactory.LINK_ACCOUNT,
linkAccountManager = linkAccountManager,
linkConfirmationHandler = linkConfirmationHandler,
logger = FakeLogger(),
navigate = {},
linkConfirmationHandler = FakeLinkConfirmationHandler(),
navigateAndClearStack = {},
dismissWithResult = {}
)
@@ -392,22 +207,4 @@ internal class WalletScreenTest {
composeTestRule.onNodeWithTag(WALLET_SCREEN_PAY_ANOTHER_WAY_BUTTON, useUnmergedTree = true)

private fun onLoader() = composeTestRule.onNodeWithTag(WALLET_LOADER_TAG)

private fun onWalletPaymentMethodRowMenuButton() =
composeTestRule.onAllNodes(hasTestTag(WALLET_PAYMENT_DETAIL_ITEM_MENU_BUTTON), useUnmergedTree = true)

private fun onWalletPaymentMethodMenu() =
composeTestRule.onNodeWithTag(WALLET_SCREEN_MENU_SHEET_TAG, useUnmergedTree = true)

private fun onWalletPaymentMethodMenuCancelTag() =
composeTestRule.onNodeWithTag(WALLET_MENU_CANCEL_TAG, useUnmergedTree = true)

private fun onWalletPaymentMethodMenuRemoveTag() =
composeTestRule.onNodeWithTag(WALLET_MENU_REMOVE_ITEM_TAG, useUnmergedTree = true)

private fun onWalletPaymentMethodMenuUpdateTag() =
composeTestRule.onNodeWithTag(WALLET_MENU_EDIT_CARD_TAG, useUnmergedTree = true)

private fun onWalletPaymentMethodMenuSetAsDefaultTag() =
composeTestRule.onNodeWithTag(WALLET_MENU_SET_AS_DEFAULT_TAG, useUnmergedTree = true)
}

Large diffs are not rendered by default.