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 @@ -68,14 +68,14 @@ interface SettingsRepository {
var isScreenCaptureAllowed: Boolean

/**
* A set of Bitwarden account IDs that have previously been synced.
* Whether or not screen capture is allowed for the current user.
*/
var previouslySyncedBitwardenAccountIds: Set<String>
val isScreenCaptureAllowedStateFlow: StateFlow<Boolean>

/**
* Whether or not screen capture is allowed for the current user.
* A set of Bitwarden account IDs that have previously been synced.
*/
val isScreenCaptureAllowedStateFlow: StateFlow<Boolean>
Comment on lines 70 to -78
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this change intentional?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes, I moved isScreenCaptureAllowedStateFlow to be right after the isScreenCaptureAllowed.

There was previouslySyncedBitwardenAccountIds between them, very minor but made me not see it immediately

var previouslySyncedBitwardenAccountIds: Set<String>

/**
* Clears any previously stored encrypted user key used with biometrics for the current user.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import com.bitwarden.ui.platform.base.util.cardStyle
import com.bitwarden.ui.platform.base.util.mirrorIfRtl
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.bitwarden.ui.platform.components.appbar.BitwardenMediumTopAppBar
import com.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
import com.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectButton
import com.bitwarden.ui.platform.components.header.BitwardenListHeaderText
import com.bitwarden.ui.platform.components.model.CardStyle
Expand Down Expand Up @@ -157,6 +158,13 @@ fun SettingsScreen(
)
}
},
onScreenCaptureChange = remember(viewModel) {
{
viewModel.trySendAction(
SettingsAction.SecurityClick.AllowScreenCaptureToggle(it),
)
}
},
)
Spacer(modifier = Modifier.height(16.dp))
VaultSettings(
Expand Down Expand Up @@ -256,27 +264,44 @@ private fun SecuritySettings(
state: SettingsState,
biometricsManager: BiometricsManager = LocalBiometricsManager.current,
onBiometricToggle: (Boolean) -> Unit,
onScreenCaptureChange: (Boolean) -> Unit,
) {
if (!biometricsManager.isBiometricsSupported) return
Spacer(modifier = Modifier.height(height = 12.dp))
BitwardenListHeaderText(
modifier = Modifier
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
label = stringResource(id = BitwardenString.security),
)

Spacer(modifier = Modifier.height(8.dp))
UnlockWithBiometricsRow(
val hasBiometrics = biometricsManager.isBiometricsSupported
if (hasBiometrics) {
UnlockWithBiometricsRow(
modifier = Modifier
.testTag("UnlockWithBiometricsSwitch")
.fillMaxWidth()
.standardHorizontalMargin(),
isChecked = state.isUnlockWithBiometricsEnabled,
onBiometricToggle = { onBiometricToggle(it) },
biometricsManager = biometricsManager,
)
}

ScreenCaptureRow(
Copy link
Collaborator

@david-livefront david-livefront Oct 15, 2025

Choose a reason for hiding this comment

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

You're missing the 8dp spacer before this item

Copy link
Collaborator

Choose a reason for hiding this comment

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

Unless the intention is to make this a single card?

Copy link
Contributor Author

@aj-rosado aj-rosado Oct 16, 2025

Choose a reason for hiding this comment

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

It is missing

currentValue = state.allowScreenCapture,
cardStyle = if (hasBiometrics) {
CardStyle.Bottom
} else {
CardStyle.Full
},
onValueChange = onScreenCaptureChange,
modifier = Modifier
.testTag("UnlockWithBiometricsSwitch")
.fillMaxWidth()
.testTag(tag = "AllowScreenCaptureSwitch")
.standardHorizontalMargin(),
isChecked = state.isUnlockWithBiometricsEnabled,
onBiometricToggle = { onBiometricToggle(it) },
biometricsManager = biometricsManager,
)
}

//endregion

//region Data settings
Expand Down Expand Up @@ -421,7 +446,7 @@ private fun UnlockWithBiometricsRow(
var showBiometricsPrompt by rememberSaveable { mutableStateOf(false) }
BitwardenSwitch(
modifier = modifier,
cardStyle = CardStyle.Full,
cardStyle = CardStyle.Top(),
label = stringResource(BitwardenString.unlock_with_biometrics),
isChecked = isChecked || showBiometricsPrompt,
onCheckedChange = { toggled ->
Expand All @@ -443,6 +468,47 @@ private fun UnlockWithBiometricsRow(
)
}

@Composable
private fun ScreenCaptureRow(
currentValue: Boolean,
cardStyle: CardStyle,
onValueChange: (Boolean) -> Unit,
modifier: Modifier = Modifier,
) {
var shouldShowScreenCaptureConfirmDialog by remember { mutableStateOf(false) }

BitwardenSwitch(
label = stringResource(id = BitwardenString.allow_screen_capture),
isChecked = currentValue,
onCheckedChange = {
if (currentValue) {
onValueChange(false)
} else {
shouldShowScreenCaptureConfirmDialog = true
}
},
cardStyle = cardStyle,
modifier = modifier,
)

if (shouldShowScreenCaptureConfirmDialog) {
BitwardenTwoButtonDialog(
title = stringResource(BitwardenString.allow_screen_capture),
message = stringResource(
id = BitwardenString.are_you_sure_you_want_to_enable_screen_capture,
),
confirmButtonText = stringResource(BitwardenString.yes),
dismissButtonText = stringResource(id = BitwardenString.cancel),
onConfirmClick = {
onValueChange(true)
shouldShowScreenCaptureConfirmDialog = false
},
onDismissClick = { shouldShowScreenCaptureConfirmDialog = false },
onDismissRequest = { shouldShowScreenCaptureConfirmDialog = false },
)
}
}

//endregion Data settings

//region Appearance settings
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ class SettingsViewModel @Inject constructor(
accountSyncState = authenticatorBridgeManager.accountSyncStateFlow.value,
defaultSaveOption = settingsRepository.defaultSaveOption,
sharedAccountsState = authenticatorRepository.sharedCodesStateFlow.value,
isScreenCaptureAllowed = settingsRepository.isScreenCaptureAllowed,
),
) {

Expand Down Expand Up @@ -126,6 +127,10 @@ class SettingsViewModel @Inject constructor(
is SettingsAction.SecurityClick.UnlockWithBiometricToggle -> {
handleBiometricsSetupClick(action)
}

is SettingsAction.SecurityClick.AllowScreenCaptureToggle -> {
handleAllowScreenCaptureToggle(action)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Thanks for that! ๐Ÿ˜„

}
}
}

Expand Down Expand Up @@ -173,6 +178,13 @@ class SettingsViewModel @Inject constructor(
}
}

private fun handleAllowScreenCaptureToggle(
action: SettingsAction.SecurityClick.AllowScreenCaptureToggle,
) {
settingsRepository.isScreenCaptureAllowed = action.enabled
mutableStateFlow.update { it.copy(allowScreenCapture = action.enabled) }
}

private fun handleVaultClick(action: SettingsAction.DataClick) {
when (action) {
SettingsAction.DataClick.ExportClick -> handleExportClick()
Expand Down Expand Up @@ -319,6 +331,7 @@ class SettingsViewModel @Inject constructor(
isSubmitCrashLogsEnabled: Boolean,
accountSyncState: AccountSyncState,
sharedAccountsState: SharedVerificationCodesState,
isScreenCaptureAllowed: Boolean,
): SettingsState {
val currentYear = Year.now(clock)
val copyrightInfo = "ยฉ Bitwarden Inc. 2015-$currentYear".asText()
Expand All @@ -343,6 +356,7 @@ class SettingsViewModel @Inject constructor(
defaultSaveOption = defaultSaveOption,
showSyncWithBitwarden = shouldShowSyncWithBitwarden,
showDefaultSaveOptionRow = shouldShowDefaultSaveOption,
allowScreenCapture = isScreenCaptureAllowed,
)
}
}
Expand All @@ -362,6 +376,7 @@ data class SettingsState(
val dialog: Dialog?,
val version: Text,
val copyrightInfo: Text,
val allowScreenCapture: Boolean,
) : Parcelable {

/**
Expand Down Expand Up @@ -460,13 +475,18 @@ sealed class SettingsAction(
}

/**
* Indicates the user clicked the Unlock with biometrics button.
* Models actions for the Security section of settings.
*/
sealed class SecurityClick : SettingsAction() {
/**
* Indicates the user clicked unlock with biometrics toggle.
*/
data class UnlockWithBiometricToggle(val enabled: Boolean) : SecurityClick()

/**
* Indicates the user clicked allow screen capture toggle.
*/
data class AllowScreenCaptureToggle(val enabled: Boolean) : SecurityClick()
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import com.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
import com.bitwarden.ui.platform.manager.IntentManager
import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.util.asText
import com.bitwarden.ui.util.assertNoDialogExists
import com.bitwarden.ui.util.concat
import io.mockk.every
import io.mockk.just
Expand Down Expand Up @@ -175,6 +176,38 @@ class SettingsScreenTest : AuthenticatorComposeTest() {
.onNode(isDialog())
.assertDoesNotExist()
}

@Test
fun `on allow screen capture confirm should send AllowScreenCaptureToggle`() {
composeTestRule.onNodeWithText("Allow screen capture").performScrollTo().performClick()
composeTestRule.onNodeWithText("Yes").performClick()
composeTestRule.assertNoDialogExists()

verify {
viewModel.trySendAction(
SettingsAction.SecurityClick.AllowScreenCaptureToggle(true),
)
}
}

@Test
fun `on allow screen capture cancel should dismiss dialog`() {
composeTestRule.onNodeWithText("Allow screen capture").performScrollTo().performClick()
composeTestRule
.onAllNodesWithText("Cancel")
.filterToOne(hasAnyAncestor(isDialog()))
.performClick()
composeTestRule.assertNoDialogExists()
}

@Test
fun `on allow screen capture row click should display confirm enable screen capture dialog`() {
composeTestRule.onNodeWithText("Allow screen capture").performScrollTo().performClick()
composeTestRule
.onAllNodesWithText("Allow screen capture")
.filterToOne(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
}
}

private val APP_LANGUAGE = AppLanguage.ENGLISH
Expand All @@ -194,4 +227,5 @@ private val DEFAULT_STATE = SettingsState(
version = BitwardenString.version.asText()
.concat(": ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})".asText()),
copyrightInfo = "ยฉ Bitwarden Inc. 2015-2024".asText(),
allowScreenCapture = false,
)
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,17 @@ class SettingsViewModelTest : BaseViewModelTest() {
every { sharedCodesStateFlow } returns mutableSharedCodesFlow
}
private val mutableDefaultSaveOptionFlow = bufferedMutableSharedFlow<DefaultSaveOption>()
private val mutableScreenCaptureAllowedStateFlow = MutableStateFlow(false)
private val settingsRepository: SettingsRepository = mockk {
every { appLanguage } returns APP_LANGUAGE
every { appTheme } returns APP_THEME
every { defaultSaveOption } returns DEFAULT_SAVE_OPTION
every { defaultSaveOptionFlow } returns mutableDefaultSaveOptionFlow
every { isUnlockWithBiometricsEnabled } returns true
every { isCrashLoggingEnabled } returns true
every { isScreenCaptureAllowedStateFlow } returns mutableScreenCaptureAllowedStateFlow
every { isScreenCaptureAllowed } answers { mutableScreenCaptureAllowedStateFlow.value }
every { isScreenCaptureAllowed = any() } just runs
}
private val clipboardManager: BitwardenClipboardManager = mockk()

Expand Down Expand Up @@ -197,6 +201,30 @@ class SettingsViewModelTest : BaseViewModelTest() {
}
}

@Test
fun `on AllowScreenCaptureToggled should update value in state and SettingsRepository`() =
runTest {
val viewModel = createViewModel()
val newScreenCaptureAllowedValue = true

viewModel.trySendAction(
SettingsAction.SecurityClick.AllowScreenCaptureToggle(
newScreenCaptureAllowedValue,
),
)

verify(exactly = 1) {
settingsRepository.isScreenCaptureAllowed = newScreenCaptureAllowedValue
}

viewModel.stateFlow.test {
assertEquals(
DEFAULT_STATE.copy(allowScreenCapture = true),
awaitItem(),
)
}
}

private fun createViewModel(
savedState: SettingsState? = DEFAULT_STATE,
) = SettingsViewModel(
Expand Down Expand Up @@ -231,4 +259,5 @@ private val DEFAULT_STATE = SettingsState(
version = BitwardenString.version.asText()
.concat(": ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})".asText()),
copyrightInfo = "ยฉ Bitwarden Inc. 2015-2024".asText(),
allowScreenCapture = false,
)
Loading