Skip to content
Merged
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
Expand Up @@ -5,6 +5,7 @@ import androidx.annotation.RestrictTo
import androidx.compose.ui.semantics.SemanticsActions
import androidx.compose.ui.semantics.SemanticsProperties
import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.assertAny
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsEnabled
import androidx.compose.ui.test.assertIsNotEnabled
Expand All @@ -18,6 +19,7 @@ import androidx.compose.ui.test.isEnabled
import androidx.compose.ui.test.isNotEnabled
import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onChildren
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
Expand All @@ -28,6 +30,9 @@ import androidx.compose.ui.test.performTextReplacement
import androidx.compose.ui.test.performTouchInput
import com.stripe.android.paymentsheet.R
import com.stripe.android.testing.waitUntilWithIdle
import com.stripe.android.ui.core.elements.TEST_TAG_DIALOG_CONFIRM_BUTTON
import com.stripe.android.ui.core.elements.TEST_TAG_DIALOG_DISMISS_BUTTON
import com.stripe.android.ui.core.elements.TEST_TAG_SIMPLE_DIALOG
import androidx.compose.ui.R as ComposeUiR

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
Expand All @@ -42,6 +47,10 @@ class AddressElementPage(
context.getString(R.string.stripe_paymentsheet_close)
)
private val scrim = composeTestRule.onNodeWithContentDescription(context.getString(ComposeUiR.string.close_sheet))
private val discardDialogTitle = context.getString(
R.string.stripe_paymentsheet_address_element_discard_changes_title
)
private val discardDialogBody = context.getString(R.string.stripe_paymentsheet_address_element_discard_changes_body)

