From 8f72c10f8e93660d8f3a15dd309d028b35527138 Mon Sep 17 00:00:00 2001
From: Patrick Honkonen <1883101+SaintPatrck@users.noreply.github.com>
Date: Wed, 20 May 2026 18:28:05 -0400
Subject: [PATCH] [PM-37804] feat: Confirm before leaving the app to Stripe
checkout (#6958)
---
.../feature/premium/plan/PlanScreen.kt | 26 +++++++-
.../feature/premium/plan/PlanScreenTest.kt | 60 +++++++++++++++++--
ui/src/main/res/values/strings.xml | 2 +-
3 files changed, 81 insertions(+), 7 deletions(-)
diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/premium/plan/PlanScreen.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/premium/plan/PlanScreen.kt
index 20946f07d6..d80f6a60a4 100644
--- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/premium/plan/PlanScreen.kt
+++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/premium/plan/PlanScreen.kt
@@ -269,6 +269,7 @@ private fun FreeCloudContent(
handlers: PlanHandlers,
modifier: Modifier = Modifier,
) {
+ var shouldShowUpgradeDialog by rememberSaveable { mutableStateOf(false) }
Column(
modifier = modifier
.fillMaxSize()
@@ -285,7 +286,7 @@ private fun FreeCloudContent(
BitwardenFilledButton(
label = stringResource(id = BitwardenString.upgrade_now),
- onClick = handlers.onUpgradeNowClick,
+ onClick = { shouldShowUpgradeDialog = true },
icon = rememberVectorPainter(id = BitwardenDrawable.ic_external_link),
modifier = Modifier
.standardHorizontalMargin()
@@ -296,7 +297,10 @@ private fun FreeCloudContent(
Spacer(modifier = Modifier.height(12.dp))
Text(
- text = stringResource(id = BitwardenString.stripe_checkout_footer),
+ text = stringResource(
+ id = BitwardenString
+ .youll_go_to_stripes_secure_checkout_to_complete_your_purchase,
+ ),
style = BitwardenTheme.typography.bodyMedium,
color = BitwardenTheme.colorScheme.text.secondary,
textAlign = TextAlign.Center,
@@ -309,6 +313,24 @@ private fun FreeCloudContent(
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.navigationBarsPadding())
}
+
+ if (shouldShowUpgradeDialog) {
+ BitwardenTwoButtonDialog(
+ title = stringResource(id = BitwardenString.continue_to_stripe),
+ message = stringResource(
+ id = BitwardenString
+ .youll_go_to_stripes_secure_checkout_to_complete_your_purchase,
+ ),
+ confirmButtonText = stringResource(id = BitwardenString.continue_text),
+ dismissButtonText = stringResource(id = BitwardenString.cancel),
+ onConfirmClick = {
+ shouldShowUpgradeDialog = false
+ handlers.onUpgradeNowClick()
+ },
+ onDismissClick = { shouldShowUpgradeDialog = false },
+ onDismissRequest = { shouldShowUpgradeDialog = false },
+ )
+ }
}
@Suppress("MaxLineLength")
diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/premium/plan/PlanScreenTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/premium/plan/PlanScreenTest.kt
index 7bf9127644..c3ba27512b 100644
--- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/premium/plan/PlanScreenTest.kt
+++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/premium/plan/PlanScreenTest.kt
@@ -136,14 +136,66 @@ class PlanScreenTest : BitwardenComposeTest() {
}
@Test
- fun `upgrade now button click should send UpgradeNowClick action`() {
+ fun `upgrade now button click should show continue to Stripe confirmation dialog`() {
+ composeTestRule
+ .onAllNodesWithText("Continue to Stripe?")
+ .filterToOne(hasAnyAncestor(isDialog()))
+ .assertDoesNotExist()
+
composeTestRule
.onNodeWithTag("UpgradeNowButton")
.performScrollTo()
.performClick()
- verify {
- viewModel.trySendAction(PlanAction.UpgradeNowClick)
- }
+
+ composeTestRule
+ .onAllNodesWithText("Continue to Stripe?")
+ .filterToOne(hasAnyAncestor(isDialog()))
+ .assertExists()
+ composeTestRule
+ .onAllNodesWithText(
+ "You’ll go to Stripe’s secure checkout to complete your purchase.",
+ )
+ .filterToOne(hasAnyAncestor(isDialog()))
+ .assertExists()
+ verify(exactly = 0) { viewModel.trySendAction(PlanAction.UpgradeNowClick) }
+ }
+
+ @Test
+ fun `upgrade now dialog continue click should send UpgradeNowClick action and dismiss`() {
+ composeTestRule
+ .onNodeWithTag("UpgradeNowButton")
+ .performScrollTo()
+ .performClick()
+
+ composeTestRule
+ .onAllNodesWithText("Continue")
+ .filterToOne(hasAnyAncestor(isDialog()))
+ .performClick()
+
+ verify { viewModel.trySendAction(PlanAction.UpgradeNowClick) }
+ composeTestRule
+ .onAllNodesWithText("Continue to Stripe?")
+ .filterToOne(hasAnyAncestor(isDialog()))
+ .assertDoesNotExist()
+ }
+
+ @Test
+ fun `upgrade now dialog cancel click should dismiss without sending action`() {
+ composeTestRule
+ .onNodeWithTag("UpgradeNowButton")
+ .performScrollTo()
+ .performClick()
+
+ composeTestRule
+ .onAllNodesWithText("Cancel")
+ .filterToOne(hasAnyAncestor(isDialog()))
+ .performClick()
+
+ verify(exactly = 0) { viewModel.trySendAction(PlanAction.UpgradeNowClick) }
+ composeTestRule
+ .onAllNodesWithText("Continue to Stripe?")
+ .filterToOne(hasAnyAncestor(isDialog()))
+ .assertDoesNotExist()
}
@Test
diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml
index 3baca079ce..83bd98b565 100644
--- a/ui/src/main/res/values/strings.xml
+++ b/ui/src/main/res/values/strings.xml
@@ -1278,7 +1278,7 @@ Do you want to switch to this account?
Secure file storage
Breach monitoring
Upgrade now
- You’ll go to Stripe’s secure checkout to complete your purchase.
+ You’ll go to Stripe’s secure checkout to complete your purchase.
Opening checkout…
Secure checkout didn’t load
We had trouble opening the payment page, so try again.