diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/repository/DebugMenuRepository.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/repository/DebugMenuRepository.kt index a7f3812b38..b774592d5a 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/repository/DebugMenuRepository.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/repository/DebugMenuRepository.kt @@ -54,4 +54,9 @@ interface DebugMenuRepository { * Clears all stored SSO cookie configurations. */ fun clearSsoCookies() + + /** + * Resets the premium upgrade banner dismiss status for the current user. + */ + fun resetPremiumUpgradeBannerDismiss() } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/repository/DebugMenuRepositoryImpl.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/repository/DebugMenuRepositoryImpl.kt index 41eeacb626..9fc8ff6f4f 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/repository/DebugMenuRepositoryImpl.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/repository/DebugMenuRepositoryImpl.kt @@ -74,4 +74,12 @@ class DebugMenuRepositoryImpl( override fun clearSsoCookies() { cookieDiskSource.clearCookies() } + + override fun resetPremiumUpgradeBannerDismiss() { + val currentUserId = authDiskSource.userState?.activeUserId ?: return + settingsDiskSource.storePremiumUpgradeBannerDismissed( + userId = currentUserId, + isDismissed = null, + ) + } } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuScreen.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuScreen.kt index f36fb388fe..c84e736f13 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuScreen.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuScreen.kt @@ -110,6 +110,15 @@ fun DebugMenuScreen( .fillMaxWidth() .standardHorizontalMargin(), ) + Spacer(Modifier.height(height = 16.dp)) + BitwardenHorizontalDivider() + Spacer(Modifier.height(height = 16.dp)) + BitwardenListHeaderText( + label = stringResource(BitwardenString.cookies), + modifier = Modifier + .standardHorizontalMargin() + .padding(horizontal = 16.dp), + ) Spacer(Modifier.height(height = 8.dp)) BitwardenFilledButton( label = stringResource(BitwardenString.trigger_cookie_acquisition), @@ -135,6 +144,27 @@ fun DebugMenuScreen( Spacer(Modifier.height(height = 16.dp)) BitwardenHorizontalDivider() Spacer(Modifier.height(height = 16.dp)) + BitwardenListHeaderText( + label = stringResource(BitwardenString.premium), + modifier = Modifier + .standardHorizontalMargin() + .padding(horizontal = 16.dp), + ) + Spacer(Modifier.height(height = 8.dp)) + BitwardenFilledButton( + label = stringResource(BitwardenString.reset_premium_upgrade_banner), + onClick = { + viewModel.trySendAction( + DebugMenuAction.ResetPremiumUpgradeBanner, + ) + }, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) + Spacer(Modifier.height(height = 16.dp)) + BitwardenHorizontalDivider() + Spacer(Modifier.height(height = 16.dp)) BitwardenListHeaderText( label = stringResource(BitwardenString.error_reports), modifier = Modifier diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuViewModel.kt index fcffa3895e..dd3a4c7d0e 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuViewModel.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuViewModel.kt @@ -65,6 +65,7 @@ class DebugMenuViewModel @Inject constructor( DebugMenuAction.GenerateErrorReportClick -> handleErrorReportClick() DebugMenuAction.TriggerCookieAcquisition -> handleTriggerCookieAcquisition() DebugMenuAction.ClearSsoCookies -> handleClearSsoCookies() + DebugMenuAction.ResetPremiumUpgradeBanner -> handleResetPremiumUpgradeBanner() } } @@ -105,6 +106,10 @@ class DebugMenuViewModel @Inject constructor( debugMenuRepository.clearSsoCookies() } + private fun handleResetPremiumUpgradeBanner() { + debugMenuRepository.resetPremiumUpgradeBannerDismiss() + } + private fun handleTriggerCookieAcquisition() { cookieAcquisitionRequestManager.setPendingCookieAcquisition( data = CookieAcquisitionRequest( @@ -206,6 +211,11 @@ sealed class DebugMenuAction { */ data object ClearSsoCookies : DebugMenuAction() + /** + * User has clicked to reset the premium upgrade banner dismiss status. + */ + data object ResetPremiumUpgradeBanner : DebugMenuAction() + /** * Internal actions not triggered from the UI. */ diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultContent.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultContent.kt index 185fae6ccd..a98dda636d 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultContent.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultContent.kt @@ -488,7 +488,19 @@ private fun ActionCard( ) { when (actionCardState) { VaultState.ActionCardState.UpgradePremium -> { - // UI implementation in PM-33515. + BitwardenActionCard( + cardTitle = stringResource( + id = BitwardenString.unlock_advanced_security_features, + ), + cardSubtitle = stringResource( + id = BitwardenString + .a_premium_plan_gives_you_more_tools_to_stay_secure_and_in_control, + ), + actionText = stringResource(id = BitwardenString.upgrade_to_premium), + onActionClick = { vaultHandlers.actionCardClick(actionCardState) }, + onDismissClick = { vaultHandlers.dismissActionCardClick(actionCardState) }, + modifier = modifier, + ) } VaultState.ActionCardState.IntroducingArchive -> { diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/repository/DebugMenuRepositoryTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/repository/DebugMenuRepositoryTest.kt index b1416141ff..47ee5e223e 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/repository/DebugMenuRepositoryTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/repository/DebugMenuRepositoryTest.kt @@ -182,6 +182,44 @@ class DebugMenuRepositoryTest { mockCookieDiskSource.clearCookies() } } + + @Test + fun `resetPremiumUpgradeBannerDismiss should store null for the current user`() { + val userId = "testUserId" + val mockUserStateJson = mockk(relaxed = true) { + every { activeUserId } returns userId + } + every { mockAuthDiskSource.userState } returns mockUserStateJson + every { + mockSettingsDiskSource.storePremiumUpgradeBannerDismissed( + userId = any(), + isDismissed = any(), + ) + } just runs + + debugMenuRepository.resetPremiumUpgradeBannerDismiss() + + verify(exactly = 1) { + mockSettingsDiskSource.storePremiumUpgradeBannerDismissed( + userId = userId, + isDismissed = null, + ) + } + } + + @Test + fun `resetPremiumUpgradeBannerDismiss should do nothing if no active user`() { + every { mockAuthDiskSource.userState } returns null + + debugMenuRepository.resetPremiumUpgradeBannerDismiss() + + verify(exactly = 0) { + mockSettingsDiskSource.storePremiumUpgradeBannerDismissed( + userId = any(), + isDismissed = any(), + ) + } + } } private const val TEST_STRING_VALUE = "test" diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuScreenTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuScreenTest.kt index 12accf0ad1..5b9fbadcab 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuScreenTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuScreenTest.kt @@ -159,6 +159,18 @@ class DebugMenuScreenTest : BitwardenComposeTest() { verify(exactly = 1) { viewModel.trySendAction(DebugMenuAction.ClearSsoCookies) } } + @Test + fun `reset premium upgrade banner should send ResetPremiumUpgradeBanner action`() { + composeTestRule + .onNodeWithText("Reset premium upgrade banner") + .performScrollTo() + .performClick() + + verify(exactly = 1) { + viewModel.trySendAction(DebugMenuAction.ResetPremiumUpgradeBanner) + } + } + @Test fun `reset all coach mark tours should send ResetCoachMarkTourStatuses action`() { composeTestRule diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuViewModelTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuViewModelTest.kt index 452a761691..af422f3375 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuViewModelTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuViewModelTest.kt @@ -152,6 +152,16 @@ class DebugMenuViewModelTest : BaseViewModelTest() { } } + @Suppress("MaxLineLength") + @Test + fun `ResetPremiumUpgradeBanner should call resetPremiumUpgradeBannerDismiss on DebugMenuRepository`() { + val viewModel = createViewModel() + viewModel.trySendAction(DebugMenuAction.ResetPremiumUpgradeBanner) + verify(exactly = 1) { + mockDebugMenuRepository.resetPremiumUpgradeBannerDismiss() + } + } + @Test fun `TriggerCookieAcquisition should set pending cookie acquisition`() = runTest { diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/search/SearchScreenTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/search/SearchScreenTest.kt index 0bc475cec2..00162b0e2e 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/search/SearchScreenTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/search/SearchScreenTest.kt @@ -1047,7 +1047,7 @@ class SearchScreenTest : BitwardenComposeTest() { .assert(hasAnyAncestor(isDialog())) .assertIsDisplayed() composeTestRule - .onNodeWithText(text = "Upgrade to premium") + .onNodeWithText(text = "Upgrade to Premium") .assert(hasAnyAncestor(isDialog())) .performClick() diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/addedit/AddEditSendScreenTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/addedit/AddEditSendScreenTest.kt index bacf5e5439..24c5f11905 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/addedit/AddEditSendScreenTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/addedit/AddEditSendScreenTest.kt @@ -978,7 +978,7 @@ class AddEditSendScreenTest : BitwardenComposeTest() { } composeTestRule - .onNodeWithText("Upgrade to premium") + .onNodeWithText("Upgrade to Premium") .performClick() verify { viewModel.trySendAction(AddEditSendAction.UpgradeToPremiumClick) } diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreenTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreenTest.kt index a2e1a0aeb2..b510b68413 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreenTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreenTest.kt @@ -275,7 +275,7 @@ class VaultAddEditScreenTest : BitwardenComposeTest() { ) composeTestRule - .onNodeWithText(text = "Upgrade to premium") + .onNodeWithText(text = "Upgrade to Premium") .assert(hasAnyAncestor(isDialog())) .performClick() diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreenTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreenTest.kt index 2b1a3a9851..251903eaca 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreenTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreenTest.kt @@ -253,7 +253,7 @@ class VaultItemScreenTest : BitwardenComposeTest() { } composeTestRule - .onNodeWithText(text = "Upgrade to premium") + .onNodeWithText(text = "Upgrade to Premium") .assert(hasAnyAncestor(isDialog())) .performClick() @@ -774,7 +774,7 @@ class VaultItemScreenTest : BitwardenComposeTest() { .assertIsDisplayed() composeTestRule - .onAllNodesWithText(text = "Upgrade to premium") + .onAllNodesWithText(text = "Upgrade to Premium") .filterToOne(hasAnyAncestor(isDialog())) .performClick() diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreenTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreenTest.kt index 79fa2cc695..02e3d55df3 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreenTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreenTest.kt @@ -2516,7 +2516,7 @@ class VaultItemListingScreenTest : BitwardenComposeTest() { .assert(hasAnyAncestor(isDialog())) .assertIsDisplayed() composeTestRule - .onNodeWithText(text = "Upgrade to premium") + .onNodeWithText(text = "Upgrade to Premium") .assert(hasAnyAncestor(isDialog())) .performClick() diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreenTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreenTest.kt index e0c88fc7e0..eaf5129889 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreenTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreenTest.kt @@ -874,7 +874,7 @@ class VaultScreenTest : BitwardenComposeTest() { .assert(hasAnyAncestor(isDialog())) .assertIsDisplayed() composeTestRule - .onNodeWithText(text = "Upgrade to premium") + .onNodeWithText(text = "Upgrade to Premium") .assert(hasAnyAncestor(isDialog())) .performClick() @@ -1554,6 +1554,61 @@ class VaultScreenTest : BitwardenComposeTest() { } } + @Test + fun `UpgradePremium action card should display when eligible`() { + mutableStateFlow.value = DEFAULT_STATE.copy( + isPremiumUpgradeBannerEligible = true, + viewState = DEFAULT_CONTENT_VIEW_STATE, + ) + + composeTestRule + .onNodeWithText(text = "Unlock advanced security features") + .assertIsDisplayed() + composeTestRule + .onNodeWithText(text = "Upgrade to Premium") + .assertIsDisplayed() + } + + @Test + fun `UpgradePremium action card CTA click should send ActionCardClick`() { + mutableStateFlow.value = DEFAULT_STATE.copy( + isPremiumUpgradeBannerEligible = true, + viewState = DEFAULT_CONTENT_VIEW_STATE, + ) + + composeTestRule + .onNodeWithText(text = "Upgrade to Premium") + .assertIsDisplayed() + .performClick() + + verify(exactly = 1) { + viewModel.trySendAction( + VaultAction.ActionCardClick( + actionCard = VaultState.ActionCardState.UpgradePremium, + ), + ) + } + } + + @Test + fun `UpgradePremium action card dismiss click should send DismissActionCardClick`() { + mutableStateFlow.value = DEFAULT_STATE.copy( + isPremiumUpgradeBannerEligible = true, + viewState = DEFAULT_CONTENT_VIEW_STATE, + ) + + composeTestRule + .onNodeWithContentDescription(label = "Close") + .assertIsDisplayed() + .performClick() + + verify(exactly = 1) { + viewModel.trySendAction( + VaultAction.DismissActionCardClick(VaultState.ActionCardState.UpgradePremium), + ) + } + } + @Test fun `collection data should update according to the state`() { val collectionsHeader = "COLLECTIONS (1)" diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml index 22ae08be2b..2153e65643 100644 --- a/ui/src/main/res/values/strings.xml +++ b/ui/src/main/res/values/strings.xml @@ -1204,7 +1204,9 @@ Do you want to switch to this account? Item moved to vault Archive unavailable Archiving items is a Premium feature. Your current plan does not include access to this feature. - Upgrade to premium + Upgrade to Premium + Unlock advanced security features + A Premium plan gives you more tools to stay secure and in control. This item is archived. Introducing archive Keep items you don’t need right now safe but out of sight. diff --git a/ui/src/main/res/values/strings_non_localized.xml b/ui/src/main/res/values/strings_non_localized.xml index 0bedf0423c..c190292bf3 100644 --- a/ui/src/main/res/values/strings_non_localized.xml +++ b/ui/src/main/res/values/strings_non_localized.xml @@ -30,6 +30,9 @@ Generate crash Generate error report Error reports + Cookies + Premium + Reset premium upgrade banner Bitwarden authentication enabled Bitwarden (.json) 2FAS (no password)