fun waitUntilVisible() {
waitForNode(hasText("Shipping Address"))
Expand Down Expand Up @@ -89,6 +98,29 @@ class AddressElementPage(
closeButton.performClick()
}

fun assertDiscardConfirmation(loadingIndicatorTestTag: String) {
primaryButton.assertIsNotEnabled()
closeButton.assertIsNotEnabled()
val dialog = composeTestRule.onNodeWithTag(TEST_TAG_SIMPLE_DIALOG)
dialog.assertIsDisplayed()
dialog.onChildren().assertAny(hasText(discardDialogTitle))
dialog.onChildren().assertAny(hasText(discardDialogBody))
composeTestRule.onNodeWithTag(loadingIndicatorTestTag, useUnmergedTree = true)
.assertDoesNotExist()
}

fun assertDiscardConfirmationNotDisplayed() {
composeTestRule.onNodeWithTag(TEST_TAG_SIMPLE_DIALOG).assertDoesNotExist()
}

fun keepEditing() {
composeTestRule.onNodeWithTag(TEST_TAG_DIALOG_DISMISS_BUTTON).performClick()
}

fun discardChanges() {
composeTestRule.onNodeWithTag(TEST_TAG_DIALOG_CONFIRM_BUTTON).performClick()
}

fun dismissViaScrimAccessibilityAction() {
scrim.performSemanticsAction(SemanticsActions.OnClick)
}
Expand Down Expand Up @@ -159,6 +191,10 @@ class AddressElementPage(
replaceText("Full name", name)
}

fun assertName(name: String) {
assertTextFieldContains("Full name", name)
}

private fun replaceText(label: String, text: String) {
val matcher = hasText(label).and(hasSetTextAction()).and(isEnabled())
waitForNode(matcher)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import androidx.compose.material.ModalBottomSheetValue
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.core.view.WindowCompat
import androidx.lifecycle.ViewModelProvider
Expand All @@ -29,6 +30,7 @@ import com.stripe.android.paymentsheet.parseAppearance
import com.stripe.android.paymentsheet.ui.PaymentElementTheme
import com.stripe.android.uicore.elements.bottomsheet.StripeBottomSheetState
import com.stripe.android.uicore.elements.bottomsheet.rememberStripeBottomSheetState
import com.stripe.android.uicore.utils.collectAsState
import com.stripe.android.uicore.utils.fadeOut
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.filterIsInstance
Expand Down Expand Up @@ -66,9 +68,7 @@ internal class AddressElementActivity : ComponentActivity() {
viewModel.navigator.navigationController = navController

val bottomSheetState = rememberStripeBottomSheetState(
confirmValueChange = { target ->
target != ModalBottomSheetValue.Hidden || viewModel.resultStateHolder.state.value !is State.Saving
},
confirmValueChange = ::confirmValueChange,
)

LaunchedEffect(bottomSheetState) {
Expand All @@ -84,7 +84,7 @@ internal class AddressElementActivity : ComponentActivity() {
if (viewModel.resultStateHolder.state.value != State.Idle) return@BackHandler

if (navController.previousBackStackEntry == null || !viewModel.navigator.onBack()) {
viewModel.resultStateHolder.onUserCancel()
dismiss()
}
}

Expand All @@ -97,18 +97,24 @@ internal class AddressElementActivity : ComponentActivity() {
bottomSheetState: StripeBottomSheetState,
navController: NavHostController,
) {
val resultState by viewModel.resultStateHolder.state.collectAsState()

PaymentElementTheme(appearance = starterArgs?.config?.appearance ?: PaymentSheet.Appearance()) {
ElementsBottomSheetLayout(
state = bottomSheetState,
onDismissed = viewModel.resultStateHolder::onUserCancel,
onDismissed = ::dismiss,
) {
Surface(modifier = Modifier.fillMaxSize()) {
NavHost(
navController = navController,
startDestination = AddressElementScreen.InputAddress.route,
) {
composable(AddressElementScreen.InputAddress.route) {
InputAddressScreen(viewModel.inputAddressViewModelSubcomponentFactoryProvider)
InputAddressScreen(
inputAddressViewModelSubcomponentFactoryProvider =
viewModel.inputAddressViewModelSubcomponentFactoryProvider,
onCloseClick = ::dismiss,
)
}
composable(
AddressElementScreen.Autocomplete.route,
Expand All @@ -132,9 +138,33 @@ internal class AddressElementActivity : ComponentActivity() {
}
}
}

if (resultState == State.ConfirmingCancellation) {
AddressElementDismissalDialog(
onDiscardChanges = viewModel.resultStateHolder::onDiscardChanges,
onKeepEditing = viewModel.resultStateHolder::onKeepEditing,
)
}
}
}

@VisibleForTesting
internal fun confirmValueChange(target: ModalBottomSheetValue): Boolean {
return target != ModalBottomSheetValue.Hidden || when (viewModel.resultStateHolder.state.value) {
is State.Finished -> true
else -> {
dismiss()
false
}
}
}

private fun dismiss() {
viewModel.resultStateHolder.onUserCancel(
shouldConfirmCancellation = starterArgs is AddressElementActivityContract.Args.CheckoutShipping,
)
}

private fun finishWithResult(result: AddressElementActivityContract.Result) {
setResult(
result.resultCode,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.stripe.android.paymentsheet.addresselement

import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.stripe.android.paymentsheet.R
import com.stripe.android.ui.core.elements.SimpleDialogElementUI

@Composable
internal fun AddressElementDismissalDialog(
onDiscardChanges: () -> Unit,
onKeepEditing: () -> Unit,
) {
SimpleDialogElementUI(
Comment thread
amk-stripe marked this conversation as resolved.
titleText = stringResource(R.string.stripe_paymentsheet_address_element_discard_changes_title),
messageText = stringResource(R.string.stripe_paymentsheet_address_element_discard_changes_body),
confirmText = stringResource(R.string.stripe_paymentsheet_address_element_discard_changes_confirm),
dismissText = stringResource(android.R.string.cancel),
destructive = true,
onConfirmListener = onDiscardChanges,
onDismissListener = onKeepEditing,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,21 @@ internal class AddressElementResultStateHolder @Inject constructor() {
_state.compareAndSet(expect = State.Saving, update = State.Finished(result))
}

fun onUserCancel(): Boolean {
fun onUserCancel(shouldConfirmCancellation: Boolean): Boolean {
return _state.compareAndSet(
expect = State.Idle,
update = State.Canceling,
update = if (shouldConfirmCancellation) State.ConfirmingCancellation else State.Canceling,
)
}

fun onKeepEditing() {
_state.compareAndSet(expect = State.ConfirmingCancellation, update = State.Idle)
}

fun onDiscardChanges() {
_state.compareAndSet(expect = State.ConfirmingCancellation, update = State.Canceling)
}

fun onCancelCompleted() {
_state.compareAndSet(
expect = State.Canceling,
Expand All @@ -42,6 +50,8 @@ internal class AddressElementResultStateHolder @Inject constructor() {

data object Saving : State

data object ConfirmingCancellation : State

data object Canceling : State

data class Finished(val result: AddressElementActivityContract.Result) : State
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import com.stripe.android.common.ui.PrimaryButton
import com.stripe.android.core.strings.ResolvableString
import com.stripe.android.core.strings.resolvableString
import com.stripe.android.paymentsheet.R
import com.stripe.android.paymentsheet.addresselement.AddressElementResultStateHolder.State
import com.stripe.android.paymentsheet.injection.InputAddressViewModelSubcomponent
import com.stripe.android.paymentsheet.ui.AddressOptionsAppBar
import com.stripe.android.paymentsheet.ui.ErrorMessage
Expand Down Expand Up @@ -111,7 +112,8 @@ internal fun InputAddressScreen(

@Composable
internal fun InputAddressScreen(
inputAddressViewModelSubcomponentFactoryProvider: Provider<InputAddressViewModelSubcomponent.Factory>
inputAddressViewModelSubcomponentFactoryProvider: Provider<InputAddressViewModelSubcomponent.Factory>,
onCloseClick: () -> Unit,
) {
val viewModel: InputAddressViewModel = viewModel(
factory = InputAddressViewModel.Factory(
Expand All @@ -132,13 +134,14 @@ internal fun InputAddressScreen(
R.string.stripe_paymentsheet_address_element_shipping_address
)
val formEnabled by viewModel.formEnabled.collectAsState()
val resultState by viewModel.resultStateHolder.state.collectAsState()
val saveError by viewModel.saveError.collectAsState()
val checkboxChecked by viewModel.checkboxChecked.collectAsState()
val billingSameAsShippingState by viewModel.shippingSameAsBillingState.collectAsState()

InputAddressScreen(
primaryButtonEnabled = completeValues != null && formEnabled,
primaryButtonLoading = !formEnabled,
primaryButtonLoading = resultState == State.Saving,
closeButtonEnabled = formEnabled,
primaryButtonText = buttonText,
title = titleText,
Expand All @@ -154,7 +157,7 @@ internal fun InputAddressScreen(
checkboxChecked = checkboxChecked
)
},
onCloseClick = viewModel.resultStateHolder::onUserCancel,
onCloseClick = onCloseClick,
topContent = {
val currentState = billingSameAsShippingState

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.app.Application
import androidx.compose.ui.test.junit4.createEmptyComposeRule
import androidx.test.core.app.ApplicationProvider
import com.google.common.truth.Truth.assertThat
import com.stripe.android.common.ui.PRIMARY_BUTTON_LOADING_INDICATOR_TEST_TAG
import com.stripe.android.core.networking.AnalyticsRequest
import com.stripe.android.core.networking.ApiRequest
import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodMetadataFixtures.DEFAULT_API_CONFIG
Expand Down Expand Up @@ -78,65 +79,87 @@ internal class AddressElementActivityAnalyticsTest {
awaitAnalytics(failedRequest)
addressPage.assertReadyToSave()

val canceledRequest = expectShippingAnalytics("elements.shipping_address.canceled")
addressPage.clickClose()
addressPage.assertDiscardConfirmation(PRIMARY_BUTTON_LOADING_INDICATOR_TEST_TAG)
val canceledRequest = expectShippingAnalytics("elements.shipping_address.canceled")
addressPage.discardChanges()

assertThat(awaitResult()).isEqualTo(AddressElementActivityContract.Result.Canceled)
awaitAnalytics(canceledRequest)
}

@Test
fun `root back reports cancellation`() = runScenario {
fun `root back reports cancellation after discard`() = runScenario {
addressPage.assertReadyToSave()

val canceledRequest = expectShippingAnalytics("elements.shipping_address.canceled")
activityScenario.onActivity { activity ->
activity.onBackPressedDispatcher.onBackPressed()
}
addressPage.assertDiscardConfirmation(PRIMARY_BUTTON_LOADING_INDICATOR_TEST_TAG)

val canceledRequest = expectShippingAnalytics("elements.shipping_address.canceled")
addressPage.discardChanges()

assertThat(awaitResult()).isEqualTo(AddressElementActivityContract.Result.Canceled)
awaitAnalytics(canceledRequest)
}

@Test
fun `close reports cancellation`() = runScenario {
fun `close and keep editing do not report until discard uses current data`() = runScenario {
addressPage.assertReadyToSave()

val canceledRequest = expectShippingAnalytics("elements.shipping_address.canceled")
addressPage.clickClose()
addressPage.assertDiscardConfirmation(PRIMARY_BUTTON_LOADING_INDICATOR_TEST_TAG)
networkRule.validate()

addressPage.keepEditing()
addressPage.assertDiscardConfirmationNotDisplayed()
networkRule.validate()

addressPage.selectCountry("Canada")
addressPage.clickClose()
addressPage.assertDiscardConfirmation(PRIMARY_BUTTON_LOADING_INDICATOR_TEST_TAG)
val canceledRequest = expectShippingAnalytics("elements.shipping_address.canceled", country = "CA")
addressPage.discardChanges()

assertThat(awaitResult()).isEqualTo(AddressElementActivityContract.Result.Canceled)
awaitAnalytics(canceledRequest)
}

@Test
fun `scrim accessibility action reports cancellation`() = runScenario {
fun `scrim accessibility action reports cancellation after discard`() = runScenario {
addressPage.assertReadyToSave()

val canceledRequest = expectShippingAnalytics("elements.shipping_address.canceled")
addressPage.dismissViaScrimAccessibilityAction()
addressPage.assertDiscardConfirmation(PRIMARY_BUTTON_LOADING_INDICATOR_TEST_TAG)

val canceledRequest = expectShippingAnalytics("elements.shipping_address.canceled")
addressPage.discardChanges()

assertThat(awaitResult()).isEqualTo(AddressElementActivityContract.Result.Canceled)
awaitAnalytics(canceledRequest)
}

@Test
fun `back reports the current country once even when pressed twice`() = runScenario {
fun `repeated root back reports current country once after discard`() = runScenario {
addressPage.assertReadyToSave()
addressPage.selectCountry("Canada")

val canceledRequest = expectShippingAnalytics("elements.shipping_address.canceled", country = "CA")
activityScenario.onActivity { activity ->
activity.onBackPressedDispatcher.onBackPressed()
activity.onBackPressedDispatcher.onBackPressed()
}
addressPage.assertDiscardConfirmation(PRIMARY_BUTTON_LOADING_INDICATOR_TEST_TAG)

val canceledRequest = expectShippingAnalytics("elements.shipping_address.canceled", country = "CA")
addressPage.discardChanges()

assertThat(awaitResult()).isEqualTo(AddressElementActivityContract.Result.Canceled)
awaitAnalytics(canceledRequest)
}

@Test
fun `close after recreation reports the retained edited country`() = runScenario {
fun `close after recreation reports the retained edited country after discard`() = runScenario {
addressPage.assertReadyToSave()
addressPage.selectCountry("Canada")

Expand All @@ -151,8 +174,10 @@ internal class AddressElementActivityAnalyticsTest {
awaitAnalytics(shownRequest)

addressPage.assertVisible()
val canceledRequest = expectShippingAnalytics("elements.shipping_address.canceled", country = "CA")
addressPage.clickClose()
addressPage.assertDiscardConfirmation(PRIMARY_BUTTON_LOADING_INDICATOR_TEST_TAG)
val canceledRequest = expectShippingAnalytics("elements.shipping_address.canceled", country = "CA")
addressPage.discardChanges()

assertThat(awaitResult()).isEqualTo(AddressElementActivityContract.Result.Canceled)
awaitAnalytics(canceledRequest)
Expand Down
Loading
Loading