PM-14179: Create and apply card style to UI (#4567)

This commit is contained in:
David Perez authored and GitHub committed 2025-01-15 22:51:14 +00:00
1 parent 2672f426dc
commit 5840bcdf85
110 files changed
+3442 -2010

No files matched your search

@@ -42,6 +42,7 @@ import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
import com.x8bit.bitwarden.ui.platform.components.image.BitwardenGifImage
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
@@ -167,6 +168,7 @@ private fun SetupAutoFillContent(
),
isChecked = state.autofillEnabled,
onCheckedChange = onAutofillServiceChanged,
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
@@ -46,11 +46,13 @@ import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenUnlockWithBiometricsSwitch
import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenUnlockWithPinSwitch
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.x8bit.bitwarden.ui.platform.composition.LocalBiometricsManager
import com.x8bit.bitwarden.ui.platform.manager.biometrics.BiometricSupportStatus
import com.x8bit.bitwarden.ui.platform.manager.biometrics.BiometricsManager
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
import com.x8bit.bitwarden.ui.platform.util.isPortrait
@@ -156,11 +158,13 @@ private fun SetupUnlockScreenContent(
}
Spacer(modifier = Modifier.height(height = 24.dp))
val biometricSupportStatus = biometricsManager.biometricSupportStatus
BitwardenUnlockWithBiometricsSwitch(
biometricSupportStatus = biometricsManager.biometricSupportStatus,
biometricSupportStatus = biometricSupportStatus,
isChecked = state.isUnlockWithBiometricsEnabled || showBiometricsPrompt,
onDisableBiometrics = handler.onDisableBiometrics,
onEnableBiometrics = handler.onEnableBiometrics,
cardStyle = CardStyle.Top(),
modifier = Modifier
.testTag(tag = "UnlockWithBiometricsSwitch")
.fillMaxWidth()
@@ -170,6 +174,11 @@ private fun SetupUnlockScreenContent(
isUnlockWithPasswordEnabled = state.isUnlockWithPasswordEnabled,
isUnlockWithPinEnabled = state.isUnlockWithPinEnabled,
onUnlockWithPinToggleAction = handler.onUnlockWithPinToggle,
cardStyle = if (biometricSupportStatus == BiometricSupportStatus.NOT_SUPPORTED) {
CardStyle.Full
} else {
CardStyle.Bottom
},
modifier = Modifier
.testTag(tag = "UnlockWithPinSwitch")
.fillMaxWidth()
@@ -5,6 +5,7 @@ import android.widget.Toast
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
@@ -52,6 +53,7 @@ import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordField
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.text.BitwardenClickableText
import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch
@@ -162,6 +164,7 @@ fun CompleteRegistrationScreen(
.fillMaxSize()
.verticalScroll(rememberScrollState()),
) {
Spacer(modifier = Modifier.height(height = 12.dp))
CompleteRegistrationContent(
passwordInput = state.passwordInput,
passwordStrengthState = state.passwordStrengthState,
@@ -175,6 +178,7 @@ fun CompleteRegistrationScreen(
showNewOnboardingUi = state.onboardingEnabled,
userEmail = state.userEmail,
)
Spacer(modifier = Modifier.height(height = 16.dp))
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
@@ -200,7 +204,6 @@ private fun CompleteRegistrationContent(
modifier = modifier
.fillMaxWidth(),
) {
Spacer(modifier = Modifier.height(8.dp))
if (showNewOnboardingUi) {
CompleteRegistrationContentHeader(
modifier = Modifier
@@ -242,23 +245,22 @@ private fun CompleteRegistrationContent(
showPasswordChange = { showPassword = it },
value = passwordInput,
onValueChange = handler.onPasswordInputChange,
hint = stringResource(id = R.string.master_password_important_hint)
.takeIf { !showNewOnboardingUi },
showPasswordTestTag = "PasswordVisibilityToggle",
imeAction = ImeAction.Next,
supportingTextContent = {
PasswordStrengthIndicator(
state = passwordStrengthState,
currentCharacterCount = passwordInput.length,
minimumCharacterCount = minimumPasswordLength.takeIf { showNewOnboardingUi },
modifier = Modifier.fillMaxWidth(),
)
},
cardStyle = CardStyle.Top(dividerPadding = 0.dp),
modifier = Modifier
.testTag("MasterPasswordEntry")
.fillMaxWidth()
.standardHorizontalMargin(),
showPasswordTestTag = "PasswordVisibilityToggle",
imeAction = ImeAction.Next,
)
Spacer(modifier = Modifier.height(8.dp))
PasswordStrengthIndicator(
state = passwordStrengthState,
currentCharacterCount = passwordInput.length,
minimumCharacterCount = minimumPasswordLength.takeIf { showNewOnboardingUi },
modifier = Modifier.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(16.dp))
BitwardenPasswordField(
label = stringResource(
id = R.string.retype_master_password_required
@@ -269,13 +271,13 @@ private fun CompleteRegistrationContent(
showPassword = showPassword,
showPasswordChange = { showPassword = it },
onValueChange = handler.onConfirmPasswordInputChange,
showPasswordTestTag = "ConfirmPasswordVisibilityToggle",
cardStyle = CardStyle.Middle(dividerPadding = 0.dp),
modifier = Modifier
.testTag("ConfirmMasterPasswordEntry")
.fillMaxWidth()
.standardHorizontalMargin(),
showPasswordTestTag = "ConfirmPasswordVisibilityToggle",
)
Spacer(modifier = Modifier.height(16.dp))
BitwardenTextField(
label = stringResource(
id = R.string.master_password_hint_not_specified
@@ -284,37 +286,46 @@ private fun CompleteRegistrationContent(
),
value = passwordHintInput,
onValueChange = handler.onPasswordHintChange,
hint = stringResource(
id = R.string.bitwarden_cannot_recover_a_lost_or_forgotten_master_password
.takeIf { showNewOnboardingUi }
?: R.string.master_password_hint_description,
),
supportingTextContent = {
Text(
text = stringResource(
id = R.string.bitwarden_cannot_recover_a_lost_or_forgotten_master_password
.takeIf { showNewOnboardingUi }
?: R.string.master_password_hint_description,
),
style = BitwardenTheme.typography.bodySmall,
color = BitwardenTheme.colorScheme.text.secondary,
modifier = Modifier.fillMaxWidth(),
)
if (showNewOnboardingUi) {
BitwardenClickableText(
label = stringResource(
id = R.string.learn_about_other_ways_to_prevent_account_lockout,
),
onClick = handler.onLearnToPreventLockout,
style = BitwardenTheme.typography.labelMedium,
innerPadding = PaddingValues(vertical = 4.dp),
)
}
},
textFieldTestTag = "MasterPasswordHintLabel",
cardStyle = CardStyle.Bottom,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
textFieldTestTag = "MasterPasswordHintLabel",
)
if (showNewOnboardingUi) {
BitwardenClickableText(
label = stringResource(
id = R.string.learn_about_other_ways_to_prevent_account_lockout,
),
onClick = handler.onLearnToPreventLockout,
style = BitwardenTheme.typography.labelMedium,
modifier = Modifier.standardHorizontalMargin(),
)
}
Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.height(height = 8.dp))
BitwardenSwitch(
label = stringResource(id = R.string.check_known_data_breaches_for_this_password),
isChecked = isCheckDataBreachesToggled,
onCheckedChange = handler.onCheckDataBreachesToggle,
cardStyle = CardStyle.Full,
modifier = Modifier
.testTag("CheckExposedMasterPasswordToggle")
.standardHorizontalMargin(),
)
if (showNewOnboardingUi) {
Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenFilledButton(
label = callToActionText,
isEnabled = nextButtonEnabled,
@@ -20,6 +20,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.graphicsLayer
@@ -37,9 +38,9 @@ import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
@Suppress("LongMethod", "CyclomaticComplexMethod", "MagicNumber")
@Composable
fun PasswordStrengthIndicator(
modifier: Modifier = Modifier,
state: PasswordStrengthState,
currentCharacterCount: Int,
modifier: Modifier = Modifier,
minimumCharacterCount: Int? = null,
) {
val minimumRequirementMet = (minimumCharacterCount == null) ||
@@ -86,6 +87,7 @@ fun PasswordStrengthIndicator(
Modifier
.fillMaxWidth()
.height(4.dp)
.clip(shape = BitwardenTheme.shapes.progressIndicator)
.background(BitwardenTheme.colorScheme.sliderButton.unfilled),
) {
Box(
@@ -53,6 +53,7 @@ import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.Ter
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountEvent.NavigateToPrivacyPolicy
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountEvent.NavigateToTerms
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
@@ -60,6 +61,7 @@ import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordField
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.text.BitwardenClickableText
import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch
@@ -179,43 +181,50 @@ fun CreateAccountScreen(
.fillMaxSize()
.verticalScroll(rememberScrollState()),
) {
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(height = 12.dp))
BitwardenTextField(
label = stringResource(id = R.string.email_address),
value = state.emailInput,
onValueChange = remember(viewModel) {
{ viewModel.trySendAction(EmailInputChange(it)) }
},
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
keyboardType = KeyboardType.Email,
textFieldTestTag = "EmailAddressEntry",
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(height = 8.dp))
var showPassword by rememberSaveable { mutableStateOf(false) }
BitwardenPasswordField(
label = stringResource(id = R.string.master_password),
showPassword = showPassword,
showPasswordChange = { showPassword = it },
value = state.passwordInput,
hint = state.passwordLengthLabel(),
onValueChange = remember(viewModel) {
{ viewModel.trySendAction(PasswordInputChange(it)) }
},
showPasswordTestTag = "PasswordVisibilityToggle",
supportingTextContent = {
PasswordStrengthIndicator(
modifier = Modifier.fillMaxWidth(),
state = state.passwordStrengthState,
currentCharacterCount = state.passwordInput.length,
)
Text(
modifier = Modifier.fillMaxWidth(),
text = state.passwordLengthLabel(),
style = BitwardenTheme.typography.bodySmall,
color = BitwardenTheme.colorScheme.text.secondary,
)
},
cardStyle = CardStyle.Top(dividerPadding = 0.dp),
modifier = Modifier
.testTag("MasterPasswordEntry")
.fillMaxWidth()
.padding(horizontal = 16.dp),
showPasswordTestTag = "PasswordVisibilityToggle",
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(8.dp))
PasswordStrengthIndicator(
modifier = Modifier.padding(horizontal = 16.dp),
state = state.passwordStrengthState,
currentCharacterCount = state.passwordInput.length,
)
Spacer(modifier = Modifier.height(8.dp))
BitwardenPasswordField(
label = stringResource(id = R.string.retype_master_password),
value = state.confirmPasswordInput,
@@ -224,26 +233,27 @@ fun CreateAccountScreen(
onValueChange = remember(viewModel) {
{ viewModel.trySendAction(ConfirmPasswordInputChange(it)) }
},
showPasswordTestTag = "ConfirmPasswordVisibilityToggle",
cardStyle = CardStyle.Middle(dividerPadding = 0.dp),
modifier = Modifier
.testTag("ConfirmMasterPasswordEntry")
.fillMaxWidth()
.padding(horizontal = 16.dp),
showPasswordTestTag = "ConfirmPasswordVisibilityToggle",
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(16.dp))
BitwardenTextField(
label = stringResource(id = R.string.master_password_hint),
value = state.passwordHintInput,
onValueChange = remember(viewModel) {
{ viewModel.trySendAction(PasswordHintChange(it)) }
},
hint = stringResource(id = R.string.master_password_hint_description),
supportingText = stringResource(id = R.string.master_password_hint_description),
textFieldTestTag = "MasterPasswordHintLabel",
cardStyle = CardStyle.Bottom,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
textFieldTestTag = "MasterPasswordHintLabel",
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.height(height = 8.dp))
BitwardenSwitch(
label = stringResource(id = R.string.check_known_data_breaches_for_this_password),
isChecked = state.isCheckDataBreachesToggled,
@@ -252,11 +262,12 @@ fun CreateAccountScreen(
viewModel.trySendAction(CheckDataBreachesToggle(newState = newState))
}
},
cardStyle = CardStyle.Top(),
modifier = Modifier
.testTag("CheckExposedMasterPasswordToggle")
.padding(horizontal = 16.dp),
.fillMaxWidth()
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(8.dp))
TermsAndPrivacySwitch(
isChecked = state.isAcceptPoliciesToggled,
onCheckedChange = remember(viewModel) {
@@ -270,8 +281,9 @@ fun CreateAccountScreen(
},
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(height = 16.dp))
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
@@ -292,7 +304,7 @@ private fun TermsAndPrivacySwitch(
isChecked = isChecked,
contentDescription = "AcceptPoliciesToggle",
onCheckedChange = onCheckedChange,
subContent = {
supportingTextContent = {
FlowRow(
horizontalArrangement = Arrangement.Start,
modifier = Modifier.fillMaxWidth(),
@@ -301,7 +313,7 @@ private fun TermsAndPrivacySwitch(
label = stringResource(id = R.string.terms_of_service),
onClick = onTermsClick,
style = BitwardenTheme.typography.bodyMedium,
innerPadding = PaddingValues(vertical = 4.dp, horizontal = 0.dp),
innerPadding = PaddingValues(vertical = 8.dp, horizontal = 0.dp),
)
Text(
text = ",",
@@ -314,9 +326,10 @@ private fun TermsAndPrivacySwitch(
label = stringResource(id = R.string.privacy_policy),
onClick = onPrivacyPolicyClick,
style = BitwardenTheme.typography.bodyMedium,
innerPadding = PaddingValues(vertical = 4.dp, horizontal = 0.dp),
innerPadding = PaddingValues(vertical = 8.dp, horizontal = 0.dp),
)
}
},
cardStyle = CardStyle.Bottom,
)
}
@@ -6,7 +6,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -27,11 +27,13 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager
@@ -144,28 +146,31 @@ private fun EnterpriseSignOnScreenContent(
.verticalScroll(rememberScrollState())
.fillMaxWidth(),
) {
Spacer(modifier = Modifier.height(height = 12.dp))
Text(
text = stringResource(id = R.string.log_in_sso_summary),
textAlign = TextAlign.Start,
style = BitwardenTheme.typography.bodyMedium,
color = BitwardenTheme.colorScheme.text.primary,
modifier = Modifier
.padding(horizontal = 16.dp)
.standardHorizontalMargin()
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(height = 8.dp))
BitwardenTextField(
modifier = Modifier
.padding(horizontal = 16.dp)
.standardHorizontalMargin()
.fillMaxWidth(),
value = state.orgIdentifierInput,
onValueChange = onOrgIdentifierInputChange,
label = stringResource(id = R.string.org_identifier),
textFieldTestTag = "OrgSSOIdentifierEntry",
cardStyle = CardStyle.Full,
)
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
@@ -29,11 +29,13 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.x8bit.bitwarden.BuildConfig
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
import kotlinx.collections.immutable.persistentListOf
@@ -100,20 +102,22 @@ fun EnvironmentScreen(
.imePadding()
.verticalScroll(rememberScrollState()),
) {
Spacer(modifier = Modifier.height(height = 12.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.self_hosted_environment),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(height = 8.dp))
BitwardenTextField(
label = stringResource(id = R.string.server_url),
value = state.serverUrl,
placeholder = "ex. https://bitwarden.company.com",
hint = stringResource(id = R.string.self_hosted_environment_footer),
supportingText = stringResource(id = R.string.self_hosted_environment_footer),
onValueChange = remember(viewModel) {
{ viewModel.trySendAction(EnvironmentAction.ServerUrlChange(it)) }
},
@@ -127,22 +131,24 @@ fun EnvironmentScreen(
persistentListOf()
},
keyboardType = KeyboardType.Uri,
textFieldTestTag = "ServerUrlEntry",
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
textFieldTestTag = "ServerUrlEntry",
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.custom_environment),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(height = 8.dp))
BitwardenTextField(
label = stringResource(id = R.string.web_vault_url),
@@ -151,13 +157,14 @@ fun EnvironmentScreen(
{ viewModel.trySendAction(EnvironmentAction.WebVaultServerUrlChange(it)) }
},
keyboardType = KeyboardType.Uri,
textFieldTestTag = "WebVaultUrlEntry",
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
textFieldTestTag = "WebVaultUrlEntry",
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(height = 8.dp))
BitwardenTextField(
label = stringResource(id = R.string.api_url),
@@ -166,13 +173,14 @@ fun EnvironmentScreen(
{ viewModel.trySendAction(EnvironmentAction.ApiServerUrlChange(it)) }
},
keyboardType = KeyboardType.Uri,
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.testTag("ApiUrlEntry")
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(height = 8.dp))
BitwardenTextField(
label = stringResource(id = R.string.identity_url),
@@ -181,13 +189,14 @@ fun EnvironmentScreen(
{ viewModel.trySendAction(EnvironmentAction.IdentityServerUrlChange(it)) }
},
keyboardType = KeyboardType.Uri,
textFieldTestTag = "IdentityUrlEntry",
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
textFieldTestTag = "IdentityUrlEntry",
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(height = 8.dp))
BitwardenTextField(
label = stringResource(id = R.string.icons_url),
@@ -195,14 +204,16 @@ fun EnvironmentScreen(
onValueChange = remember(viewModel) {
{ viewModel.trySendAction(EnvironmentAction.IconsServerUrlChange(it)) }
},
hint = stringResource(id = R.string.custom_environment_footer),
supportingText = stringResource(id = R.string.custom_environment_footer),
keyboardType = KeyboardType.Uri,
textFieldTestTag = "IconsUrlEntry",
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
textFieldTestTag = "IconsUrlEntry",
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(height = 16.dp))
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
@@ -10,7 +10,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.rememberScrollState
@@ -39,6 +38,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.platform.repository.model.Environment
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.x8bit.bitwarden.ui.platform.components.account.BitwardenAccountSwitcher
import com.x8bit.bitwarden.ui.platform.components.account.BitwardenPlaceholderAccountActionItem
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
@@ -48,6 +48,7 @@ import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
import com.x8bit.bitwarden.ui.platform.components.dropdown.EnvironmentSelector
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
@@ -216,7 +217,7 @@ private fun LandingScreenContent(
colorFilter = ColorFilter.tint(BitwardenTheme.colorScheme.icon.secondary),
contentDescription = null,
modifier = Modifier
.padding(horizontal = 16.dp)
.standardHorizontalMargin()
.width(220.dp)
.height(74.dp)
.fillMaxWidth(),
@@ -232,7 +233,7 @@ private fun LandingScreenContent(
style = BitwardenTheme.typography.headlineSmall,
color = BitwardenTheme.colorScheme.text.primary,
modifier = Modifier
.padding(horizontal = 24.dp)
.standardHorizontalMargin()
.wrapContentHeight(),
)
@@ -242,38 +243,40 @@ private fun LandingScreenContent(
BitwardenTextField(
modifier = Modifier
.padding(horizontal = 16.dp)
.standardHorizontalMargin()
.fillMaxWidth(),
value = state.emailInput,
onValueChange = onEmailInputChange,
label = stringResource(id = R.string.email_address),
keyboardType = KeyboardType.Email,
textFieldTestTag = "EmailAddressEntry",
cardStyle = CardStyle.Full,
supportingTextContent = {
EnvironmentSelector(
labelText = stringResource(id = R.string.logging_in_on),
selectedOption = state.selectedEnvironmentType,
onOptionSelected = onEnvironmentTypeSelect,
modifier = Modifier
.testTag("RegionSelectorDropdown")
.fillMaxWidth(),
)
},
)
Spacer(modifier = Modifier.height(2.dp))
EnvironmentSelector(
labelText = stringResource(id = R.string.logging_in_on),
selectedOption = state.selectedEnvironmentType,
onOptionSelected = onEnvironmentTypeSelect,
modifier = Modifier
.testTag("RegionSelectorDropdown")
.padding(horizontal = 16.dp)
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(height = 8.dp))
BitwardenSwitch(
label = stringResource(id = R.string.remember_me),
isChecked = state.isRememberMeEnabled,
onCheckedChange = onRememberMeToggle,
cardStyle = CardStyle.Full,
modifier = Modifier
.testTag("RememberMeSwitch")
.padding(horizontal = 16.dp)
.standardHorizontalMargin()
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(32.dp))
Spacer(modifier = Modifier.height(height = 24.dp))
BitwardenFilledButton(
label = stringResource(id = R.string.continue_text),
@@ -281,17 +284,17 @@ private fun LandingScreenContent(
isEnabled = state.isContinueButtonEnabled,
modifier = Modifier
.testTag("ContinueButton")
.padding(horizontal = 16.dp)
.standardHorizontalMargin()
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.height(height = 24.dp))
Row(
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.padding(horizontal = 16.dp)
.standardHorizontalMargin()
.fillMaxWidth()
.wrapContentHeight(),
) {
@@ -309,7 +312,7 @@ private fun LandingScreenContent(
)
}
Spacer(modifier = Modifier.height(58.dp))
Spacer(modifier = Modifier.height(height = 16.dp))
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
@@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -22,6 +21,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
@@ -35,6 +35,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.x8bit.bitwarden.ui.platform.components.account.BitwardenAccountSwitcher
import com.x8bit.bitwarden.ui.platform.components.account.BitwardenPlaceholderAccountActionItem
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
@@ -45,6 +46,7 @@ import com.x8bit.bitwarden.ui.platform.components.button.BitwardenOutlinedButton
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordField
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.text.BitwardenClickableText
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
@@ -230,28 +232,35 @@ private fun LoginScreenContent(
.imePadding()
.verticalScroll(rememberScrollState()),
) {
Spacer(modifier = Modifier.height(height = 12.dp))
BitwardenPasswordField(
autoFocus = true,
modifier = Modifier
.testTag("MasterPasswordEntry")
.padding(horizontal = 16.dp)
.fillMaxWidth(),
value = state.passwordInput,
onValueChange = onPasswordInputChanged,
label = stringResource(id = R.string.master_password),
showPasswordTestTag = "PasswordVisibilityToggle",
)
BitwardenClickableText(
label = stringResource(id = R.string.get_master_passwordword_hint),
onClick = onMasterPasswordClick,
style = BitwardenTheme.typography.bodySmall,
supportingTextContent = {
BitwardenClickableText(
label = stringResource(id = R.string.get_master_passwordword_hint),
onClick = onMasterPasswordClick,
style = BitwardenTheme.typography.bodySmall,
innerPadding = PaddingValues(
top = 8.dp,
bottom = 8.dp,
start = 0.dp,
end = 16.dp,
),
modifier = Modifier.testTag("GetMasterPasswordHintLabel"),
)
},
cardStyle = CardStyle.Full,
modifier = Modifier
.padding(horizontal = 16.dp)
.testTag("GetMasterPasswordHintLabel"),
.testTag("MasterPasswordEntry")
.standardHorizontalMargin()
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(20.dp))
Spacer(modifier = Modifier.height(height = 24.dp))
BitwardenFilledButton(
label = stringResource(id = R.string.log_in_with_master_password),
@@ -259,7 +268,7 @@ private fun LoginScreenContent(
isEnabled = state.isLoginButtonEnabled,
modifier = Modifier
.testTag("LogInWithMasterPasswordButton")
.padding(horizontal = 16.dp)
.standardHorizontalMargin()
.fillMaxWidth(),
)
@@ -272,7 +281,7 @@ private fun LoginScreenContent(
onClick = onLoginWithDeviceClick,
modifier = Modifier
.testTag("LogInWithAnotherDeviceButton")
.padding(horizontal = 16.dp)
.standardHorizontalMargin()
.fillMaxWidth(),
)
@@ -285,7 +294,7 @@ private fun LoginScreenContent(
onClick = onSingleSignOnClick,
modifier = Modifier
.testTag("LogInWithSsoButton")
.padding(horizontal = 16.dp)
.standardHorizontalMargin()
.fillMaxWidth(),
)
@@ -297,12 +306,12 @@ private fun LoginScreenContent(
state.emailAddress,
state.environmentLabel,
),
textAlign = TextAlign.Start,
textAlign = TextAlign.Center,
style = BitwardenTheme.typography.bodyMedium,
color = BitwardenTheme.colorScheme.text.primary,
modifier = Modifier
.testTag("LoggingInAsLabel")
.padding(horizontal = 16.dp)
.standardHorizontalMargin()
.fillMaxWidth(),
)
@@ -311,8 +320,12 @@ private fun LoginScreenContent(
onClick = onNotYouButtonClick,
style = BitwardenTheme.typography.labelLarge,
innerPadding = PaddingValues(vertical = 8.dp, horizontal = 16.dp),
modifier = Modifier.testTag("NotYouLabel"),
modifier = Modifier
.standardHorizontalMargin()
.align(alignment = Alignment.CenterHorizontally)
.testTag("NotYouLabel"),
)
Spacer(modifier = Modifier.height(height = 16.dp))
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
@@ -33,6 +33,7 @@ import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenFilledButton
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarData
import com.x8bit.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarHost
@@ -122,6 +123,7 @@ fun MasterPasswordGeneratorScreen(
},
modifier = Modifier.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(height = 16.dp))
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
@@ -137,7 +139,7 @@ private fun MasterPasswordGeneratorContent(
Column(
modifier = modifier.fillMaxWidth(),
) {
Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.height(height = 12.dp))
BitwardenTextField(
label = "",
value = generatedPassword,
@@ -146,6 +148,7 @@ private fun MasterPasswordGeneratorContent(
shouldAddCustomLineBreaks = true,
textStyle = BitwardenTheme.typography.sensitiveInfoSmall,
visualTransformation = nonLetterColorVisualTransformation(),
cardStyle = CardStyle.Full,
modifier = Modifier.fillMaxWidth(),
)
@@ -1,11 +1,12 @@
package com.x8bit.bitwarden.ui.auth.feature.masterpasswordhint
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
@@ -21,14 +22,15 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
/**
* The top level composable for the Login screen.
@@ -107,9 +109,10 @@ fun MasterPasswordHintScreen(
Column(
modifier = Modifier.fillMaxSize(),
) {
Spacer(modifier = Modifier.height(height = 12.dp))
BitwardenTextField(
modifier = Modifier
.padding(horizontal = 16.dp)
.standardHorizontalMargin()
.fillMaxWidth(),
value = state.emailInput,
onValueChange = remember(viewModel) {
@@ -118,19 +121,11 @@ fun MasterPasswordHintScreen(
label = stringResource(id = R.string.email_address),
keyboardType = KeyboardType.Email,
textFieldTestTag = "MasterPasswordHintEmailField",
supportingText = stringResource(id = R.string.enter_email_for_hint),
cardStyle = CardStyle.Full,
)
Text(
text = stringResource(id = R.string.enter_email_for_hint),
style = BitwardenTheme.typography.bodySmall,
color = BitwardenTheme.colorScheme.text.secondary,
modifier = Modifier
.fillMaxWidth()
.padding(
vertical = 4.dp,
horizontal = 16.dp,
),
)
Spacer(modifier = Modifier.height(height = 16.dp))
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
}
@@ -37,6 +37,7 @@ import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.x8bit.bitwarden.ui.platform.base.util.toAnnotatedString
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenFilledButton
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.text.BitwardenClickableText
import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch
@@ -190,15 +191,15 @@ private fun MainContent(
),
),
)
Column {
BitwardenSwitch(
label = stringResource(id = R.string.yes_i_can_reliably_access_my_email),
isChecked = isEmailAccessEnabled,
onCheckedChange = onEmailAccessToggleChanged,
modifier = Modifier
.testTag("EmailAccessToggle"),
)
}
Spacer(modifier = Modifier.height(height = 12.dp))
BitwardenSwitch(
label = stringResource(id = R.string.yes_i_can_reliably_access_my_email),
isChecked = isEmailAccessEnabled,
onCheckedChange = onEmailAccessToggleChanged,
cardStyle = CardStyle.Full,
modifier = Modifier
.testTag("EmailAccessToggle"),
)
}
}
@@ -31,6 +31,7 @@ import com.x8bit.bitwarden.ui.platform.components.button.BitwardenFilledButton
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordField
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
@@ -87,7 +88,7 @@ private fun RemovePasswordScreenContent(
modifier = modifier
.verticalScroll(rememberScrollState()),
) {
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.height(height = 12.dp))
Text(
text = state.description(),
@@ -104,11 +105,12 @@ private fun RemovePasswordScreenContent(
value = state.input,
onValueChange = onInputChanged,
showPasswordTestTag = "PasswordVisibilityToggle",
autoFocus = true,
cardStyle = CardStyle.Full,
modifier = Modifier
.testTag(tag = "MasterPasswordEntry")
.standardHorizontalMargin()
.fillMaxWidth(),
autoFocus = true,
)
Spacer(modifier = Modifier.height(24.dp))
@@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -29,6 +28,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.datasource.disk.model.ForcePasswordResetReason
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton
import com.x8bit.bitwarden.ui.platform.components.card.BitwardenInfoCalloutCard
@@ -37,6 +37,7 @@ import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordField
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
/**
@@ -149,7 +150,7 @@ private fun ResetPasswordScreenContent(
.imePadding()
.verticalScroll(rememberScrollState()),
) {
Spacer(modifier = Modifier.height(height = 12.dp))
val instructionsTextId =
if (state.resetReason == ForcePasswordResetReason.WEAK_MASTER_PASSWORD_ON_LOGIN) {
R.string.update_weak_master_password_warning
@@ -159,7 +160,7 @@ private fun ResetPasswordScreenContent(
BitwardenInfoCalloutCard(
text = stringResource(id = instructionsTextId),
modifier = Modifier
.padding(horizontal = 16.dp)
.standardHorizontalMargin()
.fillMaxWidth(),
)
@@ -174,7 +175,7 @@ private fun ResetPasswordScreenContent(
BitwardenInfoCalloutCard(
text = passwordPolicyContent,
modifier = Modifier
.padding(horizontal = 16.dp)
.standardHorizontalMargin()
.fillMaxWidth(),
)
@@ -184,13 +185,12 @@ private fun ResetPasswordScreenContent(
label = stringResource(id = R.string.current_master_password),
value = state.currentPasswordInput,
onValueChange = onCurrentPasswordInputChanged,
cardStyle = CardStyle.Top(dividerPadding = 0.dp),
modifier = Modifier
.testTag("MasterPasswordField")
.padding(horizontal = 16.dp)
.standardHorizontalMargin()
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(16.dp))
}
var isPasswordVisible by rememberSaveable { mutableStateOf(false) }
@@ -200,37 +200,41 @@ private fun ResetPasswordScreenContent(
onValueChange = onPasswordInputChanged,
showPassword = isPasswordVisible,
showPasswordChange = { isPasswordVisible = it },
cardStyle = if (
state.resetReason == ForcePasswordResetReason.WEAK_MASTER_PASSWORD_ON_LOGIN) {
CardStyle.Middle(dividerPadding = 0.dp)
} else {
CardStyle.Top(dividerPadding = 0.dp)
},
modifier = Modifier
.testTag("NewPasswordField")
.padding(horizontal = 16.dp)
.standardHorizontalMargin()
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(16.dp))
BitwardenPasswordField(
label = stringResource(id = R.string.retype_master_password),
value = state.retypePasswordInput,
onValueChange = onRetypePasswordInputChanged,
showPassword = isPasswordVisible,
showPasswordChange = { isPasswordVisible = it },
cardStyle = CardStyle.Middle(dividerPadding = 0.dp),
modifier = Modifier
.testTag("RetypePasswordField")
.padding(horizontal = 16.dp)
.standardHorizontalMargin()
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(16.dp))
BitwardenTextField(
label = stringResource(id = R.string.master_password_hint),
value = state.passwordHintInput,
onValueChange = onPasswordHintInputChanged,
hint = stringResource(id = R.string.master_password_hint_description),
supportingText = stringResource(id = R.string.master_password_hint_description),
textFieldTestTag = "MasterPasswordHintLabel",
cardStyle = CardStyle.Bottom,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
textFieldTestTag = "MasterPasswordHintLabel",
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.navigationBarsPadding())
@@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -28,6 +27,7 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton
import com.x8bit.bitwarden.ui.platform.components.card.BitwardenInfoCalloutCard
@@ -35,6 +35,7 @@ import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordField
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
@@ -114,6 +115,7 @@ private fun SetPasswordScreenContent(
modifier = modifier
.verticalScroll(rememberScrollState()),
) {
Spacer(modifier = Modifier.height(height = 12.dp))
Text(
text = stringResource(
id = R.string.your_organization_requires_you_to_set_a_master_password,
@@ -121,7 +123,7 @@ private fun SetPasswordScreenContent(
style = BitwardenTheme.typography.bodyMedium,
color = BitwardenTheme.colorScheme.text.primary,
modifier = Modifier
.padding(horizontal = 16.dp)
.standardHorizontalMargin()
.fillMaxWidth(),
)
@@ -130,7 +132,7 @@ private fun SetPasswordScreenContent(
BitwardenInfoCalloutCard(
text = stringResource(id = R.string.reset_password_auto_enroll_invite_warning),
modifier = Modifier
.padding(horizontal = 16.dp)
.standardHorizontalMargin()
.fillMaxWidth(),
)
@@ -143,40 +145,40 @@ private fun SetPasswordScreenContent(
onValueChange = onPasswordInputChanged,
showPassword = isPasswordVisible,
showPasswordChange = { isPasswordVisible = it },
hint = stringResource(id = R.string.master_password_description),
supportingText = stringResource(id = R.string.master_password_description),
cardStyle = CardStyle.Top(dividerPadding = 0.dp),
modifier = Modifier
.testTag("NewPasswordField")
.padding(horizontal = 16.dp)
.standardHorizontalMargin()
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(16.dp))
BitwardenPasswordField(
label = stringResource(id = R.string.retype_master_password),
value = state.retypePasswordInput,
onValueChange = onRetypePasswordInputChanged,
showPassword = isPasswordVisible,
showPasswordChange = { isPasswordVisible = it },
cardStyle = CardStyle.Middle(dividerPadding = 0.dp),
modifier = Modifier
.testTag("RetypePasswordField")
.padding(horizontal = 16.dp)
.standardHorizontalMargin()
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(16.dp))
BitwardenTextField(
label = stringResource(id = R.string.master_password_hint),
value = state.passwordHintInput,
onValueChange = onPasswordHintInputChanged,
hint = stringResource(id = R.string.master_password_hint_description),
supportingText = stringResource(id = R.string.master_password_hint_description),
textFieldTestTag = "MasterPasswordHintLabel",
cardStyle = CardStyle.Bottom,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
textFieldTestTag = "MasterPasswordHintLabel",
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(height = 12.dp))
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
@@ -59,6 +59,7 @@ import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
import com.x8bit.bitwarden.ui.platform.components.dropdown.EnvironmentSelector
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
@@ -177,6 +178,7 @@ fun StartRegistrationScreen(
isNewOnboardingUiEnabled = state.showNewOnboardingUi,
handler = handler,
)
Spacer(modifier = Modifier.height(height = 16.dp))
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
@@ -195,7 +197,7 @@ private fun StartRegistrationContent(
modifier: Modifier = Modifier,
) {
Column(modifier = modifier) {
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(height = 12.dp))
if (isNewOnboardingUiEnabled) {
Image(
painter = rememberVectorPainter(id = R.drawable.vault),
@@ -210,50 +212,50 @@ private fun StartRegistrationContent(
label = stringResource(id = R.string.name),
value = nameInput,
onValueChange = handler.onNameInputChange,
textFieldTestTag = "NameEntry",
cardStyle = CardStyle.Top(dividerPadding = 0.dp),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
textFieldTestTag = "NameEntry",
)
Spacer(modifier = Modifier.height(16.dp))
BitwardenTextField(
label = stringResource(
id = R.string.email_address,
),
label = stringResource(id = R.string.email_address),
placeholder = stringResource(R.string.email_address_required),
value = emailInput,
onValueChange = handler.onEmailInputChange,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
keyboardType = KeyboardType.Email,
textFieldTestTag = "EmailAddressEntry",
)
Spacer(modifier = Modifier.height(2.dp))
Row(
supportingTextContent = {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth(),
) {
EnvironmentSelector(
labelText = stringResource(id = R.string.creating_on),
selectedOption = selectedEnvironmentType,
onOptionSelected = handler.onEnvironmentTypeSelect,
modifier = Modifier.testTag("RegionSelectorDropdown"),
)
if (isNewOnboardingUiEnabled) {
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_question_circle_small,
contentDescription = stringResource(
R.string.help_with_server_geolocations,
),
onClick = handler.onServerGeologyHelpClick,
contentColor = BitwardenTheme.colorScheme.icon.secondary,
// Align with design but keep accessible touch target of IconButton.
modifier = Modifier.offset(x = 16.dp),
)
}
}
},
cardStyle = CardStyle.Bottom,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
horizontalArrangement = Arrangement.SpaceBetween,
) {
EnvironmentSelector(
labelText = stringResource(id = R.string.creating_on),
selectedOption = selectedEnvironmentType,
onOptionSelected = handler.onEnvironmentTypeSelect,
modifier = Modifier
.testTag("RegionSelectorDropdown"),
)
if (isNewOnboardingUiEnabled) {
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_question_circle_small,
contentDescription = stringResource(R.string.help_with_server_geolocations),
onClick = handler.onServerGeologyHelpClick,
contentColor = BitwardenTheme.colorScheme.icon.secondary,
// Align with design but keep accessible touch target of IconButton.
modifier = Modifier.offset(y = (-8f).dp, x = 16.dp),
)
}
}
)
Spacer(modifier = Modifier.height(24.dp))
if (selectedEnvironmentType != Environment.Type.SELF_HOSTED) {
@@ -367,6 +369,7 @@ private fun ReceiveMarketingEmailsSwitch(
isChecked = isChecked,
onCheckedChange = onCheckedChange,
contentDescription = "ReceiveMarketingEmailsToggle",
cardStyle = CardStyle.Full,
)
}
@@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -18,11 +17,13 @@ import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
@@ -30,12 +31,14 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.auth.feature.trusteddevice.handlers.TrustedDeviceHandlers
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
import com.x8bit.bitwarden.ui.platform.components.appbar.NavigationIcon
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenFilledButton
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenOutlinedButton
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.text.BitwardenClickableText
import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch
@@ -118,15 +121,16 @@ private fun TrustedDeviceScaffold(
.fillMaxSize()
.verticalScroll(rememberScrollState()),
) {
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.height(height = 12.dp))
BitwardenSwitch(
label = stringResource(id = R.string.remember_this_device),
description = stringResource(id = R.string.turn_off_using_public_device),
supportingText = stringResource(id = R.string.turn_off_using_public_device),
isChecked = state.isRemembered,
onCheckedChange = handlers.onRememberToggle,
cardStyle = CardStyle.Full,
modifier = Modifier
.testTag("RememberThisDeviceSwitch")
.padding(horizontal = 16.dp)
.standardHorizontalMargin()
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(24.dp))
@@ -136,7 +140,7 @@ private fun TrustedDeviceScaffold(
label = stringResource(id = R.string.continue_text),
onClick = handlers.onContinueClick,
modifier = Modifier
.padding(horizontal = 16.dp)
.standardHorizontalMargin()
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(12.dp))
@@ -148,7 +152,7 @@ private fun TrustedDeviceScaffold(
onClick = handlers.onApproveWithDeviceClick,
modifier = Modifier
.testTag("ApproveWithOtherDeviceButton")
.padding(horizontal = 16.dp)
.standardHorizontalMargin()
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(12.dp))
@@ -159,7 +163,7 @@ private fun TrustedDeviceScaffold(
label = stringResource(id = R.string.request_admin_approval),
onClick = handlers.onApproveWithAdminClick,
modifier = Modifier
.padding(horizontal = 16.dp)
.standardHorizontalMargin()
.fillMaxWidth()
.testTag("RequestAdminApprovalButton"),
)
@@ -172,7 +176,7 @@ private fun TrustedDeviceScaffold(
onClick = handlers.onApproveWithPasswordClick,
modifier = Modifier
.testTag("ApproveWithMasterPasswordButton")
.padding(horizontal = 16.dp)
.standardHorizontalMargin()
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(12.dp))
@@ -187,9 +191,10 @@ private fun TrustedDeviceScaffold(
),
style = BitwardenTheme.typography.bodyMedium,
color = BitwardenTheme.colorScheme.text.secondary,
textAlign = TextAlign.Center,
modifier = Modifier
.testTag("LoggingInAsLabel")
.padding(horizontal = 16.dp)
.standardHorizontalMargin()
.fillMaxWidth(),
)
@@ -198,9 +203,12 @@ private fun TrustedDeviceScaffold(
onClick = handlers.onNotYouButtonClick,
style = BitwardenTheme.typography.labelLarge,
innerPadding = PaddingValues(vertical = 8.dp, horizontal = 16.dp),
modifier = Modifier.testTag("NotYouLabel"),
modifier = Modifier
.align(alignment = Alignment.CenterHorizontally)
.testTag("NotYouLabel"),
)
Spacer(modifier = Modifier.height(height = 16.dp))
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
@@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
@@ -42,6 +43,7 @@ import com.x8bit.bitwarden.ui.auth.feature.twofactorlogin.util.description
import com.x8bit.bitwarden.ui.auth.feature.twofactorlogin.util.title
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.base.util.LivecycleEventEffect
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
import com.x8bit.bitwarden.ui.platform.components.appbar.action.BitwardenOverflowActionItem
import com.x8bit.bitwarden.ui.platform.components.appbar.action.OverflowMenuItemData
@@ -50,6 +52,7 @@ import com.x8bit.bitwarden.ui.platform.components.button.BitwardenOutlinedButton
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordField
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
@@ -217,13 +220,14 @@ private fun TwoFactorLoginScreenContent(
.imePadding()
.verticalScroll(rememberScrollState()),
) {
Spacer(modifier = Modifier.height(height = 12.dp))
Text(
text = state.authMethod.description(state.displayEmail)(),
textAlign = TextAlign.Start,
style = BitwardenTheme.typography.bodyMedium,
color = BitwardenTheme.colorScheme.text.primary,
modifier = Modifier
.padding(horizontal = 16.dp)
.standardHorizontalMargin()
.fillMaxWidth(),
)
@@ -255,31 +259,33 @@ private fun TwoFactorLoginScreenContent(
keyboardActions = KeyboardActions(
onDone = { onContinueButtonClick() },
),
cardStyle = CardStyle.Full,
modifier = Modifier
.padding(horizontal = 16.dp)
.standardHorizontalMargin()
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(12.dp))
Spacer(modifier = Modifier.height(height = 8.dp))
}
BitwardenSwitch(
label = stringResource(id = R.string.remember_me),
isChecked = state.isRememberMeEnabled,
onCheckedChange = onRememberMeToggle,
cardStyle = CardStyle.Full,
modifier = Modifier
.padding(horizontal = 16.dp)
.standardHorizontalMargin()
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(12.dp))
Spacer(modifier = Modifier.height(height = 24.dp))
BitwardenFilledButton(
label = state.buttonText(),
onClick = onContinueButtonClick,
isEnabled = state.isContinueButtonEnabled,
modifier = Modifier
.padding(horizontal = 16.dp)
.standardHorizontalMargin()
.fillMaxWidth(),
)
@@ -290,10 +296,12 @@ private fun TwoFactorLoginScreenContent(
label = stringResource(id = R.string.send_verification_code_again),
onClick = onResendEmailButtonClick,
modifier = Modifier
.padding(horizontal = 16.dp)
.standardHorizontalMargin()
.fillMaxWidth(),
)
}
Spacer(modifier = Modifier.height(height = 16.dp))
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
@@ -303,7 +311,7 @@ private fun TwoFactorLoginScreenContentPreview() {
BitwardenTheme {
TwoFactorLoginScreenContent(
state = TwoFactorLoginState(
TwoFactorAuthMethod.EMAIL,
authMethod = TwoFactorAuthMethod.EMAIL,
availableAuthMethods = listOf(TwoFactorAuthMethod.EMAIL),
codeInput = "",
dialogState = null,
@@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.verticalScroll
@@ -47,6 +46,9 @@ import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.util.unlockScreenMessage
import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.util.unlockScreenTitle
import com.x8bit.bitwarden.ui.autofill.fido2.manager.Fido2CompletionManager
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.base.util.cardBackground
import com.x8bit.bitwarden.ui.platform.base.util.cardPadding
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.x8bit.bitwarden.ui.platform.components.account.BitwardenAccountActionItem
import com.x8bit.bitwarden.ui.platform.components.account.BitwardenAccountSwitcher
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
@@ -58,6 +60,7 @@ import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLogoutConfirmationDialog
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordField
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.composition.LocalBiometricsManager
import com.x8bit.bitwarden.ui.platform.composition.LocalFido2CompletionManager
@@ -247,6 +250,7 @@ fun VaultUnlockScreen(
.fillMaxSize()
.verticalScroll(rememberScrollState()),
) {
Spacer(modifier = Modifier.height(12.dp))
if (!state.hideInput) {
BitwardenPasswordField(
label = state.vaultUnlockType.unlockScreenInputLabel(),
@@ -258,10 +262,6 @@ fun VaultUnlockScreen(
showPasswordTestTag = state
.vaultUnlockType
.inputFieldVisibilityToggleTestTag,
modifier = Modifier
.testTag(state.vaultUnlockType.unlockScreenInputTestTag)
.padding(horizontal = 16.dp)
.fillMaxWidth(),
autoFocus = state.showKeyboard,
imeAction = ImeAction.Done,
keyboardActions = KeyboardActions(
@@ -269,17 +269,18 @@ fun VaultUnlockScreen(
{ viewModel.trySendAction(VaultUnlockAction.UnlockClick) }
},
),
)
Spacer(modifier = Modifier.height(24.dp))
Text(
text = state.vaultUnlockType.unlockScreenMessage(),
style = BitwardenTheme.typography.bodyMedium,
color = BitwardenTheme.colorScheme.text.primary,
supportingText = state.vaultUnlockType.unlockScreenMessage(),
cardStyle = CardStyle.Top(hasDivider = false),
modifier = Modifier
.padding(horizontal = 16.dp)
.testTag(tag = state.vaultUnlockType.unlockScreenInputTestTag)
.standardHorizontalMargin()
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(8.dp))
}
val trailingTextCardStyle = if (state.hideInput) {
CardStyle.Full
} else {
CardStyle.Bottom
}
Text(
text = stringResource(
@@ -287,11 +288,19 @@ fun VaultUnlockScreen(
state.email,
state.environmentUrl,
),
style = BitwardenTheme.typography.bodyMedium,
color = BitwardenTheme.colorScheme.text.primary,
style = BitwardenTheme.typography.bodySmall,
color = BitwardenTheme.colorScheme.text.secondary,
modifier = Modifier
.testTag("UserAndEnvironmentDataLabel")
.padding(horizontal = 16.dp)
.standardHorizontalMargin()
.cardBackground(cardStyle = trailingTextCardStyle)
.cardPadding(
cardStyle = trailingTextCardStyle,
start = 16.dp,
end = 16.dp,
top = 0.dp,
bottom = 12.dp,
)
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(24.dp))
@@ -302,7 +311,7 @@ fun VaultUnlockScreen(
{ viewModel.trySendAction(VaultUnlockAction.BiometricsUnlockClick) }
},
modifier = Modifier
.padding(horizontal = 16.dp)
.standardHorizontalMargin()
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(12.dp))
@@ -312,7 +321,7 @@ fun VaultUnlockScreen(
textAlign = TextAlign.Start,
style = BitwardenTheme.typography.bodyMedium,
color = BitwardenTheme.colorScheme.status.error,
modifier = Modifier.padding(horizontal = 16.dp),
modifier = Modifier.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(12.dp))
}
@@ -325,7 +334,7 @@ fun VaultUnlockScreen(
isEnabled = state.input.isNotEmpty(),
modifier = Modifier
.testTag("UnlockVaultButton")
.padding(horizontal = 16.dp)
.standardHorizontalMargin()
.fillMaxWidth(),
)
}
@@ -1,5 +1,9 @@
package com.x8bit.bitwarden.ui.platform.base.util
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
/**
* Returns null if all entries in a given list are equal to a provided [value], otherwise
* the original list is returned.
@@ -10,3 +14,21 @@ fun <T> List<T>.nullIfAllEqual(value: T): List<T>? =
} else {
this
}
/**
* Returns the appropriate [CardStyle] based on the current [index] in the list.
*/
fun <T> Collection<T>.toListItemCardStyle(
index: Int,
hasDivider: Boolean = true,
dividerPadding: Dp = 16.dp,
): CardStyle =
if (this.size == 1) {
CardStyle.Full
} else if (index == 0) {
CardStyle.Top(hasDivider = hasDivider, dividerPadding = dividerPadding)
} else if (index == this.size - 1) {
CardStyle.Bottom
} else {
CardStyle.Middle(hasDivider = hasDivider, dividerPadding = dividerPadding)
}
@@ -1,5 +1,7 @@
package com.x8bit.bitwarden.ui.platform.base.util
import android.os.Build
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.TopAppBarScrollBehavior
@@ -7,12 +9,14 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.ui.CombinedModifier
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.draw.scale
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.isShiftPressed
@@ -26,6 +30,7 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
import com.x8bit.bitwarden.ui.platform.util.isPortrait
@@ -159,3 +164,72 @@ fun Modifier.standardHorizontalMargin(
val config = LocalConfiguration.current
return this.padding(horizontal = if (config.isPortrait) portrait else landscape)
}
/**
* This is a [Modifier] extension that applies a card background to the content.
*/
@OmitFromCoverage
@Stable
@Composable
fun Modifier.cardBackground(
cardStyle: CardStyle?,
color: Color = BitwardenTheme.colorScheme.background.secondary,
): Modifier {
cardStyle ?: return this
val shape = if ("robolectric" == Build.FINGERPRINT) {
// TODO: This is here to ensure our click events work in robolectric tests because of the
// uneven rounded corners that need to be clipped. This should be removed when the bug is
// resolved: https://issuetracker.google.com/issues/366255137
RectangleShape
} else {
when (cardStyle) {
is CardStyle.Top -> BitwardenTheme.shapes.contentTop
is CardStyle.Middle -> BitwardenTheme.shapes.contentMiddle
CardStyle.Bottom -> BitwardenTheme.shapes.contentBottom
CardStyle.Full -> BitwardenTheme.shapes.content
}
}
return this
.clip(shape = shape)
.background(color = color, shape = shape)
.bottomDivider(
paddingStart = cardStyle.dividerPadding,
enabled = cardStyle.hasDivider,
)
}
/**
* This is a [Modifier] extension that applies card padding to the content.
*/
@OmitFromCoverage
@Stable
@Composable
fun Modifier.cardPadding(
cardStyle: CardStyle?,
start: Dp = 0.dp,
top: Dp = 12.dp,
end: Dp = 0.dp,
bottom: Dp = 12.dp,
): Modifier {
cardStyle ?: return this
return this.padding(start = start, top = top, end = end, bottom = bottom)
}
/**
* This is a [Modifier] extension that applies card padding to the content.
*/
@OmitFromCoverage
@Stable
@Composable
fun Modifier.cardPadding(
cardStyle: CardStyle?,
vertical: Dp = 12.dp,
horizontal: Dp = 0.dp,
): Modifier =
this.cardPadding(
cardStyle = cardStyle,
start = horizontal,
top = vertical,
end = horizontal,
bottom = vertical,
)
@@ -19,7 +19,6 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
@@ -74,10 +73,7 @@ fun BitwardenSearchTopAppBar(
},
title = {
TextField(
colors = bitwardenTextFieldColors(
focusedBorderColor = Color.Transparent,
unfocusedBorderColor = Color.Transparent,
),
colors = bitwardenTextFieldColors(),
textStyle = BitwardenTheme.typography.bodyLarge,
placeholder = { Text(text = placeholder) },
value = searchTerm,
@@ -5,6 +5,7 @@ import androidx.compose.material3.FilledIconButton
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.tooling.preview.Preview
import com.x8bit.bitwarden.R
@@ -30,7 +31,9 @@ fun BitwardenFilledIconButton(
isEnabled: Boolean = true,
) {
FilledIconButton(
modifier = modifier.semantics(mergeDescendants = true) {},
modifier = modifier.semantics(mergeDescendants = true) {
this.contentDescription = contentDescription
},
onClick = onClick,
colors = bitwardenFilledIconButtonColors(),
enabled = isEnabled,
@@ -7,6 +7,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.tooling.preview.Preview
import com.x8bit.bitwarden.R
@@ -61,7 +62,9 @@ fun BitwardenStandardIconButton(
contentColor: Color = BitwardenTheme.colorScheme.icon.primary,
) {
IconButton(
modifier = modifier.semantics(mergeDescendants = true) {},
modifier = modifier.semantics(mergeDescendants = true) {
this.contentDescription = contentDescription
},
onClick = onClick,
colors = bitwardenStandardIconButtonColors(contentColor = contentColor),
enabled = isEnabled,
@@ -5,6 +5,7 @@ import androidx.compose.material3.FilledIconButton
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.tooling.preview.Preview
import com.x8bit.bitwarden.R
@@ -30,7 +31,9 @@ fun BitwardenTonalIconButton(
isEnabled: Boolean = true,
) {
FilledIconButton(
modifier = modifier.semantics(mergeDescendants = true) {},
modifier = modifier.semantics(mergeDescendants = true) {
this.contentDescription = contentDescription
},
onClick = onClick,
colors = bitwardenTonalIconButtonColors(),
enabled = isEnabled,
@@ -2,6 +2,8 @@ package com.x8bit.bitwarden.ui.platform.components.dialog
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.DatePicker
import androidx.compose.material3.DatePickerColors
import androidx.compose.material3.DatePickerDialog
@@ -9,7 +11,9 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.rememberDatePickerState
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -27,10 +31,14 @@ import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTag
import androidx.compose.ui.semantics.testTagsAsResourceId
import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.cardBackground
import com.x8bit.bitwarden.ui.platform.base.util.cardPadding
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton
import com.x8bit.bitwarden.ui.platform.components.field.color.bitwardenTextFieldButtonColors
import com.x8bit.bitwarden.ui.platform.components.field.color.bitwardenTextFieldColors
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
import com.x8bit.bitwarden.ui.platform.util.orNow
@@ -51,6 +59,7 @@ import java.time.ZonedDateTime
* @param onDateSelect The callback to be invoked when a new date is selected.
* @param isEnabled Whether the button is enabled.
* @param modifier A [Modifier] that you can use to apply custom modifications to the composable.
* @param cardStyle Indicates the type of card style to be applied.
*/
@Suppress("LongMethod")
@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
@@ -62,6 +71,7 @@ fun BitwardenDateSelectButton(
onDateSelect: (ZonedDateTime) -> Unit,
isEnabled: Boolean,
modifier: Modifier = Modifier,
cardStyle: CardStyle? = null,
) {
var shouldShowDialog: Boolean by rememberSaveable { mutableStateOf(false) }
val formattedDate by remember(currentZonedDateTime) {
@@ -72,18 +82,22 @@ fun BitwardenDateSelectButton(
)
}
OutlinedTextField(
TextField(
modifier = modifier
.clearAndSetSemantics {
role = Role.DropdownList
contentDescription = "$label, $formattedDate"
}
.defaultMinSize(minHeight = 60.dp)
.cardBackground(cardStyle = cardStyle)
.clickable(
enabled = isEnabled,
indication = null,
indication = ripple(color = BitwardenTheme.colorScheme.background.pressed),
interactionSource = remember { MutableInteractionSource() },
onClick = { shouldShowDialog = !shouldShowDialog },
),
)
.cardPadding(cardStyle = cardStyle)
.padding(top = 4.dp),
textStyle = BitwardenTheme.typography.bodyLarge,
readOnly = true,
label = { Text(text = label) },
@@ -173,5 +187,9 @@ private fun bitwardenDatePickerColors(): DatePickerColors = DatePickerColors(
dayInSelectionRangeContainerColor = BitwardenTheme.colorScheme.filledButton.background,
dividerColor = BitwardenTheme.colorScheme.stroke.divider,
dayInSelectionRangeContentColor = BitwardenTheme.colorScheme.text.primary,
dateTextFieldColors = bitwardenTextFieldColors(),
dateTextFieldColors = bitwardenTextFieldColors(
disabledBorderColor = BitwardenTheme.colorScheme.outlineButton.borderDisabled,
focusedBorderColor = BitwardenTheme.colorScheme.stroke.border,
unfocusedBorderColor = BitwardenTheme.colorScheme.stroke.divider,
),
)
@@ -22,6 +22,7 @@ import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordField
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
/**
@@ -76,10 +77,11 @@ fun BitwardenMasterPasswordDialog(
label = stringResource(id = R.string.master_password),
value = masterPassword,
onValueChange = { masterPassword = it },
autoFocus = true,
cardStyle = CardStyle.Full,
modifier = Modifier
.testTag("AlertInputField")
.imePadding(),
autoFocus = true,
)
}
},
@@ -1,5 +1,6 @@
package com.x8bit.bitwarden.ui.platform.components.dialog
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Text
@@ -18,6 +19,7 @@ import androidx.compose.ui.semantics.testTagsAsResourceId
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordField
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
/**
@@ -63,10 +65,12 @@ fun BitwardenPinDialog(
label = stringResource(id = R.string.pin),
value = pin,
onValueChange = { pin = it },
autoFocus = true,
cardStyle = CardStyle.Full,
modifier = Modifier
.testTag("AlertInputField")
.fillMaxWidth()
.imePadding(),
autoFocus = true,
)
},
shape = BitwardenTheme.shapes.dialog,
@@ -1,5 +1,6 @@
package com.x8bit.bitwarden.ui.platform.components.dialog
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -21,6 +22,7 @@ import androidx.compose.ui.semantics.testTagsAsResourceId
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
/**
@@ -77,12 +79,12 @@ fun BitwardenTextEntryDialog(
label = textFieldLabel,
value = text,
onValueChange = { text = it },
textFieldTestTag = "AlertContentText",
cardStyle = CardStyle.Full,
modifier = Modifier
.focusRequester(focusRequester)
.onGloballyPositioned {
shouldRequestFocus = true
},
textFieldTestTag = "AlertContentText",
.onGloballyPositioned { shouldRequestFocus = true }
.fillMaxWidth(),
)
},
shape = BitwardenTheme.shapes.dialog,
@@ -2,9 +2,13 @@ package com.x8bit.bitwarden.ui.platform.components.dialog
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Icon
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -16,8 +20,12 @@ import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.role
import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.cardBackground
import com.x8bit.bitwarden.ui.platform.base.util.cardPadding
import com.x8bit.bitwarden.ui.platform.components.field.color.bitwardenTextFieldButtonColors
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
import com.x8bit.bitwarden.ui.platform.util.orNow
@@ -36,6 +44,7 @@ import java.time.ZonedDateTime
* @param onTimeSelect The callback to be invoked when a new time is selected.
* @param isEnabled Whether the button is enabled.
* @param modifier A [Modifier] that you can use to apply custom modifications to the composable.
* @param cardStyle Indicates the type of card style to be applied.
* @param is24Hour Indicates if the time selector should use a 24 hour format or a 12 hour format
* with AM/PM.
*/
@@ -47,6 +56,7 @@ fun BitwardenTimeSelectButton(
onTimeSelect: (hour: Int, minute: Int) -> Unit,
isEnabled: Boolean,
modifier: Modifier = Modifier,
cardStyle: CardStyle? = null,
is24Hour: Boolean = false,
) {
var shouldShowDialog: Boolean by rememberSaveable { mutableStateOf(false) }
@@ -57,18 +67,22 @@ fun BitwardenTimeSelectButton(
?: "--:-- --",
)
}
OutlinedTextField(
TextField(
modifier = modifier
.clearAndSetSemantics {
role = Role.DropdownList
contentDescription = "$label, $formattedTime"
}
.defaultMinSize(minHeight = 60.dp)
.cardBackground(cardStyle = cardStyle)
.clickable(
enabled = isEnabled,
indication = null,
indication = ripple(color = BitwardenTheme.colorScheme.background.pressed),
interactionSource = remember { MutableInteractionSource() },
onClick = { shouldShowDialog = !shouldShowDialog },
),
)
.cardPadding(cardStyle = cardStyle)
.padding(top = 4.dp),
textStyle = BitwardenTheme.typography.bodyLarge,
readOnly = true,
label = { Text(text = label) },
@@ -2,14 +2,20 @@ package com.x8bit.bitwarden.ui.platform.components.dropdown
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Icon
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -17,6 +23,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.CustomAccessibilityAction
import androidx.compose.ui.semantics.Role
@@ -29,11 +36,16 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.base.util.cardBackground
import com.x8bit.bitwarden.ui.platform.base.util.cardPadding
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenStandardIconButton
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenSelectionDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.row.BitwardenSelectionRow
import com.x8bit.bitwarden.ui.platform.components.divider.BitwardenHorizontalDivider
import com.x8bit.bitwarden.ui.platform.components.field.color.bitwardenTextFieldButtonColors
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.model.TooltipData
import com.x8bit.bitwarden.ui.platform.components.row.BitwardenRowOfActions
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
import kotlinx.collections.immutable.ImmutableList
@@ -55,6 +67,12 @@ import kotlinx.collections.immutable.persistentListOf
* @param modifier A [Modifier] that you can use to apply custom modifications to the composable.
* @param supportingText A optional supporting text that will appear below the text field.
* @param tooltip A nullable [TooltipData], representing the tooltip icon.
* @param insets Inner padding to be applied withing the card.
* @param cardStyle Indicates the type of card style to be applied.
* @param actionsPadding Padding to be applied to the [actions] block.
* @param actions A lambda containing the set of actions (usually icons or similar) to display
* in the app bar's trailing side. This lambda extends [RowScope], allowing flexibility in
* defining the layout of the actions.
*/
@Suppress("LongMethod")
@Composable
@@ -67,10 +85,14 @@ fun BitwardenMultiSelectButton(
isEnabled: Boolean = true,
supportingText: String? = null,
tooltip: TooltipData? = null,
insets: PaddingValues = PaddingValues(),
cardStyle: CardStyle? = null,
actionsPadding: PaddingValues = PaddingValues(),
actions: @Composable RowScope.() -> Unit = {},
) {
var shouldShowDialog by rememberSaveable { mutableStateOf(false) }
OutlinedTextField(
Column(
modifier = modifier
.clearAndSetSemantics {
role = Role.DropdownList
@@ -89,7 +111,7 @@ fun BitwardenMultiSelectButton(
},
)
}
.fillMaxWidth()
.cardBackground(cardStyle = cardStyle)
.clickable(
indication = ripple(
color = BitwardenTheme.colorScheme.background.pressed,
@@ -98,48 +120,74 @@ fun BitwardenMultiSelectButton(
interactionSource = remember { MutableInteractionSource() },
) {
shouldShowDialog = !shouldShowDialog
},
textStyle = BitwardenTheme.typography.bodyLarge,
readOnly = true,
label = {
Row {
Text(
text = label,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
tooltip?.let {
Spacer(modifier = Modifier.width(3.dp))
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_question_circle_small,
contentDescription = it.contentDescription,
onClick = it.onClick,
isEnabled = isEnabled,
contentColor = BitwardenTheme.colorScheme.icon.secondary,
modifier = Modifier.size(16.dp),
}
.cardPadding(cardStyle = cardStyle, vertical = 6.dp)
.padding(paddingValues = insets),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
TextField(
textStyle = BitwardenTheme.typography.bodyLarge,
readOnly = true,
label = {
Row {
Text(
text = label,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
tooltip?.let {
Spacer(modifier = Modifier.width(3.dp))
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_question_circle_small,
contentDescription = it.contentDescription,
onClick = it.onClick,
isEnabled = isEnabled,
contentColor = BitwardenTheme.colorScheme.icon.secondary,
modifier = Modifier.size(16.dp),
)
}
}
},
value = selectedOption.orEmpty(),
onValueChange = onOptionSelected,
enabled = shouldShowDialog,
trailingIcon = {
Icon(
painter = rememberVectorPainter(id = R.drawable.ic_chevron_down),
contentDescription = null,
)
}
}
},
value = selectedOption.orEmpty(),
onValueChange = onOptionSelected,
enabled = shouldShowDialog,
trailingIcon = {
Icon(
painter = rememberVectorPainter(id = R.drawable.ic_chevron_down),
contentDescription = null,
},
colors = bitwardenTextFieldButtonColors(),
modifier = Modifier
.weight(weight = 1f)
.fillMaxWidth(),
)
},
colors = bitwardenTextFieldButtonColors(),
supportingText = supportingText?.let {
{
Text(
text = supportingText,
style = BitwardenTheme.typography.bodySmall,
)
}
},
)
BitwardenRowOfActions(
modifier = Modifier.padding(paddingValues = actionsPadding),
actions = actions,
)
}
supportingText?.let {
Spacer(modifier = Modifier.height(height = 8.dp))
BitwardenHorizontalDivider(
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp),
)
Spacer(modifier = Modifier.height(height = 12.dp))
Text(
text = it,
style = BitwardenTheme.typography.bodySmall,
color = BitwardenTheme.colorScheme.text.secondary,
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
}
if (shouldShowDialog) {
BitwardenSelectionDialog(
title = label,
@@ -64,10 +64,7 @@ fun EnvironmentSelector(
interactionSource = remember { MutableInteractionSource() },
onClick = { shouldShowDialog = !shouldShowDialog },
)
.padding(
vertical = 8.dp,
horizontal = 16.dp,
),
.padding(vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
@@ -1,8 +1,8 @@
package com.x8bit.bitwarden.ui.platform.components.field
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
@@ -10,8 +10,10 @@ import androidx.compose.ui.platform.LocalTextToolbar
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.tooling.preview.Preview
import com.x8bit.bitwarden.ui.platform.base.util.cardBackground
import com.x8bit.bitwarden.ui.platform.components.field.color.bitwardenTextFieldColors
import com.x8bit.bitwarden.ui.platform.components.field.toolbar.BitwardenEmptyTextToolbar
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
/**
@@ -20,16 +22,18 @@ import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
* @param label Label for the text field.
* @param value Current text on the text field.
* @param modifier Modifier for the composable.
* @param cardStyle Indicates the type of card style to be applied.
*/
@Composable
fun BitwardenHiddenPasswordField(
label: String,
value: String,
modifier: Modifier = Modifier,
cardStyle: CardStyle? = null,
) {
CompositionLocalProvider(value = LocalTextToolbar provides BitwardenEmptyTextToolbar) {
OutlinedTextField(
modifier = modifier,
TextField(
modifier = modifier.cardBackground(cardStyle = cardStyle),
textStyle = BitwardenTheme.typography.sensitiveInfoSmall,
label = { Text(text = label) },
value = value,
@@ -51,6 +55,7 @@ private fun BitwardenHiddenPasswordField_preview() {
BitwardenHiddenPasswordField(
label = "Label",
value = "Password",
cardStyle = CardStyle.Full,
)
}
}
@@ -1,9 +1,16 @@
package com.x8bit.bitwarden.ui.platform.components.field
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
@@ -28,12 +35,17 @@ import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.cardBackground
import com.x8bit.bitwarden.ui.platform.base.util.cardPadding
import com.x8bit.bitwarden.ui.platform.base.util.tabNavigation
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenStandardIconButton
import com.x8bit.bitwarden.ui.platform.components.divider.BitwardenHorizontalDivider
import com.x8bit.bitwarden.ui.platform.components.field.color.bitwardenTextFieldColors
import com.x8bit.bitwarden.ui.platform.components.field.toolbar.BitwardenCutCopyTextToolbar
import com.x8bit.bitwarden.ui.platform.components.field.toolbar.BitwardenEmptyTextToolbar
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.model.TextToolbarType
import com.x8bit.bitwarden.ui.platform.components.util.nonLetterColorVisualTransformation
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
@@ -48,11 +60,12 @@ import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
* @param showPassword Whether or not password should be shown.
* @param showPasswordChange Lambda that is called when user request show/hide be toggled.
* @param onValueChange Callback that is triggered when the password changes.
* @param supportingTextContent An optional supporting text composable will appear below the text
* input.
* @param modifier Modifier for the composable.
* @param readOnly `true` if the input should be read-only and not accept user interactions.
* @param singleLine when `true`, this text field becomes a single line that horizontally scrolls
* instead of wrapping onto multiple lines.
* @param hint optional hint text that will appear below the text input.
* @param showPasswordTestTag The test tag to be used on the show password button (testing tool).
* @param autoFocus When set to true, the view will request focus after the first recomposition.
* Setting this to true on multiple fields at once may have unexpected consequences.
@@ -61,6 +74,7 @@ import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
* @param imeAction the preferred IME action for the keyboard to have.
* @param keyboardActions the callbacks of keyboard actions.
* @param textToolbarType The type of [TextToolbar] to use on the text field.
* @param cardStyle Indicates the type of card style to be applied.
*/
@Suppress("LongMethod")
@Composable
@@ -70,16 +84,17 @@ fun BitwardenPasswordField(
showPassword: Boolean,
showPasswordChange: (Boolean) -> Unit,
onValueChange: (String) -> Unit,
supportingTextContent: @Composable (ColumnScope.() -> Unit)?,
modifier: Modifier = Modifier,
readOnly: Boolean = false,
singleLine: Boolean = true,
hint: String? = null,
showPasswordTestTag: String? = null,
autoFocus: Boolean = false,
keyboardType: KeyboardType = KeyboardType.Password,
imeAction: ImeAction = ImeAction.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default,
textToolbarType: TextToolbarType = TextToolbarType.DEFAULT,
cardStyle: CardStyle? = null,
) {
val focusRequester = remember { FocusRequester() }
var textFieldValueState by remember { mutableStateOf(TextFieldValue(text = value)) }
@@ -103,63 +118,214 @@ fun BitwardenPasswordField(
}
var lastTextValue by remember(value) { mutableStateOf(value = value) }
CompositionLocalProvider(value = LocalTextToolbar provides textToolbar) {
OutlinedTextField(
Column(
modifier = modifier
.defaultMinSize(minHeight = 60.dp)
.cardBackground(cardStyle = cardStyle)
.cardPadding(cardStyle = cardStyle, vertical = 6.dp)
.tabNavigation()
.focusRequester(focusRequester),
colors = bitwardenTextFieldColors(),
textStyle = BitwardenTheme.typography.sensitiveInfoSmall,
label = { Text(text = label) },
value = textFieldValue,
onValueChange = {
textFieldValueState = it
val stringChangedSinceLastInvocation = lastTextValue != it.text
lastTextValue = it.text
if (stringChangedSinceLastInvocation) {
onValueChange(it.text)
}
},
visualTransformation = when {
!showPassword -> PasswordVisualTransformation()
readOnly -> nonLetterColorVisualTransformation()
else -> VisualTransformation.None
},
singleLine = singleLine,
readOnly = readOnly,
keyboardOptions = KeyboardOptions(
keyboardType = keyboardType,
imeAction = imeAction,
),
keyboardActions = keyboardActions,
supportingText = hint?.let {
{
Text(
text = hint,
style = BitwardenTheme.typography.bodySmall,
.focusRequester(focusRequester = focusRequester),
) {
TextField(
colors = bitwardenTextFieldColors(),
textStyle = BitwardenTheme.typography.sensitiveInfoSmall,
label = { Text(text = label) },
value = textFieldValue,
onValueChange = {
textFieldValueState = it
val stringChangedSinceLastInvocation = lastTextValue != it.text
lastTextValue = it.text
if (stringChangedSinceLastInvocation) {
onValueChange(it.text)
}
},
visualTransformation = when {
!showPassword -> PasswordVisualTransformation()
readOnly -> nonLetterColorVisualTransformation()
else -> VisualTransformation.None
},
singleLine = singleLine,
readOnly = readOnly,
keyboardOptions = KeyboardOptions(
keyboardType = keyboardType,
imeAction = imeAction,
),
keyboardActions = keyboardActions,
trailingIcon = {
BitwardenStandardIconButton(
modifier = Modifier.semantics { showPasswordTestTag?.let { testTag = it } },
vectorIconRes = if (showPassword) {
R.drawable.ic_eye_slash
} else {
R.drawable.ic_eye
},
contentDescription = stringResource(
id = if (showPassword) R.string.hide else R.string.show,
),
onClick = { showPasswordChange.invoke(!showPassword) },
)
}
},
trailingIcon = {
BitwardenStandardIconButton(
modifier = Modifier.semantics { showPasswordTestTag?.let { testTag = it } },
vectorIconRes = if (showPassword) {
R.drawable.ic_eye_slash
} else {
R.drawable.ic_eye
},
contentDescription = stringResource(
id = if (showPassword) R.string.hide else R.string.show,
),
onClick = { showPasswordChange.invoke(!showPassword) },
},
modifier = Modifier.fillMaxWidth(),
)
supportingTextContent?.let {
Spacer(modifier = Modifier.height(height = 8.dp))
BitwardenHorizontalDivider(
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp),
)
},
)
Spacer(modifier = Modifier.height(height = 12.dp))
Column(
modifier = Modifier.padding(horizontal = 16.dp),
content = it,
)
Spacer(modifier = Modifier.height(height = 6.dp))
}
}
}
if (autoFocus) {
LaunchedEffect(Unit) { focusRequester.requestFocus() }
}
}
/**
* Represents a Bitwarden-styled password field that hoists show/hide password state to the caller.
*
* See overloaded [BitwardenPasswordField] for self managed show/hide state.
*
* @param label Label for the text field.
* @param value Current next on the text field.
* @param showPassword Whether or not password should be shown.
* @param showPasswordChange Lambda that is called when user request show/hide be toggled.
* @param onValueChange Callback that is triggered when the password changes.
* @param modifier Modifier for the composable.
* @param readOnly `true` if the input should be read-only and not accept user interactions.
* @param singleLine when `true`, this text field becomes a single line that horizontally scrolls
* instead of wrapping onto multiple lines.
* @param supportingText An optional supporting text that will appear below the text input.
* @param showPasswordTestTag The test tag to be used on the show password button (testing tool).
* @param autoFocus When set to true, the view will request focus after the first recomposition.
* Setting this to true on multiple fields at once may have unexpected consequences.
* @param keyboardType The type of keyboard the user has access to when inputting values into
* the password field.
* @param imeAction the preferred IME action for the keyboard to have.
* @param keyboardActions the callbacks of keyboard actions.
* @param textToolbarType The type of [TextToolbar] to use on the text field.
* @param cardStyle Indicates the type of card style to be applied.
*/
@Composable
fun BitwardenPasswordField(
label: String,
value: String,
showPassword: Boolean,
showPasswordChange: (Boolean) -> Unit,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
readOnly: Boolean = false,
singleLine: Boolean = true,
supportingText: String? = null,
showPasswordTestTag: String? = null,
autoFocus: Boolean = false,
keyboardType: KeyboardType = KeyboardType.Password,
imeAction: ImeAction = ImeAction.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default,
textToolbarType: TextToolbarType = TextToolbarType.DEFAULT,
cardStyle: CardStyle? = null,
) {
BitwardenPasswordField(
label = label,
value = value,
showPassword = showPassword,
showPasswordChange = showPasswordChange,
showPasswordTestTag = showPasswordTestTag,
onValueChange = onValueChange,
modifier = modifier,
readOnly = readOnly,
singleLine = singleLine,
supportingTextContent = supportingText?.let {
{
Text(
text = it,
style = BitwardenTheme.typography.bodySmall,
color = BitwardenTheme.colorScheme.text.secondary,
modifier = Modifier.fillMaxWidth(),
)
}
},
autoFocus = autoFocus,
keyboardType = keyboardType,
imeAction = imeAction,
keyboardActions = keyboardActions,
textToolbarType = textToolbarType,
cardStyle = cardStyle,
)
}
/**
* Represents a Bitwarden-styled password field that hoists show/hide password state to the caller.
*
* See overloaded [BitwardenPasswordField] for self managed show/hide state.
*
* @param label Label for the text field.
* @param value Current next on the text field.
* @param onValueChange Callback that is triggered when the password changes.
* @param modifier Modifier for the composable.
* @param initialShowPassword The initial state of the show/hide password control. A value of
* `false` (the default) indicates that that password should begin in the hidden state.
* @param readOnly `true` if the input should be read-only and not accept user interactions.
* @param singleLine when `true`, this text field becomes a single line that horizontally scrolls
* instead of wrapping onto multiple lines.
* @param supportingTextContent An optional supporting text composable will appear below the text
* input.
* @param showPasswordTestTag The test tag to be used on the show password button (testing tool).
* @param autoFocus When set to true, the view will request focus after the first recomposition.
* Setting this to true on multiple fields at once may have unexpected consequences.
* @param keyboardType The type of keyboard the user has access to when inputting values into
* the password field.
* @param imeAction the preferred IME action for the keyboard to have.
* @param keyboardActions the callbacks of keyboard actions.
* @param textToolbarType The type of [TextToolbar] to use on the text field.
* @param cardStyle Indicates the type of card style to be applied.
*/
@Composable
fun BitwardenPasswordField(
label: String,
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
initialShowPassword: Boolean = false,
readOnly: Boolean = false,
singleLine: Boolean = true,
supportingTextContent: @Composable (ColumnScope.() -> Unit)?,
showPasswordTestTag: String? = null,
autoFocus: Boolean = false,
keyboardType: KeyboardType = KeyboardType.Password,
imeAction: ImeAction = ImeAction.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default,
textToolbarType: TextToolbarType = TextToolbarType.DEFAULT,
cardStyle: CardStyle? = null,
) {
var showPassword by rememberSaveable { mutableStateOf(value = initialShowPassword) }
BitwardenPasswordField(
label = label,
value = value,
showPassword = showPassword,
showPasswordChange = { showPassword = !showPassword },
showPasswordTestTag = showPasswordTestTag,
onValueChange = onValueChange,
modifier = modifier,
readOnly = readOnly,
singleLine = singleLine,
supportingTextContent = supportingTextContent,
autoFocus = autoFocus,
keyboardType = keyboardType,
imeAction = imeAction,
keyboardActions = keyboardActions,
textToolbarType = textToolbarType,
cardStyle = cardStyle,
)
}
/**
* Represents a Bitwarden-styled password field that manages the state of a show/hide indicator
* internally.
@@ -171,7 +337,7 @@ fun BitwardenPasswordField(
* @param readOnly `true` if the input should be read-only and not accept user interactions.
* @param singleLine when `true`, this text field becomes a single line that horizontally scrolls
* instead of wrapping onto multiple lines.
* @param hint optional hint text that will appear below the text input.
* @param supportingText An optional supporting text that will appear below the text input.
* @param initialShowPassword The initial state of the show/hide password control. A value of
* `false` (the default) indicates that that password should begin in the hidden state.
* @param showPasswordTestTag The test tag to be used on the show password button (testing tool).
@@ -181,6 +347,7 @@ fun BitwardenPasswordField(
* the password field.
* @param imeAction the preferred IME action for the keyboard to have.
* @param keyboardActions the callbacks of keyboard actions.
* @param cardStyle Indicates the type of card style to be applied.
*/
@Composable
fun BitwardenPasswordField(
@@ -190,13 +357,14 @@ fun BitwardenPasswordField(
modifier: Modifier = Modifier,
readOnly: Boolean = false,
singleLine: Boolean = true,
hint: String? = null,
supportingText: String? = null,
initialShowPassword: Boolean = false,
showPasswordTestTag: String? = null,
autoFocus: Boolean = false,
keyboardType: KeyboardType = KeyboardType.Password,
imeAction: ImeAction = ImeAction.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default,
cardStyle: CardStyle? = null,
) {
var showPassword by rememberSaveable { mutableStateOf(initialShowPassword) }
BitwardenPasswordField(
@@ -208,59 +376,50 @@ fun BitwardenPasswordField(
onValueChange = onValueChange,
readOnly = readOnly,
singleLine = singleLine,
hint = hint,
supportingText = supportingText,
showPasswordTestTag = showPasswordTestTag,
autoFocus = autoFocus,
keyboardType = keyboardType,
imeAction = imeAction,
keyboardActions = keyboardActions,
cardStyle = cardStyle,
)
}
@Preview
@Composable
private fun BitwardenPasswordField_preview_withInput_hidePassword() {
BitwardenPasswordField(
label = "Label",
value = "Password",
onValueChange = {},
initialShowPassword = false,
hint = "Hint",
)
}
@Preview
@Composable
private fun BitwardenPasswordField_preview_withInput_showPassword() {
BitwardenPasswordField(
label = "Label",
value = "Password",
onValueChange = {},
initialShowPassword = true,
hint = "Hint",
)
}
@Preview
@Composable
private fun BitwardenPasswordField_preview_withoutInput_hidePassword() {
BitwardenPasswordField(
label = "Label",
value = "",
onValueChange = {},
initialShowPassword = false,
hint = "Hint",
)
}
@Preview
@Composable
private fun BitwardenPasswordField_preview_withoutInput_showPassword() {
BitwardenPasswordField(
label = "Label",
value = "",
onValueChange = {},
initialShowPassword = true,
hint = "Hint",
)
private fun BitwardenPasswordField_preview() {
BitwardenTheme {
Column {
BitwardenPasswordField(
label = "Label",
value = "Password",
onValueChange = {},
initialShowPassword = false,
cardStyle = CardStyle.Top(),
)
BitwardenPasswordField(
label = "Label",
value = "Password",
onValueChange = {},
initialShowPassword = true,
cardStyle = CardStyle.Middle(),
)
BitwardenPasswordField(
label = "Label",
value = "",
onValueChange = {},
initialShowPassword = false,
cardStyle = CardStyle.Middle(),
)
BitwardenPasswordField(
label = "Label",
value = "",
onValueChange = {},
initialShowPassword = true,
supportingText = "Hint",
cardStyle = CardStyle.Bottom,
)
}
}
}
@@ -1,8 +1,16 @@
package com.x8bit.bitwarden.ui.platform.components.field
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -16,8 +24,13 @@ import androidx.compose.ui.semantics.testTag
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTonalIconButton
import com.x8bit.bitwarden.ui.platform.base.util.cardBackground
import com.x8bit.bitwarden.ui.platform.base.util.cardPadding
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenStandardIconButton
import com.x8bit.bitwarden.ui.platform.components.divider.BitwardenHorizontalDivider
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.model.TextToolbarType
import com.x8bit.bitwarden.ui.platform.components.row.BitwardenRowOfActions
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
/**
@@ -32,10 +45,14 @@ import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
* @param showPasswordChange Lambda that is called when user request show/hide be toggled.
* @param onValueChange Callback that is triggered when the password changes.
* @param modifier Modifier for the composable.
* @param supportingText An optional supporting text that will appear below the text field and
* actions.
* @param readOnly `true` if the input should be read-only and not accept user interactions.
* @param singleLine when `true`, this text field becomes a single line that horizontally scrolls
* instead of wrapping onto multiple lines.
* @param textToolbarType The type of [TextToolbar] to use on the text field.
* @param cardStyle Indicates the type of card style to be applied.
* @param actionsPadding Padding to be applied to the [actions] block.
* @param actions A lambda containing the set of actions (usually icons or similar) to display
* in the app bar's trailing side. This lambda extends [RowScope], allowing flexibility in
* defining the layout of the actions.
@@ -48,33 +65,62 @@ fun BitwardenPasswordFieldWithActions(
showPasswordChange: (Boolean) -> Unit,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
supportingText: String? = null,
readOnly: Boolean = false,
singleLine: Boolean = false,
showPasswordTestTag: String? = null,
passwordFieldTestTag: String? = null,
textToolbarType: TextToolbarType = TextToolbarType.DEFAULT,
cardStyle: CardStyle? = null,
actionsPadding: PaddingValues = PaddingValues(end = 4.dp),
actions: @Composable RowScope.() -> Unit = {},
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = modifier.semantics(mergeDescendants = true) {},
Column(
modifier = modifier
.defaultMinSize(minHeight = 60.dp)
.cardBackground(cardStyle = cardStyle)
.cardPadding(cardStyle = cardStyle, vertical = 6.dp)
.semantics(mergeDescendants = true) { },
verticalArrangement = Arrangement.Center,
) {
BitwardenPasswordField(
label = label,
value = value,
showPasswordChange = showPasswordChange,
showPassword = showPassword,
onValueChange = onValueChange,
readOnly = readOnly,
singleLine = singleLine,
modifier = Modifier
.semantics { passwordFieldTestTag?.let { testTag = it } }
.weight(1f)
.padding(end = 8.dp),
showPasswordTestTag = showPasswordTestTag,
textToolbarType = textToolbarType,
)
actions()
Row(
verticalAlignment = Alignment.CenterVertically,
) {
BitwardenPasswordField(
label = label,
value = value,
showPasswordChange = showPasswordChange,
showPassword = showPassword,
onValueChange = onValueChange,
readOnly = readOnly,
singleLine = singleLine,
showPasswordTestTag = showPasswordTestTag,
textToolbarType = textToolbarType,
modifier = Modifier
.semantics { passwordFieldTestTag?.let { testTag = it } }
.weight(weight = 1f),
)
BitwardenRowOfActions(
modifier = Modifier.padding(paddingValues = actionsPadding),
actions = actions,
)
}
supportingText?.let {
Spacer(modifier = Modifier.height(height = 8.dp))
BitwardenHorizontalDivider(
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp),
)
Spacer(modifier = Modifier.height(height = 12.dp))
Text(
text = it,
style = BitwardenTheme.typography.bodySmall,
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp),
)
}
}
}
@@ -94,6 +140,11 @@ fun BitwardenPasswordFieldWithActions(
* @param initialShowPassword The initial state of the show/hide password control. A value of
* `false` (the default) indicates that that password should begin in the hidden state.
* @param actions A lambda containing the set of actions (usually icons or similar) to display
* @param cardStyle Indicates the type of card style to be applied.
* in the app bar's trailing side. This lambda extends [RowScope], allowing flexibility in
* defining the layout of the actions.
* @param actionsPadding Padding to be applied to the [actions] block.
* @param actions A lambda containing the set of actions (usually icons or similar) to display
* in the app bar's trailing side. This lambda extends [RowScope], allowing flexibility in
* defining the layout of the actions.
*/
@@ -108,6 +159,8 @@ fun BitwardenPasswordFieldWithActions(
initialShowPassword: Boolean = false,
showPasswordTestTag: String? = null,
passwordFieldTestTag: String? = null,
cardStyle: CardStyle? = null,
actionsPadding: PaddingValues = PaddingValues(end = 4.dp),
actions: @Composable RowScope.() -> Unit = {},
) {
var shouldShowPassword by remember { mutableStateOf(initialShowPassword) }
@@ -122,11 +175,12 @@ fun BitwardenPasswordFieldWithActions(
singleLine = singleLine,
showPasswordTestTag = showPasswordTestTag,
passwordFieldTestTag = passwordFieldTestTag,
actionsPadding = actionsPadding,
actions = actions,
)
}
@Preview(showBackground = true)
@Preview
@Composable
private fun BitwardenPasswordFieldWithActions_preview() {
BitwardenTheme {
@@ -135,12 +189,13 @@ private fun BitwardenPasswordFieldWithActions_preview() {
value = "samplePassword",
onValueChange = {},
actions = {
BitwardenTonalIconButton(
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_check_mark,
contentDescription = "",
onClick = {},
)
},
cardStyle = CardStyle.Full,
)
}
}
@@ -1,13 +1,19 @@
package com.x8bit.bitwarden.ui.platform.components.field
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
@@ -33,12 +39,16 @@ import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.PopupProperties
import com.x8bit.bitwarden.ui.platform.base.util.cardBackground
import com.x8bit.bitwarden.ui.platform.base.util.cardPadding
import com.x8bit.bitwarden.ui.platform.base.util.toPx
import com.x8bit.bitwarden.ui.platform.base.util.withLineBreaksAtWidth
import com.x8bit.bitwarden.ui.platform.components.appbar.color.bitwardenMenuItemColors
import com.x8bit.bitwarden.ui.platform.components.divider.BitwardenHorizontalDivider
import com.x8bit.bitwarden.ui.platform.components.field.color.bitwardenTextFieldColors
import com.x8bit.bitwarden.ui.platform.components.field.toolbar.BitwardenCutCopyTextToolbar
import com.x8bit.bitwarden.ui.platform.components.field.toolbar.BitwardenEmptyTextToolbar
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.model.IconResource
import com.x8bit.bitwarden.ui.platform.components.model.TextToolbarType
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
@@ -57,7 +67,7 @@ import kotlinx.collections.immutable.toImmutableList
* the [value] is empty.
* @param leadingIconResource the optional resource for the leading icon on the text field.
* @param trailingIconContent the content for the trailing icon in the text field.
* @param hint optional hint text that will appear below the text input.
* @param supportingText optional supporting text that will appear below the text input.
* @param singleLine when `true`, this text field becomes a single line that horizontally scrolls
* instead of wrapping onto multiple lines.
* @param readOnly `true` if the input should be read-only and not accept user interactions.
@@ -68,8 +78,8 @@ import kotlinx.collections.immutable.toImmutableList
* @param visualTransformation Transforms the visual representation of the input [value].
* @param keyboardType the preferred type of keyboard input.
* @param textToolbarType The type of [TextToolbar] to use on the text field.
* @param cardStyle Indicates the type of card style to be applied.
*/
@Suppress("LongMethod", "CyclomaticComplexMethod")
@Composable
fun BitwardenTextField(
label: String?,
@@ -79,7 +89,7 @@ fun BitwardenTextField(
placeholder: String? = null,
leadingIconResource: IconResource? = null,
trailingIconContent: (@Composable () -> Unit)? = null,
hint: String? = null,
supportingText: String? = null,
singleLine: Boolean = true,
readOnly: Boolean = false,
enabled: Boolean = true,
@@ -92,6 +102,91 @@ fun BitwardenTextField(
textToolbarType: TextToolbarType = TextToolbarType.DEFAULT,
autoCompleteOptions: ImmutableList<String> = persistentListOf(),
textFieldTestTag: String? = null,
cardStyle: CardStyle? = null,
) {
BitwardenTextField(
modifier = modifier,
label = label,
value = value,
onValueChange = onValueChange,
placeholder = placeholder,
leadingIconResource = leadingIconResource,
trailingIconContent = trailingIconContent,
supportingTextContent = supportingText?.let {
{
Text(
text = it,
style = BitwardenTheme.typography.bodySmall,
color = BitwardenTheme.colorScheme.text.secondary,
modifier = Modifier.fillMaxWidth(),
)
}
},
singleLine = singleLine,
readOnly = readOnly,
enabled = enabled,
textStyle = textStyle,
shouldAddCustomLineBreaks = shouldAddCustomLineBreaks,
keyboardType = keyboardType,
isError = isError,
autoFocus = autoFocus,
visualTransformation = visualTransformation,
textToolbarType = textToolbarType,
autoCompleteOptions = autoCompleteOptions,
textFieldTestTag = textFieldTestTag,
cardStyle = cardStyle,
)
}
/**
* Component that allows the user to input text. This composable will manage the state of
* the user's input.
* @param label label for the text field.
* @param value current next on the text field.
* @param modifier modifier for the composable.
* @param onValueChange callback that is triggered when the input of the text field changes.
* @param supportingTextContent An optional supporting text composable that will appear below the
* text input.
* @param placeholder the optional placeholder to be displayed when the text field is in focus and
* the [value] is empty.
* @param leadingIconResource the optional resource for the leading icon on the text field.
* @param trailingIconContent the content for the trailing icon in the text field.
* @param singleLine when `true`, this text field becomes a single line that horizontally scrolls
* instead of wrapping onto multiple lines.
* @param readOnly `true` if the input should be read-only and not accept user interactions.
* @param enabled Whether or not the text field is enabled.
* @param textStyle An optional style that may be used to override the default used.
* @param shouldAddCustomLineBreaks If `true`, line breaks will be inserted to allow for filling
* an entire line before breaking. `false` by default.
* @param visualTransformation Transforms the visual representation of the input [value].
* @param keyboardType the preferred type of keyboard input.
* @param textToolbarType The type of [TextToolbar] to use on the text field.
* @param cardStyle Indicates the type of card style to be applied.
*/
@Suppress("LongMethod", "CyclomaticComplexMethod")
@Composable
fun BitwardenTextField(
label: String?,
value: String,
onValueChange: (String) -> Unit,
supportingTextContent: (@Composable ColumnScope.() -> Unit)?,
modifier: Modifier = Modifier,
placeholder: String? = null,
leadingIconResource: IconResource? = null,
trailingIconContent: (@Composable () -> Unit)? = null,
singleLine: Boolean = true,
readOnly: Boolean = false,
enabled: Boolean = true,
textStyle: TextStyle = BitwardenTheme.typography.bodyLarge,
shouldAddCustomLineBreaks: Boolean = false,
keyboardType: KeyboardType = KeyboardType.Text,
isError: Boolean = false,
autoFocus: Boolean = false,
visualTransformation: VisualTransformation = VisualTransformation.None,
textToolbarType: TextToolbarType = TextToolbarType.DEFAULT,
autoCompleteOptions: ImmutableList<String> = persistentListOf(),
textFieldTestTag: String? = null,
cardStyle: CardStyle? = null,
) {
var widthPx by remember { mutableIntStateOf(0) }
val focusRequester = remember { FocusRequester() }
@@ -126,59 +221,71 @@ fun BitwardenTextField(
var lastTextValue by remember(value) { mutableStateOf(value = value) }
CompositionLocalProvider(value = LocalTextToolbar provides textToolbar) {
var hasFocused by remember { mutableStateOf(value = false) }
Box(modifier = modifier) {
OutlinedTextField(
colors = bitwardenTextFieldColors(),
Box(modifier = modifier.defaultMinSize(minHeight = 60.dp)) {
Column(
modifier = Modifier
.testTag(textFieldTestTag.orEmpty())
.onGloballyPositioned { widthPx = it.size.width }
.onFocusEvent { focusState -> hasFocused = focusState.hasFocus }
.focusRequester(focusRequester)
.cardBackground(cardStyle = cardStyle)
.cardPadding(cardStyle = cardStyle, vertical = 6.dp)
.fillMaxWidth(),
enabled = enabled,
label = label?.let { { Text(text = it) } },
value = textFieldValue,
leadingIcon = leadingIconResource?.let { iconResource ->
{
Icon(
painter = iconResource.iconPainter,
contentDescription = iconResource.contentDescription,
)
}
},
trailingIcon = trailingIconContent,
placeholder = placeholder?.let {
{
Text(
text = it,
style = textStyle,
)
}
},
supportingText = hint?.let {
{
Text(
text = hint,
style = BitwardenTheme.typography.bodySmall,
)
}
},
onValueChange = {
hasFocused = true
textFieldValueState = it
val stringChangedSinceLastInvocation = lastTextValue != it.text
lastTextValue = it.text
if (stringChangedSinceLastInvocation) {
onValueChange(it.text)
}
},
singleLine = singleLine,
readOnly = readOnly,
textStyle = textStyle,
keyboardOptions = KeyboardOptions.Default.copy(keyboardType = keyboardType),
isError = isError,
visualTransformation = visualTransformation,
)
) {
TextField(
colors = bitwardenTextFieldColors(),
enabled = enabled,
label = label?.let { { Text(text = it) } },
value = textFieldValue,
leadingIcon = leadingIconResource?.let { iconResource ->
{
Icon(
painter = iconResource.iconPainter,
contentDescription = iconResource.contentDescription,
)
}
},
trailingIcon = trailingIconContent,
placeholder = placeholder?.let {
{
Text(
text = it,
style = textStyle,
)
}
},
onValueChange = {
hasFocused = true
textFieldValueState = it
val stringChangedSinceLastInvocation = lastTextValue != it.text
lastTextValue = it.text
if (stringChangedSinceLastInvocation) {
onValueChange(it.text)
}
},
singleLine = singleLine,
readOnly = readOnly,
textStyle = textStyle,
keyboardOptions = KeyboardOptions.Default.copy(keyboardType = keyboardType),
isError = isError,
visualTransformation = visualTransformation,
modifier = Modifier.fillMaxWidth(),
)
supportingTextContent?.let {
Spacer(modifier = Modifier.height(height = 8.dp))
BitwardenHorizontalDivider(
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp),
)
Spacer(modifier = Modifier.height(height = 12.dp))
Column(
modifier = Modifier.padding(horizontal = 16.dp),
content = it,
)
Spacer(modifier = Modifier.height(height = 6.dp))
}
}
val filteredAutoCompleteList = autoCompleteOptions
.filter { option ->
option.startsWith(textFieldValue.text) && option != textFieldValue.text
@@ -208,22 +315,23 @@ fun BitwardenTextField(
@Preview
@Composable
private fun BitwardenTextField_preview_withInput() {
BitwardenTextField(
label = "Label",
value = "Input",
onValueChange = {},
hint = "Hint",
)
}
@Preview
@Composable
private fun BitwardenTextField_preview_withoutInput() {
BitwardenTextField(
label = "Label",
value = "",
onValueChange = {},
hint = "Hint",
)
private fun BitwardenTextField_preview() {
BitwardenTheme {
Column {
BitwardenTextField(
label = "Label",
value = "Input",
onValueChange = {},
supportingText = "Hint",
cardStyle = CardStyle.Top(),
)
BitwardenTextField(
label = "Label",
value = "",
onValueChange = {},
supportingText = "Hint",
cardStyle = CardStyle.Bottom,
)
}
}
}
@@ -1,9 +1,17 @@
package com.x8bit.bitwarden.ui.platform.components.field
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -14,7 +22,12 @@ import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.cardBackground
import com.x8bit.bitwarden.ui.platform.base.util.cardPadding
import com.x8bit.bitwarden.ui.platform.components.divider.BitwardenHorizontalDivider
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.model.TextToolbarType
import com.x8bit.bitwarden.ui.platform.components.row.BitwardenRowOfActions
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
@@ -29,6 +42,8 @@ import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
* @param value Current text in the text field.
* @param onValueChange Callback that is triggered when the text content changes.
* @param modifier [Modifier] applied to this layout composable.
* @param supportingText An optional supporting text that will appear below the text field and
* actions.
* @param textStyle The [TextStyle], or null if default.
* @param shouldAddCustomLineBreaks If `true`, line breaks will be inserted to allow for filling
* an entire line before breaking. `false` by default.
@@ -41,9 +56,11 @@ import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
* @param actions A lambda containing the set of actions (usually icons or similar) to display
* next to the text field. This lambda extends [RowScope],
* providing flexibility in the layout definition.
* @param actionsPadding Padding to be applied to the [actions] block.
* @param actionsTestTag The test tag to use for the row of actions, or null if there is none.
* @param textFieldTestTag The test tag to be used on the text field.
* @param textToolbarType The type of [TextToolbar] to use on the text field.
* @param cardStyle Indicates the type of card style to be applied.
*/
@Composable
fun BitwardenTextFieldWithActions(
@@ -51,6 +68,7 @@ fun BitwardenTextFieldWithActions(
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
supportingText: String? = null,
textStyle: TextStyle = BitwardenTheme.typography.bodyLarge,
shouldAddCustomLineBreaks: Boolean = false,
visualTransformation: VisualTransformation = VisualTransformation.None,
@@ -59,39 +77,65 @@ fun BitwardenTextFieldWithActions(
keyboardType: KeyboardType = KeyboardType.Text,
trailingIconContent: (@Composable () -> Unit)? = null,
actions: @Composable RowScope.() -> Unit = {},
actionsPadding: PaddingValues = PaddingValues(end = 4.dp),
actionsTestTag: String? = null,
textFieldTestTag: String? = null,
textToolbarType: TextToolbarType = TextToolbarType.DEFAULT,
cardStyle: CardStyle? = null,
) {
Row(
Column(
modifier = modifier
.fillMaxWidth()
.defaultMinSize(minHeight = 60.dp)
.cardBackground(cardStyle = cardStyle)
.cardPadding(cardStyle = cardStyle, vertical = 6.dp)
.semantics(mergeDescendants = true) { },
verticalAlignment = Alignment.CenterVertically,
verticalArrangement = Arrangement.Center,
) {
BitwardenTextField(
modifier = Modifier.weight(1f),
label = label,
value = value,
readOnly = readOnly,
singleLine = singleLine,
onValueChange = onValueChange,
keyboardType = keyboardType,
trailingIconContent = trailingIconContent,
textStyle = textStyle,
shouldAddCustomLineBreaks = shouldAddCustomLineBreaks,
visualTransformation = visualTransformation,
textToolbarType = textToolbarType,
textFieldTestTag = textFieldTestTag,
)
BitwardenRowOfActions(
modifier = Modifier.run { actionsTestTag?.let { testTag(it) } ?: this },
actions = actions,
)
Row(
verticalAlignment = Alignment.CenterVertically,
) {
BitwardenTextField(
modifier = Modifier.weight(weight = 1f),
label = label,
value = value,
readOnly = readOnly,
singleLine = singleLine,
onValueChange = onValueChange,
keyboardType = keyboardType,
trailingIconContent = trailingIconContent,
textStyle = textStyle,
shouldAddCustomLineBreaks = shouldAddCustomLineBreaks,
visualTransformation = visualTransformation,
textToolbarType = textToolbarType,
textFieldTestTag = textFieldTestTag,
)
BitwardenRowOfActions(
modifier = Modifier
.padding(paddingValues = actionsPadding)
.run { actionsTestTag?.let { testTag(it) } ?: this },
actions = actions,
)
}
supportingText?.let {
Spacer(modifier = Modifier.height(height = 8.dp))
BitwardenHorizontalDivider(
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp),
)
Spacer(modifier = Modifier.height(height = 12.dp))
Text(
text = it,
style = BitwardenTheme.typography.bodySmall,
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp),
)
}
}
}
@Preview(showBackground = true)
@Preview
@Composable
private fun BitwardenTextFieldWithActions_preview() {
BitwardenTheme {
@@ -99,6 +143,7 @@ private fun BitwardenTextFieldWithActions_preview() {
label = "Username",
value = "user@example.com",
onValueChange = {},
supportingText = "I'm here for support",
actions = {
Icon(
painter = rememberVectorPainter(id = R.drawable.ic_question_circle),
@@ -109,6 +154,7 @@ private fun BitwardenTextFieldWithActions_preview() {
contentDescription = "Action 2",
)
},
cardStyle = CardStyle.Full,
)
}
}
@@ -11,10 +11,10 @@ import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
*/
@Composable
fun bitwardenTextFieldButtonColors(): TextFieldColors = bitwardenTextFieldColors(
unfocusedBorderColor = BitwardenTheme.colorScheme.stroke.divider,
focusedBorderColor = BitwardenTheme.colorScheme.stroke.divider,
unfocusedBorderColor = Color.Transparent,
focusedBorderColor = Color.Transparent,
disabledTextColor = BitwardenTheme.colorScheme.text.primary,
disabledBorderColor = BitwardenTheme.colorScheme.stroke.divider,
disabledBorderColor = Color.Transparent,
disabledLeadingIconColor = BitwardenTheme.colorScheme.icon.primary,
disabledTrailingIconColor = BitwardenTheme.colorScheme.icon.primary,
disabledLabelColor = BitwardenTheme.colorScheme.text.secondary,
@@ -27,10 +27,10 @@ fun bitwardenTextFieldButtonColors(): TextFieldColors = bitwardenTextFieldColors
*/
@Composable
fun bitwardenTextFieldColors(
unfocusedBorderColor: Color = BitwardenTheme.colorScheme.stroke.divider,
focusedBorderColor: Color = BitwardenTheme.colorScheme.stroke.border,
focusedBorderColor: Color = Color.Transparent,
unfocusedBorderColor: Color = Color.Transparent,
disabledTextColor: Color = BitwardenTheme.colorScheme.outlineButton.foregroundDisabled,
disabledBorderColor: Color = BitwardenTheme.colorScheme.outlineButton.borderDisabled,
disabledBorderColor: Color = Color.Transparent,
disabledLeadingIconColor: Color = BitwardenTheme.colorScheme.outlineButton.foregroundDisabled,
disabledTrailingIconColor: Color = BitwardenTheme.colorScheme.outlineButton.foregroundDisabled,
disabledLabelColor: Color = BitwardenTheme.colorScheme.outlineButton.foregroundDisabled,
@@ -1,12 +1,10 @@
package com.x8bit.bitwarden.ui.platform.components.header
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
/**
@@ -27,10 +25,7 @@ fun BitwardenListHeaderText(
text = "${label.uppercase()}$supportLabel",
style = BitwardenTheme.typography.eyebrowMedium,
color = BitwardenTheme.colorScheme.text.secondary,
modifier = modifier.padding(
top = 12.dp,
bottom = 4.dp,
),
modifier = modifier,
)
}
@@ -4,7 +4,7 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
@@ -19,8 +19,9 @@ import androidx.compose.ui.semantics.testTag
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.bottomDivider
import com.x8bit.bitwarden.ui.platform.base.util.mirrorIfRtl
import com.x8bit.bitwarden.ui.platform.base.util.cardBackground
import com.x8bit.bitwarden.ui.platform.base.util.cardPadding
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
@@ -35,6 +36,7 @@ import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
* @param modifier The [Modifier] to be applied to the [Row] composable that holds the list item.
* @param showDivider Indicates whether the divider should be shown or not.
* @param startIconTestTag The optional test tag for the [startIcon].
* @param cardStyle Indicates the type of card style to be applied.
*/
@Composable
fun BitwardenGroupItem(
@@ -45,9 +47,12 @@ fun BitwardenGroupItem(
modifier: Modifier = Modifier,
showDivider: Boolean = true,
startIconTestTag: String? = null,
cardStyle: CardStyle?,
) {
Row(
modifier = Modifier
modifier = modifier
.defaultMinSize(minHeight = 60.dp)
.cardBackground(cardStyle = cardStyle)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = ripple(
@@ -55,16 +60,7 @@ fun BitwardenGroupItem(
),
onClick = onClick,
)
.bottomDivider(
enabled = showDivider,
paddingStart = 16.dp,
)
.padding(
top = 16.dp,
bottom = 16.dp,
end = 8.dp,
)
.then(modifier),
.cardPadding(cardStyle = cardStyle, horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp),
) {
@@ -73,6 +69,7 @@ fun BitwardenGroupItem(
contentDescription = null,
tint = BitwardenTheme.colorScheme.icon.primary,
modifier = Modifier
.defaultMinSize(minHeight = 36.dp)
.semantics { startIconTestTag?.let { testTag = it } }
.size(24.dp),
)
@@ -89,19 +86,10 @@ fun BitwardenGroupItem(
style = BitwardenTheme.typography.labelSmall,
color = BitwardenTheme.colorScheme.text.primary,
)
Icon(
painter = rememberVectorPainter(id = R.drawable.ic_chevron_right),
contentDescription = null,
tint = BitwardenTheme.colorScheme.icon.primary,
modifier = Modifier
.mirrorIfRtl()
.size(24.dp),
)
}
}
@Preview(showBackground = true)
@Preview
@Composable
private fun BitwardenGroupItem_preview() {
BitwardenTheme {
@@ -111,6 +99,7 @@ private fun BitwardenGroupItem_preview() {
startIcon = rememberVectorPainter(id = R.drawable.ic_file_text),
startIconTestTag = "Test Tag",
onClick = {},
cardStyle = CardStyle.Full,
)
}
}
@@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Icon
@@ -30,10 +29,14 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.cardBackground
import com.x8bit.bitwarden.ui.platform.base.util.cardPadding
import com.x8bit.bitwarden.ui.platform.base.util.orNullIfBlank
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenStandardIconButton
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenSelectionDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.row.BitwardenBasicDialogRow
import com.x8bit.bitwarden.ui.platform.components.icon.BitwardenIcon
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.model.IconData
import com.x8bit.bitwarden.ui.platform.components.model.IconResource
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
@@ -59,6 +62,7 @@ import kotlinx.collections.immutable.persistentListOf
* @param supportingLabelTestTag The optional test tag for the [supportingLabel].
* @param startIconTestTag The optional test tag for the [startIcon].
* @param trailingLabelIcons An optional list of small icons to be displayed after the [label].
* @param cardStyle Indicates the type of card style to be applied.
*/
@Suppress("LongMethod")
@Composable
@@ -76,10 +80,13 @@ fun BitwardenListItem(
supportingLabelTestTag: String? = null,
startIconTestTag: String? = null,
trailingLabelIcons: ImmutableList<IconResource> = persistentListOf(),
cardStyle: CardStyle? = null,
) {
var shouldShowDialog by rememberSaveable { mutableStateOf(false) }
Row(
modifier = Modifier
modifier = modifier
.defaultMinSize(minHeight = 60.dp)
.cardBackground(cardStyle = cardStyle)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = ripple(
@@ -87,9 +94,11 @@ fun BitwardenListItem(
),
onClick = onClick,
)
.defaultMinSize(minHeight = 72.dp)
.padding(vertical = 8.dp)
.then(modifier),
.cardPadding(
cardStyle = cardStyle,
start = 16.dp,
end = 4.dp,
),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp),
) {
@@ -130,7 +139,7 @@ fun BitwardenListItem(
}
}
secondSupportingLabel?.let { secondSupportLabel ->
secondSupportingLabel.orNullIfBlank()?.let { secondSupportLabel ->
Text(
text = secondSupportLabel,
style = BitwardenTheme.typography.bodyMedium,
@@ -141,7 +150,7 @@ fun BitwardenListItem(
)
}
supportingLabel?.let { supportLabel ->
supportingLabel.orNullIfBlank()?.let { supportLabel ->
Text(
text = supportLabel,
style = BitwardenTheme.typography.bodyMedium,
@@ -0,0 +1,51 @@
package com.x8bit.bitwarden.ui.platform.components.model
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
/**
* Defines the possible display options for a card background on a composable component.
*/
sealed class CardStyle {
/**
* Indicates if a divider should be displayed in this card.
*/
abstract val hasDivider: Boolean
/**
* Provides start padding to the divider when present.
*/
abstract val dividerPadding: Dp
/**
* Defines a top card that has top rounded corners.
*/
data class Top(
override val hasDivider: Boolean = true,
override val dividerPadding: Dp = 16.dp,
) : CardStyle()
/**
* Defines a middle card that has no rounded corners.
*/
data class Middle(
override val hasDivider: Boolean = true,
override val dividerPadding: Dp = 16.dp,
) : CardStyle()
/**
* Defines a bottom card that has bottom rounded corners.
*/
data object Bottom : CardStyle() {
override val hasDivider: Boolean = false
override val dividerPadding: Dp = 0.dp
}
/**
* Defines a full card that has rounded corners.
*/
data object Full : CardStyle() {
override val hasDivider: Boolean = false
override val dividerPadding: Dp = 0.dp
}
}
@@ -12,6 +12,7 @@ import androidx.compose.ui.tooling.preview.Preview
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.mirrorIfRtl
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
@@ -31,6 +32,7 @@ import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
* user clicks this item.
* @param dialogDismissButtonText The text on the dismiss button of the dialog displayed when the
* user clicks this item.
* @param cardStyle Indicates the type of card style to be applied.
*/
@Composable
fun BitwardenExternalLinkRow(
@@ -43,6 +45,7 @@ fun BitwardenExternalLinkRow(
dialogMessage: String,
dialogConfirmButtonText: String = stringResource(id = R.string.continue_text),
dialogDismissButtonText: String = stringResource(id = R.string.cancel),
cardStyle: CardStyle? = null,
) {
var shouldShowDialog by rememberSaveable { mutableStateOf(false) }
BitwardenTextRow(
@@ -51,6 +54,7 @@ fun BitwardenExternalLinkRow(
onClick = { shouldShowDialog = true },
modifier = modifier,
withDivider = withDivider,
cardStyle = cardStyle,
) {
Icon(
modifier = Modifier.mirrorIfRtl(),
@@ -1,6 +1,5 @@
package com.x8bit.bitwarden.ui.platform.components.row
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.padding
@@ -32,7 +31,6 @@ fun BitwardenRowOfActions(
) {
Row(
modifier = modifier.padding(start = 8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
content = actions,
)
@@ -15,9 +15,13 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.ui.platform.base.util.cardBackground
import com.x8bit.bitwarden.ui.platform.base.util.cardPadding
import com.x8bit.bitwarden.ui.platform.components.divider.BitwardenHorizontalDivider
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
/**
@@ -28,10 +32,12 @@ import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
* @param onClick The callback when the row is clicked.
* @param modifier The modifier to be applied to the layout.
* @param description An optional description label to be displayed below the [text].
* @param textTestTag The optional test tag for the inner text component.
* @param isEnabled Indicates if the row is enabled or not, a disabled row will not be clickable
* and it's contents will be dimmed.
* @param withDivider Indicates if a divider should be drawn on the bottom of the row, defaults
* to `false`.
* @param cardStyle Indicates the type of card style to be applied.
* @param content The content of the [BitwardenTextRow].
*/
@Composable
@@ -40,13 +46,17 @@ fun BitwardenTextRow(
onClick: () -> Unit,
modifier: Modifier = Modifier,
description: String? = null,
textTestTag: String? = null,
isEnabled: Boolean = true,
withDivider: Boolean = false,
cardStyle: CardStyle? = null,
content: (@Composable () -> Unit)? = null,
) {
Box(
contentAlignment = Alignment.BottomCenter,
contentAlignment = Alignment.CenterStart,
modifier = modifier
.defaultMinSize(minHeight = 60.dp)
.cardBackground(cardStyle = cardStyle)
.clickable(
enabled = isEnabled,
interactionSource = remember { MutableInteractionSource() },
@@ -55,13 +65,11 @@ fun BitwardenTextRow(
),
onClick = onClick,
)
.cardPadding(cardStyle = cardStyle, horizontal = 16.dp)
.semantics(mergeDescendants = true) { },
) {
Row(
modifier = Modifier
.defaultMinSize(minHeight = 56.dp)
.padding(start = 16.dp, end = 24.dp, top = 8.dp, bottom = 8.dp)
.fillMaxWidth(),
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
@@ -78,6 +86,7 @@ fun BitwardenTextRow(
} else {
BitwardenTheme.colorScheme.filledButton.foregroundDisabled
},
modifier = Modifier.run { textTestTag?.let { testTag(it) } ?: this },
)
description?.let {
Text(
@@ -2,6 +2,7 @@ package com.x8bit.bitwarden.ui.platform.components.slider
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentWidth
@@ -22,7 +23,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onPreviewKeyEvent
@@ -37,8 +37,11 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.cardBackground
import com.x8bit.bitwarden.ui.platform.base.util.cardPadding
import com.x8bit.bitwarden.ui.platform.base.util.toDp
import com.x8bit.bitwarden.ui.platform.components.field.color.bitwardenTextFieldColors
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.slider.color.bitwardenSliderColors
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
@@ -52,6 +55,7 @@ import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
* @param modifier The [Modifier] to be applied to this radio button.
* @param sliderTag The option test tag for the slider component.
* @param valueTag The option test tag for the value field component.
* @param cardStyle Indicates the type of card style to be applied.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Suppress("LongMethod")
@@ -63,13 +67,18 @@ fun BitwardenSlider(
modifier: Modifier = Modifier,
sliderTag: String? = null,
valueTag: String? = null,
cardStyle: CardStyle? = null,
) {
val sliderValue by rememberUpdatedState(newValue = value.coerceIn(range = range))
var labelTextWidth by remember { mutableStateOf(value = Dp.Unspecified) }
val density = LocalDensity.current
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = modifier.semantics(mergeDescendants = true) {},
modifier = modifier
.defaultMinSize(minHeight = 60.dp)
.cardBackground(cardStyle = cardStyle)
.cardPadding(cardStyle = cardStyle, end = 16.dp)
.semantics(mergeDescendants = true) {},
) {
TextField(
value = sliderValue.toString(),
@@ -79,6 +88,7 @@ fun BitwardenSlider(
label = {
Text(
text = stringResource(id = R.string.length),
style = BitwardenTheme.typography.bodySmall,
modifier = Modifier.onGloballyPositioned { layoutCoordinates ->
if (labelTextWidth == Dp.Unspecified) {
labelTextWidth = layoutCoordinates.size.width.toDp(density = density)
@@ -88,11 +98,7 @@ fun BitwardenSlider(
},
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
colors = bitwardenTextFieldColors(
disabledBorderColor = Color.Transparent,
focusedBorderColor = Color.Transparent,
unfocusedBorderColor = Color.Transparent,
),
colors = bitwardenTextFieldColors(),
modifier = Modifier
.onPreviewKeyEvent { keyEvent ->
when (keyEvent.key) {
@@ -1,14 +1,29 @@
package com.x8bit.bitwarden.ui.platform.components.stepper
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.ZERO_WIDTH_CHARACTER
import com.x8bit.bitwarden.ui.platform.base.util.cardBackground
import com.x8bit.bitwarden.ui.platform.base.util.cardPadding
import com.x8bit.bitwarden.ui.platform.base.util.orNullIfBlank
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenFilledIconButton
import com.x8bit.bitwarden.ui.platform.components.divider.BitwardenHorizontalDivider
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextFieldWithActions
import com.x8bit.bitwarden.ui.platform.components.field.color.bitwardenTextFieldColors
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
/**
* Displays a stepper that allows the user to increment and decrement an int value.
@@ -19,24 +34,84 @@ import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextFieldWithAc
* @param onValueChange callback invoked when the user increments or decrements the count. Note
* that this will not be called if the attempts to move value outside of [range].
* @param modifier Modifier.
* @param supportingText An optional supporting text that will appear below the stepper.
* @param range Range of valid values.
* @param isIncrementEnabled whether or not the increment button should be enabled.
* @param isDecrementEnabled whether or not the decrement button should be enabled.
* @param textFieldReadOnly whether or not the text field should be read only. The stepper
* increment and decrement buttons function regardless of this value.
* @param cardStyle Indicates the type of card style to be applied.
*/
@Suppress("CyclomaticComplexMethod")
@Composable
fun BitwardenStepper(
label: String,
value: Int?,
onValueChange: (Int) -> Unit,
modifier: Modifier = Modifier,
supportingText: String? = null,
range: ClosedRange<Int> = 1..Int.MAX_VALUE,
isIncrementEnabled: Boolean = true,
isDecrementEnabled: Boolean = true,
textFieldReadOnly: Boolean = true,
stepperActionsTestTag: String? = null,
cardStyle: CardStyle? = null,
) {
BitwardenStepper(
modifier = modifier,
label = label,
value = value,
onValueChange = onValueChange,
supportingTextContent = supportingText?.let {
{
Text(
text = it,
style = BitwardenTheme.typography.bodySmall,
color = bitwardenTextFieldColors().focusedSupportingTextColor,
modifier = Modifier.fillMaxWidth(),
)
}
},
range = range,
isIncrementEnabled = isIncrementEnabled,
isDecrementEnabled = isDecrementEnabled,
textFieldReadOnly = textFieldReadOnly,
stepperActionsTestTag = stepperActionsTestTag,
cardStyle = cardStyle,
)
}
/**
* Displays a stepper that allows the user to increment and decrement an int value.
*
* @param label Label for the stepper.
* @param value Value to display. Null will display nothing. Will be clamped to [range] before
* display.
* @param onValueChange callback invoked when the user increments or decrements the count. Note
* that this will not be called if the attempts to move value outside of [range].
* @param modifier Modifier.
* @param supportingTextContent An optional supporting text composable that will appear below the
* stepper.
* @param range Range of valid values.
* @param isIncrementEnabled whether or not the increment button should be enabled.
* @param isDecrementEnabled whether or not the decrement button should be enabled.
* @param textFieldReadOnly whether or not the text field should be read only. The stepper
* increment and decrement buttons function regardless of this value.
* @param cardStyle Indicates the type of card style to be applied.
*/
@Suppress("LongMethod", "CyclomaticComplexMethod")
@Composable
fun BitwardenStepper(
label: String,
value: Int?,
onValueChange: (Int) -> Unit,
modifier: Modifier = Modifier,
supportingTextContent: @Composable (ColumnScope.() -> Unit)?,
range: ClosedRange<Int> = 1..Int.MAX_VALUE,
isIncrementEnabled: Boolean = true,
isDecrementEnabled: Boolean = true,
textFieldReadOnly: Boolean = true,
stepperActionsTestTag: String? = null,
cardStyle: CardStyle? = null,
) {
val clampedValue = value?.coerceIn(range)
if (clampedValue != value && clampedValue != null) {
@@ -44,50 +119,67 @@ fun BitwardenStepper(
}
val isAtRangeMinimum = clampedValue?.let { (it - 1) !in range } ?: true
val isAtRangeMaximum = clampedValue?.let { (it + 1) !in range } ?: false
BitwardenTextFieldWithActions(
label = label,
// We use the zero width character instead of an empty string to make sure label is shown
// small and above the input
value = clampedValue?.toString() ?: ZERO_WIDTH_CHARACTER,
actionsTestTag = stepperActionsTestTag,
actions = {
BitwardenFilledIconButton(
vectorIconRes = R.drawable.ic_minus,
contentDescription = "\u2212",
onClick = {
val decrementedValue = ((clampedValue ?: 0) - 1).coerceIn(range)
if (decrementedValue != clampedValue) {
onValueChange(decrementedValue)
}
},
isEnabled = isDecrementEnabled && !isAtRangeMinimum,
modifier = Modifier.testTag("DecrementValue"),
Column(
modifier = modifier
.defaultMinSize(minHeight = 60.dp)
.cardBackground(cardStyle = cardStyle)
.cardPadding(cardStyle = cardStyle, vertical = 8.dp),
) {
BitwardenTextFieldWithActions(
label = label,
value = clampedValue?.toString().orEmpty(),
actionsTestTag = stepperActionsTestTag,
actions = {
BitwardenFilledIconButton(
vectorIconRes = R.drawable.ic_minus,
contentDescription = "\u2212",
onClick = {
val decrementedValue = ((clampedValue ?: 0) - 1).coerceIn(range)
if (decrementedValue != clampedValue) {
onValueChange(decrementedValue)
}
},
isEnabled = isDecrementEnabled && !isAtRangeMinimum,
modifier = Modifier.testTag("DecrementValue"),
)
BitwardenFilledIconButton(
vectorIconRes = R.drawable.ic_plus,
contentDescription = "+",
onClick = {
val incrementedValue = ((clampedValue ?: 0) + 1).coerceIn(range)
if (incrementedValue != clampedValue) {
onValueChange(incrementedValue)
}
},
isEnabled = isIncrementEnabled && !isAtRangeMaximum,
modifier = Modifier.testTag("IncrementValue"),
)
},
readOnly = textFieldReadOnly,
keyboardType = KeyboardType.Number,
onValueChange = { newValue ->
onValueChange(
newValue
.orNullIfBlank()
?.let { it.toIntOrNull()?.coerceIn(range) ?: clampedValue }
?: range.start,
)
},
actionsPadding = PaddingValues(end = 12.dp),
)
supportingTextContent?.let {
Spacer(modifier = Modifier.height(height = 8.dp))
BitwardenHorizontalDivider(
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp),
)
BitwardenFilledIconButton(
vectorIconRes = R.drawable.ic_plus,
contentDescription = "+",
onClick = {
val incrementedValue = ((clampedValue ?: 0) + 1).coerceIn(range)
if (incrementedValue != clampedValue) {
onValueChange(incrementedValue)
}
},
isEnabled = isIncrementEnabled && !isAtRangeMaximum,
modifier = Modifier.testTag("IncrementValue"),
Spacer(modifier = Modifier.height(height = 12.dp))
Column(
modifier = Modifier.padding(horizontal = 16.dp),
content = it,
)
},
readOnly = textFieldReadOnly,
keyboardType = KeyboardType.Number,
onValueChange = { newValue ->
onValueChange(
newValue
// Make sure the placeholder is gone, since it will mess up the int conversion
.replace(ZERO_WIDTH_CHARACTER, "")
.orNullIfBlank()
?.let { it.toIntOrNull()?.coerceIn(range) ?: clampedValue }
?: range.start,
)
},
modifier = modifier,
)
Spacer(modifier = Modifier.height(height = 4.dp))
}
}
}
@@ -34,6 +34,7 @@ fun BitwardenClickableText(
style: TextStyle,
modifier: Modifier = Modifier,
innerPadding: PaddingValues = PaddingValues(vertical = 4.dp, horizontal = 16.dp),
isEnabled: Boolean = true,
cornerSize: Dp = 28.dp,
color: Color = BitwardenTheme.colorScheme.text.interaction,
) {
@@ -46,6 +47,7 @@ fun BitwardenClickableText(
color = BitwardenTheme.colorScheme.background.pressed,
),
interactionSource = remember { MutableInteractionSource() },
enabled = isEnabled,
onClick = onClick,
)
.padding(innerPadding),
@@ -2,14 +2,17 @@ package com.x8bit.bitwarden.ui.platform.components.toggle
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.ripple
@@ -26,8 +29,12 @@ import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.cardBackground
import com.x8bit.bitwarden.ui.platform.base.util.cardPadding
import com.x8bit.bitwarden.ui.platform.base.util.toAnnotatedString
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenStandardIconButton
import com.x8bit.bitwarden.ui.platform.components.divider.BitwardenHorizontalDivider
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.toggle.color.bitwardenSwitchColors
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
@@ -38,11 +45,12 @@ import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
* @param isChecked The current state of the switch (either checked or unchecked).
* @param onCheckedChange A lambda that is invoked when the switch's state changes.
* @param modifier A [Modifier] that you can use to apply custom modifications to the composable.
* @param description An optional description label to be displayed below the [label].
* @param supportingText An optional supporting text to be displayed below the [label].
* @param contentDescription A description of the switch's UI for accessibility purposes.
* @param readOnly Disables the click functionality without modifying the other UI characteristics.
* @param enabled Whether or not this switch is enabled. This is similar to setting [readOnly] but
* comes with some additional visual changes.
* @param cardStyle Indicates the type of card style to be applied.
* @param actions A lambda containing the set of actions (usually icons or similar) to display
* in between the [label] and the toggle. This lambda extends [RowScope], allowing flexibility in
* defining the layout of the actions.
@@ -53,10 +61,11 @@ fun BitwardenSwitch(
isChecked: Boolean,
onCheckedChange: ((Boolean) -> Unit)?,
modifier: Modifier = Modifier,
description: String? = null,
supportingText: String? = null,
contentDescription: String? = null,
readOnly: Boolean = false,
enabled: Boolean = true,
cardStyle: CardStyle? = null,
actions: (@Composable RowScope.() -> Unit)? = null,
) {
BitwardenSwitch(
@@ -67,9 +76,10 @@ fun BitwardenSwitch(
contentDescription = contentDescription,
readOnly = readOnly,
enabled = enabled,
cardStyle = cardStyle,
actions = actions,
subContent = {
description?.let {
supportingTextContent = supportingText?.let {
{
Text(
text = it,
style = BitwardenTheme.typography.bodyMedium,
@@ -91,11 +101,12 @@ fun BitwardenSwitch(
* @param isChecked The current state of the switch (either checked or unchecked).
* @param onCheckedChange A lambda that is invoked when the switch's state changes.
* @param modifier A [Modifier] that you can use to apply custom modifications to the composable.
* @param description An optional description label to be displayed below the [label].
* @param supportingText An optional supporting text to be displayed below the [label].
* @param contentDescription A description of the switch's UI for accessibility purposes.
* @param readOnly Disables the click functionality without modifying the other UI characteristics.
* @param enabled Whether or not this switch is enabled. This is similar to setting [readOnly] but
* comes with some additional visual changes.
* @param cardStyle Indicates the type of card style to be applied.
* @param actions A lambda containing the set of actions (usually icons or similar) to display
* in between the [label] and the toggle. This lambda extends [RowScope], allowing flexibility in
* defining the layout of the actions.
@@ -106,10 +117,11 @@ fun BitwardenSwitch(
isChecked: Boolean,
onCheckedChange: ((Boolean) -> Unit)?,
modifier: Modifier = Modifier,
description: String? = null,
supportingText: String? = null,
contentDescription: String? = null,
readOnly: Boolean = false,
enabled: Boolean = true,
cardStyle: CardStyle? = null,
actions: (@Composable RowScope.() -> Unit)? = null,
) {
BitwardenSwitch(
@@ -120,9 +132,10 @@ fun BitwardenSwitch(
contentDescription = contentDescription,
readOnly = readOnly,
enabled = enabled,
cardStyle = cardStyle,
actions = actions,
subContent = {
description?.let {
supportingTextContent = supportingText?.let {
{
Text(
text = it,
style = BitwardenTheme.typography.bodyMedium,
@@ -148,10 +161,11 @@ fun BitwardenSwitch(
* @param readOnly Disables the click functionality without modifying the other UI characteristics.
* @param enabled Whether or not this switch is enabled. This is similar to setting [readOnly] but
* comes with some additional visual changes.
* @param cardStyle Indicates the type of card style to be applied.
* @param actions A lambda containing the set of actions (usually icons or similar) to display
* in between the [label] and the toggle. This lambda extends [RowScope], allowing flexibility in
* defining the layout of the actions.
* @param subContent A lambda containing content directly below the label.
* @param supportingTextContent A lambda containing content directly below the label.
*/
@Composable
fun BitwardenSwitch(
@@ -162,8 +176,9 @@ fun BitwardenSwitch(
contentDescription: String? = null,
readOnly: Boolean = false,
enabled: Boolean = true,
cardStyle: CardStyle? = null,
actions: (@Composable RowScope.() -> Unit)? = null,
subContent: @Composable () -> Unit,
supportingTextContent: (@Composable ColumnScope.() -> Unit)?,
) {
BitwardenSwitch(
modifier = modifier,
@@ -173,8 +188,9 @@ fun BitwardenSwitch(
contentDescription = contentDescription,
readOnly = readOnly,
enabled = enabled,
cardStyle = cardStyle,
actions = actions,
subContent = subContent,
supportingTextContent = supportingTextContent,
)
}
@@ -189,10 +205,11 @@ fun BitwardenSwitch(
* @param readOnly Disables the click functionality without modifying the other UI characteristics.
* @param enabled Whether or not this switch is enabled. This is similar to setting [readOnly] but
* comes with some additional visual changes.
* @param cardStyle Indicates the type of card style to be applied.
* @param actions A lambda containing the set of actions (usually icons or similar) to display
* in between the [label] and the toggle. This lambda extends [RowScope], allowing flexibility in
* defining the layout of the actions.
* @param subContent A lambda containing content directly below the label.
* @param supportingTextContent A lambda containing content directly below the label.
*/
@Suppress("LongMethod")
@Composable
@@ -204,35 +221,44 @@ fun BitwardenSwitch(
contentDescription: String? = null,
readOnly: Boolean = false,
enabled: Boolean = true,
cardStyle: CardStyle? = null,
actions: (@Composable RowScope.() -> Unit)? = null,
subContent: @Composable () -> Unit,
supportingTextContent: @Composable (ColumnScope.() -> Unit)?,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.wrapContentHeight()
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = ripple(
color = BitwardenTheme.colorScheme.background.pressed,
),
onClick = { onCheckedChange?.invoke(!isChecked) },
enabled = !readOnly && enabled,
)
Column(
verticalArrangement = Arrangement.Center,
modifier = modifier
.defaultMinSize(minHeight = 60.dp)
.cardBackground(cardStyle = cardStyle)
.run {
onCheckedChange
?.let {
this.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = ripple(
color = BitwardenTheme.colorScheme.background.pressed,
),
onClick = { it(!isChecked) },
enabled = !readOnly && enabled,
)
}
?: this
}
.cardPadding(cardStyle = cardStyle)
.semantics(mergeDescendants = true) {
toggleableState = ToggleableState(isChecked)
contentDescription?.let { this.contentDescription = it }
}
.then(modifier),
},
) {
Row(
modifier = Modifier.weight(weight = 1f),
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.defaultMinSize(minHeight = 36.dp)
.padding(horizontal = 16.dp),
) {
Column(
modifier = Modifier
.weight(weight = 1f, fill = false)
.padding(vertical = 8.dp),
Row(
modifier = Modifier.weight(weight = 1f),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = label,
@@ -244,23 +270,33 @@ fun BitwardenSwitch(
},
modifier = Modifier.testTag(tag = "SwitchText"),
)
subContent()
actions?.invoke(this)
}
actions
?.invoke(this)
?: Spacer(modifier = Modifier.width(width = 16.dp))
Spacer(modifier = Modifier.width(width = 16.dp))
Switch(
modifier = Modifier
.defaultMinSize(minHeight = 48.dp)
.testTag(tag = "SwitchToggle"),
enabled = enabled,
checked = isChecked,
onCheckedChange = null,
colors = bitwardenSwitchColors(),
)
}
supportingTextContent?.let {
Spacer(modifier = Modifier.height(height = 12.dp))
BitwardenHorizontalDivider(
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp),
)
Spacer(modifier = Modifier.height(height = 12.dp))
Column(
modifier = Modifier.padding(horizontal = 16.dp),
content = it,
)
}
Switch(
modifier = Modifier
.height(height = 56.dp)
.testTag(tag = "SwitchToggle"),
enabled = enabled,
checked = isChecked,
onCheckedChange = null,
colors = bitwardenSwitchColors(),
)
}
}
@@ -270,7 +306,7 @@ private fun BitwardenSwitch_preview() {
Column {
BitwardenSwitch(
label = "Label",
description = "description",
supportingText = "description",
isChecked = true,
onCheckedChange = {},
)
@@ -281,7 +317,7 @@ private fun BitwardenSwitch_preview() {
)
BitwardenSwitch(
label = "Label",
description = "description",
supportingText = "description",
isChecked = true,
onCheckedChange = {},
actions = {
@@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.manager.biometrics.BiometricSupportStatus
/**
@@ -21,6 +22,7 @@ fun BitwardenUnlockWithBiometricsSwitch(
isChecked: Boolean,
onDisableBiometrics: () -> Unit,
onEnableBiometrics: () -> Unit,
cardStyle: CardStyle,
modifier: Modifier = Modifier,
) {
val biometricsDescription: String = when (biometricSupportStatus) {
@@ -49,6 +51,7 @@ fun BitwardenUnlockWithBiometricsSwitch(
}
},
enabled = biometricSupportStatus == BiometricSupportStatus.CLASS_3_SUPPORTED,
description = biometricsDescription,
supportingText = biometricsDescription,
cardStyle = cardStyle,
)
}
@@ -10,6 +10,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.PinInputDialog
/**
@@ -19,6 +20,7 @@ import com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.PinInput
* @param isUnlockWithPinEnabled Indicates whether or not the pin unlocking is enabled.
* @param onUnlockWithPinToggleAction Callback that is invoked when the current state of the switch
* changes.
* @param cardStyle Indicates the type of card style to be applied.
* @param modifier The [Modifier] to be applied to the switch.
*/
@Suppress("LongMethod")
@@ -27,6 +29,7 @@ fun BitwardenUnlockWithPinSwitch(
isUnlockWithPasswordEnabled: Boolean,
isUnlockWithPinEnabled: Boolean,
onUnlockWithPinToggleAction: (UnlockWithPinState) -> Unit,
cardStyle: CardStyle,
modifier: Modifier = Modifier,
) {
var shouldShowPinInputDialog by rememberSaveable { mutableStateOf(value = false) }
@@ -43,6 +46,7 @@ fun BitwardenUnlockWithPinSwitch(
onUnlockWithPinToggleAction(UnlockWithPinState.Disabled)
}
},
cardStyle = cardStyle,
modifier = modifier,
)
@@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -27,6 +28,7 @@ import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.x8bit.bitwarden.ui.platform.base.util.toListItemCardStyle
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
import com.x8bit.bitwarden.ui.platform.components.appbar.NavigationIcon
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenFilledButton
@@ -78,7 +80,7 @@ fun DebugMenuScreen(
Column(
modifier = Modifier.verticalScroll(rememberScrollState()),
) {
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(height = 12.dp))
FeatureFlagContent(
featureFlagMap = state.featureFlags,
onValueChange = remember(viewModel) {
@@ -92,7 +94,9 @@ fun DebugMenuScreen(
}
},
)
Spacer(Modifier.height(12.dp))
Spacer(Modifier.height(height = 16.dp))
BitwardenHorizontalDivider()
Spacer(Modifier.height(height = 16.dp))
// Pulled these into variable to avoid over-nested formatting in the composable call.
val isRestartOnboardingEnabled = state.featureFlags[FlagKey.OnboardingFlow] as? Boolean
val isRestartOnboardingCarouselEnabled = state
@@ -111,6 +115,7 @@ fun DebugMenuScreen(
}
},
)
Spacer(modifier = Modifier.height(height = 16.dp))
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
@@ -126,22 +131,26 @@ private fun FeatureFlagContent(
Column(
modifier = modifier,
) {
Spacer(modifier = Modifier.height(8.dp))
BitwardenListHeaderText(
label = stringResource(R.string.feature_flags),
modifier = Modifier.standardHorizontalMargin(),
modifier = Modifier
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(8.dp))
BitwardenHorizontalDivider()
featureFlagMap.forEach { featureFlag ->
featureFlag.key.ListItemContent(
currentValue = featureFlag.value,
onValueChange = onValueChange,
modifier = Modifier.standardHorizontalMargin(),
cardStyle = featureFlagMap.keys.toListItemCardStyle(
index = featureFlagMap.keys.indexOf(element = featureFlag.key),
),
modifier = Modifier
.standardHorizontalMargin()
.fillMaxWidth(),
)
BitwardenHorizontalDivider()
}
Spacer(modifier = Modifier.height(12.dp))
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenFilledButton(
label = stringResource(R.string.reset_values),
onClick = onResetValues,
@@ -149,7 +158,6 @@ private fun FeatureFlagContent(
.standardHorizontalMargin()
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(8.dp))
}
}
@@ -167,11 +175,11 @@ private fun OnboardingOverrideContent(
Column(modifier) {
BitwardenListHeaderText(
label = stringResource(R.string.onboarding_override),
modifier = Modifier.standardHorizontalMargin(),
modifier = Modifier
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(8.dp))
BitwardenHorizontalDivider()
Spacer(modifier = Modifier.height(12.dp))
BitwardenFilledButton(
label = stringResource(R.string.restart_onboarding_cta),
onClick = onStartOnboarding,
@@ -5,6 +5,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch
/**
@@ -15,6 +16,7 @@ import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch
fun <T : Any> FlagKey<T>.ListItemContent(
currentValue: T,
onValueChange: (key: FlagKey<T>, value: T) -> Unit,
cardStyle: CardStyle,
modifier: Modifier = Modifier,
) = when (val flagKey = this) {
FlagKey.DummyBoolean,
@@ -41,6 +43,7 @@ fun <T : Any> FlagKey<T>.ListItemContent(
key = flagKey as FlagKey<Boolean>,
currentValue = currentValue as Boolean,
onValueChange = onValueChange as (FlagKey<Boolean>, Boolean) -> Unit,
cardStyle = cardStyle,
modifier = modifier,
)
}
@@ -54,14 +57,14 @@ private fun BooleanFlagItem(
key: FlagKey<Boolean>,
currentValue: Boolean,
onValueChange: (key: FlagKey<Boolean>, value: Boolean) -> Unit,
cardStyle: CardStyle,
modifier: Modifier = Modifier,
) {
BitwardenSwitch(
label = label,
isChecked = currentValue,
onCheckedChange = {
onValueChange(key, it)
},
onCheckedChange = { onValueChange(key, it) },
cardStyle = cardStyle,
modifier = modifier,
)
}
@@ -2,10 +2,10 @@ package com.x8bit.bitwarden.ui.platform.feature.search
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -16,6 +16,8 @@ import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.x8bit.bitwarden.ui.platform.base.util.toListItemCardStyle
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenMasterPasswordDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenSelectionDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
@@ -107,7 +109,10 @@ fun SearchContent(
LazyColumn(
modifier = modifier,
) {
items(viewState.displayItems) {
item {
Spacer(modifier = Modifier.height(height = 12.dp))
}
itemsIndexed(viewState.displayItems) { index, it ->
BitwardenListItem(
startIcon = it.iconData,
label = it.title,
@@ -158,19 +163,19 @@ fun SearchContent(
)
}
.toPersistentList(),
cardStyle = viewState.displayItems.toListItemCardStyle(
index = index,
dividerPadding = 56.dp,
),
modifier = Modifier
.testTag(searchType.searchItemTestTag)
.fillMaxWidth()
.padding(
start = 16.dp,
// There is some built-in padding to the menu button that makes up
// the visual difference here.
end = 12.dp,
),
.standardHorizontalMargin(),
)
}
item {
Spacer(modifier = Modifier.height(height = 16.dp))
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
@@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
@@ -36,10 +37,14 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.base.util.Text
import com.x8bit.bitwarden.ui.platform.base.util.bottomDivider
import com.x8bit.bitwarden.ui.platform.base.util.cardBackground
import com.x8bit.bitwarden.ui.platform.base.util.cardPadding
import com.x8bit.bitwarden.ui.platform.base.util.mirrorIfRtl
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.x8bit.bitwarden.ui.platform.base.util.toListItemCardStyle
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenMediumTopAppBar
import com.x8bit.bitwarden.ui.platform.components.badge.NotificationBadge
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
@@ -86,20 +91,22 @@ fun SettingsScreen(
.fillMaxSize()
.verticalScroll(state = rememberScrollState()),
) {
Settings.entries.forEach { settingEntry ->
Spacer(modifier = Modifier.height(height = 12.dp))
Settings.entries.forEachIndexed { index, settingEntry ->
SettingsRow(
text = settingEntry.text,
onClick = remember(viewModel) {
{ viewModel.trySendAction(SettingsAction.SettingsClick(settingEntry)) }
},
modifier = Modifier
.testTag(settingEntry.testTag)
.padding(horizontal = 16.dp)
.fillMaxWidth(),
notificationCount = state.notificationBadgeCountMap.getOrDefault(
key = settingEntry,
defaultValue = 0,
),
cardStyle = Settings.entries.toListItemCardStyle(index = index),
modifier = Modifier
.testTag(tag = settingEntry.testTag)
.standardHorizontalMargin()
.fillMaxWidth(),
)
}
}
@@ -110,11 +117,14 @@ fun SettingsScreen(
private fun SettingsRow(
text: Text,
onClick: () -> Unit,
modifier: Modifier = Modifier,
notificationCount: Int,
cardStyle: CardStyle?,
modifier: Modifier = Modifier,
) {
Row(
modifier = Modifier
modifier = modifier
.defaultMinSize(minHeight = 60.dp)
.cardBackground(cardStyle = cardStyle)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = ripple(
@@ -122,17 +132,14 @@ private fun SettingsRow(
),
onClick = onClick,
)
.bottomDivider(paddingStart = 16.dp)
.defaultMinSize(minHeight = 56.dp)
.padding(end = 8.dp, top = 8.dp, bottom = 8.dp)
.then(modifier),
.cardPadding(cardStyle = cardStyle, start = 16.dp, end = 12.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
modifier = Modifier
.padding(end = 16.dp)
.weight(1f),
.weight(weight = 1f),
text = text(),
style = BitwardenTheme.typography.bodyLarge,
color = BitwardenTheme.colorScheme.text.primary,
@@ -164,7 +171,7 @@ private fun TrailingContent(
tint = BitwardenTheme.colorScheme.icon.primary,
modifier = Modifier
.mirrorIfRtl()
.size(24.dp),
.size(size = 16.dp),
)
}
}
@@ -185,6 +192,7 @@ private fun SettingsRows_preview() {
text = it.text,
onClick = { },
notificationCount = index % 3,
cardStyle = Settings.entries.toListItemCardStyle(index = index),
)
}
}
@@ -1,11 +1,7 @@
package com.x8bit.bitwarden.ui.platform.feature.settings.about
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxSize
@@ -19,18 +15,14 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
@@ -40,9 +32,12 @@ import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.base.util.Text
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.base.util.mirrorIfRtl
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
import com.x8bit.bitwarden.ui.platform.components.divider.BitwardenHorizontalDivider
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.row.BitwardenExternalLinkRow
import com.x8bit.bitwarden.ui.platform.components.row.BitwardenTextRow
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
@@ -161,72 +156,104 @@ private fun ContentColumn(
modifier = modifier
.verticalScroll(rememberScrollState()),
) {
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.height(height = 12.dp))
if (state.shouldShowCrashLogsButton) {
BitwardenSwitch(
label = stringResource(id = R.string.submit_crash_logs),
contentDescription = stringResource(id = R.string.submit_crash_logs),
isChecked = state.isSubmitCrashLogsEnabled,
onCheckedChange = onSubmitCrashLogsCheckedChange,
cardStyle = CardStyle.Top(),
modifier = Modifier
.testTag("SubmitCrashLogsSwitch")
.fillMaxWidth()
.padding(horizontal = 16.dp),
contentDescription = stringResource(id = R.string.submit_crash_logs),
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(16.dp))
}
BitwardenExternalLinkRow(
text = stringResource(id = R.string.bitwarden_help_center),
onConfirmClick = onHelpCenterClick,
dialogTitle = stringResource(id = R.string.continue_to_help_center),
modifier = Modifier.testTag("BitwardenHelpCenterRow"),
dialogMessage = stringResource(
id = R.string.learn_more_about_how_to_use_bitwarden_on_the_help_center,
),
withDivider = false,
cardStyle = if (state.shouldShowCrashLogsButton) {
CardStyle.Middle()
} else {
CardStyle.Top()
},
modifier = Modifier
.standardHorizontalMargin()
.fillMaxWidth()
.testTag(tag = "BitwardenHelpCenterRow"),
)
BitwardenExternalLinkRow(
text = stringResource(id = R.string.privacy_policy),
onConfirmClick = onPrivacyPolicyClick,
dialogTitle = stringResource(id = R.string.continue_to_privacy_policy),
modifier = Modifier.testTag("PrivacyPolicyRow"),
dialogMessage = stringResource(
id = R.string.privacy_policy_description_long,
),
withDivider = false,
cardStyle = CardStyle.Middle(),
modifier = Modifier
.standardHorizontalMargin()
.fillMaxWidth()
.testTag(tag = "PrivacyPolicyRow"),
)
BitwardenExternalLinkRow(
text = stringResource(id = R.string.web_vault),
onConfirmClick = onWebVaultClick,
modifier = Modifier.testTag("BitwardenWebVaultRow"),
dialogTitle = stringResource(id = R.string.continue_to_web_app),
dialogMessage = stringResource(
id = R.string.explore_more_features_of_your_bitwarden_account_on_the_web_app,
),
withDivider = false,
cardStyle = CardStyle.Middle(),
modifier = Modifier
.standardHorizontalMargin()
.fillMaxWidth()
.testTag(tag = "BitwardenWebVaultRow"),
)
BitwardenExternalLinkRow(
text = stringResource(id = R.string.learn_org),
onConfirmClick = onLearnAboutOrgsClick,
dialogTitle = stringResource(id = R.string.continue_to_x, "bitwarden.com"),
modifier = Modifier.testTag("LearnAboutOrganizationsRow"),
dialogMessage = stringResource(
id = R.string.learn_about_organizations_description_long,
),
withDivider = false,
cardStyle = CardStyle.Middle(),
modifier = Modifier
.standardHorizontalMargin()
.fillMaxWidth()
.testTag(tag = "LearnAboutOrganizationsRow"),
)
BitwardenExternalLinkRow(
text = stringResource(R.string.give_feedback),
onConfirmClick = onGiveFeedbackClick,
modifier = Modifier.testTag("GiveFeedbackRow"),
dialogTitle = stringResource(R.string.continue_to_give_feedback),
dialogMessage = stringResource(R.string.continue_to_provide_feedback),
withDivider = false,
cardStyle = CardStyle.Middle(),
modifier = Modifier
.standardHorizontalMargin()
.fillMaxWidth()
.testTag(tag = "GiveFeedbackRow"),
)
CopyRow(
text = state.version,
onClick = onVersionClick,
modifier = Modifier.testTag("CopyAboutInfoRow"),
modifier = Modifier
.standardHorizontalMargin()
.fillMaxWidth()
.testTag("CopyAboutInfoRow"),
)
Box(
modifier = Modifier
.defaultMinSize(minHeight = 56.dp)
.padding(horizontal = 16.dp, vertical = 8.dp)
.defaultMinSize(minHeight = 60.dp)
.standardHorizontalMargin()
.fillMaxWidth(),
contentAlignment = Alignment.Center,
) {
@@ -246,44 +273,19 @@ private fun CopyRow(
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val resources = LocalContext.current.resources
Box(
contentAlignment = Alignment.BottomCenter,
modifier = modifier
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = ripple(
color = BitwardenTheme.colorScheme.background.pressed,
),
onClick = onClick,
)
.semantics(mergeDescendants = true) {
contentDescription = text.toString(resources)
},
BitwardenTextRow(
text = text(),
onClick = onClick,
withDivider = false,
cardStyle = CardStyle.Bottom,
modifier = modifier,
) {
Row(
modifier = Modifier
.defaultMinSize(minHeight = 56.dp)
.padding(start = 16.dp, end = 24.dp, top = 8.dp, bottom = 8.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
modifier = Modifier
.padding(end = 16.dp)
.weight(1f),
text = text(),
style = BitwardenTheme.typography.bodySmall,
color = BitwardenTheme.colorScheme.text.primary,
)
Icon(
painter = rememberVectorPainter(id = R.drawable.ic_copy),
contentDescription = null,
tint = BitwardenTheme.colorScheme.icon.primary,
)
}
BitwardenHorizontalDivider(modifier = Modifier.padding(start = 16.dp))
Icon(
painter = rememberVectorPainter(id = R.drawable.ic_copy),
contentDescription = null,
tint = BitwardenTheme.colorScheme.icon.primary,
modifier = Modifier.mirrorIfRtl(),
)
}
}
@@ -3,6 +3,7 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity
import android.widget.Toast
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
@@ -51,6 +52,7 @@ import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTimePickerDial
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.row.BitwardenSelectionRow
import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.row.BitwardenExternalLinkRow
import com.x8bit.bitwarden.ui.platform.components.row.BitwardenTextRow
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
@@ -177,6 +179,7 @@ fun AccountSecurityScreen(
.fillMaxSize()
.verticalScroll(rememberScrollState()),
) {
Spacer(modifier = Modifier.height(height = 12.dp))
AnimatedVisibility(
visible = state.shouldShowUnlockActionCard,
label = "UnlockActionCard",
@@ -200,7 +203,7 @@ fun AccountSecurityScreen(
},
modifier = Modifier
.standardHorizontalMargin()
.padding(top = 12.dp, bottom = 16.dp),
.padding(bottom = 16.dp),
)
}
@@ -208,15 +211,19 @@ fun AccountSecurityScreen(
label = stringResource(id = R.string.approve_login_requests),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
BitwardenTextRow(
text = stringResource(id = R.string.pending_log_in_requests),
onClick = remember(viewModel) {
{ viewModel.trySendAction(AccountSecurityAction.PendingLoginRequestsClick) }
},
cardStyle = CardStyle.Full,
modifier = Modifier
.testTag("PendingLogInRequestsLabel")
.standardHorizontalMargin()
.fillMaxWidth(),
)
@@ -225,10 +232,14 @@ fun AccountSecurityScreen(
label = stringResource(id = R.string.unlock_options),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
val biometricSupportStatus = biometricsManager.biometricSupportStatus
BitwardenUnlockWithBiometricsSwitch(
biometricSupportStatus = biometricsManager.biometricSupportStatus,
biometricSupportStatus = biometricSupportStatus,
isChecked = state.isUnlockWithBiometricsEnabled || showBiometricsPrompt,
onDisableBiometrics = remember(viewModel) {
{
@@ -240,21 +251,24 @@ fun AccountSecurityScreen(
onEnableBiometrics = remember(viewModel) {
{ viewModel.trySendAction(AccountSecurityAction.EnableBiometricsClick) }
},
cardStyle = CardStyle.Full,
modifier = Modifier
.testTag("UnlockWithBiometricsSwitch")
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(height = 8.dp))
BitwardenUnlockWithPinSwitch(
isUnlockWithPasswordEnabled = state.isUnlockWithPasswordEnabled,
isUnlockWithPinEnabled = state.isUnlockWithPinEnabled,
onUnlockWithPinToggleAction = remember(viewModel) {
{ viewModel.trySendAction(AccountSecurityAction.UnlockWithPinToggle(it)) }
},
cardStyle = CardStyle.Full,
modifier = Modifier
.testTag("UnlockWithPinSwitch")
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
Spacer(Modifier.height(16.dp))
if (state.shouldShowEnableAuthenticatorSync) {
@@ -274,14 +288,16 @@ fun AccountSecurityScreen(
label = stringResource(id = R.string.session_timeout),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
SessionTimeoutPolicyRow(
vaultTimeoutPolicyMinutes = state.vaultTimeoutPolicyMinutes,
vaultTimeoutPolicyAction = state.vaultTimeoutPolicyAction,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
SessionTimeoutRow(
vaultTimeoutPolicyMinutes = state.vaultTimeoutPolicyMinutes,
@@ -291,7 +307,8 @@ fun AccountSecurityScreen(
},
modifier = Modifier
.testTag("VaultTimeoutChooser")
.fillMaxWidth(),
.fillMaxWidth()
.standardHorizontalMargin(),
)
(state.vaultTimeout as? VaultTimeout.Custom)?.let { customTimeout ->
SessionCustomTimeoutRow(
@@ -304,7 +321,9 @@ fun AccountSecurityScreen(
)
}
},
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
SessionTimeoutActionRow(
@@ -316,7 +335,8 @@ fun AccountSecurityScreen(
},
modifier = Modifier
.testTag("VaultTimeoutActionChooser")
.fillMaxWidth(),
.fillMaxWidth()
.standardHorizontalMargin(),
)
Spacer(Modifier.height(16.dp))
@@ -324,15 +344,19 @@ fun AccountSecurityScreen(
label = stringResource(id = R.string.other),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
BitwardenTextRow(
text = stringResource(id = R.string.account_fingerprint_phrase),
onClick = remember(viewModel) {
{ viewModel.trySendAction(AccountSecurityAction.AccountFingerprintPhraseClick) }
},
cardStyle = CardStyle.Top(),
modifier = Modifier
.testTag("AccountFingerprintPhraseLabel")
.standardHorizontalMargin()
.fillMaxWidth(),
)
BitwardenExternalLinkRow(
@@ -343,8 +367,10 @@ fun AccountSecurityScreen(
withDivider = false,
dialogTitle = stringResource(id = R.string.continue_to_web_app),
dialogMessage = stringResource(id = R.string.two_step_login_description_long),
cardStyle = CardStyle.Middle(),
modifier = Modifier
.testTag("TwoStepLoginLinkItemView")
.standardHorizontalMargin()
.fillMaxWidth(),
)
if (state.isUnlockWithPasswordEnabled) {
@@ -358,7 +384,10 @@ fun AccountSecurityScreen(
dialogMessage = stringResource(
id = R.string.change_master_password_description_long,
),
modifier = Modifier.fillMaxWidth(),
cardStyle = CardStyle.Middle(),
modifier = Modifier
.standardHorizontalMargin()
.fillMaxWidth(),
)
}
if (state.hasUnlockMechanism) {
@@ -367,8 +396,10 @@ fun AccountSecurityScreen(
onClick = remember(viewModel) {
{ viewModel.trySendAction(AccountSecurityAction.LockNowClick) }
},
cardStyle = CardStyle.Middle(),
modifier = Modifier
.testTag("LockNowLabel")
.standardHorizontalMargin()
.fillMaxWidth(),
)
}
@@ -377,8 +408,10 @@ fun AccountSecurityScreen(
onClick = remember(viewModel) {
{ viewModel.trySendAction(AccountSecurityAction.LogoutClick) }
},
cardStyle = CardStyle.Middle(),
modifier = Modifier
.testTag("LogOutLabel")
.standardHorizontalMargin()
.fillMaxWidth(),
)
BitwardenTextRow(
@@ -386,10 +419,13 @@ fun AccountSecurityScreen(
onClick = remember(viewModel) {
{ viewModel.trySendAction(AccountSecurityAction.DeleteAccountClick) }
},
cardStyle = CardStyle.Bottom,
modifier = Modifier
.testTag("DeleteAccountLabel")
.standardHorizontalMargin()
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(height = 16.dp))
}
}
}
@@ -474,6 +510,7 @@ private fun SessionTimeoutRow(
BitwardenTextRow(
text = stringResource(id = R.string.session_timeout),
onClick = { shouldShowSelectionDialog = true },
cardStyle = CardStyle.Top(),
modifier = modifier,
) {
Text(
@@ -545,6 +582,7 @@ private fun SessionCustomTimeoutRow(
BitwardenTextRow(
text = stringResource(id = R.string.custom),
onClick = { shouldShowTimePickerDialog = true },
cardStyle = CardStyle.Middle(),
modifier = modifier,
) {
val formattedTime = LocalTime
@@ -625,6 +663,7 @@ private fun SessionTimeoutActionRow(
if (vaultTimeoutPolicyAction != null) return@BitwardenTextRow
shouldShowSelectionDialog = true
},
cardStyle = CardStyle.Bottom,
modifier = modifier,
) {
Text(
@@ -685,7 +724,7 @@ private fun SessionTimeoutActionRow(
}
@Composable
private fun SyncWithAuthenticatorRow(
private fun ColumnScope.SyncWithAuthenticatorRow(
isChecked: Boolean,
onCheckedChange: (Boolean) -> Unit,
) {
@@ -693,12 +732,15 @@ private fun SyncWithAuthenticatorRow(
label = stringResource(R.string.authenticator_sync),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
BitwardenSwitch(
label = stringResource(R.string.allow_bitwarden_authenticator_syncing),
onCheckedChange = onCheckedChange,
isChecked = isChecked,
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
@@ -34,6 +34,7 @@ import com.x8bit.bitwarden.ui.platform.components.button.BitwardenFilledButton
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton
import com.x8bit.bitwarden.ui.platform.components.divider.BitwardenHorizontalDivider
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.util.maxDialogHeight
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
@@ -115,9 +116,9 @@ fun PinInputDialog(
pin = newValue.filter { it.isDigit() || !isPinCreation }
},
keyboardType = KeyboardType.Number,
modifier = Modifier
.fillMaxWidth(),
textFieldTestTag = "AlertInputField",
cardStyle = CardStyle.Full,
modifier = Modifier.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(height = 16.dp))
}
@@ -11,15 +11,16 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -49,8 +50,10 @@ import com.x8bit.bitwarden.data.platform.util.isBuildVersionBelow
import com.x8bit.bitwarden.data.platform.util.isFdroid
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.base.util.LivecycleEventEffect
import com.x8bit.bitwarden.ui.platform.base.util.bottomDivider
import com.x8bit.bitwarden.ui.platform.base.util.cardBackground
import com.x8bit.bitwarden.ui.platform.base.util.cardPadding
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.x8bit.bitwarden.ui.platform.base.util.toListItemCardStyle
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
import com.x8bit.bitwarden.ui.platform.components.bottomsheet.BitwardenModalBottomSheet
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenFilledButton
@@ -58,6 +61,7 @@ import com.x8bit.bitwarden.ui.platform.components.button.BitwardenOutlinedButton
import com.x8bit.bitwarden.ui.platform.components.content.BitwardenErrorContent
import com.x8bit.bitwarden.ui.platform.components.content.BitwardenLoadingContent
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.scaffold.rememberBitwardenPullToRefreshState
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
@@ -223,20 +227,27 @@ private fun PendingRequestsContent(
LazyColumn(
modifier = Modifier.weight(weight = 1f, fill = false),
) {
items(state.requests) { request ->
item {
Spacer(modifier = Modifier.height(height = 12.dp))
}
itemsIndexed(state.requests) { index, request ->
PendingRequestItem(
fingerprintPhrase = request.fingerprintPhrase,
platform = request.platform,
timestamp = request.timestamp,
onNavigateToLoginApproval = onNavigateToLoginApproval,
cardStyle = state.requests.toListItemCardStyle(
index = index,
dividerPadding = 0.dp,
),
modifier = Modifier
.testTag("LoginRequestCell")
.fillMaxWidth()
.bottomDivider(),
.standardHorizontalMargin(),
)
}
}
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(height = 24.dp))
BitwardenOutlinedButton(
label = stringResource(id = R.string.decline_all_requests),
@@ -244,9 +255,11 @@ private fun PendingRequestsContent(
onClick = { shouldShowDeclineAllRequestsConfirm = true },
modifier = Modifier
.testTag("DeclineAllRequestsButton")
.padding(horizontal = 16.dp)
.standardHorizontalMargin()
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(height = 16.dp))
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
@@ -260,29 +273,29 @@ private fun PendingRequestItem(
platform: String,
timestamp: String,
onNavigateToLoginApproval: (fingerprintPhrase: String) -> Unit,
cardStyle: CardStyle,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.defaultMinSize(minHeight = 60.dp)
.cardBackground(cardStyle = cardStyle)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = ripple(
color = BitwardenTheme.colorScheme.background.pressed,
),
onClick = { onNavigateToLoginApproval(fingerprintPhrase) },
),
)
.cardPadding(cardStyle = cardStyle, horizontal = 16.dp),
horizontalAlignment = Alignment.Start,
) {
Spacer(modifier = Modifier.height(16.dp))
Text(
text = stringResource(id = R.string.fingerprint_phrase),
style = BitwardenTheme.typography.labelMedium,
color = BitwardenTheme.colorScheme.text.primary,
textAlign = TextAlign.Start,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
modifier = Modifier.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(8.dp))
@@ -294,8 +307,7 @@ private fun PendingRequestItem(
textAlign = TextAlign.Start,
modifier = Modifier
.testTag("FingerprintValueLabel")
.fillMaxWidth()
.padding(horizontal = 16.dp),
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(12.dp))
@@ -310,18 +322,15 @@ private fun PendingRequestItem(
style = BitwardenTheme.typography.bodyMedium,
color = BitwardenTheme.colorScheme.text.secondary,
textAlign = TextAlign.Start,
modifier = Modifier.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.width(width = 16.dp))
Text(
text = timestamp,
style = BitwardenTheme.typography.labelSmall,
color = BitwardenTheme.colorScheme.text.secondary,
textAlign = TextAlign.End,
modifier = Modifier.padding(horizontal = 16.dp),
)
}
Spacer(modifier = Modifier.height(16.dp))
}
}
@@ -1,9 +1,11 @@
package com.x8bit.bitwarden.ui.platform.feature.settings.appearance
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -26,10 +28,12 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.base.util.Text
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenSelectionDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.row.BitwardenSelectionRow
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.row.BitwardenTextRow
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch
@@ -78,6 +82,7 @@ fun AppearanceScreen(
.fillMaxSize()
.verticalScroll(rememberScrollState()),
) {
Spacer(modifier = Modifier.height(height = 12.dp))
LanguageSelectionRow(
currentSelection = state.language,
onLanguageSelection = remember(viewModel) {
@@ -85,9 +90,10 @@ fun AppearanceScreen(
},
modifier = Modifier
.testTag("LanguageChooser")
.standardHorizontalMargin()
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(height = 8.dp))
ThemeSelectionRow(
currentSelection = state.theme,
onThemeSelection = remember(viewModel) {
@@ -95,21 +101,24 @@ fun AppearanceScreen(
},
modifier = Modifier
.testTag("ThemeChooser")
.standardHorizontalMargin()
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(height = 8.dp))
BitwardenSwitch(
label = stringResource(id = R.string.show_website_icons),
description = stringResource(id = R.string.show_website_icons_description),
supportingText = stringResource(id = R.string.show_website_icons_description),
isChecked = state.showWebsiteIcons,
onCheckedChange = remember(viewModel) {
{ viewModel.trySendAction(AppearanceAction.ShowWebsiteIconsToggle(it)) }
},
cardStyle = CardStyle.Full,
modifier = Modifier
.testTag("ShowWebsiteIconsSwitch")
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
}
@@ -133,6 +142,7 @@ private fun LanguageSelectionRow(
BitwardenTextRow(
text = stringResource(id = R.string.language),
onClick = { shouldShowLanguageSelectionDialog = true },
cardStyle = CardStyle.Full,
modifier = modifier,
) {
Text(
@@ -174,6 +184,7 @@ private fun ThemeSelectionRow(
text = stringResource(id = R.string.theme),
description = stringResource(id = R.string.theme_description),
onClick = { shouldShowThemeSelectionDialog = true },
cardStyle = CardStyle.Full,
modifier = modifier,
) {
Text(
@@ -41,6 +41,7 @@ import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenSelectionDialo
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.row.BitwardenSelectionRow
import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.row.BitwardenExternalLinkRow
import com.x8bit.bitwarden.ui.platform.components.row.BitwardenTextRow
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
@@ -128,6 +129,7 @@ fun AutoFillScreen(
.fillMaxSize()
.verticalScroll(rememberScrollState()),
) {
Spacer(modifier = Modifier.height(height = 12.dp))
AnimatedVisibility(
visible = state.showAutofillActionCard,
label = "AutofillActionCard",
@@ -151,31 +153,35 @@ fun AutoFillScreen(
},
modifier = Modifier
.standardHorizontalMargin()
.padding(top = 12.dp, bottom = 16.dp),
.padding(bottom = 16.dp),
)
}
BitwardenListHeaderText(
label = stringResource(id = R.string.autofill),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
BitwardenSwitch(
label = stringResource(id = R.string.autofill_services),
description = stringResource(id = R.string.autofill_services_explanation_long),
supportingText = stringResource(id = R.string.autofill_services_explanation_long),
isChecked = state.isAutoFillServicesEnabled,
onCheckedChange = remember(viewModel) {
{ viewModel.trySendAction(AutoFillAction.AutoFillServicesClick(it)) }
},
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.testTag("AutofillServicesSwitch")
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(height = 8.dp))
if (state.showInlineAutofillOption) {
BitwardenSwitch(
label = stringResource(id = R.string.inline_autofill),
description = stringResource(
supportingText = stringResource(
id = R.string.use_inline_autofill_explanation_long,
),
isChecked = state.isUseInlineAutoFillEnabled,
@@ -183,11 +189,13 @@ fun AutoFillScreen(
{ viewModel.trySendAction(AutoFillAction.UseInlineAutofillClick(it)) }
},
enabled = state.canInteractWithInlineAutofillToggle,
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.testTag("InlineAutofillSwitch")
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
if (state.showPasskeyManagementRow) {
BitwardenExternalLinkRow(
@@ -203,7 +211,12 @@ fun AutoFillScreen(
id = R.string.set_bitwarden_as_passkey_manager_description,
),
withDivider = false,
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
AccessibilityAutofillSwitch(
isAccessibilityAutoFillEnabled = state.isAccessibilityAutofillEnabled,
@@ -212,39 +225,45 @@ fun AutoFillScreen(
},
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.additional_options),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(8.dp))
BitwardenSwitch(
label = stringResource(id = R.string.copy_totp_automatically),
description = stringResource(id = R.string.copy_totp_automatically_description),
supportingText = stringResource(id = R.string.copy_totp_automatically_description),
isChecked = state.isCopyTotpAutomaticallyEnabled,
onCheckedChange = remember(viewModel) {
{ viewModel.trySendAction(AutoFillAction.CopyTotpAutomaticallyClick(it)) }
},
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.testTag("CopyTotpAutomaticallySwitch")
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(8.dp))
BitwardenSwitch(
label = stringResource(id = R.string.ask_to_add_login),
description = stringResource(id = R.string.ask_to_add_login_description),
supportingText = stringResource(id = R.string.ask_to_add_login_description),
isChecked = state.isAskToAddLoginEnabled,
onCheckedChange = remember(viewModel) {
{ viewModel.trySendAction(AutoFillAction.AskToAddLoginClick(it)) }
},
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.testTag("AskToAddLoginSwitch")
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(8.dp))
DefaultUriMatchTypeRow(
selectedUriMatchType = state.defaultUriMatchType,
onUriMatchTypeSelect = remember(viewModel) {
@@ -252,8 +271,10 @@ fun AutoFillScreen(
},
modifier = Modifier
.testTag("DefaultUriMatchDetectionChooser")
.standardHorizontalMargin()
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextRow(
text = stringResource(id = R.string.block_auto_fill),
description = stringResource(
@@ -262,7 +283,10 @@ fun AutoFillScreen(
onClick = remember(viewModel) {
{ viewModel.trySendAction(AutoFillAction.BlockAutoFillClick) }
},
modifier = Modifier.fillMaxWidth(),
cardStyle = CardStyle.Full,
modifier = Modifier
.standardHorizontalMargin()
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(16.dp))
}
@@ -278,7 +302,7 @@ private fun AccessibilityAutofillSwitch(
var shouldShowDialog by rememberSaveable { mutableStateOf(value = false) }
BitwardenSwitch(
label = stringResource(id = R.string.accessibility),
description = stringResource(id = R.string.accessibility_description5),
supportingText = stringResource(id = R.string.accessibility_description5),
isChecked = isAccessibilityAutoFillEnabled,
onCheckedChange = {
if (isAccessibilityAutoFillEnabled) {
@@ -287,6 +311,7 @@ private fun AccessibilityAutofillSwitch(
shouldShowDialog = true
}
},
cardStyle = CardStyle.Full,
modifier = modifier.testTag(tag = "AccessibilityAutofillSwitch"),
)
@@ -318,6 +343,7 @@ private fun DefaultUriMatchTypeRow(
text = stringResource(id = R.string.default_uri_match_detection),
description = stringResource(id = R.string.default_uri_match_detection_description),
onClick = { shouldShowDialog = true },
cardStyle = CardStyle.Full,
modifier = modifier,
) {
Text(
@@ -21,10 +21,12 @@ import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenFilledButton
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton
import com.x8bit.bitwarden.ui.platform.components.divider.BitwardenHorizontalDivider
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.util.maxDialogHeight
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
@@ -82,15 +84,16 @@ fun AddEditBlockedUriDialog(
BitwardenTextField(
label = stringResource(id = R.string.enter_uri),
isError = errorMessage != null,
hint = errorMessage ?: stringResource(
supportingText = errorMessage ?: stringResource(
id = R.string.format_x_separate_multiple_ur_is_with_a_comma,
"http://domain.com",
),
value = uri,
onValueChange = onUriChange,
keyboardType = KeyboardType.Uri,
cardStyle = CardStyle.Full,
modifier = Modifier
.padding(start = 16.dp, end = 16.dp)
.standardHorizontalMargin()
.fillMaxWidth(),
)
}
@@ -249,6 +249,7 @@ private fun BlockAutoFillListItem(
) {
Row(
modifier = Modifier
.defaultMinSize(minHeight = 60.dp)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = ripple(
@@ -257,7 +258,6 @@ private fun BlockAutoFillListItem(
onClick = onClick,
)
.bottomDivider(paddingStart = 16.dp)
.defaultMinSize(minHeight = 56.dp)
.padding(end = 8.dp, top = 16.dp, bottom = 16.dp)
.then(modifier),
horizontalArrangement = Arrangement.SpaceBetween,
@@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -35,6 +34,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.PasswordStrengthIndicator
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenOutlinedButton
import com.x8bit.bitwarden.ui.platform.components.card.BitwardenInfoCalloutCard
@@ -43,6 +43,7 @@ import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
import com.x8bit.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectButton
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordField
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager
@@ -194,13 +195,13 @@ private fun ExportVaultScreenContent(
.imePadding()
.verticalScroll(rememberScrollState()),
) {
Spacer(modifier = Modifier.height(height = 12.dp))
if (state.policyPreventsExport) {
BitwardenInfoCalloutCard(
text = stringResource(id = R.string.disable_personal_vault_export_policy_in_effect),
modifier = Modifier
.testTag("DisablePrivateVaultPolicyLabel")
.padding(horizontal = 16.dp)
.standardHorizontalMargin()
.fillMaxWidth(),
)
@@ -219,15 +220,15 @@ private fun ExportVaultScreenContent(
onExportFormatOptionSelected(selectedOption)
},
isEnabled = !state.policyPreventsExport,
cardStyle = CardStyle.Full,
modifier = Modifier
.testTag("FileFormatPicker")
.padding(horizontal = 16.dp)
.standardHorizontalMargin()
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(8.dp))
if (state.exportFormat == ExportVaultFormat.JSON_ENCRYPTED) {
Spacer(modifier = Modifier.height(8.dp))
var showPassword by rememberSaveable { mutableStateOf(false) }
BitwardenPasswordField(
label = stringResource(id = R.string.file_password),
@@ -235,20 +236,27 @@ private fun ExportVaultScreenContent(
onValueChange = onFilePasswordInputChanged,
showPassword = showPassword,
showPasswordChange = { showPassword = it },
hint = stringResource(id = R.string.password_used_to_export),
supportingTextContent = {
PasswordStrengthIndicator(
state = state.passwordStrengthState,
currentCharacterCount = state.passwordInput.length,
modifier = Modifier.fillMaxWidth(),
)
Text(
text = stringResource(id = R.string.password_used_to_export),
style = BitwardenTheme.typography.bodySmall,
color = BitwardenTheme.colorScheme.text.secondary,
modifier = Modifier.fillMaxWidth(),
)
},
cardStyle = CardStyle.Full,
modifier = Modifier
.testTag("FilePasswordEntry")
.padding(horizontal = 16.dp)
.standardHorizontalMargin()
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(8.dp))
PasswordStrengthIndicator(
modifier = Modifier.padding(horizontal = 16.dp),
state = state.passwordStrengthState,
currentCharacterCount = state.passwordInput.length,
)
Spacer(modifier = Modifier.height(4.dp))
Spacer(modifier = Modifier.height(8.dp))
BitwardenPasswordField(
label = stringResource(id = R.string.confirm_file_password),
@@ -256,16 +264,15 @@ private fun ExportVaultScreenContent(
onValueChange = onConfirmFilePasswordInputChanged,
showPassword = showPassword,
showPasswordChange = { showPassword = it },
cardStyle = CardStyle.Full,
modifier = Modifier
.testTag("ConfirmFilePasswordEntry")
.padding(horizontal = 16.dp)
.standardHorizontalMargin()
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(16.dp))
}
if (state.showSendCodeButton) {
Spacer(modifier = Modifier.height(16.dp))
Text(
@@ -274,7 +281,7 @@ private fun ExportVaultScreenContent(
style = BitwardenTheme.typography.bodyMedium,
color = BitwardenTheme.colorScheme.text.primary,
modifier = Modifier
.padding(horizontal = 16.dp)
.standardHorizontalMargin()
.fillMaxWidth(),
)
@@ -286,7 +293,7 @@ private fun ExportVaultScreenContent(
isEnabled = !state.policyPreventsExport,
modifier = Modifier
.testTag("SendTOTPCodeButton")
.padding(horizontal = 16.dp)
.standardHorizontalMargin()
.fillMaxWidth(),
)
@@ -296,49 +303,40 @@ private fun ExportVaultScreenContent(
label = stringResource(id = R.string.verification_code),
value = state.passwordInput,
readOnly = state.policyPreventsExport,
hint = stringResource(id = R.string.confirm_your_identity),
supportingText = stringResource(id = R.string.confirm_your_identity),
onValueChange = onPasswordInputChanged,
keyboardType = KeyboardType.Number,
cardStyle = CardStyle.Full,
modifier = Modifier
.padding(horizontal = 16.dp)
.standardHorizontalMargin()
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(16.dp))
} else {
Spacer(modifier = Modifier.height(height = 8.dp))
BitwardenPasswordField(
label = stringResource(id = R.string.master_password),
supportingText = stringResource(
id = R.string.export_vault_master_password_description,
),
value = state.passwordInput,
readOnly = state.policyPreventsExport,
onValueChange = onPasswordInputChanged,
cardStyle = CardStyle.Full,
modifier = Modifier
.testTag("MasterPasswordEntry")
.padding(horizontal = 16.dp)
.standardHorizontalMargin()
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = stringResource(id = R.string.export_vault_master_password_description),
textAlign = TextAlign.Start,
style = BitwardenTheme.typography.bodyMedium,
color = BitwardenTheme.colorScheme.text.secondary,
modifier = Modifier
.padding(horizontal = 16.dp)
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(8.dp))
}
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenOutlinedButton(
label = stringResource(id = R.string.export_vault),
onClick = onExportVaultClick,
isEnabled = !state.policyPreventsExport,
modifier = Modifier
.testTag("ExportVaultButton")
.padding(horizontal = 16.dp)
.standardHorizontalMargin()
.fillMaxWidth(),
)
}
@@ -1,22 +1,19 @@
package com.x8bit.bitwarden.ui.platform.feature.settings.folders
import android.widget.Toast
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
@@ -31,11 +28,13 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.base.util.bottomDivider
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.x8bit.bitwarden.ui.platform.base.util.toListItemCardStyle
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
import com.x8bit.bitwarden.ui.platform.components.content.BitwardenErrorContent
import com.x8bit.bitwarden.ui.platform.components.content.BitwardenLoadingContent
import com.x8bit.bitwarden.ui.platform.components.fab.BitwardenFloatingActionButton
import com.x8bit.bitwarden.ui.platform.components.row.BitwardenTextRow
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.x8bit.bitwarden.ui.platform.feature.settings.folders.model.FolderDisplayItem
@@ -150,33 +149,24 @@ private fun FoldersContent(
LazyColumn(
modifier = modifier,
) {
items(foldersList) {
Row(
item {
Spacer(modifier = Modifier.height(height = 12.dp))
}
itemsIndexed(foldersList) { index, it ->
BitwardenTextRow(
text = it.name,
onClick = { onItemClick(it.id) },
textTestTag = "FolderName",
cardStyle = foldersList.toListItemCardStyle(index = index),
modifier = Modifier
.testTag("FolderCell")
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = ripple(
color = BitwardenTheme.colorScheme.background.pressed,
),
onClick = { onItemClick(it.id) },
)
.bottomDivider(paddingStart = 16.dp)
.defaultMinSize(minHeight = 56.dp)
.padding(vertical = 16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
modifier = Modifier
.testTag("FolderName")
.padding(start = 16.dp)
.weight(1f),
text = it.name,
style = BitwardenTheme.typography.bodyLarge,
color = BitwardenTheme.colorScheme.text.primary,
)
}
.fillMaxWidth()
.standardHorizontalMargin()
.testTag(tag = "FolderCell"),
)
}
item {
Spacer(modifier = Modifier.height(height = 88.dp))
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
}
@@ -2,9 +2,11 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.folders.addedit
import android.widget.Toast
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
@@ -24,6 +26,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
import com.x8bit.bitwarden.ui.platform.components.appbar.action.BitwardenOverflowActionItem
import com.x8bit.bitwarden.ui.platform.components.appbar.action.OverflowMenuItemData
@@ -34,6 +37,7 @@ import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
@@ -127,6 +131,7 @@ fun FolderAddEditScreen(
Column(
modifier = Modifier.fillMaxSize(),
) {
Spacer(modifier = Modifier.height(height = 12.dp))
BitwardenTextField(
label = stringResource(id = R.string.name),
value = viewState.folderName,
@@ -134,10 +139,12 @@ fun FolderAddEditScreen(
{ viewModel.trySendAction(FolderAddEditAction.NameTextChange(it)) }
},
textFieldTestTag = "FolderNameField",
cardStyle = CardStyle.Full,
modifier = Modifier
.padding(16.dp)
.standardHorizontalMargin()
.fillMaxWidth(),
)
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
@@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
@@ -33,6 +34,7 @@ import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.platform.repository.model.ClearClipboardFrequency
import com.x8bit.bitwarden.data.platform.repository.util.displayLabel
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenOutlinedButton
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
@@ -40,6 +42,7 @@ import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenSelectionDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.row.BitwardenSelectionRow
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.row.BitwardenTextRow
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch
@@ -102,20 +105,22 @@ fun OtherScreen(
.fillMaxSize()
.verticalScroll(rememberScrollState()),
) {
Spacer(modifier = Modifier.height(height = 12.dp))
BitwardenSwitch(
label = stringResource(id = R.string.enable_sync_on_refresh),
description = stringResource(id = R.string.enable_sync_on_refresh_description),
supportingText = stringResource(id = R.string.enable_sync_on_refresh_description),
isChecked = state.allowSyncOnRefresh,
onCheckedChange = remember(viewModel) {
{ viewModel.trySendAction(OtherAction.AllowSyncToggle(it)) }
},
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.testTag("SyncOnRefreshSwitch")
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenOutlinedButton(
onClick = remember(viewModel) {
@@ -125,10 +130,10 @@ fun OtherScreen(
modifier = Modifier
.fillMaxWidth()
.testTag("SyncNowButton")
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(4.dp))
Spacer(modifier = Modifier.height(height = 8.dp))
Row(
modifier = Modifier
@@ -151,7 +156,7 @@ fun OtherScreen(
)
}
Spacer(modifier = Modifier.height(12.dp))
Spacer(modifier = Modifier.height(height = 16.dp))
ClearClipboardFrequencyRow(
currentSelection = state.clearClipboardFrequency,
@@ -160,9 +165,12 @@ fun OtherScreen(
},
modifier = Modifier
.testTag("ClearClipboardChooser")
.fillMaxWidth(),
.fillMaxWidth()
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(height = 16.dp))
ScreenCaptureRow(
currentValue = state.allowScreenCapture,
onValueChange = remember(viewModel) {
@@ -171,8 +179,10 @@ fun OtherScreen(
modifier = Modifier
.fillMaxWidth()
.testTag("AllowScreenCaptureSwitch")
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
}
@@ -195,6 +205,7 @@ private fun ScreenCaptureRow(
shouldShowScreenCaptureConfirmDialog = true
}
},
cardStyle = CardStyle.Full,
modifier = modifier,
)
@@ -226,6 +237,7 @@ private fun ClearClipboardFrequencyRow(
text = stringResource(id = R.string.clear_clipboard),
description = stringResource(id = R.string.clear_clipboard_description),
onClick = { shouldShowClearClipboardDialog = true },
cardStyle = CardStyle.Full,
modifier = modifier,
) {
Text(
@@ -3,8 +3,10 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.vault
import android.widget.Toast
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
@@ -30,6 +32,7 @@ import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
import com.x8bit.bitwarden.ui.platform.components.badge.NotificationBadge
import com.x8bit.bitwarden.ui.platform.components.card.BitwardenActionCard
import com.x8bit.bitwarden.ui.platform.components.card.actionCardExitAnimation
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.row.BitwardenExternalLinkRow
import com.x8bit.bitwarden.ui.platform.components.row.BitwardenTextRow
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
@@ -106,6 +109,7 @@ fun VaultSettingsScreen(
.fillMaxSize()
.verticalScroll(rememberScrollState()),
) {
Spacer(modifier = Modifier.height(12.dp))
AnimatedVisibility(
visible = state.shouldShowImportCard,
label = "ImportLoginsActionCard",
@@ -132,7 +136,7 @@ fun VaultSettingsScreen(
},
modifier = Modifier
.standardHorizontalMargin()
.padding(top = 12.dp, bottom = 16.dp),
.padding(bottom = 16.dp),
)
}
BitwardenTextRow(
@@ -140,9 +144,11 @@ fun VaultSettingsScreen(
onClick = remember(viewModel) {
{ viewModel.trySendAction(VaultSettingsAction.FoldersButtonClick) }
},
withDivider = true,
withDivider = false,
cardStyle = CardStyle.Top(),
modifier = Modifier
.testTag("FoldersLabel")
.standardHorizontalMargin()
.fillMaxWidth(),
)
@@ -151,9 +157,11 @@ fun VaultSettingsScreen(
onClick = remember(viewModel) {
{ viewModel.trySendAction(VaultSettingsAction.ExportVaultClick) }
},
withDivider = true,
withDivider = false,
cardStyle = CardStyle.Middle(),
modifier = Modifier
.testTag("ExportVaultLabel")
.standardHorizontalMargin()
.fillMaxWidth(),
)
@@ -163,9 +171,11 @@ fun VaultSettingsScreen(
onClick = remember(viewModel) {
{ viewModel.trySendAction(VaultSettingsAction.ImportItemsClick) }
},
withDivider = true,
withDivider = false,
cardStyle = CardStyle.Bottom,
modifier = Modifier
.testTag("ImportItemsLinkItemView")
.standardHorizontalMargin()
.fillMaxWidth(),
)
} else {
@@ -174,15 +184,16 @@ fun VaultSettingsScreen(
onConfirmClick = remember(viewModel) {
{ viewModel.trySendAction(VaultSettingsAction.ImportItemsClick) }
},
withDivider = true,
withDivider = false,
dialogTitle = stringResource(id = R.string.continue_to_web_app),
dialogMessage =
stringResource(
dialogMessage = stringResource(
id = R.string.you_can_import_data_to_your_vault_on_x,
state.importUrl,
),
cardStyle = CardStyle.Bottom,
modifier = Modifier
.testTag("ImportItemsLinkItemView")
.standardHorizontalMargin()
.fillMaxWidth(),
)
}
@@ -13,11 +13,13 @@ data class BitwardenShapes(
val coachmark: CornerBasedShape,
val content: CornerBasedShape,
val contentBottom: CornerBasedShape,
val contentMiddle: CornerBasedShape,
val contentTop: CornerBasedShape,
val dialog: CornerBasedShape,
val fab: CornerBasedShape,
val infoCallout: CornerBasedShape,
val menu: CornerBasedShape,
val progressIndicator: CornerBasedShape,
val segmentedControl: CornerBasedShape,
val snackbar: CornerBasedShape,
)
@@ -13,11 +13,13 @@ val bitwardenShapes: BitwardenShapes = BitwardenShapes(
coachmark = RoundedCornerShape(size = 8.dp),
content = RoundedCornerShape(size = 8.dp),
contentBottom = RoundedCornerShape(bottomStart = 8.dp, bottomEnd = 8.dp),
contentMiddle = RoundedCornerShape(0.dp),
contentTop = RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp),
dialog = RoundedCornerShape(size = 28.dp),
fab = CircleShape,
infoCallout = RoundedCornerShape(size = 8.dp),
menu = RoundedCornerShape(size = 4.dp),
progressIndicator = CircleShape,
segmentedControl = CircleShape,
snackbar = RoundedCornerShape(size = 8.dp),
)
@@ -8,7 +8,8 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -35,19 +36,20 @@ import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.base.util.LivecycleEventEffect
import com.x8bit.bitwarden.ui.platform.base.util.scrolledContainerBottomDivider
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenMediumTopAppBar
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
import com.x8bit.bitwarden.ui.platform.components.appbar.action.BitwardenOverflowActionItem
import com.x8bit.bitwarden.ui.platform.components.appbar.action.OverflowMenuItemData
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenFilledButton
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenStandardIconButton
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTonalIconButton
import com.x8bit.bitwarden.ui.platform.components.card.BitwardenInfoCalloutCard
import com.x8bit.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectButton
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordField
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextFieldWithActions
import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.model.TextToolbarType
import com.x8bit.bitwarden.ui.platform.components.model.TooltipData
import com.x8bit.bitwarden.ui.platform.components.model.TopAppBarDividerStyle
@@ -301,6 +303,7 @@ private fun ScrollContent(
Column(
modifier = modifier
.fillMaxHeight()
.imePadding()
.verticalScroll(rememberScrollState()),
) {
Spacer(modifier = Modifier.height(12.dp))
@@ -309,7 +312,7 @@ private fun ScrollContent(
text = stringResource(id = R.string.password_generator_policy_in_effect),
modifier = Modifier
.testTag("PasswordGeneratorPolicyInEffectLabel")
.padding(horizontal = 16.dp)
.standardHorizontalMargin()
.fillMaxWidth(),
)
@@ -320,7 +323,7 @@ private fun ScrollContent(
generatedText = state.generatedText,
onRegenerateClick = onRegenerateClick,
modifier = Modifier
.padding(horizontal = 16.dp)
.standardHorizontalMargin()
.fillMaxWidth(),
)
@@ -332,20 +335,11 @@ private fun ScrollContent(
modifier = Modifier
.testTag(tag = "CopyValueButton")
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(24.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.options),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(8.dp))
when (val selectedType = state.selectedType) {
is GeneratorState.MainType.Passphrase -> {
PassphraseTypeContent(
@@ -373,6 +367,8 @@ private fun ScrollContent(
)
}
}
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
@@ -388,7 +384,7 @@ private fun GeneratedStringItem(
value = generatedText,
singleLine = false,
actions = {
BitwardenTonalIconButton(
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_generate,
contentDescription = stringResource(id = R.string.generate_password),
onClick = onRegenerateClick,
@@ -402,6 +398,7 @@ private fun GeneratedStringItem(
visualTransformation = nonLetterColorVisualTransformation(),
modifier = modifier,
textToolbarType = TextToolbarType.NONE,
cardStyle = CardStyle.Full,
)
}
@@ -459,8 +456,6 @@ private fun ColumnScope.PasswordTypeContent(
passwordTypeState: GeneratorState.MainType.Password,
passwordHandlers: PasswordHandlers,
) {
Spacer(modifier = Modifier.height(8.dp))
BitwardenSlider(
value = passwordTypeState.length,
onValueChange = { newValue, isUserInteracting ->
@@ -471,41 +466,58 @@ private fun ColumnScope.PasswordTypeContent(
range = passwordTypeState.minLength..passwordTypeState.maxLength,
sliderTag = "PasswordLengthSlider",
valueTag = "PasswordLengthLabel",
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.padding(end = 28.dp),
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(8.dp))
Column(
modifier = Modifier.fillMaxWidth(),
) {
PasswordCapitalLettersToggleItem(
useCapitals = passwordTypeState.useCapitals,
onPasswordToggleCapitalLettersChange = passwordHandlers
.onPasswordToggleCapitalLettersChange,
enabled = passwordTypeState.capitalsEnabled,
)
PasswordLowercaseLettersToggleItem(
useLowercase = passwordTypeState.useLowercase,
onPasswordToggleLowercaseLettersChange = passwordHandlers
.onPasswordToggleLowercaseLettersChange,
enabled = passwordTypeState.lowercaseEnabled,
)
PasswordNumbersToggleItem(
useNumbers = passwordTypeState.useNumbers,
onPasswordToggleNumbersChange = passwordHandlers.onPasswordToggleNumbersChange,
enabled = passwordTypeState.numbersEnabled,
)
PasswordSpecialCharactersToggleItem(
useSpecialChars = passwordTypeState.useSpecialChars,
onPasswordToggleSpecialCharactersChange = passwordHandlers
.onPasswordToggleSpecialCharactersChange,
enabled = passwordTypeState.specialCharsEnabled,
)
}
PasswordCapitalLettersToggleItem(
useCapitals = passwordTypeState.useCapitals,
onPasswordToggleCapitalLettersChange = passwordHandlers
.onPasswordToggleCapitalLettersChange,
enabled = passwordTypeState.capitalsEnabled,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
PasswordLowercaseLettersToggleItem(
useLowercase = passwordTypeState.useLowercase,
onPasswordToggleLowercaseLettersChange = passwordHandlers
.onPasswordToggleLowercaseLettersChange,
enabled = passwordTypeState.lowercaseEnabled,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
PasswordNumbersToggleItem(
useNumbers = passwordTypeState.useNumbers,
onPasswordToggleNumbersChange = passwordHandlers.onPasswordToggleNumbersChange,
enabled = passwordTypeState.numbersEnabled,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
PasswordSpecialCharactersToggleItem(
useSpecialChars = passwordTypeState.useSpecialChars,
onPasswordToggleSpecialCharactersChange = passwordHandlers
.onPasswordToggleSpecialCharactersChange,
enabled = passwordTypeState.specialCharsEnabled,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
PasswordAvoidAmbiguousCharsToggleItem(
avoidAmbiguousChars = passwordTypeState.avoidAmbiguousChars,
onPasswordToggleAvoidAmbiguousCharsChange = passwordHandlers
.onPasswordToggleAvoidAmbiguousCharsChange,
enabled = passwordTypeState.ambiguousCharsEnabled,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(8.dp))
@@ -514,25 +526,20 @@ private fun ColumnScope.PasswordTypeContent(
onPasswordMinNumbersCounterChange = passwordHandlers.onPasswordMinNumbersCounterChange,
maxValue = max(passwordTypeState.maxNumbersAllowed, passwordTypeState.minNumbersAllowed),
minValue = passwordTypeState.minNumbersAllowed,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(8.dp))
PasswordMinSpecialCharactersCounterItem(
minSpecial = passwordTypeState.minSpecial,
onPasswordMinSpecialCharactersChange = passwordHandlers
.onPasswordMinSpecialCharactersChange,
maxValue = max(passwordTypeState.maxSpecialAllowed, passwordTypeState.minSpecialAllowed),
minValue = passwordTypeState.minSpecialAllowed,
)
Spacer(modifier = Modifier.height(16.dp))
PasswordAvoidAmbiguousCharsToggleItem(
avoidAmbiguousChars = passwordTypeState.avoidAmbiguousChars,
onPasswordToggleAvoidAmbiguousCharsChange = passwordHandlers
.onPasswordToggleAvoidAmbiguousCharsChange,
enabled = passwordTypeState.ambiguousCharsEnabled,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
@@ -549,10 +556,8 @@ private fun PasswordCapitalLettersToggleItem(
isChecked = useCapitals,
onCheckedChange = onPasswordToggleCapitalLettersChange,
enabled = enabled,
modifier = modifier
.fillMaxWidth()
.testTag("UppercaseAtoZToggle")
.padding(horizontal = 16.dp),
cardStyle = CardStyle.Top(),
modifier = modifier.testTag(tag = "UppercaseAtoZToggle"),
)
}
@@ -569,10 +574,8 @@ private fun PasswordLowercaseLettersToggleItem(
isChecked = useLowercase,
onCheckedChange = onPasswordToggleLowercaseLettersChange,
enabled = enabled,
modifier = modifier
.fillMaxWidth()
.testTag("LowercaseAtoZToggle")
.padding(horizontal = 16.dp),
cardStyle = CardStyle.Middle(),
modifier = modifier.testTag(tag = "LowercaseAtoZToggle"),
)
}
@@ -589,10 +592,8 @@ private fun PasswordNumbersToggleItem(
isChecked = useNumbers,
onCheckedChange = onPasswordToggleNumbersChange,
enabled = enabled,
modifier = modifier
.fillMaxWidth()
.testTag("NumbersZeroToNineToggle")
.padding(horizontal = 16.dp),
cardStyle = CardStyle.Middle(),
modifier = modifier.testTag(tag = "NumbersZeroToNineToggle"),
)
}
@@ -609,10 +610,8 @@ private fun PasswordSpecialCharactersToggleItem(
isChecked = useSpecialChars,
onCheckedChange = onPasswordToggleSpecialCharactersChange,
enabled = enabled,
modifier = modifier
.fillMaxWidth()
.testTag("SpecialCharactersToggle")
.padding(horizontal = 16.dp),
cardStyle = CardStyle.Middle(),
modifier = modifier.testTag(tag = "SpecialCharactersToggle"),
)
}
@@ -629,9 +628,8 @@ private fun PasswordMinNumbersCounterItem(
value = minNumbers.coerceIn(minValue, maxValue),
range = minValue..maxValue,
onValueChange = onPasswordMinNumbersCounterChange,
modifier = modifier
.testTag("MinNumberValueLabel")
.padding(horizontal = 16.dp),
cardStyle = CardStyle.Top(),
modifier = modifier.testTag(tag = "MinNumberValueLabel"),
)
}
@@ -648,9 +646,8 @@ private fun PasswordMinSpecialCharactersCounterItem(
value = minSpecial.coerceIn(minValue, maxValue),
range = minValue..maxValue,
onValueChange = onPasswordMinSpecialCharactersChange,
modifier = modifier
.testTag("MinSpecialValueLabel")
.padding(horizontal = 16.dp),
cardStyle = CardStyle.Bottom,
modifier = modifier.testTag(tag = "MinSpecialValueLabel"),
)
}
@@ -666,10 +663,8 @@ private fun PasswordAvoidAmbiguousCharsToggleItem(
isChecked = avoidAmbiguousChars,
enabled = enabled,
onCheckedChange = onPasswordToggleAvoidAmbiguousCharsChange,
modifier = modifier
.fillMaxWidth()
.testTag("AvoidAmbiguousCharsToggle")
.padding(horizontal = 16.dp),
cardStyle = CardStyle.Bottom,
modifier = modifier.testTag(tag = "AvoidAmbiguousCharsToggle"),
)
}
@@ -682,13 +677,14 @@ private fun ColumnScope.PassphraseTypeContent(
passphraseTypeState: GeneratorState.MainType.Passphrase,
passphraseHandlers: PassphraseHandlers,
) {
Spacer(modifier = Modifier.height(8.dp))
PassphraseNumWordsCounterItem(
numWords = passphraseTypeState.numWords,
onPassphraseNumWordsCounterChange = passphraseHandlers.onPassphraseNumWordsCounterChange,
minValue = passphraseTypeState.minNumWords,
maxValue = passphraseTypeState.maxNumWords,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(8.dp))
@@ -696,26 +692,31 @@ private fun ColumnScope.PassphraseTypeContent(
PassphraseWordSeparatorInputItem(
wordSeparator = passphraseTypeState.wordSeparator,
onPassphraseWordSeparatorChange = passphraseHandlers.onPassphraseWordSeparatorChange,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(8.dp))
Column(
modifier = Modifier.fillMaxWidth(),
) {
PassphraseCapitalizeToggleItem(
capitalize = passphraseTypeState.capitalize,
onPassphraseCapitalizeToggleChange = passphraseHandlers
.onPassphraseCapitalizeToggleChange,
enabled = passphraseTypeState.capitalizeEnabled,
)
PassphraseIncludeNumberToggleItem(
includeNumber = passphraseTypeState.includeNumber,
onPassphraseIncludeNumberToggleChange = passphraseHandlers
.onPassphraseIncludeNumberToggleChange,
enabled = passphraseTypeState.includeNumberEnabled,
)
}
PassphraseCapitalizeToggleItem(
capitalize = passphraseTypeState.capitalize,
onPassphraseCapitalizeToggleChange = passphraseHandlers
.onPassphraseCapitalizeToggleChange,
enabled = passphraseTypeState.capitalizeEnabled,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
PassphraseIncludeNumberToggleItem(
includeNumber = passphraseTypeState.includeNumber,
onPassphraseIncludeNumberToggleChange = passphraseHandlers
.onPassphraseIncludeNumberToggleChange,
enabled = passphraseTypeState.includeNumberEnabled,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
@Composable
@@ -726,17 +727,14 @@ private fun PassphraseNumWordsCounterItem(
minValue: Int = PASSPHRASE_MIN_NUMBER_OF_WORDS,
maxValue: Int = PASSPHRASE_MAX_NUMBER_OF_WORDS,
) {
val coercedNumWords = numWords.coerceIn(minValue, maxValue)
BitwardenStepper(
label = stringResource(id = R.string.number_of_words),
value = coercedNumWords,
value = numWords.coerceIn(minimumValue = minValue, maximumValue = maxValue),
range = minValue..maxValue,
onValueChange = onPassphraseNumWordsCounterChange,
stepperActionsTestTag = "NumberOfWordsStepper",
modifier = modifier
.testTag("NumberOfWordsLabel")
.padding(horizontal = 16.dp),
cardStyle = CardStyle.Full,
modifier = modifier.testTag(tag = "NumberOfWordsLabel"),
)
}
@@ -758,10 +756,9 @@ private fun PassphraseWordSeparatorInputItem(
onPassphraseWordSeparatorChange(char)
}
},
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
cardStyle = CardStyle.Full,
textFieldTestTag = "WordSeparatorEntry",
modifier = modifier,
)
}
@@ -777,10 +774,8 @@ private fun PassphraseCapitalizeToggleItem(
isChecked = capitalize,
onCheckedChange = onPassphraseCapitalizeToggleChange,
enabled = enabled,
modifier = modifier
.testTag("CapitalizePassphraseToggle")
.fillMaxWidth()
.padding(horizontal = 16.dp),
cardStyle = CardStyle.Top(),
modifier = modifier.testTag(tag = "CapitalizePassphraseToggle"),
)
}
@@ -796,10 +791,8 @@ private fun PassphraseIncludeNumberToggleItem(
isChecked = includeNumber,
enabled = enabled,
onCheckedChange = onPassphraseIncludeNumberToggleChange,
modifier = modifier
.testTag("IncludeNumbersToggle")
.fillMaxWidth()
.padding(horizontal = 16.dp),
cardStyle = CardStyle.Bottom,
modifier = modifier.testTag(tag = "IncludeNumbersToggle"),
)
}
@@ -817,7 +810,14 @@ private fun ColumnScope.UsernameTypeItems(
catchAllEmailHandlers: CatchAllEmailHandlers,
randomWordHandlers: RandomWordHandlers,
) {
UsernameOptionsItem(usernameState, onSubStateOptionClicked, usernameTypeHandlers)
UsernameOptionsItem(
currentSubState = usernameState,
onSubStateOptionClicked = onSubStateOptionClicked,
usernameTypeHandlers = usernameTypeHandlers,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
when (val selectedType = usernameState.selectedType) {
is GeneratorState.MainType.Username.UsernameType.PlusAddressedEmail -> {
@@ -876,10 +876,8 @@ private fun UsernameOptionsItem(
onClick = usernameTypeHandlers.onUsernameTooltipClicked,
contentDescription = stringResource(id = R.string.learn_more),
),
modifier = modifier
.padding(horizontal = 16.dp)
.fillMaxWidth()
.testTag("UsernameTypePicker"),
cardStyle = CardStyle.Full,
modifier = modifier.testTag(tag = "UsernameTypePicker"),
)
}
@@ -898,20 +896,23 @@ private fun ColumnScope.ForwardedEmailAliasTypeContent(
ServiceTypeOptionsItem(
currentSubState = usernameTypeState,
onSubStateOptionClicked = forwardedEmailAliasHandlers.onServiceChange,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(8.dp))
when (usernameTypeState.selectedServiceType) {
is ServiceType.AddyIo -> {
BitwardenPasswordField(
label = stringResource(id = R.string.api_access_token),
value = usernameTypeState.selectedServiceType.apiAccessToken,
onValueChange = forwardedEmailAliasHandlers.onAddyIoAccessTokenTextChange,
showPasswordTestTag = "ShowForwardedEmailApiSecretButton",
cardStyle = CardStyle.Full,
modifier = Modifier
.padding(horizontal = 16.dp)
.standardHorizontalMargin()
.testTag("ForwardedEmailApiSecretEntry")
.fillMaxWidth(),
)
@@ -922,10 +923,11 @@ private fun ColumnScope.ForwardedEmailAliasTypeContent(
label = stringResource(id = R.string.domain_name_required_parenthesis),
value = usernameTypeState.selectedServiceType.domainName,
onValueChange = forwardedEmailAliasHandlers.onAddyIoDomainNameTextChange,
modifier = Modifier
.padding(horizontal = 16.dp)
.fillMaxWidth(),
textFieldTestTag = "AnonAddyDomainNameEntry",
cardStyle = CardStyle.Full,
modifier = Modifier
.standardHorizontalMargin()
.fillMaxWidth(),
)
}
@@ -935,8 +937,9 @@ private fun ColumnScope.ForwardedEmailAliasTypeContent(
value = usernameTypeState.selectedServiceType.apiKey,
onValueChange = forwardedEmailAliasHandlers.onDuckDuckGoApiKeyTextChange,
showPasswordTestTag = "ShowForwardedEmailApiSecretButton",
cardStyle = CardStyle.Full,
modifier = Modifier
.padding(horizontal = 16.dp)
.standardHorizontalMargin()
.testTag("ForwardedEmailApiSecretEntry")
.fillMaxWidth(),
)
@@ -948,8 +951,9 @@ private fun ColumnScope.ForwardedEmailAliasTypeContent(
value = usernameTypeState.selectedServiceType.apiKey,
onValueChange = forwardedEmailAliasHandlers.onFastMailApiKeyTextChange,
showPasswordTestTag = "ShowForwardedEmailApiSecretButton",
cardStyle = CardStyle.Full,
modifier = Modifier
.padding(horizontal = 16.dp)
.standardHorizontalMargin()
.testTag("ForwardedEmailApiSecretEntry")
.fillMaxWidth(),
)
@@ -961,8 +965,9 @@ private fun ColumnScope.ForwardedEmailAliasTypeContent(
value = usernameTypeState.selectedServiceType.apiAccessToken,
onValueChange = forwardedEmailAliasHandlers.onFirefoxRelayAccessTokenTextChange,
showPasswordTestTag = "ShowForwardedEmailApiSecretButton",
cardStyle = CardStyle.Full,
modifier = Modifier
.padding(horizontal = 16.dp)
.standardHorizontalMargin()
.testTag("ForwardedEmailApiSecretEntry")
.fillMaxWidth(),
)
@@ -974,8 +979,9 @@ private fun ColumnScope.ForwardedEmailAliasTypeContent(
value = usernameTypeState.selectedServiceType.apiKey,
onValueChange = forwardedEmailAliasHandlers.onForwardEmailApiKeyTextChange,
showPasswordTestTag = "ShowForwardedEmailApiSecretButton",
cardStyle = CardStyle.Full,
modifier = Modifier
.padding(horizontal = 16.dp)
.standardHorizontalMargin()
.testTag("ForwardedEmailApiSecretEntry")
.fillMaxWidth(),
)
@@ -986,10 +992,11 @@ private fun ColumnScope.ForwardedEmailAliasTypeContent(
label = stringResource(id = R.string.domain_name_required_parenthesis),
value = usernameTypeState.selectedServiceType.domainName,
onValueChange = forwardedEmailAliasHandlers.onForwardEmailDomainNameTextChange,
modifier = Modifier
.padding(horizontal = 16.dp)
.fillMaxWidth(),
textFieldTestTag = "ForwardedEmailDomainNameEntry",
cardStyle = CardStyle.Full,
modifier = Modifier
.standardHorizontalMargin()
.fillMaxWidth(),
)
}
@@ -999,8 +1006,9 @@ private fun ColumnScope.ForwardedEmailAliasTypeContent(
value = usernameTypeState.selectedServiceType.apiKey,
onValueChange = forwardedEmailAliasHandlers.onSimpleLoginApiKeyTextChange,
showPasswordTestTag = "ShowForwardedEmailApiSecretButton",
cardStyle = CardStyle.Full,
modifier = Modifier
.padding(horizontal = 16.dp)
.standardHorizontalMargin()
.testTag("ForwardedEmailApiSecretEntry")
.fillMaxWidth(),
)
@@ -1013,8 +1021,9 @@ private fun ColumnScope.ForwardedEmailAliasTypeContent(
value = obfuscatedTextField,
onValueChange = { obfuscatedTextField = it },
showPasswordTestTag = "ShowForwardedEmailApiSecretButton",
cardStyle = CardStyle.Full,
modifier = Modifier
.padding(horizontal = 16.dp)
.standardHorizontalMargin()
.testTag("ForwardedEmailApiSecretEntry")
.fillMaxWidth(),
)
@@ -1042,10 +1051,8 @@ private fun ServiceTypeOptionsItem(
optionsWithStrings.entries.first { it.value == selectedOption }.key
onSubStateOptionClicked(selectedOptionId)
},
modifier = modifier
.padding(horizontal = 16.dp)
.testTag("ServiceTypePicker")
.fillMaxWidth(),
cardStyle = CardStyle.Full,
modifier = modifier.testTag(tag = "ServiceTypePicker"),
)
}
@@ -1061,6 +1068,9 @@ private fun ColumnScope.PlusAddressedEmailTypeContent(
PlusAddressedEmailTextInputItem(
email = usernameTypeState.email,
onPlusAddressedEmailTextChange = plusAddressedEmailHandlers.onEmailChange,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
@@ -1074,10 +1084,9 @@ private fun PlusAddressedEmailTextInputItem(
label = stringResource(id = R.string.email_required_parenthesis),
value = email,
onValueChange = onPlusAddressedEmailTextChange,
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
textFieldTestTag = "PlusAddressedEmailEntry",
cardStyle = CardStyle.Full,
modifier = modifier,
)
}
@@ -1095,6 +1104,9 @@ private fun ColumnScope.CatchAllEmailTypeContent(
CatchAllEmailTextInputItem(
domain = usernameTypeState.domainName,
onDomainTextChange = catchAllEmailHandlers.onDomainChange,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
@@ -1108,10 +1120,9 @@ private fun CatchAllEmailTextInputItem(
label = stringResource(id = R.string.domain_name_required_parenthesis),
value = domain,
onValueChange = onDomainTextChange,
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
textFieldTestTag = "CatchAllEmailDomainEntry",
cardStyle = CardStyle.Full,
modifier = modifier,
)
}
@@ -1124,16 +1135,22 @@ private fun ColumnScope.RandomWordTypeContent(
randomWordTypeState: GeneratorState.MainType.Username.UsernameType.RandomWord,
randomWordHandlers: RandomWordHandlers,
) {
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(8.dp))
RandomWordCapitalizeToggleItem(
capitalize = randomWordTypeState.capitalize,
onRandomWordCapitalizeToggleChange = randomWordHandlers.onCapitalizeChange,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
RandomWordIncludeNumberToggleItem(
includeNumber = randomWordTypeState.includeNumber,
onRandomWordIncludeNumberToggleChange = randomWordHandlers.onIncludeNumberChange,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
@@ -1147,10 +1164,8 @@ private fun RandomWordCapitalizeToggleItem(
label = stringResource(id = R.string.capitalize),
isChecked = capitalize,
onCheckedChange = onRandomWordCapitalizeToggleChange,
modifier = modifier
.fillMaxWidth()
.testTag("CapitalizeRandomWordUsernameToggle")
.padding(horizontal = 16.dp),
cardStyle = CardStyle.Top(),
modifier = modifier.testTag(tag = "CapitalizeRandomWordUsernameToggle"),
)
}
@@ -1164,10 +1179,8 @@ private fun RandomWordIncludeNumberToggleItem(
label = stringResource(id = R.string.include_number),
isChecked = includeNumber,
onCheckedChange = onRandomWordIncludeNumberToggleChange,
modifier = modifier
.fillMaxWidth()
.testTag("IncludeNumberRandomWordUsernameToggle")
.padding(horizontal = 16.dp),
cardStyle = CardStyle.Bottom,
modifier = modifier.testTag(tag = "IncludeNumberRandomWordUsernameToggle"),
)
}
@@ -18,9 +18,11 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.cardBackground
import com.x8bit.bitwarden.ui.platform.base.util.withLineBreaksAtWidth
import com.x8bit.bitwarden.ui.platform.base.util.withVisualTransformation
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenStandardIconButton
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.util.nonLetterColorVisualTransformation
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
@@ -37,12 +39,18 @@ fun PasswordHistoryListItem(
label: String,
supportingLabel: String,
onCopyClick: () -> Unit,
cardStyle: CardStyle,
modifier: Modifier = Modifier,
) {
Row(
modifier = Modifier
.padding(vertical = 16.dp)
.then(modifier),
modifier = modifier
.cardBackground(cardStyle = cardStyle)
.padding(
top = 12.dp,
bottom = 12.dp,
start = 16.dp,
end = 4.dp,
),
verticalAlignment = Alignment.CenterVertically,
) {
Column(modifier = Modifier.weight(1f)) {
@@ -90,6 +98,7 @@ private fun PasswordHistoryListItem_preview() {
label = "8gr6uY8CLYQwzr#",
supportingLabel = "8/24/2023 11:07 AM",
onCopyClick = {},
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
@@ -7,11 +7,11 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
@@ -30,10 +30,11 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.x8bit.bitwarden.ui.platform.base.util.toListItemCardStyle
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
import com.x8bit.bitwarden.ui.platform.components.appbar.action.BitwardenOverflowActionItem
import com.x8bit.bitwarden.ui.platform.components.appbar.action.OverflowMenuItemData
import com.x8bit.bitwarden.ui.platform.components.divider.BitwardenHorizontalDivider
import com.x8bit.bitwarden.ui.platform.components.indicator.BitwardenCircularProgressIndicator
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
@@ -72,8 +73,8 @@ fun PasswordHistoryScreen(
BitwardenTopAppBar(
title = stringResource(id = R.string.password_history),
scrollBehavior = scrollBehavior,
navigationIcon = rememberVectorPainter(id = R.drawable.ic_close),
navigationIconContentDescription = stringResource(id = R.string.close),
navigationIcon = rememberVectorPainter(id = R.drawable.ic_back),
navigationIconContentDescription = stringResource(id = R.string.back),
onNavigationIconClick = remember(viewModel) {
{ viewModel.trySendAction(PasswordHistoryAction.CloseClick) }
},
@@ -155,19 +156,23 @@ private fun PasswordHistoryContent(
modifier: Modifier = Modifier,
) {
LazyColumn(modifier = modifier) {
items(state.passwords) { password ->
item {
Spacer(modifier = Modifier.height(height = 12.dp))
}
itemsIndexed(state.passwords) { index, password ->
PasswordHistoryListItem(
label = password.password,
supportingLabel = password.date,
onCopyClick = { onPasswordCopyClick(password) },
cardStyle = state.passwords.toListItemCardStyle(index = index),
modifier = Modifier
.testTag("GeneratedPasswordRow")
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
BitwardenHorizontalDivider(modifier = Modifier.fillMaxWidth())
}
item {
Spacer(modifier = Modifier.height(height = 16.dp))
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
@@ -6,16 +6,19 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.x8bit.bitwarden.ui.platform.base.util.toListItemCardStyle
import com.x8bit.bitwarden.ui.platform.components.card.BitwardenInfoCalloutCard
import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText
import com.x8bit.bitwarden.ui.platform.components.listitem.BitwardenGroupItem
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.model.IconData
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.x8bit.bitwarden.ui.tools.feature.send.handlers.SendHandlers
@@ -36,14 +39,18 @@ fun SendContent(
) {
LazyColumn(modifier = modifier) {
item {
if (policyDisablesSend) {
Spacer(modifier = Modifier.height(height = 12.dp))
}
if (policyDisablesSend) {
item {
BitwardenInfoCalloutCard(
text = stringResource(id = R.string.send_disabled_warning),
modifier = Modifier
.testTag("SendOptionsPolicyInEffectLabel")
.padding(horizontal = 16.dp)
.standardHorizontalMargin()
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
}
@@ -53,8 +60,10 @@ fun SendContent(
supportingLabel = SEND_TYPES_COUNT.toString(),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
item {
@@ -63,10 +72,11 @@ fun SendContent(
supportingLabel = state.textTypeCount.toString(),
startIcon = rememberVectorPainter(id = R.drawable.ic_file_text),
onClick = sendHandlers.onTextTypeClick,
cardStyle = CardStyle.Top(dividerPadding = 56.dp),
modifier = Modifier
.fillMaxWidth()
.testTag("SendTextFilter")
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
}
@@ -76,10 +86,11 @@ fun SendContent(
supportingLabel = state.fileTypeCount.toString(),
startIcon = rememberVectorPainter(id = R.drawable.ic_file),
onClick = sendHandlers.onFileTypeClick,
cardStyle = CardStyle.Bottom,
modifier = Modifier
.fillMaxWidth()
.testTag("SendFileFilter")
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
}
@@ -90,11 +101,13 @@ fun SendContent(
supportingLabel = state.sendItems.size.toString(),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
items(state.sendItems) {
itemsIndexed(state.sendItems) { index, it ->
SendListItem(
startIcon = IconData.Local(it.type.iconRes),
label = it.name,
@@ -111,14 +124,12 @@ fun SendContent(
} else {
null
},
cardStyle = state
.sendItems
.toListItemCardStyle(index = index, dividerPadding = 56.dp),
modifier = Modifier
.testTag("SendCell")
.padding(
start = 16.dp,
// There is some built-in padding to the menu button that makes up
// the visual difference here.
end = 12.dp,
)
.standardHorizontalMargin()
.fillMaxWidth(),
)
}
@@ -13,6 +13,7 @@ import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
import com.x8bit.bitwarden.ui.platform.components.listitem.BitwardenListItem
import com.x8bit.bitwarden.ui.platform.components.listitem.SelectionItemData
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.model.IconData
import com.x8bit.bitwarden.ui.platform.components.model.IconRes
import com.x8bit.bitwarden.ui.platform.components.model.IconResource
@@ -37,6 +38,7 @@ import kotlinx.collections.immutable.toPersistentList
* @param onDeleteClick The lambda to be invoked when the delete option is clicked from the menu.
* @param onRemovePasswordClick The lambda to be invoked when the remove password option is clicked
* from the menu, if `null` the remove password button is not displayed.
* @param cardStyle Indicates the type of card style to be applied.
* @param modifier An optional [Modifier] for this Composable, defaulting to an empty Modifier.
* This allows the caller to specify things like padding, size, etc.
*/
@@ -54,6 +56,7 @@ fun SendListItem(
onShareClick: () -> Unit,
onDeleteClick: () -> Unit,
onRemovePasswordClick: (() -> Unit)?,
cardStyle: CardStyle,
modifier: Modifier = Modifier,
) {
var shouldShowDeleteConfirmationDialog by rememberSaveable { mutableStateOf(false) }
@@ -98,6 +101,7 @@ fun SendListItem(
.filter { showMoreOptions }
.toPersistentList(),
optionsTestTag = "Options",
cardStyle = cardStyle,
modifier = modifier,
)
if (shouldShowDeleteConfirmationDialog) {
@@ -116,7 +120,7 @@ fun SendListItem(
}
}
@Preview(showBackground = true)
@Preview
@Composable
private fun SendListItem_preview() {
BitwardenTheme {
@@ -132,6 +136,7 @@ private fun SendListItem_preview() {
onShareClick = {},
onDeleteClick = {},
onRemovePasswordClick = null,
cardStyle = CardStyle.Full,
)
}
}
@@ -8,8 +8,10 @@ import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
@@ -21,6 +23,7 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.minimumInteractiveComponentSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -34,13 +37,20 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.base.util.cardBackground
import com.x8bit.bitwarden.ui.platform.base.util.cardPadding
import com.x8bit.bitwarden.ui.platform.base.util.concat
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenOutlinedButton
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton
import com.x8bit.bitwarden.ui.platform.components.card.BitwardenInfoCalloutCard
import com.x8bit.bitwarden.ui.platform.components.divider.BitwardenHorizontalDivider
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordField
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.stepper.BitwardenStepper
import com.x8bit.bitwarden.ui.platform.components.text.BitwardenClickableText
import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.x8bit.bitwarden.ui.platform.manager.permissions.PermissionsManager
@@ -69,12 +79,12 @@ fun AddSendContent(
Column(
modifier = modifier.verticalScroll(rememberScrollState()),
) {
Spacer(modifier = Modifier.height(height = 12.dp))
if (policyDisablesSend) {
Spacer(modifier = Modifier.height(8.dp))
BitwardenInfoCalloutCard(
text = stringResource(id = R.string.send_disabled_warning),
modifier = Modifier
.padding(horizontal = 16.dp)
.standardHorizontalMargin()
.fillMaxWidth()
.testTag("SendPolicyInEffectLabel"),
)
@@ -86,35 +96,36 @@ fun AddSendContent(
text = stringResource(id = R.string.send_options_policy_in_effect),
modifier = Modifier
.testTag(tag = "SendPolicyInEffectLabel")
.padding(horizontal = 16.dp)
.standardHorizontalMargin()
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(16.dp))
}
BitwardenTextField(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
label = stringResource(id = R.string.name),
hint = stringResource(id = R.string.name_info),
supportingText = stringResource(id = R.string.name_info),
readOnly = policyDisablesSend,
value = state.common.name,
onValueChange = addSendHandlers.onNamChange,
textFieldTestTag = "SendNameEntry",
cardStyle = CardStyle.Full,
)
Spacer(modifier = Modifier.height(8.dp))
when (val type = state.selectedType) {
is AddSendState.ViewState.Content.SendType.File -> {
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.file),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(height = 8.dp))
if (isShared) {
Text(
text = type.name.orEmpty(),
@@ -122,6 +133,7 @@ fun AddSendContent(
style = BitwardenTheme.typography.bodyMedium,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(8.dp))
@@ -131,22 +143,31 @@ fun AddSendContent(
style = BitwardenTheme.typography.bodySmall,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
} else if (isAddMode) {
Text(
modifier = Modifier
.testTag(tag = "SendCurrentFileNameLabel")
.align(Alignment.CenterHorizontally),
text = type.name ?: stringResource(id = R.string.no_file_chosen),
color = BitwardenTheme.colorScheme.text.secondary,
style = BitwardenTheme.typography.bodySmall,
)
Spacer(modifier = Modifier.height(8.dp))
type.name?.let {
Text(
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.defaultMinSize(minHeight = 60.dp)
.cardBackground(cardStyle = CardStyle.Full)
.padding(
horizontal = 16.dp,
vertical = 12.dp,
)
.testTag(tag = "SendCurrentFileNameLabel"),
text = it,
color = BitwardenTheme.colorScheme.text.primary,
style = BitwardenTheme.typography.bodyLarge,
)
Spacer(modifier = Modifier.height(8.dp))
}
BitwardenOutlinedButton(
label = stringResource(id = R.string.choose_file),
onClick = {
@Suppress("MaxLineLength")
if (permissionsManager.checkPermission(Manifest.permission.CAMERA)) {
addSendHandlers.onChooseFileClick(true)
} else {
@@ -158,31 +179,30 @@ fun AddSendContent(
modifier = Modifier
.testTag(tag = "SendChooseFileButton")
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(4.dp))
Spacer(modifier = Modifier.height(height = 8.dp))
Text(
text = stringResource(id = R.string.max_file_size),
color = BitwardenTheme.colorScheme.text.secondary,
style = BitwardenTheme.typography.bodySmall,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 32.dp),
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = stringResource(id = R.string.type_file_info),
color = BitwardenTheme.colorScheme.text.secondary,
style = BitwardenTheme.typography.bodySmall,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
} else {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin()
.defaultMinSize(minHeight = 60.dp)
.cardBackground(cardStyle = CardStyle.Full)
.padding(
horizontal = 16.dp,
vertical = 12.dp,
),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = type.name.orEmpty(),
@@ -193,7 +213,7 @@ fun AddSendContent(
Spacer(modifier = Modifier.width(8.dp))
Text(
text = type.displaySize.orEmpty(),
color = BitwardenTheme.colorScheme.text.primary,
color = BitwardenTheme.colorScheme.text.secondary,
style = BitwardenTheme.typography.bodyMedium,
)
}
@@ -201,33 +221,35 @@ fun AddSendContent(
}
is AddSendState.ViewState.Content.SendType.Text -> {
Spacer(modifier = Modifier.height(height = 8.dp))
BitwardenTextField(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
label = stringResource(id = R.string.text),
hint = stringResource(id = R.string.type_text_info),
supportingText = stringResource(id = R.string.type_text_info),
readOnly = policyDisablesSend,
value = type.input,
singleLine = false,
onValueChange = addSendHandlers.onTextChange,
textFieldTestTag = "SendTextContentEntry",
cardStyle = CardStyle.Full,
)
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(height = 8.dp))
BitwardenSwitch(
modifier = Modifier
.testTag(tag = "SendHideTextByDefaultToggle")
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
label = stringResource(id = R.string.hide_text_by_default),
isChecked = type.isHideByDefaultChecked,
onCheckedChange = addSendHandlers.onIsHideByDefaultToggle,
readOnly = policyDisablesSend,
cardStyle = CardStyle.Full,
)
}
}
Spacer(modifier = Modifier.height(16.dp))
AddSendOptions(
state = state,
sendRestrictionPolicy = policyDisablesSend,
@@ -235,7 +257,7 @@ fun AddSendContent(
addSendHandlers = addSendHandlers,
)
Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.height(height = 12.dp))
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
@@ -271,12 +293,14 @@ private fun AddSendOptions(
},
onClick = { isExpanded = !isExpanded },
)
.padding(16.dp)
.minimumInteractiveComponentSize()
.padding(top = 16.dp, bottom = 8.dp)
.standardHorizontalMargin()
.semantics(mergeDescendants = true) {},
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = stringResource(id = R.string.options),
text = stringResource(id = R.string.additional_options),
color = BitwardenTheme.colorScheme.text.interaction,
style = BitwardenTheme.typography.labelLarge,
modifier = Modifier.padding(end = 8.dp),
@@ -306,7 +330,7 @@ private fun AddSendOptions(
modifier = Modifier
.testTag("SendDeletionOptionsPicker")
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
dateFormatPattern = state.common.dateFormatPattern,
timeFormatPattern = state.common.timeFormatPattern,
currentZonedDateTime = state.common.deletionDate,
@@ -318,7 +342,7 @@ private fun AddSendOptions(
modifier = Modifier
.testTag("SendExpirationOptionsPicker")
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
dateFormatPattern = state.common.dateFormatPattern,
timeFormatPattern = state.common.timeFormatPattern,
currentZonedDateTime = state.common.expirationDate,
@@ -326,174 +350,174 @@ private fun AddSendOptions(
isEnabled = !sendRestrictionPolicy,
)
} else {
BitwardenListHeaderText(
label = stringResource(id = R.string.deletion_date),
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(8.dp))
AddSendCustomDateChooser(
modifier = Modifier
.testTag("SendCustomDeletionDatePicker")
.fillMaxWidth()
.padding(horizontal = 16.dp),
dateLabel = stringResource(id = R.string.deletion_date),
timeLabel = stringResource(id = R.string.deletion_time),
dateFormatPattern = state.common.dateFormatPattern,
timeFormatPattern = state.common.timeFormatPattern,
currentZonedDateTime = state.common.deletionDate,
isEnabled = !sendRestrictionPolicy,
onDateSelect = { addSendHandlers.onDeletionDateChange(requireNotNull(it)) },
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = stringResource(id = R.string.deletion_date_info),
style = BitwardenTheme.typography.bodySmall,
color = BitwardenTheme.colorScheme.text.primary,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.expiration_date),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(8.dp))
AddSendCustomDateChooser(
modifier = Modifier
.testTag("SendCustomExpirationDatePicker")
.fillMaxWidth()
.padding(horizontal = 16.dp),
dateLabel = stringResource(id = R.string.expiration_date),
timeLabel = stringResource(id = R.string.expiration_time),
dateFormatPattern = state.common.dateFormatPattern,
timeFormatPattern = state.common.timeFormatPattern,
currentZonedDateTime = state.common.expirationDate,
onDateSelect = addSendHandlers.onExpirationDateChange,
isEnabled = !sendRestrictionPolicy,
)
Spacer(modifier = Modifier.height(4.dp))
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically,
.standardHorizontalMargin()
.defaultMinSize(minHeight = 60.dp)
.cardBackground(cardStyle = CardStyle.Full)
.cardPadding(cardStyle = CardStyle.Full, vertical = 0.dp),
) {
Text(
text = stringResource(id = R.string.expiration_date_info),
style = BitwardenTheme.typography.bodySmall,
color = BitwardenTheme.colorScheme.text.primary,
modifier = Modifier.weight(1f),
)
Spacer(modifier = Modifier.width(4.dp))
BitwardenTextButton(
label = stringResource(id = R.string.clear),
onClick = addSendHandlers.onClearExpirationDateClick,
isEnabled = state.common.expirationDate != null && !sendRestrictionPolicy,
AddSendCustomDateChooser(
modifier = Modifier
.testTag("SendClearExpirationDateButton")
.wrapContentWidth(),
.testTag("SendCustomDeletionDatePicker")
.fillMaxWidth(),
dateLabel = stringResource(id = R.string.deletion_date),
timeLabel = stringResource(id = R.string.deletion_time),
dateFormatPattern = state.common.dateFormatPattern,
timeFormatPattern = state.common.timeFormatPattern,
currentZonedDateTime = state.common.deletionDate,
isEnabled = !sendRestrictionPolicy,
onDateSelect = { addSendHandlers.onDeletionDateChange(requireNotNull(it)) },
)
BitwardenHorizontalDivider(modifier = Modifier.padding(start = 16.dp))
Spacer(modifier = Modifier.height(height = 12.dp))
Text(
text = stringResource(id = R.string.deletion_date_info),
style = BitwardenTheme.typography.bodySmall,
color = BitwardenTheme.colorScheme.text.secondary,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 12.dp))
}
Spacer(modifier = Modifier.height(height = 8.dp))
Column(
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.defaultMinSize(minHeight = 60.dp)
.cardBackground(cardStyle = CardStyle.Full)
.cardPadding(cardStyle = CardStyle.Full, vertical = 0.dp),
) {
AddSendCustomDateChooser(
modifier = Modifier
.testTag("SendCustomExpirationDatePicker")
.fillMaxWidth(),
dateLabel = stringResource(id = R.string.expiration_date),
timeLabel = stringResource(id = R.string.expiration_time),
dateFormatPattern = state.common.dateFormatPattern,
timeFormatPattern = state.common.timeFormatPattern,
currentZonedDateTime = state.common.expirationDate,
onDateSelect = addSendHandlers.onExpirationDateChange,
isEnabled = !sendRestrictionPolicy,
)
BitwardenHorizontalDivider(modifier = Modifier.padding(start = 16.dp))
Spacer(modifier = Modifier.height(height = 12.dp))
Row(
modifier = Modifier
.padding(start = 16.dp)
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = stringResource(id = R.string.expiration_date_info),
style = BitwardenTheme.typography.bodySmall,
color = BitwardenTheme.colorScheme.text.secondary,
modifier = Modifier.weight(weight = 1f),
)
Spacer(modifier = Modifier.width(width = 4.dp))
BitwardenClickableText(
label = stringResource(id = R.string.clear),
onClick = addSendHandlers.onClearExpirationDateClick,
isEnabled = state.common.expirationDate != null &&
!sendRestrictionPolicy,
style = BitwardenTheme.typography.labelLarge,
innerPadding = PaddingValues(vertical = 6.dp, horizontal = 16.dp),
modifier = Modifier
.testTag("SendClearExpirationDateButton")
.wrapContentWidth(),
)
}
Spacer(modifier = Modifier.height(height = 12.dp))
}
}
Spacer(modifier = Modifier.height(8.dp))
BitwardenStepper(
label = stringResource(id = R.string.maximum_access_count),
supportingTextContent = {
Text(
text = stringResource(id = R.string.maximum_access_count_info),
style = BitwardenTheme.typography.bodySmall,
color = BitwardenTheme.colorScheme.text.secondary,
modifier = Modifier.fillMaxWidth(),
)
state.common.currentAccessCount.takeUnless { isAddMode }?.let {
Spacer(modifier = Modifier.height(16.dp))
Text(
text = R.string.current_access_count
.asText()
.concat(": ".asText(), it.toString().asText())
.invoke(),
style = BitwardenTheme.typography.bodySmall,
color = BitwardenTheme.colorScheme.text.secondary,
modifier = Modifier.fillMaxWidth(),
)
}
},
value = state.common.maxAccessCount,
onValueChange = addSendHandlers.onMaxAccessCountChange,
isDecrementEnabled = state.common.maxAccessCount != null && !sendRestrictionPolicy,
isIncrementEnabled = !sendRestrictionPolicy,
range = 0..Int.MAX_VALUE,
textFieldReadOnly = sendRestrictionPolicy,
cardStyle = CardStyle.Full,
modifier = Modifier
.testTag("SendMaxAccessCountEntry")
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = stringResource(id = R.string.maximum_access_count_info),
style = BitwardenTheme.typography.bodySmall,
color = BitwardenTheme.colorScheme.text.secondary,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 32.dp),
)
if (!isAddMode) {
state.common.currentAccessCount?.let {
Spacer(modifier = Modifier.height(16.dp))
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 32.dp),
) {
Text(
text = stringResource(id = R.string.current_access_count) + ":",
style = BitwardenTheme.typography.bodySmall,
color = BitwardenTheme.colorScheme.text.primary,
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = it.toString(),
style = BitwardenTheme.typography.bodySmall,
color = BitwardenTheme.colorScheme.text.primary,
)
}
Spacer(modifier = Modifier.height(16.dp))
}
}
Spacer(modifier = Modifier.height(8.dp))
BitwardenPasswordField(
label = stringResource(id = R.string.new_password),
hint = stringResource(id = R.string.password_info),
supportingText = stringResource(id = R.string.password_info),
readOnly = sendRestrictionPolicy,
value = state.common.passwordInput,
onValueChange = addSendHandlers.onPasswordChange,
cardStyle = CardStyle.Full,
modifier = Modifier
.testTag("SendNewPasswordEntry")
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
label = stringResource(id = R.string.notes),
hint = stringResource(id = R.string.notes_info),
supportingText = stringResource(id = R.string.notes_info),
readOnly = sendRestrictionPolicy,
value = state.common.noteInput,
singleLine = false,
onValueChange = addSendHandlers.onNoteChange,
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(height = 8.dp))
BitwardenSwitch(
modifier = Modifier
.testTag("SendHideEmailSwitch")
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
label = stringResource(id = R.string.hide_email),
isChecked = state.common.isHideEmailChecked,
onCheckedChange = addSendHandlers.onHideEmailToggle,
readOnly = sendRestrictionPolicy,
enabled = state.common.isHideEmailChecked || state.common.isHideEmailAddressEnabled,
cardStyle = CardStyle.Full,
)
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(height = 8.dp))
BitwardenSwitch(
modifier = Modifier
.testTag("SendDeactivateSwitch")
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
label = stringResource(id = R.string.disable_send),
isChecked = state.common.isDeactivateChecked,
onCheckedChange = addSendHandlers.onDeactivateSendToggle,
readOnly = sendRestrictionPolicy,
cardStyle = CardStyle.Full,
)
}
}
@@ -2,7 +2,9 @@ package com.x8bit.bitwarden.ui.tools.feature.send.addsend
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
@@ -18,7 +20,11 @@ import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.Text
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.base.util.cardBackground
import com.x8bit.bitwarden.ui.platform.base.util.cardPadding
import com.x8bit.bitwarden.ui.platform.components.divider.BitwardenHorizontalDivider
import com.x8bit.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectButton
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
import kotlinx.collections.immutable.toImmutableList
import java.time.ZonedDateTime
@@ -43,7 +49,10 @@ fun SendDeletionDateChooser(
val options = DeletionOptions.entries.associateWith { it.text() }
var selectedOption: DeletionOptions by rememberSaveable { mutableStateOf(defaultOption) }
Column(
modifier = modifier,
modifier = modifier
.defaultMinSize(minHeight = 60.dp)
.cardBackground(cardStyle = CardStyle.Full)
.cardPadding(cardStyle = CardStyle.Full, vertical = 0.dp),
) {
BitwardenMultiSelectButton(
label = stringResource(id = R.string.deletion_date),
@@ -59,11 +68,11 @@ fun SendDeletionDateChooser(
)
}
},
insets = PaddingValues(top = 6.dp, bottom = 4.dp),
)
AnimatedVisibility(visible = selectedOption == DeletionOptions.CUSTOM) {
Column {
Spacer(modifier = Modifier.height(8.dp))
BitwardenHorizontalDivider(modifier = Modifier.padding(start = 16.dp))
AddSendCustomDateChooser(
dateLabel = stringResource(id = R.string.deletion_date),
timeLabel = stringResource(id = R.string.deletion_time),
@@ -75,8 +84,8 @@ fun SendDeletionDateChooser(
)
}
}
Spacer(modifier = Modifier.height(4.dp))
BitwardenHorizontalDivider(modifier = Modifier.padding(start = 16.dp))
Spacer(modifier = Modifier.height(height = 12.dp))
Text(
text = stringResource(id = R.string.deletion_date_info),
style = BitwardenTheme.typography.bodySmall,
@@ -85,6 +94,7 @@ fun SendDeletionDateChooser(
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 12.dp))
}
}
@@ -2,7 +2,9 @@ package com.x8bit.bitwarden.ui.tools.feature.send.addsend
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
@@ -18,7 +20,11 @@ import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.Text
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.base.util.cardBackground
import com.x8bit.bitwarden.ui.platform.base.util.cardPadding
import com.x8bit.bitwarden.ui.platform.components.divider.BitwardenHorizontalDivider
import com.x8bit.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectButton
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
import kotlinx.collections.immutable.toImmutableList
import java.time.ZonedDateTime
@@ -43,7 +49,10 @@ fun SendExpirationDateChooser(
val options = ExpirationOptions.entries.associateWith { it.text() }
var selectedOption: ExpirationOptions by rememberSaveable { mutableStateOf(defaultOption) }
Column(
modifier = modifier,
modifier = modifier
.defaultMinSize(minHeight = 60.dp)
.cardBackground(cardStyle = CardStyle.Full)
.cardPadding(cardStyle = CardStyle.Full, vertical = 0.dp),
) {
BitwardenMultiSelectButton(
label = stringResource(id = R.string.expiration_date),
@@ -61,11 +70,11 @@ fun SendExpirationDateChooser(
)
}
},
insets = PaddingValues(top = 6.dp, bottom = 4.dp),
)
AnimatedVisibility(visible = selectedOption == ExpirationOptions.CUSTOM) {
Column {
Spacer(modifier = Modifier.height(8.dp))
BitwardenHorizontalDivider(modifier = Modifier.padding(start = 16.dp))
AddSendCustomDateChooser(
dateLabel = stringResource(id = R.string.expiration_date),
timeLabel = stringResource(id = R.string.expiration_time),
@@ -77,8 +86,8 @@ fun SendExpirationDateChooser(
)
}
}
Spacer(modifier = Modifier.height(4.dp))
BitwardenHorizontalDivider(modifier = Modifier.padding(start = 16.dp))
Spacer(modifier = Modifier.height(height = 12.dp))
Text(
text = stringResource(id = R.string.expiration_date_info),
style = BitwardenTheme.typography.bodySmall,
@@ -87,6 +96,7 @@ fun SendExpirationDateChooser(
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 12.dp))
}
}
@@ -6,7 +6,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.Text
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -14,6 +14,8 @@ import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.x8bit.bitwarden.ui.platform.base.util.toListItemCardStyle
import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText
import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
@@ -30,39 +32,40 @@ fun LazyListScope.collectionItemsSelector(
if (isCollectionsTitleVisible) {
item {
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.collections),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
}
if (collectionList?.isNotEmpty() == true) {
items(collectionList) {
Spacer(modifier = Modifier.height(8.dp))
itemsIndexed(collectionList) { index, it ->
BitwardenSwitch(
label = it.name,
isChecked = it.isSelected,
onCheckedChange = { _ ->
onCollectionSelect(it)
},
cardStyle = collectionList.toListItemCardStyle(index = index),
modifier = Modifier
.fillMaxWidth()
.testTag("CollectionItemCell")
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
}
} else {
item {
Spacer(modifier = Modifier.height(8.dp))
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
) {
Text(
text = stringResource(id = R.string.no_collections_to_list),
@@ -17,11 +17,13 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenStandardIconButton
import com.x8bit.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectButton
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordField
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
import com.x8bit.bitwarden.ui.vault.components.collectionItemsSelector
@@ -51,10 +53,11 @@ fun LazyListScope.vaultAddEditCardItems(
label = stringResource(id = R.string.name),
value = commonState.name,
onValueChange = commonHandlers.onNameTextChange,
textFieldTestTag = "ItemNameEntry",
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
textFieldTestTag = "ItemNameEntry",
.standardHorizontalMargin(),
)
}
item {
@@ -63,14 +66,14 @@ fun LazyListScope.vaultAddEditCardItems(
label = stringResource(id = R.string.cardholder_name),
value = cardState.cardHolderName,
onValueChange = cardHandlers.onCardHolderNameTextChange,
textFieldTestTag = "CardholderNameEntry",
cardStyle = CardStyle.Top(),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
textFieldTestTag = "CardholderNameEntry",
.standardHorizontalMargin(),
)
}
item {
Spacer(modifier = Modifier.height(8.dp))
var showNumber by rememberSaveable { mutableStateOf(value = false) }
BitwardenPasswordField(
label = stringResource(id = R.string.number),
@@ -81,16 +84,16 @@ fun LazyListScope.vaultAddEditCardItems(
showNumber = !showNumber
cardHandlers.onNumberVisibilityChange(showNumber)
},
showPasswordTestTag = "ShowCardNumberButton",
cardStyle = CardStyle.Middle(),
modifier = Modifier
.testTag("CardNumberEntry")
.fillMaxWidth()
.padding(horizontal = 16.dp),
showPasswordTestTag = "ShowCardNumberButton",
.standardHorizontalMargin(),
)
}
item {
val resources = LocalContext.current.resources
Spacer(modifier = Modifier.height(8.dp))
BitwardenMultiSelectButton(
label = stringResource(id = R.string.brand),
options = VaultCardBrand
@@ -105,14 +108,15 @@ fun LazyListScope.vaultAddEditCardItems(
.first { it.longName.toString(resources) == selectedString },
)
},
cardStyle = CardStyle.Middle(),
modifier = Modifier
.testTag("CardBrandPicker")
.padding(horizontal = 16.dp),
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
item {
val resources = LocalContext.current.resources
Spacer(modifier = Modifier.height(8.dp))
BitwardenMultiSelectButton(
label = stringResource(id = R.string.expiration_month),
options = VaultCardExpirationMonth
@@ -127,26 +131,27 @@ fun LazyListScope.vaultAddEditCardItems(
.first { it.value.toString(resources) == selectedString },
)
},
cardStyle = CardStyle.Middle(),
modifier = Modifier
.testTag("CardExpirationMonthPicker")
.padding(horizontal = 16.dp),
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
label = stringResource(id = R.string.expiration_year),
value = cardState.expirationYear,
onValueChange = cardHandlers.onExpirationYearTextChange,
keyboardType = KeyboardType.Number,
textFieldTestTag = "CardExpirationYearEntry",
cardStyle = CardStyle.Middle(),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
textFieldTestTag = "CardExpirationYearEntry",
.standardHorizontalMargin(),
)
}
item {
Spacer(modifier = Modifier.height(8.dp))
var showSecurityCode by rememberSaveable { mutableStateOf(value = false) }
BitwardenPasswordField(
label = stringResource(id = R.string.security_code),
@@ -158,24 +163,26 @@ fun LazyListScope.vaultAddEditCardItems(
cardHandlers.onSecurityCodeVisibilityChange(showSecurityCode)
},
keyboardType = KeyboardType.NumberPassword,
showPasswordTestTag = "CardShowSecurityCodeButton",
cardStyle = CardStyle.Bottom,
modifier = Modifier
.testTag("CardSecurityCodeEntry")
.fillMaxWidth()
.padding(horizontal = 16.dp),
showPasswordTestTag = "CardShowSecurityCodeButton",
.standardHorizontalMargin(),
)
}
item {
Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.miscellaneous),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenMultiSelectButton(
label = stringResource(id = R.string.folder),
options = commonState
@@ -190,37 +197,39 @@ fun LazyListScope.vaultAddEditCardItems(
.first { it.name == selectedFolderName },
)
},
cardStyle = CardStyle.Full,
modifier = Modifier
.testTag("FolderPicker")
.padding(horizontal = 16.dp),
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
item {
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(height = 8.dp))
BitwardenSwitch(
label = stringResource(
id = R.string.favorite,
),
isChecked = commonState.favorite,
onCheckedChange = commonHandlers.onToggleFavorite,
cardStyle = if (commonState.isUnlockWithPasswordEnabled) {
CardStyle.Top()
} else {
CardStyle.Full
},
modifier = Modifier
.testTag("ItemFavoriteToggle")
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
}
if (commonState.isUnlockWithPasswordEnabled) {
item {
Spacer(modifier = Modifier.height(16.dp))
BitwardenSwitch(
label = stringResource(id = R.string.password_prompt),
isChecked = commonState.masterPasswordReprompt,
onCheckedChange = commonHandlers.onToggleMasterPasswordReprompt,
modifier = Modifier
.testTag("MasterPasswordRepromptToggle")
.fillMaxWidth()
.padding(horizontal = 16.dp),
actions = {
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_question_circle,
@@ -231,53 +240,58 @@ fun LazyListScope.vaultAddEditCardItems(
contentColor = BitwardenTheme.colorScheme.icon.secondary,
)
},
cardStyle = CardStyle.Bottom,
modifier = Modifier
.testTag("MasterPasswordRepromptToggle")
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
}
item {
Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.notes),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
singleLine = false,
label = stringResource(id = R.string.notes),
value = commonState.notes,
onValueChange = commonHandlers.onNotesTextChange,
textFieldTestTag = "ItemNotesEntry",
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
textFieldTestTag = "ItemNotesEntry",
.standardHorizontalMargin(),
)
}
item {
Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.custom_fields),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
}
items(commonState.customFieldData) { customItem ->
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.height(height = 8.dp))
VaultAddEditCustomField(
customField = customItem,
onCustomFieldValueChange = commonHandlers.onCustomFieldValueChange,
onCustomFieldAction = commonHandlers.onCustomFieldActionSelect,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
supportedLinkedTypes = persistentListOf(
VaultLinkedFieldType.CARDHOLDER_NAME,
VaultLinkedFieldType.EXPIRATION_MONTH,
@@ -287,6 +301,10 @@ fun LazyListScope.vaultAddEditCardItems(
VaultLinkedFieldType.NUMBER,
),
onHiddenVisibilityChanged = commonHandlers.onHiddenFieldVisibilityChange,
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
@@ -296,23 +314,24 @@ fun LazyListScope.vaultAddEditCardItems(
onFinishNamingClick = commonHandlers.onAddNewCustomFieldClick,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
}
if (isAddItemMode && commonState.hasOrganizations) {
item {
Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.ownership),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenMultiSelectButton(
label = stringResource(id = R.string.who_owns_this_item),
options = commonState
@@ -327,10 +346,11 @@ fun LazyListScope.vaultAddEditCardItems(
.first { it.name == selectedOwnerName },
)
},
cardStyle = CardStyle.Full,
modifier = Modifier
.testTag("ItemOwnershipPicker")
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
}
if (commonState.selectedOwnerId != null) {
@@ -340,7 +360,4 @@ fun LazyListScope.vaultAddEditCardItems(
)
}
}
item {
Spacer(modifier = Modifier.height(24.dp))
}
}
@@ -1,7 +1,12 @@
package com.x8bit.bitwarden.ui.vault.feature.addedit
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -11,16 +16,21 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTonalIconButton
import com.x8bit.bitwarden.ui.platform.base.util.cardBackground
import com.x8bit.bitwarden.ui.platform.base.util.cardPadding
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenStandardIconButton
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenSelectionDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTextEntryDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.row.BitwardenBasicDialogRow
import com.x8bit.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectButton
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordFieldWithActions
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextFieldWithActions
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.row.BitwardenRowOfActions
import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.CustomFieldAction
import com.x8bit.bitwarden.ui.vault.model.VaultLinkedFieldType
import kotlinx.collections.immutable.ImmutableList
@@ -33,9 +43,10 @@ import kotlinx.collections.immutable.toImmutableList
* @param customField The field that is to be displayed.
* @param onCustomFieldValueChange Invoked when the user changes the value.
* @param onCustomFieldAction Invoked when the user chooses an action.
* @param onHiddenVisibilityChanged Emits when the visibility of a hidden custom field changes.
* @param cardStyle Indicates the type of card style to be applied.
* @param modifier Modifier for the UI elements.
* @param supportedLinkedTypes The supported linked types for the vault item.
* @param onHiddenVisibilityChanged Emits when the visibility of a hidden custom field changes.
*/
@Composable
@Suppress("LongMethod")
@@ -43,9 +54,10 @@ fun VaultAddEditCustomField(
customField: VaultAddEditState.Custom,
onCustomFieldValueChange: (VaultAddEditState.Custom) -> Unit,
onCustomFieldAction: (CustomFieldAction, VaultAddEditState.Custom) -> Unit,
onHiddenVisibilityChanged: (Boolean) -> Unit,
cardStyle: CardStyle,
modifier: Modifier = Modifier,
supportedLinkedTypes: ImmutableList<VaultLinkedFieldType> = persistentListOf(),
onHiddenVisibilityChanged: (Boolean) -> Unit,
) {
var shouldShowChooserDialog by remember { mutableStateOf(false) }
var shouldShowEditDialog by remember { mutableStateOf(false) }
@@ -85,6 +97,7 @@ fun VaultAddEditCustomField(
value = customField.value,
onValueChanged = { onCustomFieldValueChange(customField.copy(value = it)) },
onEditValue = { shouldShowChooserDialog = true },
cardStyle = cardStyle,
modifier = modifier,
)
}
@@ -98,6 +111,7 @@ fun VaultAddEditCustomField(
},
onVisibilityChanged = onHiddenVisibilityChanged,
onEditValue = { shouldShowChooserDialog = true },
cardStyle = cardStyle,
modifier = modifier,
)
}
@@ -112,6 +126,7 @@ fun VaultAddEditCustomField(
onCustomFieldValueChange(customField.copy(vaultLinkedFieldType = it))
},
onEditValue = { shouldShowChooserDialog = true },
cardStyle = cardStyle,
modifier = modifier,
)
}
@@ -123,6 +138,7 @@ fun VaultAddEditCustomField(
value = customField.value,
onValueChanged = { onCustomFieldValueChange(customField.copy(value = it)) },
onEditValue = { shouldShowChooserDialog = true },
cardStyle = cardStyle,
modifier = modifier,
)
}
@@ -138,24 +154,33 @@ private fun CustomFieldBoolean(
value: Boolean,
onValueChanged: (Boolean) -> Unit,
onEditValue: () -> Unit,
cardStyle: CardStyle,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier
.semantics(mergeDescendants = true) {}
.defaultMinSize(minHeight = 60.dp)
.cardBackground(cardStyle = cardStyle)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = ripple(color = BitwardenTheme.colorScheme.background.pressed),
onClick = { onValueChanged(!value) },
)
.cardPadding(cardStyle = cardStyle, end = 4.dp, top = 6.dp, bottom = 6.dp)
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
BitwardenSwitch(
label = label,
isChecked = value,
onCheckedChange = onValueChanged,
onCheckedChange = null,
modifier = Modifier.weight(1f),
)
BitwardenRowOfActions(
actions = {
BitwardenTonalIconButton(
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_cog,
contentDescription = stringResource(id = R.string.edit),
onClick = onEditValue,
@@ -175,6 +200,7 @@ private fun CustomFieldHiddenField(
onValueChanged: (String) -> Unit,
onEditValue: () -> Unit,
onVisibilityChanged: (Boolean) -> Unit,
cardStyle: CardStyle,
modifier: Modifier = Modifier,
) {
var shouldShowPassword by remember { mutableStateOf(value = false) }
@@ -188,14 +214,15 @@ private fun CustomFieldHiddenField(
onVisibilityChanged(shouldShowPassword)
},
singleLine = true,
modifier = modifier,
actions = {
BitwardenTonalIconButton(
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_cog,
contentDescription = stringResource(id = R.string.edit),
onClick = onEditValue,
)
},
cardStyle = cardStyle,
modifier = modifier,
)
}
@@ -208,6 +235,7 @@ private fun CustomFieldTextField(
value: String,
onValueChanged: (String) -> Unit,
onEditValue: () -> Unit,
cardStyle: CardStyle,
modifier: Modifier = Modifier,
) {
BitwardenTextFieldWithActions(
@@ -215,14 +243,15 @@ private fun CustomFieldTextField(
value = value,
onValueChange = onValueChanged,
singleLine = true,
modifier = modifier,
actions = {
BitwardenTonalIconButton(
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_cog,
contentDescription = stringResource(id = R.string.edit),
onClick = onEditValue,
)
},
cardStyle = cardStyle,
modifier = modifier,
)
}
@@ -235,41 +264,33 @@ private fun CustomFieldLinkedField(
selectedOption: VaultLinkedFieldType,
onValueChanged: (VaultLinkedFieldType) -> Unit,
onEditValue: () -> Unit,
cardStyle: CardStyle,
modifier: Modifier = Modifier,
supportedLinkedTypes: ImmutableList<VaultLinkedFieldType> = persistentListOf(),
) {
val possibleTypesWithStrings = supportedLinkedTypes.associateWith { it.label.invoke() }
Row(
modifier = modifier
.semantics(mergeDescendants = true) {}
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
BitwardenMultiSelectButton(
label = label,
options = supportedLinkedTypes.map { it.label.invoke() }.toImmutableList(),
selectedOption = selectedOption.label.invoke(),
onOptionSelected = { selectedType ->
possibleTypesWithStrings.forEach {
if (it.value == selectedType) {
onValueChanged(it.key)
}
BitwardenMultiSelectButton(
modifier = modifier,
label = label,
options = supportedLinkedTypes.map { it.label.invoke() }.toImmutableList(),
selectedOption = selectedOption.label.invoke(),
onOptionSelected = { selectedType ->
possibleTypesWithStrings.forEach {
if (it.value == selectedType) {
onValueChanged(it.key)
}
},
modifier = Modifier.weight(1f),
)
BitwardenRowOfActions(
actions = {
BitwardenTonalIconButton(
vectorIconRes = R.drawable.ic_cog,
contentDescription = stringResource(id = R.string.edit),
onClick = onEditValue,
)
},
)
}
}
},
actions = {
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_cog,
contentDescription = stringResource(id = R.string.edit),
onClick = onEditValue,
)
},
actionsPadding = PaddingValues(end = 4.dp),
cardStyle = cardStyle,
)
}
/**
@@ -13,10 +13,12 @@ import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenStandardIconButton
import com.x8bit.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectButton
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
import com.x8bit.bitwarden.ui.vault.components.collectionItemsSelector
@@ -44,10 +46,11 @@ fun LazyListScope.vaultAddEditIdentityItems(
label = stringResource(id = R.string.name),
value = commonState.name,
onValueChange = commonTypeHandlers.onNameTextChange,
textFieldTestTag = "ItemNameEntry",
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
textFieldTestTag = "ItemNameEntry",
.standardHorizontalMargin(),
)
}
item {
@@ -58,224 +61,225 @@ fun LazyListScope.vaultAddEditIdentityItems(
modifier = Modifier
.testTag("IdentityTitlePicker")
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
}
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
label = stringResource(id = R.string.first_name),
value = identityState.firstName,
onValueChange = identityItemTypeHandlers.onFirstNameTextChange,
textFieldTestTag = "IdentityFirstNameEntry",
cardStyle = CardStyle.Middle(),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
textFieldTestTag = "IdentityFirstNameEntry",
.standardHorizontalMargin(),
)
}
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
label = stringResource(id = R.string.middle_name),
value = identityState.middleName,
onValueChange = identityItemTypeHandlers.onMiddleNameTextChange,
textFieldTestTag = "IdentityMiddleNameEntry",
cardStyle = CardStyle.Middle(),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
textFieldTestTag = "IdentityMiddleNameEntry",
.standardHorizontalMargin(),
)
}
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
label = stringResource(id = R.string.last_name),
value = identityState.lastName,
onValueChange = identityItemTypeHandlers.onLastNameTextChange,
textFieldTestTag = "IdentityLastNameEntry",
cardStyle = CardStyle.Middle(),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
textFieldTestTag = "IdentityLastNameEntry",
.standardHorizontalMargin(),
)
}
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
label = stringResource(id = R.string.username),
value = identityState.username,
onValueChange = identityItemTypeHandlers.onUsernameTextChange,
textFieldTestTag = "IdentityUsernameEntry",
cardStyle = CardStyle.Middle(),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
textFieldTestTag = "IdentityUsernameEntry",
.standardHorizontalMargin(),
)
}
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
label = stringResource(id = R.string.company),
value = identityState.company,
onValueChange = identityItemTypeHandlers.onCompanyTextChange,
textFieldTestTag = "IdentityCompanyEntry",
cardStyle = CardStyle.Middle(),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
textFieldTestTag = "IdentityCompanyEntry",
.standardHorizontalMargin(),
)
}
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
label = stringResource(id = R.string.ssn),
value = identityState.ssn,
onValueChange = identityItemTypeHandlers.onSsnTextChange,
textFieldTestTag = "IdentitySsnEntry",
cardStyle = CardStyle.Middle(),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
textFieldTestTag = "IdentitySsnEntry",
.standardHorizontalMargin(),
)
}
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
label = stringResource(id = R.string.passport_number),
value = identityState.passportNumber,
onValueChange = identityItemTypeHandlers.onPassportNumberTextChange,
textFieldTestTag = "IdentityPassportNumberEntry",
cardStyle = CardStyle.Middle(),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
textFieldTestTag = "IdentityPassportNumberEntry",
.standardHorizontalMargin(),
)
}
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
label = stringResource(id = R.string.license_number),
value = identityState.licenseNumber,
onValueChange = identityItemTypeHandlers.onLicenseNumberTextChange,
textFieldTestTag = "IdentityLicenseNumberEntry",
cardStyle = CardStyle.Middle(),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
textFieldTestTag = "IdentityLicenseNumberEntry",
.standardHorizontalMargin(),
)
}
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
label = stringResource(id = R.string.email),
value = identityState.email,
onValueChange = identityItemTypeHandlers.onEmailTextChange,
textFieldTestTag = "IdentityEmailEntry",
cardStyle = CardStyle.Middle(),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
textFieldTestTag = "IdentityEmailEntry",
.standardHorizontalMargin(),
)
}
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
label = stringResource(id = R.string.phone),
value = identityState.phone,
onValueChange = identityItemTypeHandlers.onPhoneTextChange,
textFieldTestTag = "IdentityPhoneEntry",
cardStyle = CardStyle.Middle(),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
textFieldTestTag = "IdentityPhoneEntry",
.standardHorizontalMargin(),
)
}
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
label = stringResource(id = R.string.address1),
value = identityState.address1,
onValueChange = identityItemTypeHandlers.onAddress1TextChange,
textFieldTestTag = "IdentityAddressOneEntry",
cardStyle = CardStyle.Middle(),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
textFieldTestTag = "IdentityAddressOneEntry",
.standardHorizontalMargin(),
)
}
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
label = stringResource(id = R.string.address2),
value = identityState.address2,
onValueChange = identityItemTypeHandlers.onAddress2TextChange,
textFieldTestTag = "IdentityAddressTwoEntry",
cardStyle = CardStyle.Middle(),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
textFieldTestTag = "IdentityAddressTwoEntry",
.standardHorizontalMargin(),
)
}
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
label = stringResource(id = R.string.address3),
value = identityState.address3,
onValueChange = identityItemTypeHandlers.onAddress3TextChange,
textFieldTestTag = "IdentityAddressThreeEntry",
cardStyle = CardStyle.Middle(),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
textFieldTestTag = "IdentityAddressThreeEntry",
.standardHorizontalMargin(),
)
}
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
label = stringResource(id = R.string.city_town),
value = identityState.city,
onValueChange = identityItemTypeHandlers.onCityTextChange,
textFieldTestTag = "IdentityCityEntry",
cardStyle = CardStyle.Middle(),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
textFieldTestTag = "IdentityCityEntry",
.standardHorizontalMargin(),
)
}
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
label = stringResource(id = R.string.state_province),
value = identityState.state,
onValueChange = identityItemTypeHandlers.onStateTextChange,
textFieldTestTag = "IdentityStateEntry",
cardStyle = CardStyle.Middle(),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
textFieldTestTag = "IdentityStateEntry",
.standardHorizontalMargin(),
)
}
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
label = stringResource(id = R.string.zip_postal_code),
value = identityState.zip,
onValueChange = identityItemTypeHandlers.onZipTextChange,
textFieldTestTag = "IdentityPostalCodeEntry",
cardStyle = CardStyle.Middle(),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
textFieldTestTag = "IdentityPostalCodeEntry",
.standardHorizontalMargin(),
)
}
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
label = stringResource(id = R.string.country),
value = identityState.country,
onValueChange = identityItemTypeHandlers.onCountryTextChange,
textFieldTestTag = "IdentityCountryEntry",
cardStyle = CardStyle.Bottom,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
textFieldTestTag = "IdentityCountryEntry",
.standardHorizontalMargin(),
)
}
item {
Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.miscellaneous),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenMultiSelectButton(
label = stringResource(id = R.string.folder),
options = commonState
@@ -290,38 +294,40 @@ fun LazyListScope.vaultAddEditIdentityItems(
.first { it.name == selectedFolderName },
)
},
cardStyle = CardStyle.Full,
modifier = Modifier
.testTag("FolderPicker")
.padding(horizontal = 16.dp),
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
item {
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(height = 8.dp))
BitwardenSwitch(
label = stringResource(
id = R.string.favorite,
),
isChecked = commonState.favorite,
onCheckedChange = commonTypeHandlers.onToggleFavorite,
cardStyle = if (commonState.isUnlockWithPasswordEnabled) {
CardStyle.Top()
} else {
CardStyle.Full
},
modifier = Modifier
.testTag("ItemFavoriteToggle")
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
}
if (commonState.isUnlockWithPasswordEnabled) {
item {
Spacer(modifier = Modifier.height(16.dp))
BitwardenSwitch(
label = stringResource(id = R.string.password_prompt),
isChecked = commonState.masterPasswordReprompt,
onCheckedChange = commonTypeHandlers.onToggleMasterPasswordReprompt,
modifier = Modifier
.testTag("MasterPasswordRepromptToggle")
.fillMaxWidth()
.padding(horizontal = 16.dp),
actions = {
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_question_circle,
@@ -332,53 +338,58 @@ fun LazyListScope.vaultAddEditIdentityItems(
contentColor = BitwardenTheme.colorScheme.icon.secondary,
)
},
cardStyle = CardStyle.Bottom,
modifier = Modifier
.testTag("MasterPasswordRepromptToggle")
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
}
item {
Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.notes),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
singleLine = false,
label = stringResource(id = R.string.notes),
value = commonState.notes,
onValueChange = commonTypeHandlers.onNotesTextChange,
textFieldTestTag = "ItemNotesEntry",
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
textFieldTestTag = "ItemNotesEntry",
.standardHorizontalMargin(),
)
}
item {
Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.custom_fields),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
}
items(commonState.customFieldData) { customItem ->
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.height(height = 8.dp))
VaultAddEditCustomField(
customField = customItem,
onCustomFieldValueChange = commonTypeHandlers.onCustomFieldValueChange,
onCustomFieldAction = commonTypeHandlers.onCustomFieldActionSelect,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
supportedLinkedTypes = persistentListOf(
VaultLinkedFieldType.TITLE,
VaultLinkedFieldType.MIDDLE_NAME,
@@ -401,6 +412,10 @@ fun LazyListScope.vaultAddEditIdentityItems(
VaultLinkedFieldType.FULL_NAME,
),
onHiddenVisibilityChanged = commonTypeHandlers.onHiddenFieldVisibilityChange,
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
@@ -410,23 +425,24 @@ fun LazyListScope.vaultAddEditIdentityItems(
onFinishNamingClick = commonTypeHandlers.onAddNewCustomFieldClick,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
}
if (isAddItemMode && commonState.hasOrganizations) {
item {
Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.ownership),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenMultiSelectButton(
label = stringResource(id = R.string.who_owns_this_item),
options = commonState
@@ -441,10 +457,11 @@ fun LazyListScope.vaultAddEditIdentityItems(
.first { it.name == selectedOwnerName },
)
},
cardStyle = CardStyle.Full,
modifier = Modifier
.testTag("ItemOwnershipPicker")
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
}
if (commonState.selectedOwnerId != null) {
@@ -454,9 +471,6 @@ fun LazyListScope.vaultAddEditIdentityItems(
)
}
}
item {
Spacer(modifier = Modifier.height(24.dp))
}
}
@Composable
@@ -480,6 +494,7 @@ private fun TitleMultiSelectButton(
.first { it.value.toString(resources) == selectedString },
)
},
cardStyle = CardStyle.Top(),
modifier = modifier,
)
}
@@ -13,9 +13,11 @@ import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.x8bit.bitwarden.ui.platform.components.card.BitwardenInfoCalloutCard
import com.x8bit.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectButton
import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.manager.permissions.PermissionsManager
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditCardTypeHandlers
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditCommonHandlers
@@ -57,12 +59,13 @@ fun VaultAddEditContent(
)
LazyColumn(modifier = modifier) {
item {
if (state.isIndividualVaultDisabled && isAddItemMode) {
if (state.isIndividualVaultDisabled && isAddItemMode) {
item {
Spacer(modifier = Modifier.height(height = 12.dp))
BitwardenInfoCalloutCard(
text = stringResource(R.string.personal_ownership_policy_in_effect),
modifier = Modifier
.padding(horizontal = 16.dp)
.standardHorizontalMargin()
.testTag("PersonalOwnershipPolicyLabel")
.fillMaxWidth(),
)
@@ -70,23 +73,25 @@ fun VaultAddEditContent(
}
item {
Spacer(modifier = Modifier.height(height = 12.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.item_information),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
if (isAddItemMode) {
item {
Spacer(modifier = Modifier.height(8.dp))
TypeOptionsItem(
entries = typeOptions,
itemType = state.type,
onTypeOptionClicked = onTypeOptionClicked,
modifier = Modifier
.testTag("ItemTypePicker")
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
}
}
@@ -148,6 +153,7 @@ fun VaultAddEditContent(
}
item {
Spacer(modifier = Modifier.height(height = 16.dp))
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
@@ -173,6 +179,7 @@ private fun TypeOptionsItem(
.key
onTypeOptionClicked(selectedOptionId)
},
cardStyle = CardStyle.Full,
modifier = modifier,
)
}
@@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -18,9 +19,10 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.Text
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.x8bit.bitwarden.ui.platform.base.util.toListItemCardStyle
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenOutlinedButton
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenStandardIconButton
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTonalIconButton
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
import com.x8bit.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectButton
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenHiddenPasswordField
@@ -28,6 +30,7 @@ import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordFieldWi
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextFieldWithActions
import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
@@ -51,15 +54,15 @@ fun LazyListScope.vaultAddEditLoginItems(
onTotpSetupClick: () -> Unit,
) {
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
label = stringResource(id = R.string.name),
value = commonState.name,
onValueChange = commonActionHandler.onNameTextChange,
textFieldTestTag = "ItemNameEntry",
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
textFieldTestTag = "ItemNameEntry",
.standardHorizontalMargin(),
)
}
@@ -68,15 +71,20 @@ fun LazyListScope.vaultAddEditLoginItems(
UsernameRow(
username = loginState.username,
loginItemTypeHandlers = loginItemTypeHandlers,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
item {
Spacer(modifier = Modifier.height(8.dp))
PasswordRow(
password = loginState.password,
canViewPassword = loginState.canViewPassword,
loginItemTypeHandlers = loginItemTypeHandlers,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
@@ -85,23 +93,25 @@ fun LazyListScope.vaultAddEditLoginItems(
Spacer(modifier = Modifier.height(8.dp))
PasskeyField(
creationDateTime = creationDateTime,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
canRemovePasskey = loginState.canRemovePasskey,
loginItemTypeHandlers = loginItemTypeHandlers,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
}
item {
Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.authenticator_key),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
if (loginState.totp != null) {
@@ -109,7 +119,7 @@ fun LazyListScope.vaultAddEditLoginItems(
BitwardenTextFieldWithActions(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
label = stringResource(id = R.string.totp),
value = loginState.totp,
trailingIconContent = {
@@ -123,20 +133,21 @@ fun LazyListScope.vaultAddEditLoginItems(
readOnly = true,
singleLine = true,
actions = {
BitwardenTonalIconButton(
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_copy,
contentDescription = stringResource(id = R.string.copy_totp),
onClick = {
loginItemTypeHandlers.onCopyTotpKeyClick(loginState.totp)
},
)
BitwardenTonalIconButton(
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_camera,
contentDescription = stringResource(id = R.string.camera),
onClick = onTotpSetupClick,
)
},
textFieldTestTag = "LoginTotpEntry",
cardStyle = CardStyle.Full,
)
}
} else {
@@ -149,27 +160,32 @@ fun LazyListScope.vaultAddEditLoginItems(
modifier = Modifier
.testTag("SetupTotpButton")
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
}
}
item {
Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.ur_is),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
items(loginState.uriList) { uriItem ->
Spacer(modifier = Modifier.height(8.dp))
itemsIndexed(loginState.uriList) { index, uriItem ->
VaultAddEditUriItem(
uriItem = uriItem,
onUriValueChange = loginItemTypeHandlers.onUriValueChange,
onUriItemRemoved = loginItemTypeHandlers.onRemoveUriClick,
cardStyle = loginState.uriList.toListItemCardStyle(index = index),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
@@ -181,22 +197,23 @@ fun LazyListScope.vaultAddEditLoginItems(
modifier = Modifier
.testTag("LoginAddNewUriButton")
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
}
item {
Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.miscellaneous),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenMultiSelectButton(
label = stringResource(id = R.string.folder),
options = commonState
@@ -211,37 +228,39 @@ fun LazyListScope.vaultAddEditLoginItems(
.first { it.name == selectedFolderName },
)
},
cardStyle = CardStyle.Full,
modifier = Modifier
.testTag("FolderPicker")
.padding(horizontal = 16.dp),
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
item {
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(height = 8.dp))
BitwardenSwitch(
label = stringResource(
id = R.string.favorite,
),
isChecked = commonState.favorite,
onCheckedChange = commonActionHandler.onToggleFavorite,
cardStyle = if (commonState.isUnlockWithPasswordEnabled) {
CardStyle.Top()
} else {
CardStyle.Full
},
modifier = Modifier
.testTag("ItemFavoriteToggle")
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
}
if (commonState.isUnlockWithPasswordEnabled) {
item {
Spacer(modifier = Modifier.height(16.dp))
BitwardenSwitch(
label = stringResource(id = R.string.password_prompt),
isChecked = commonState.masterPasswordReprompt,
onCheckedChange = commonActionHandler.onToggleMasterPasswordReprompt,
modifier = Modifier
.testTag("MasterPasswordRepromptToggle")
.fillMaxWidth()
.padding(horizontal = 16.dp),
actions = {
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_question_circle_small,
@@ -252,58 +271,67 @@ fun LazyListScope.vaultAddEditLoginItems(
contentColor = BitwardenTheme.colorScheme.icon.secondary,
)
},
cardStyle = CardStyle.Bottom,
modifier = Modifier
.testTag("MasterPasswordRepromptToggle")
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
}
item {
Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.notes),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
singleLine = false,
label = stringResource(id = R.string.notes),
value = commonState.notes,
onValueChange = commonActionHandler.onNotesTextChange,
textFieldTestTag = "ItemNotesEntry",
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
textFieldTestTag = "ItemNotesEntry",
.standardHorizontalMargin(),
)
}
item {
Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.custom_fields),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
}
items(commonState.customFieldData) { customItem ->
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.height(height = 8.dp))
VaultAddEditCustomField(
customField = customItem,
onCustomFieldValueChange = commonActionHandler.onCustomFieldValueChange,
onCustomFieldAction = commonActionHandler.onCustomFieldActionSelect,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
supportedLinkedTypes = persistentListOf(
VaultLinkedFieldType.PASSWORD,
VaultLinkedFieldType.USERNAME,
),
onHiddenVisibilityChanged = commonActionHandler.onHiddenFieldVisibilityChange,
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
@@ -313,23 +341,24 @@ fun LazyListScope.vaultAddEditLoginItems(
onFinishNamingClick = commonActionHandler.onAddNewCustomFieldClick,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
}
if (isAddItemMode && commonState.hasOrganizations) {
item {
Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.ownership),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenMultiSelectButton(
label = stringResource(id = R.string.who_owns_this_item),
options = commonState
@@ -344,10 +373,11 @@ fun LazyListScope.vaultAddEditLoginItems(
.first { it.name == selectedOwnerName },
)
},
cardStyle = CardStyle.Full,
modifier = Modifier
.testTag("ItemOwnershipPicker")
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
}
@@ -358,16 +388,13 @@ fun LazyListScope.vaultAddEditLoginItems(
)
}
}
item {
Spacer(modifier = Modifier.height(24.dp))
}
}
@Composable
private fun UsernameRow(
username: String,
loginItemTypeHandlers: VaultAddEditLoginTypeHandlers,
modifier: Modifier = Modifier,
) {
var shouldShowDialog by rememberSaveable { mutableStateOf(false) }
@@ -376,7 +403,7 @@ private fun UsernameRow(
value = username,
onValueChange = loginItemTypeHandlers.onUsernameTextChange,
actions = {
BitwardenTonalIconButton(
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_generate,
contentDescription = stringResource(id = R.string.generate_username),
onClick = {
@@ -389,9 +416,9 @@ private fun UsernameRow(
modifier = Modifier.testTag(tag = "GenerateUsernameButton"),
)
},
modifier = Modifier
.padding(horizontal = 16.dp),
textFieldTestTag = "LoginUsernameEntry",
cardStyle = CardStyle.Top(dividerPadding = 0.dp),
modifier = modifier,
)
if (shouldShowDialog) {
@@ -423,6 +450,7 @@ private fun PasswordRow(
password: String,
canViewPassword: Boolean,
loginItemTypeHandlers: VaultAddEditLoginTypeHandlers,
modifier: Modifier = Modifier,
) {
var shouldShowDialog by rememberSaveable { mutableStateOf(false) }
@@ -439,17 +467,16 @@ private fun PasswordRow(
},
showPasswordTestTag = "ViewPasswordButton",
passwordFieldTestTag = "LoginPasswordEntry",
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
cardStyle = CardStyle.Bottom,
modifier = modifier,
) {
BitwardenTonalIconButton(
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_check_mark,
contentDescription = stringResource(id = R.string.check_password),
onClick = loginItemTypeHandlers.onPasswordCheckerClick,
modifier = Modifier.testTag(tag = "CheckPasswordButton"),
)
BitwardenTonalIconButton(
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_generate,
contentDescription = stringResource(id = R.string.generate_password),
onClick = {
@@ -488,10 +515,8 @@ private fun PasswordRow(
BitwardenHiddenPasswordField(
label = stringResource(id = R.string.password),
value = password,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.testTag("LoginPasswordEntry"),
cardStyle = CardStyle.Bottom,
modifier = modifier.testTag("LoginPasswordEntry"),
)
}
}
@@ -509,10 +534,9 @@ private fun PasskeyField(
onValueChange = { },
readOnly = true,
singleLine = true,
modifier = modifier,
actions = {
if (canRemovePasskey) {
BitwardenTonalIconButton(
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_minus,
contentDescription = stringResource(id = R.string.remove_passkey),
onClick = loginItemTypeHandlers.onClearFido2CredentialClick,
@@ -520,5 +544,7 @@ private fun PasskeyField(
)
}
},
cardStyle = CardStyle.Full,
modifier = modifier,
)
}
@@ -11,10 +11,12 @@ import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenStandardIconButton
import com.x8bit.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectButton
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
import com.x8bit.bitwarden.ui.vault.components.collectionItemsSelector
@@ -38,25 +40,27 @@ fun LazyListScope.vaultAddEditSecureNotesItems(
label = stringResource(id = R.string.name),
value = commonState.name,
onValueChange = commonTypeHandlers.onNameTextChange,
textFieldTestTag = "ItemNameEntry",
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
textFieldTestTag = "ItemNameEntry",
.standardHorizontalMargin(),
)
}
item {
Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.miscellaneous),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenMultiSelectButton(
label = stringResource(id = R.string.folder),
options = commonState
@@ -71,36 +75,38 @@ fun LazyListScope.vaultAddEditSecureNotesItems(
.first { it.name == selectedFolderName },
)
},
cardStyle = CardStyle.Full,
modifier = Modifier
.testTag("FolderPicker")
.padding(horizontal = 16.dp),
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
item {
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(height = 8.dp))
BitwardenSwitch(
label = stringResource(id = R.string.favorite),
isChecked = commonState.favorite,
onCheckedChange = commonTypeHandlers.onToggleFavorite,
cardStyle = if (commonState.isUnlockWithPasswordEnabled) {
CardStyle.Top()
} else {
CardStyle.Full
},
modifier = Modifier
.testTag("ItemFavoriteToggle")
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
}
if (commonState.isUnlockWithPasswordEnabled) {
item {
Spacer(modifier = Modifier.height(16.dp))
BitwardenSwitch(
label = stringResource(id = R.string.password_prompt),
isChecked = commonState.masterPasswordReprompt,
onCheckedChange = commonTypeHandlers.onToggleMasterPasswordReprompt,
modifier = Modifier
.testTag("MasterPasswordRepromptToggle")
.fillMaxWidth()
.padding(horizontal = 16.dp),
actions = {
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_question_circle,
@@ -111,53 +117,61 @@ fun LazyListScope.vaultAddEditSecureNotesItems(
contentColor = BitwardenTheme.colorScheme.icon.secondary,
)
},
cardStyle = CardStyle.Bottom,
modifier = Modifier
.testTag("MasterPasswordRepromptToggle")
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
}
item {
Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.notes),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
singleLine = false,
label = stringResource(id = R.string.notes),
value = commonState.notes,
onValueChange = commonTypeHandlers.onNotesTextChange,
textFieldTestTag = "ItemNotesEntry",
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
textFieldTestTag = "ItemNotesEntry",
.standardHorizontalMargin(),
)
}
item {
Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.custom_fields),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
}
items(commonState.customFieldData) { customItem ->
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.height(height = 8.dp))
VaultAddEditCustomField(
customField = customItem,
onCustomFieldValueChange = commonTypeHandlers.onCustomFieldValueChange,
onCustomFieldAction = commonTypeHandlers.onCustomFieldActionSelect,
onHiddenVisibilityChanged = commonTypeHandlers.onHiddenFieldVisibilityChange,
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
onHiddenVisibilityChanged = commonTypeHandlers.onHiddenFieldVisibilityChange,
.standardHorizontalMargin(),
)
}
@@ -172,23 +186,24 @@ fun LazyListScope.vaultAddEditSecureNotesItems(
),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
}
if (isAddItemMode && commonState.hasOrganizations) {
item {
Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.ownership),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenMultiSelectButton(
label = stringResource(id = R.string.who_owns_this_item),
options = commonState
@@ -203,10 +218,11 @@ fun LazyListScope.vaultAddEditSecureNotesItems(
.first { it.name == selectedOwnerName },
)
},
cardStyle = CardStyle.Full,
modifier = Modifier
.testTag("ItemOwnershipPicker")
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
}
@@ -217,8 +233,4 @@ fun LazyListScope.vaultAddEditSecureNotesItems(
)
}
}
item {
Spacer(modifier = Modifier.height(24.dp))
}
}
@@ -20,6 +20,7 @@ import com.x8bit.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectB
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordField
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditCommonHandlers
@@ -44,10 +45,11 @@ fun LazyListScope.vaultAddEditSshKeyItems(
label = stringResource(id = R.string.name),
value = commonState.name,
onValueChange = commonTypeHandlers.onNameTextChange,
textFieldTestTag = "ItemNameEntry",
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
textFieldTestTag = "ItemNameEntry",
)
}
@@ -58,15 +60,15 @@ fun LazyListScope.vaultAddEditSshKeyItems(
value = sshKeyState.publicKey,
readOnly = true,
onValueChange = { },
textFieldTestTag = "PublicKeyEntry",
cardStyle = CardStyle.Top(),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
textFieldTestTag = "PublicKeyEntry",
)
}
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenPasswordField(
label = stringResource(id = R.string.private_key),
value = sshKeyState.privateKey,
@@ -75,6 +77,7 @@ fun LazyListScope.vaultAddEditSshKeyItems(
showPassword = sshKeyState.showPrivateKey,
showPasswordChange = { sshKeyTypeHandlers.onPrivateKeyVisibilityChange(it) },
showPasswordTestTag = "ViewPrivateKeyButton",
cardStyle = CardStyle.Middle(),
modifier = Modifier
.testTag("PrivateKeyEntry")
.fillMaxWidth()
@@ -83,31 +86,32 @@ fun LazyListScope.vaultAddEditSshKeyItems(
}
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
label = stringResource(id = R.string.fingerprint),
value = sshKeyState.fingerprint,
readOnly = true,
onValueChange = { /* no-op */ },
textFieldTestTag = "FingerprintEntry",
cardStyle = CardStyle.Bottom,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
textFieldTestTag = "FingerprintEntry",
)
}
item {
Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.miscellaneous),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenMultiSelectButton(
label = stringResource(id = R.string.folder),
options = commonState
@@ -122,37 +126,39 @@ fun LazyListScope.vaultAddEditSshKeyItems(
.first { it.name == selectedFolderName },
)
},
cardStyle = CardStyle.Full,
modifier = Modifier
.testTag("FolderPicker")
.padding(horizontal = 16.dp),
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
item {
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(height = 8.dp))
BitwardenSwitch(
label = stringResource(
id = R.string.favorite,
),
isChecked = commonState.favorite,
onCheckedChange = commonTypeHandlers.onToggleFavorite,
cardStyle = if (commonState.isUnlockWithPasswordEnabled) {
CardStyle.Top()
} else {
CardStyle.Full
},
modifier = Modifier
.testTag("ItemFavoriteToggle")
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
}
if (commonState.isUnlockWithPasswordEnabled) {
item {
Spacer(modifier = Modifier.height(16.dp))
BitwardenSwitch(
label = stringResource(id = R.string.password_prompt),
isChecked = commonState.masterPasswordReprompt,
onCheckedChange = commonTypeHandlers.onToggleMasterPasswordReprompt,
modifier = Modifier
.testTag("MasterPasswordRepromptToggle")
.fillMaxWidth()
.padding(horizontal = 16.dp),
actions = {
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_question_circle_small,
@@ -163,54 +169,63 @@ fun LazyListScope.vaultAddEditSshKeyItems(
contentColor = BitwardenTheme.colorScheme.icon.secondary,
)
},
cardStyle = CardStyle.Bottom,
modifier = Modifier
.testTag("MasterPasswordRepromptToggle")
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
}
item {
Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.notes),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
singleLine = false,
label = stringResource(id = R.string.notes),
value = commonState.notes,
onValueChange = commonTypeHandlers.onNotesTextChange,
textFieldTestTag = "ItemNotesEntry",
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
textFieldTestTag = "ItemNotesEntry",
.standardHorizontalMargin(),
)
}
item {
Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.custom_fields),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
}
items(commonState.customFieldData) { customItem ->
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.height(height = 8.dp))
VaultAddEditCustomField(
customField = customItem,
onCustomFieldValueChange = commonTypeHandlers.onCustomFieldValueChange,
onCustomFieldAction = commonTypeHandlers.onCustomFieldActionSelect,
onHiddenVisibilityChanged = commonTypeHandlers.onHiddenFieldVisibilityChange,
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
onHiddenVisibilityChanged = commonTypeHandlers.onHiddenFieldVisibilityChange,
.standardHorizontalMargin(),
)
}
@@ -225,7 +240,7 @@ fun LazyListScope.vaultAddEditSshKeyItems(
),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
}
}
@@ -1,6 +1,5 @@
package com.x8bit.bitwarden.ui.vault.feature.addedit
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -9,13 +8,13 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTonalIconButton
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenStandardIconButton
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenSelectionDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.row.BitwardenBasicDialogRow
import com.x8bit.bitwarden.ui.platform.components.dialog.row.BitwardenSelectionRow
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextFieldWithActions
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.UriItem
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.UriMatchDisplayType
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.toDisplayMatchType
@@ -30,6 +29,8 @@ fun VaultAddEditUriItem(
uriItem: UriItem,
onUriItemRemoved: (UriItem) -> Unit,
onUriValueChange: (UriItem) -> Unit,
cardStyle: CardStyle,
modifier: Modifier = Modifier,
) {
var shouldShowOptionsDialog by rememberSaveable { mutableStateOf(false) }
var shouldShowMatchDialog by rememberSaveable { mutableStateOf(false) }
@@ -39,16 +40,16 @@ fun VaultAddEditUriItem(
value = uriItem.uri.orEmpty(),
onValueChange = { onUriValueChange(uriItem.copy(uri = it)) },
actions = {
BitwardenTonalIconButton(
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_cog,
contentDescription = stringResource(id = R.string.options),
onClick = { shouldShowOptionsDialog = true },
modifier = Modifier.testTag(tag = "LoginUriOptionsButton"),
)
},
modifier = Modifier
.padding(horizontal = 16.dp),
textFieldTestTag = "LoginUriEntry",
cardStyle = cardStyle,
modifier = modifier,
)
if (shouldShowOptionsDialog) {
@@ -9,7 +9,7 @@ import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -24,11 +24,15 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.bottomDivider
import com.x8bit.bitwarden.ui.platform.base.util.cardBackground
import com.x8bit.bitwarden.ui.platform.base.util.cardPadding
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.x8bit.bitwarden.ui.platform.base.util.toListItemCardStyle
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenOutlinedButton
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenStandardIconButton
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
import com.x8bit.bitwarden.ui.vault.feature.attachments.handlers.AttachmentsHandlers
@@ -54,35 +58,40 @@ fun AttachmentsContent(
modifier = Modifier
.testTag("NoAttachmentsLabel")
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(8.dp))
}
} else {
items(viewState.attachments) {
item {
Spacer(modifier = Modifier.height(height = 12.dp))
}
itemsIndexed(viewState.attachments) { index, it ->
AttachmentListEntry(
attachmentItem = it,
onDeleteClick = attachmentsHandlers.onDeleteClick,
cardStyle = viewState.attachments.toListItemCardStyle(index = index),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.standardHorizontalMargin()
.testTag("AttachmentList"),
)
}
}
item {
Spacer(modifier = Modifier.height(36.dp))
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.add_new_attachment),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
item {
Spacer(modifier = Modifier.height(16.dp))
Text(
text = viewState
.newAttachment
@@ -93,7 +102,7 @@ fun AttachmentsContent(
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.standardHorizontalMargin()
.testTag("SelectedFileNameLabel"),
)
}
@@ -105,7 +114,7 @@ fun AttachmentsContent(
onClick = attachmentsHandlers.onChooseFileClick,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.standardHorizontalMargin()
.testTag("AttachmentSelectFileButton"),
)
Spacer(modifier = Modifier.height(4.dp))
@@ -115,11 +124,13 @@ fun AttachmentsContent(
style = BitwardenTheme.typography.bodySmall,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 32.dp),
.standardHorizontalMargin()
.padding(horizontal = 12.dp),
)
}
item {
Spacer(modifier = Modifier.height(height = 16.dp))
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
@@ -129,6 +140,7 @@ fun AttachmentsContent(
private fun AttachmentListEntry(
attachmentItem: AttachmentsState.AttachmentItem,
onDeleteClick: (attachmentId: String) -> Unit,
cardStyle: CardStyle,
modifier: Modifier = Modifier,
) {
var shouldShowDeleteDialog by rememberSaveable { mutableStateOf(false) }
@@ -148,12 +160,11 @@ private fun AttachmentListEntry(
}
Row(
modifier = Modifier
.bottomDivider(paddingStart = 16.dp)
.defaultMinSize(minHeight = 56.dp)
.testTag("AttachmentRow")
.padding(vertical = 8.dp)
.then(modifier),
modifier = modifier
.defaultMinSize(minHeight = 60.dp)
.cardBackground(cardStyle = cardStyle)
.cardPadding(cardStyle = cardStyle, start = 16.dp)
.testTag("AttachmentRow"),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
@@ -3,7 +3,6 @@ package com.x8bit.bitwarden.ui.vault.feature.item
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Text
@@ -19,10 +18,12 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.bottomDivider
import com.x8bit.bitwarden.ui.platform.base.util.cardBackground
import com.x8bit.bitwarden.ui.platform.base.util.cardPadding
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenStandardIconButton
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
/**
@@ -33,15 +34,16 @@ import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
fun AttachmentItemContent(
attachmentItem: VaultItemState.ViewState.Content.Common.AttachmentItem,
onAttachmentDownloadClick: (VaultItemState.ViewState.Content.Common.AttachmentItem) -> Unit,
cardStyle: CardStyle,
modifier: Modifier = Modifier,
) {
var shouldShowPremiumWarningDialog by rememberSaveable { mutableStateOf(false) }
var shouldShowSizeWarningDialog by rememberSaveable { mutableStateOf(false) }
Row(
modifier = modifier
.bottomDivider()
.defaultMinSize(minHeight = 56.dp)
.padding(vertical = 8.dp)
.defaultMinSize(minHeight = 60.dp)
.cardBackground(cardStyle = cardStyle)
.cardPadding(cardStyle = cardStyle, start = 16.dp)
.testTag("CipherAttachment"),
verticalAlignment = Alignment.CenterVertically,
) {
@@ -7,17 +7,21 @@ import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTonalIconButton
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.x8bit.bitwarden.ui.platform.base.util.toListItemCardStyle
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenStandardIconButton
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordFieldWithActions
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextFieldWithActions
import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCardItemTypeHandlers
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers
import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand
@@ -37,46 +41,53 @@ fun VaultItemCardContent(
) {
LazyColumn(modifier = modifier) {
item {
Spacer(modifier = Modifier.height(height = 12.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.item_information),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
label = stringResource(id = R.string.name),
value = commonState.name,
onValueChange = { },
readOnly = true,
singleLine = false,
textFieldTestTag = "CardItemNameEntry",
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
textFieldTestTag = "CardItemNameEntry",
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
cardState.cardholderName?.let { cardholderName ->
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
label = stringResource(id = R.string.cardholder_name),
value = cardholderName,
onValueChange = {},
readOnly = true,
singleLine = false,
textFieldTestTag = "CardholderNameEntry",
cardStyle = cardState
.propertyList
.toListItemCardStyle(
index = cardState.propertyList.indexOf(element = cardholderName),
),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
textFieldTestTag = "CardholderNameEntry",
.standardHorizontalMargin(),
)
}
}
cardState.number?.let { numberData ->
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenPasswordFieldWithActions(
label = stringResource(id = R.string.number),
value = numberData.number,
@@ -86,7 +97,7 @@ fun VaultItemCardContent(
readOnly = true,
singleLine = false,
actions = {
BitwardenTonalIconButton(
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_copy,
contentDescription = stringResource(id = R.string.copy_number),
onClick = vaultCardItemTypeHandlers.onCopyNumberClick,
@@ -95,50 +106,62 @@ fun VaultItemCardContent(
},
passwordFieldTestTag = "CardNumberEntry",
showPasswordTestTag = "CardViewNumberButton",
cardStyle = cardState
.propertyList
.toListItemCardStyle(
index = cardState.propertyList.indexOf(element = numberData),
),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
}
}
if (cardState.brand != null && cardState.brand != VaultCardBrand.SELECT) {
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
label = stringResource(id = R.string.brand),
value = cardState.brand.shortName(),
onValueChange = {},
readOnly = true,
singleLine = false,
textFieldTestTag = "CardBrandEntry",
cardStyle = cardState
.propertyList
.toListItemCardStyle(
index = cardState.propertyList.indexOf(element = cardState.brand),
),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
textFieldTestTag = "CardBrandEntry",
.standardHorizontalMargin(),
)
}
}
cardState.expiration?.let { expiration ->
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
label = stringResource(id = R.string.expiration),
value = expiration,
onValueChange = {},
readOnly = true,
singleLine = false,
textFieldTestTag = "CardExpirationEntry",
cardStyle = cardState
.propertyList
.toListItemCardStyle(
index = cardState.propertyList.indexOf(element = expiration),
),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
textFieldTestTag = "CardExpirationEntry",
.standardHorizontalMargin(),
)
}
}
cardState.securityCode?.let { securityCodeData ->
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenPasswordFieldWithActions(
label = stringResource(id = R.string.security_code),
value = securityCodeData.code,
@@ -148,29 +171,35 @@ fun VaultItemCardContent(
readOnly = true,
singleLine = false,
actions = {
BitwardenTonalIconButton(
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_copy,
contentDescription = stringResource(id = R.string.copy_security_code),
onClick = vaultCardItemTypeHandlers.onCopySecurityCodeClick,
modifier = Modifier.testTag(tag = "CardCopySecurityCodeButton"),
)
},
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
showPasswordTestTag = "CardViewSecurityCodeButton",
passwordFieldTestTag = "CardSecurityCodeEntry",
cardStyle = cardState
.propertyList
.toListItemCardStyle(
index = cardState.propertyList.indexOf(element = securityCodeData),
),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
}
commonState.notes?.let { notes ->
item {
Spacer(modifier = Modifier.height(4.dp))
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.notes),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(8.dp))
@@ -181,7 +210,7 @@ fun VaultItemCardContent(
readOnly = true,
singleLine = false,
actions = {
BitwardenTonalIconButton(
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_copy,
contentDescription = stringResource(id = R.string.copy_notes),
onClick = vaultCommonItemTypeHandlers.onCopyNotesClick,
@@ -189,69 +218,75 @@ fun VaultItemCardContent(
)
},
textFieldTestTag = "CipherNotesLabel",
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
}
}
commonState.customFields.takeUnless { it.isEmpty() }?.let { customFields ->
item {
Spacer(modifier = Modifier.height(4.dp))
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.custom_fields),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
}
items(customFields) { customField ->
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.height(height = 8.dp))
CustomField(
customField = customField,
onCopyCustomHiddenField = vaultCommonItemTypeHandlers.onCopyCustomHiddenField,
onCopyCustomTextField = vaultCommonItemTypeHandlers.onCopyCustomTextField,
onShowHiddenFieldClick = vaultCommonItemTypeHandlers.onShowHiddenFieldClick,
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
}
}
commonState.attachments.takeUnless { it?.isEmpty() == true }?.let { attachments ->
item {
Spacer(modifier = Modifier.height(4.dp))
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.attachments),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
items(attachments) { attachmentItem ->
itemsIndexed(attachments) { index, attachmentItem ->
AttachmentItemContent(
modifier = Modifier
.testTag("CipherAttachment")
.fillMaxWidth()
.padding(start = 16.dp),
.standardHorizontalMargin(),
attachmentItem = attachmentItem,
onAttachmentDownloadClick =
vaultCommonItemTypeHandlers.onAttachmentDownloadClick,
onAttachmentDownloadClick = vaultCommonItemTypeHandlers
.onAttachmentDownloadClick,
cardStyle = attachments.toListItemCardStyle(index = index),
)
}
item { Spacer(modifier = Modifier.height(8.dp)) }
}
item {
Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.height(height = 16.dp))
VaultItemUpdateText(
header = "${stringResource(id = R.string.date_updated)}: ",
text = commonState.lastUpdated,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin()
.padding(horizontal = 12.dp),
)
}
item {
@@ -4,10 +4,12 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTonalIconButton
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenStandardIconButton
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordField
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordFieldWithActions
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextFieldWithActions
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.model.IconResource
import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
@@ -15,13 +17,17 @@ import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
/**
* Custom Field UI common for all item types.
*/
@Suppress("LongMethod", "MaxLineLength")
@Suppress("LongMethod")
@Composable
fun CustomField(
customField: VaultItemState.ViewState.Content.Common.Custom,
onCopyCustomHiddenField: (String) -> Unit,
onCopyCustomTextField: (String) -> Unit,
onShowHiddenFieldClick: (VaultItemState.ViewState.Content.Common.Custom.HiddenField, Boolean) -> Unit,
onShowHiddenFieldClick: (
VaultItemState.ViewState.Content.Common.Custom.HiddenField,
Boolean,
) -> Unit,
cardStyle: CardStyle,
modifier: Modifier = Modifier,
) {
when (customField) {
@@ -31,30 +37,44 @@ fun CustomField(
isChecked = customField.value,
readOnly = true,
onCheckedChange = { },
cardStyle = cardStyle,
modifier = modifier,
)
}
is VaultItemState.ViewState.Content.Common.Custom.HiddenField -> {
BitwardenPasswordFieldWithActions(
label = customField.name,
value = customField.value,
showPasswordChange = { onShowHiddenFieldClick(customField, it) },
showPassword = customField.isVisible,
onValueChange = { },
readOnly = true,
singleLine = false,
modifier = modifier,
actions = {
if (customField.isCopyable) {
BitwardenTonalIconButton(
if (customField.isCopyable) {
BitwardenPasswordFieldWithActions(
label = customField.name,
value = customField.value,
showPasswordChange = { onShowHiddenFieldClick(customField, it) },
showPassword = customField.isVisible,
onValueChange = { },
readOnly = true,
singleLine = false,
actions = {
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_copy,
contentDescription = stringResource(id = R.string.copy),
onClick = { onCopyCustomHiddenField(customField.value) },
)
}
},
)
},
cardStyle = cardStyle,
modifier = modifier,
)
} else {
BitwardenPasswordField(
label = customField.name,
value = customField.value,
showPasswordChange = { onShowHiddenFieldClick(customField, it) },
showPassword = customField.isVisible,
onValueChange = { },
readOnly = true,
singleLine = false,
cardStyle = cardStyle,
modifier = modifier,
)
}
}
is VaultItemState.ViewState.Content.Common.Custom.LinkedField -> {
@@ -68,6 +88,7 @@ fun CustomField(
onValueChange = { },
readOnly = true,
singleLine = false,
cardStyle = cardStyle,
modifier = modifier,
)
}
@@ -79,16 +100,17 @@ fun CustomField(
onValueChange = { },
readOnly = true,
singleLine = false,
modifier = modifier,
actions = {
if (customField.isCopyable) {
BitwardenTonalIconButton(
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_copy,
contentDescription = stringResource(id = R.string.copy),
onClick = { onCopyCustomTextField(customField.value) },
)
}
},
cardStyle = cardStyle,
modifier = modifier,
)
}
}
@@ -7,16 +7,20 @@ import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTonalIconButton
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.x8bit.bitwarden.ui.platform.base.util.toListItemCardStyle
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenStandardIconButton
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextFieldWithActions
import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultIdentityItemTypeHandlers
@@ -34,30 +38,33 @@ fun VaultItemIdentityContent(
) {
LazyColumn(modifier = modifier) {
item {
Spacer(modifier = Modifier.height(height = 12.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.item_information),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
label = stringResource(id = R.string.name),
value = commonState.name,
onValueChange = { },
readOnly = true,
singleLine = false,
textFieldTestTag = "ItemNameEntry",
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
textFieldTestTag = "ItemNameEntry",
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
identityState.identityName?.let { identityName ->
item {
Spacer(modifier = Modifier.height(8.dp))
IdentityCopyField(
label = stringResource(id = R.string.identity_name),
value = identityName,
@@ -65,15 +72,20 @@ fun VaultItemIdentityContent(
textFieldTestTag = "IdentityNameEntry",
copyActionTestTag = "IdentityCopyNameButton",
onCopyClick = vaultIdentityItemTypeHandlers.onCopyIdentityNameClick,
cardStyle = identityState
.propertyList
.toListItemCardStyle(
index = identityState.propertyList.indexOf(element = identityName),
dividerPadding = 0.dp,
),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
}
}
identityState.username?.let { username ->
item {
Spacer(modifier = Modifier.height(8.dp))
IdentityCopyField(
label = stringResource(id = R.string.username),
value = username,
@@ -81,15 +93,20 @@ fun VaultItemIdentityContent(
textFieldTestTag = "IdentityUsernameEntry",
copyActionTestTag = "IdentityCopyUsernameButton",
onCopyClick = vaultIdentityItemTypeHandlers.onCopyUsernameClick,
cardStyle = identityState
.propertyList
.toListItemCardStyle(
index = identityState.propertyList.indexOf(element = username),
dividerPadding = 0.dp,
),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
}
}
identityState.company?.let { company ->
item {
Spacer(modifier = Modifier.height(8.dp))
IdentityCopyField(
label = stringResource(id = R.string.company),
value = company,
@@ -97,15 +114,20 @@ fun VaultItemIdentityContent(
textFieldTestTag = "IdentityCompanyEntry",
copyActionTestTag = "IdentityCopyCompanyButton",
onCopyClick = vaultIdentityItemTypeHandlers.onCopyCompanyClick,
cardStyle = identityState
.propertyList
.toListItemCardStyle(
index = identityState.propertyList.indexOf(element = company),
dividerPadding = 0.dp,
),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
}
}
identityState.ssn?.let { ssn ->
item {
Spacer(modifier = Modifier.height(8.dp))
IdentityCopyField(
label = stringResource(id = R.string.ssn),
value = ssn,
@@ -113,15 +135,20 @@ fun VaultItemIdentityContent(
textFieldTestTag = "IdentitySsnEntry",
copyActionTestTag = "IdentityCopySsnButton",
onCopyClick = vaultIdentityItemTypeHandlers.onCopySsnClick,
cardStyle = identityState
.propertyList
.toListItemCardStyle(
index = identityState.propertyList.indexOf(element = ssn),
dividerPadding = 0.dp,
),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
}
}
identityState.passportNumber?.let { passportNumber ->
item {
Spacer(modifier = Modifier.height(8.dp))
IdentityCopyField(
label = stringResource(id = R.string.passport_number),
value = passportNumber,
@@ -129,15 +156,20 @@ fun VaultItemIdentityContent(
textFieldTestTag = "IdentityPassportNumberEntry",
copyActionTestTag = "IdentityCopyPassportNumberButton",
onCopyClick = vaultIdentityItemTypeHandlers.onCopyPassportNumberClick,
cardStyle = identityState
.propertyList
.toListItemCardStyle(
index = identityState.propertyList.indexOf(element = passportNumber),
dividerPadding = 0.dp,
),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
}
}
identityState.licenseNumber?.let { licenseNumber ->
item {
Spacer(modifier = Modifier.height(8.dp))
IdentityCopyField(
label = stringResource(id = R.string.license_number),
value = licenseNumber,
@@ -145,15 +177,20 @@ fun VaultItemIdentityContent(
textFieldTestTag = "IdentityLicenseNumberEntry",
copyActionTestTag = "IdentityCopyLicenseNumberButton",
onCopyClick = vaultIdentityItemTypeHandlers.onCopyLicenseNumberClick,
cardStyle = identityState
.propertyList
.toListItemCardStyle(
index = identityState.propertyList.indexOf(element = licenseNumber),
dividerPadding = 0.dp,
),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
}
}
identityState.email?.let { email ->
item {
Spacer(modifier = Modifier.height(8.dp))
IdentityCopyField(
label = stringResource(id = R.string.email),
value = email,
@@ -161,15 +198,20 @@ fun VaultItemIdentityContent(
textFieldTestTag = "IdentityEmailEntry",
copyActionTestTag = "IdentityCopyEmailButton",
onCopyClick = vaultIdentityItemTypeHandlers.onCopyEmailClick,
cardStyle = identityState
.propertyList
.toListItemCardStyle(
index = identityState.propertyList.indexOf(element = email),
dividerPadding = 0.dp,
),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
}
}
identityState.phone?.let { phone ->
item {
Spacer(modifier = Modifier.height(8.dp))
IdentityCopyField(
label = stringResource(id = R.string.phone),
value = phone,
@@ -177,15 +219,20 @@ fun VaultItemIdentityContent(
textFieldTestTag = "IdentityPhoneEntry",
copyActionTestTag = "IdentityCopyPhoneButton",
onCopyClick = vaultIdentityItemTypeHandlers.onCopyPhoneClick,
cardStyle = identityState
.propertyList
.toListItemCardStyle(
index = identityState.propertyList.indexOf(element = phone),
dividerPadding = 0.dp,
),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
}
}
identityState.address?.let { address ->
item {
Spacer(modifier = Modifier.height(8.dp))
IdentityCopyField(
label = stringResource(id = R.string.address),
value = address,
@@ -193,19 +240,26 @@ fun VaultItemIdentityContent(
textFieldTestTag = "IdentityAddressEntry",
copyActionTestTag = "IdentityCopyAddressButton",
onCopyClick = vaultIdentityItemTypeHandlers.onCopyAddressClick,
cardStyle = identityState
.propertyList
.toListItemCardStyle(
index = identityState.propertyList.indexOf(element = address),
dividerPadding = 0.dp,
),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
}
}
commonState.notes?.let { notes ->
item {
Spacer(modifier = Modifier.height(4.dp))
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.notes),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(8.dp))
@@ -216,69 +270,75 @@ fun VaultItemIdentityContent(
textFieldTestTag = "CipherNotesLabel",
copyActionTestTag = "CipherNotesCopyButton",
onCopyClick = vaultCommonItemTypeHandlers.onCopyNotesClick,
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
}
}
commonState.customFields.takeUnless { it.isEmpty() }?.let { customFields ->
item {
Spacer(modifier = Modifier.height(4.dp))
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.custom_fields),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
}
items(customFields) { customField ->
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.height(height = 8.dp))
CustomField(
customField = customField,
onCopyCustomHiddenField = vaultCommonItemTypeHandlers.onCopyCustomHiddenField,
onCopyCustomTextField = vaultCommonItemTypeHandlers.onCopyCustomTextField,
onShowHiddenFieldClick = vaultCommonItemTypeHandlers.onShowHiddenFieldClick,
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
}
}
commonState.attachments.takeUnless { it?.isEmpty() == true }?.let { attachments ->
item {
Spacer(modifier = Modifier.height(4.dp))
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.attachments),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
items(attachments) { attachmentItem ->
itemsIndexed(attachments) { index, attachmentItem ->
AttachmentItemContent(
modifier = Modifier
.testTag("CipherAttachment")
.fillMaxWidth()
.padding(start = 16.dp),
.standardHorizontalMargin(),
attachmentItem = attachmentItem,
onAttachmentDownloadClick =
vaultCommonItemTypeHandlers.onAttachmentDownloadClick,
onAttachmentDownloadClick = vaultCommonItemTypeHandlers
.onAttachmentDownloadClick,
cardStyle = attachments.toListItemCardStyle(index = index),
)
}
item { Spacer(modifier = Modifier.height(8.dp)) }
}
item {
Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.height(height = 16.dp))
VaultItemUpdateText(
header = "${stringResource(id = R.string.date_updated)}: ",
text = commonState.lastUpdated,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin()
.padding(horizontal = 12.dp),
)
}
item {
@@ -296,6 +356,7 @@ private fun IdentityCopyField(
textFieldTestTag: String,
copyActionTestTag: String,
onCopyClick: () -> Unit,
cardStyle: CardStyle,
modifier: Modifier = Modifier,
) {
BitwardenTextFieldWithActions(
@@ -305,14 +366,15 @@ private fun IdentityCopyField(
readOnly = true,
singleLine = false,
actions = {
BitwardenTonalIconButton(
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_copy,
contentDescription = copyContentDescription,
onClick = onCopyClick,
modifier = Modifier.testTag(tag = copyActionTestTag),
)
},
modifier = modifier,
textFieldTestTag = textFieldTestTag,
cardStyle = cardStyle,
modifier = modifier,
)
}
@@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@@ -17,13 +18,16 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTonalIconButton
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.x8bit.bitwarden.ui.platform.base.util.toListItemCardStyle
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenStandardIconButton
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenHiddenPasswordField
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordFieldWithActions
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextFieldWithActions
import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText
import com.x8bit.bitwarden.ui.platform.components.indicator.BitwardenCircularCountdownIndicator
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultLoginItemTypeHandlers
@@ -34,7 +38,7 @@ private const val AUTH_CODE_SPACING_INTERVAL = 3
/**
* The top level content UI state for the [VaultItemScreen] when viewing a Login cipher.
*/
@Suppress("LongMethod")
@Suppress("LongMethod", "CyclomaticComplexMethod")
@Composable
fun VaultItemLoginContent(
commonState: VaultItemState.ViewState.Content.Common,
@@ -47,52 +51,73 @@ fun VaultItemLoginContent(
modifier = modifier,
) {
item {
Spacer(modifier = Modifier.height(height = 12.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.item_information),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
label = stringResource(id = R.string.name),
value = commonState.name,
onValueChange = { },
readOnly = true,
singleLine = false,
textFieldTestTag = "LoginItemNameEntry",
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
textFieldTestTag = "LoginItemNameEntry",
.standardHorizontalMargin(),
)
}
if (loginItemState.hasLoginCredentials) {
item {
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.login_credentials),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
}
loginItemState.username?.let { username ->
item {
Spacer(modifier = Modifier.height(8.dp))
UsernameField(
username = username,
onCopyUsernameClick = vaultLoginItemTypeHandlers.onCopyUsernameClick,
cardStyle = loginItemState
.passwordData?.let { CardStyle.Top(dividerPadding = 0.dp) }
?: CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
}
}
loginItemState.passwordData?.let { passwordData ->
item {
Spacer(modifier = Modifier.height(8.dp))
PasswordField(
passwordData = passwordData,
onShowPasswordClick = vaultLoginItemTypeHandlers.onShowPasswordClick,
onCheckForBreachClick = vaultLoginItemTypeHandlers.onCheckForBreachClick,
onCopyPasswordClick = vaultLoginItemTypeHandlers.onCopyPasswordClick,
cardStyle = loginItemState
.username?.let { CardStyle.Bottom }
?: CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
}
}
@@ -104,7 +129,7 @@ fun VaultItemLoginContent(
creationDate = creationDate.invoke(),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
}
}
@@ -118,41 +143,45 @@ fun VaultItemLoginContent(
onCopyTotpClick = vaultLoginItemTypeHandlers.onCopyTotpCodeClick,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
}
}
loginItemState.uris.takeUnless { it.isEmpty() }?.let { uris ->
item {
Spacer(modifier = Modifier.height(4.dp))
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.ur_is),
label = stringResource(id = R.string.autofill_options),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
items(uris) { uriData ->
Spacer(modifier = Modifier.height(8.dp))
itemsIndexed(uris) { index, uriData ->
UriField(
uriData = uriData,
onCopyUriClick = vaultLoginItemTypeHandlers.onCopyUriClick,
onLaunchUriClick = vaultLoginItemTypeHandlers.onLaunchUriClick,
cardStyle = uris.toListItemCardStyle(index = index, dividerPadding = 0.dp),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
}
}
commonState.notes?.let { notes ->
item {
Spacer(modifier = Modifier.height(4.dp))
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.notes),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(8.dp))
@@ -168,59 +197,64 @@ fun VaultItemLoginContent(
commonState.customFields.takeUnless { it.isEmpty() }?.let { customFields ->
item {
Spacer(modifier = Modifier.height(4.dp))
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.custom_fields),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
}
items(customFields) { customField ->
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.height(height = 8.dp))
CustomField(
customField = customField,
onCopyCustomHiddenField = vaultCommonItemTypeHandlers.onCopyCustomHiddenField,
onCopyCustomTextField = vaultCommonItemTypeHandlers.onCopyCustomTextField,
onShowHiddenFieldClick = vaultCommonItemTypeHandlers.onShowHiddenFieldClick,
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
}
}
commonState.attachments.takeUnless { it?.isEmpty() == true }?.let { attachments ->
item {
Spacer(modifier = Modifier.height(4.dp))
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.attachments),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
items(attachments) { attachmentItem ->
itemsIndexed(attachments) { index, attachmentItem ->
AttachmentItemContent(
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp),
.standardHorizontalMargin(),
attachmentItem = attachmentItem,
onAttachmentDownloadClick =
vaultCommonItemTypeHandlers.onAttachmentDownloadClick,
cardStyle = attachments.toListItemCardStyle(index = index),
onAttachmentDownloadClick = vaultCommonItemTypeHandlers
.onAttachmentDownloadClick,
)
}
item { Spacer(modifier = Modifier.height(8.dp)) }
}
item {
Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.height(16.dp))
VaultItemUpdateText(
header = "${stringResource(id = R.string.date_updated)}: ",
text = commonState.lastUpdated,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin()
.padding(horizontal = 12.dp),
)
}
@@ -231,7 +265,8 @@ fun VaultItemLoginContent(
text = revisionDate,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin()
.padding(horizontal = 12.dp),
)
}
}
@@ -243,7 +278,8 @@ fun VaultItemLoginContent(
onPasswordHistoryClick = vaultLoginItemTypeHandlers.onPasswordHistoryClick,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin()
.padding(horizontal = 12.dp),
)
}
}
@@ -266,6 +302,7 @@ private fun Fido2CredentialField(
onValueChange = { },
readOnly = true,
singleLine = true,
cardStyle = CardStyle.Full,
modifier = modifier,
)
}
@@ -283,7 +320,7 @@ private fun NotesField(
readOnly = true,
singleLine = false,
actions = {
BitwardenTonalIconButton(
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_copy,
contentDescription = stringResource(id = R.string.copy_notes),
onClick = onCopyAction,
@@ -291,6 +328,7 @@ private fun NotesField(
)
},
textFieldTestTag = "CipherNotesLabel",
cardStyle = CardStyle.Full,
modifier = modifier,
)
}
@@ -301,6 +339,7 @@ private fun PasswordField(
onShowPasswordClick: (Boolean) -> Unit,
onCheckForBreachClick: () -> Unit,
onCopyPasswordClick: () -> Unit,
cardStyle: CardStyle,
modifier: Modifier = Modifier,
) {
if (passwordData.canViewPassword) {
@@ -313,7 +352,7 @@ private fun PasswordField(
readOnly = true,
singleLine = false,
actions = {
BitwardenTonalIconButton(
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_check_mark,
contentDescription = stringResource(
id = R.string.check_known_data_breaches_for_this_password,
@@ -321,21 +360,23 @@ private fun PasswordField(
onClick = onCheckForBreachClick,
modifier = Modifier.testTag(tag = "LoginCheckPasswordButton"),
)
BitwardenTonalIconButton(
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_copy,
contentDescription = stringResource(id = R.string.copy_password),
onClick = onCopyPasswordClick,
modifier = Modifier.testTag(tag = "LoginCopyPasswordButton"),
)
},
modifier = modifier,
showPasswordTestTag = "LoginViewPasswordButton",
passwordFieldTestTag = "LoginPasswordEntry",
cardStyle = cardStyle,
modifier = modifier,
)
} else {
BitwardenHiddenPasswordField(
label = stringResource(id = R.string.password),
value = passwordData.password,
cardStyle = cardStyle,
modifier = modifier
.testTag("LoginPasswordEntry"),
)
@@ -373,39 +414,41 @@ private fun TotpField(
modifier: Modifier = Modifier,
) {
if (enabled) {
Row {
BitwardenTextFieldWithActions(
label = stringResource(id = R.string.verification_code_totp),
value = totpCodeItemData.verificationCode
.chunked(AUTH_CODE_SPACING_INTERVAL)
.joinToString(" "),
onValueChange = { },
readOnly = true,
singleLine = true,
actions = {
BitwardenCircularCountdownIndicator(
timeLeftSeconds = totpCodeItemData.timeLeftSeconds,
periodSeconds = totpCodeItemData.periodSeconds,
)
BitwardenTonalIconButton(
vectorIconRes = R.drawable.ic_copy,
contentDescription = stringResource(id = R.string.copy_totp),
onClick = onCopyTotpClick,
modifier = Modifier.testTag(tag = "LoginCopyTotpButton"),
)
},
modifier = modifier,
textFieldTestTag = "LoginTotpEntry",
)
}
BitwardenTextFieldWithActions(
label = stringResource(id = R.string.verification_code_totp),
value = totpCodeItemData.verificationCode
.chunked(AUTH_CODE_SPACING_INTERVAL)
.joinToString(" "),
onValueChange = { },
textStyle = BitwardenTheme.typography.sensitiveInfoSmall,
readOnly = true,
singleLine = true,
actions = {
BitwardenCircularCountdownIndicator(
timeLeftSeconds = totpCodeItemData.timeLeftSeconds,
periodSeconds = totpCodeItemData.periodSeconds,
)
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_copy,
contentDescription = stringResource(id = R.string.copy_totp),
onClick = onCopyTotpClick,
modifier = Modifier.testTag(tag = "LoginCopyTotpButton"),
)
},
textFieldTestTag = "LoginTotpEntry",
cardStyle = CardStyle.Full,
modifier = modifier,
)
} else {
BitwardenTextField(
label = stringResource(id = R.string.verification_code_totp),
value = stringResource(id = R.string.premium_subscription_required),
value = "",
supportingText = stringResource(id = R.string.premium_subscription_required),
enabled = false,
singleLine = false,
onValueChange = { },
readOnly = true,
cardStyle = CardStyle.Full,
modifier = modifier,
)
}
@@ -416,6 +459,7 @@ private fun UriField(
uriData: VaultItemState.ViewState.Content.ItemType.Login.UriData,
onCopyUriClick: (String) -> Unit,
onLaunchUriClick: (String) -> Unit,
cardStyle: CardStyle,
modifier: Modifier = Modifier,
) {
BitwardenTextFieldWithActions(
@@ -426,7 +470,7 @@ private fun UriField(
singleLine = false,
actions = {
if (uriData.isLaunchable) {
BitwardenTonalIconButton(
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_external_link,
contentDescription = stringResource(id = R.string.launch),
onClick = { onLaunchUriClick(uriData.uri) },
@@ -434,7 +478,7 @@ private fun UriField(
)
}
if (uriData.isCopyable) {
BitwardenTonalIconButton(
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_copy,
contentDescription = stringResource(id = R.string.copy),
onClick = { onCopyUriClick(uriData.uri) },
@@ -442,8 +486,9 @@ private fun UriField(
)
}
},
modifier = modifier,
textFieldTestTag = "LoginUriEntry",
cardStyle = cardStyle,
modifier = modifier,
)
}
@@ -451,6 +496,7 @@ private fun UriField(
private fun UsernameField(
username: String,
onCopyUsernameClick: () -> Unit,
cardStyle: CardStyle,
modifier: Modifier = Modifier,
) {
BitwardenTextFieldWithActions(
@@ -460,14 +506,15 @@ private fun UsernameField(
readOnly = true,
singleLine = false,
actions = {
BitwardenTonalIconButton(
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_copy,
contentDescription = stringResource(id = R.string.copy_username),
onClick = onCopyUsernameClick,
modifier = Modifier.testTag(tag = "LoginCopyUsernameButton"),
)
},
modifier = modifier,
textFieldTestTag = "LoginUsernameEntry",
cardStyle = cardStyle,
modifier = modifier,
)
}
@@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@@ -16,10 +17,13 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTonalIconButton
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.x8bit.bitwarden.ui.platform.base.util.toListItemCardStyle
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenStandardIconButton
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextFieldWithActions
import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers
@@ -35,36 +39,40 @@ fun VaultItemSecureNoteContent(
) {
LazyColumn(modifier = modifier) {
item {
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.item_information),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
label = stringResource(id = R.string.name),
value = commonState.name,
onValueChange = { },
readOnly = true,
singleLine = false,
textFieldTestTag = "ItemNameEntry",
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
textFieldTestTag = "ItemNameEntry",
.standardHorizontalMargin(),
)
}
commonState.notes?.let { notes ->
item {
Spacer(modifier = Modifier.height(4.dp))
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.notes),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(8.dp))
@@ -75,7 +83,7 @@ fun VaultItemSecureNoteContent(
readOnly = true,
singleLine = false,
actions = {
BitwardenTonalIconButton(
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_copy,
contentDescription = stringResource(id = R.string.copy_notes),
onClick = vaultCommonItemTypeHandlers.onCopyNotesClick,
@@ -83,68 +91,74 @@ fun VaultItemSecureNoteContent(
)
},
textFieldTestTag = "CipherNotesLabel",
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
}
}
commonState.customFields.takeUnless { it.isEmpty() }?.let { customFields ->
item {
Spacer(modifier = Modifier.height(4.dp))
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.custom_fields),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
}
items(customFields) { customField ->
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.height(height = 8.dp))
CustomField(
customField = customField,
onCopyCustomHiddenField = vaultCommonItemTypeHandlers.onCopyCustomHiddenField,
onCopyCustomTextField = vaultCommonItemTypeHandlers.onCopyCustomTextField,
onShowHiddenFieldClick = vaultCommonItemTypeHandlers.onShowHiddenFieldClick,
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
}
}
commonState.attachments.takeUnless { it?.isEmpty() == true }?.let { attachments ->
item {
Spacer(modifier = Modifier.height(4.dp))
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.attachments),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
items(attachments) { attachmentItem ->
itemsIndexed(attachments) { index, attachmentItem ->
AttachmentItemContent(
modifier = Modifier
.testTag("CipherAttachment")
.fillMaxWidth()
.padding(start = 16.dp),
.standardHorizontalMargin(),
attachmentItem = attachmentItem,
onAttachmentDownloadClick =
vaultCommonItemTypeHandlers.onAttachmentDownloadClick,
onAttachmentDownloadClick = vaultCommonItemTypeHandlers
.onAttachmentDownloadClick,
cardStyle = attachments.toListItemCardStyle(index = index),
)
}
item { Spacer(modifier = Modifier.height(8.dp)) }
}
item {
Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.height(height = 16.dp))
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.standardHorizontalMargin()
.padding(horizontal = 12.dp)
.semantics(mergeDescendants = true) { },
) {
Text(
@@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
@@ -14,11 +15,13 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTonalIconButton
import com.x8bit.bitwarden.ui.platform.base.util.toListItemCardStyle
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenStandardIconButton
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordFieldWithActions
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextFieldWithActions
import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultSshKeyItemTypeHandlers
@@ -36,26 +39,29 @@ fun VaultItemSshKeyContent(
) {
LazyColumn(modifier = modifier) {
item {
Spacer(modifier = Modifier.height(height = 12.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.item_information),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
label = stringResource(id = R.string.name),
value = commonState.name,
onValueChange = { },
readOnly = true,
singleLine = false,
textFieldTestTag = "SshKeyItemNameEntry",
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
textFieldTestTag = "SshKeyItemNameEntry",
.standardHorizontalMargin(),
)
}
@@ -68,13 +74,14 @@ fun VaultItemSshKeyContent(
singleLine = false,
readOnly = true,
actions = {
BitwardenTonalIconButton(
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_copy,
contentDescription = stringResource(id = R.string.copy_public_key),
onClick = vaultSshKeyItemTypeHandlers.onCopyPublicKeyClick,
modifier = Modifier.testTag(tag = "SshKeyCopyPublicKeyButton"),
)
},
cardStyle = CardStyle.Top(),
modifier = Modifier
.testTag("SshKeyItemPublicKeyEntry")
.fillMaxWidth()
@@ -83,7 +90,6 @@ fun VaultItemSshKeyContent(
}
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenPasswordFieldWithActions(
label = stringResource(id = R.string.private_key),
value = sshKeyItemState.privateKey,
@@ -91,7 +97,7 @@ fun VaultItemSshKeyContent(
singleLine = false,
readOnly = true,
actions = {
BitwardenTonalIconButton(
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_copy,
contentDescription = stringResource(id = R.string.copy_private_key),
onClick = vaultSshKeyItemTypeHandlers.onCopyPrivateKeyClick,
@@ -101,15 +107,15 @@ fun VaultItemSshKeyContent(
showPassword = sshKeyItemState.showPrivateKey,
showPasswordTestTag = "ViewPrivateKeyButton",
showPasswordChange = vaultSshKeyItemTypeHandlers.onShowPrivateKeyClick,
cardStyle = CardStyle.Middle(),
modifier = Modifier
.testTag("SshKeyItemPrivateKeyEntry")
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
}
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextFieldWithActions(
label = stringResource(id = R.string.fingerprint),
value = sshKeyItemState.fingerprint,
@@ -117,13 +123,14 @@ fun VaultItemSshKeyContent(
singleLine = false,
readOnly = true,
actions = {
BitwardenTonalIconButton(
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_copy,
contentDescription = stringResource(id = R.string.copy_fingerprint),
onClick = vaultSshKeyItemTypeHandlers.onCopyFingerprintClick,
modifier = Modifier.testTag(tag = "SshKeyCopyFingerprintButton"),
)
},
cardStyle = CardStyle.Bottom,
modifier = Modifier
.testTag("SshKeyItemFingerprintEntry")
.fillMaxWidth()
@@ -133,12 +140,13 @@ fun VaultItemSshKeyContent(
commonState.notes?.let { notes ->
item {
Spacer(modifier = Modifier.height(4.dp))
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.notes),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextFieldWithActions(
@@ -148,7 +156,7 @@ fun VaultItemSshKeyContent(
readOnly = true,
singleLine = false,
actions = {
BitwardenTonalIconButton(
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_copy,
contentDescription = stringResource(id = R.string.copy_notes),
onClick = vaultCommonItemTypeHandlers.onCopyNotesClick,
@@ -156,68 +164,74 @@ fun VaultItemSshKeyContent(
)
},
textFieldTestTag = "CipherNotesLabel",
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
}
}
commonState.customFields.takeUnless { it.isEmpty() }?.let { customFields ->
item {
Spacer(modifier = Modifier.height(4.dp))
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.custom_fields),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
}
items(customFields) { customField ->
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.height(height = 8.dp))
CustomField(
customField = customField,
onCopyCustomHiddenField = vaultCommonItemTypeHandlers.onCopyCustomHiddenField,
onCopyCustomTextField = vaultCommonItemTypeHandlers.onCopyCustomTextField,
onShowHiddenFieldClick = vaultCommonItemTypeHandlers.onShowHiddenFieldClick,
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
}
}
commonState.attachments.takeUnless { it?.isEmpty() == true }?.let { attachments ->
item {
Spacer(modifier = Modifier.height(4.dp))
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.attachments),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
items(attachments) { attachmentItem ->
itemsIndexed(attachments) { index, attachmentItem ->
AttachmentItemContent(
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp),
.standardHorizontalMargin(),
attachmentItem = attachmentItem,
onAttachmentDownloadClick = vaultCommonItemTypeHandlers
.onAttachmentDownloadClick,
cardStyle = attachments.toListItemCardStyle(index = index),
)
}
item { Spacer(modifier = Modifier.height(8.dp)) }
}
item {
Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.height(height = 16.dp))
VaultItemUpdateText(
header = "${stringResource(id = R.string.date_updated)}: ",
text = commonState.lastUpdated,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.standardHorizontalMargin()
.padding(horizontal = 12.dp)
.testTag("SshKeyItemLastUpdated"),
)
}
@@ -26,6 +26,7 @@ import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
import com.x8bit.bitwarden.ui.platform.base.util.Text
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.base.util.concat
import com.x8bit.bitwarden.ui.platform.util.persistentListOfNotNull
import com.x8bit.bitwarden.ui.vault.feature.item.model.TotpCodeItemData
import com.x8bit.bitwarden.ui.vault.feature.item.model.VaultItemStateData
import com.x8bit.bitwarden.ui.vault.feature.item.util.toViewState
@@ -1516,6 +1517,15 @@ data class VaultItemState(
val fido2CredentialCreationDateText: Text?,
) : ItemType() {
/**
* Indicates that at least one of the login credentials are present.
*/
val hasLoginCredentials: Boolean
get() = username != null ||
passwordData != null ||
fido2CredentialCreationDateText != null ||
totpCodeItemData != null
/**
* A wrapper for the password data.
*
@@ -1571,7 +1581,24 @@ data class VaultItemState(
val email: String?,
val phone: String?,
val address: String?,
) : ItemType()
) : ItemType() {
/**
* An ordered list of Card specific elements.
*/
val propertyList: List<String>
get() = persistentListOfNotNull(
identityName,
username,
company,
ssn,
passportNumber,
licenseNumber,
email,
phone,
address,
)
}
/**
* Represents the `Card` item type.
@@ -1590,6 +1617,18 @@ data class VaultItemState(
val securityCode: CodeData?,
) : ItemType() {
/**
* An ordered list of Card specific elements.
*/
val propertyList: List<Any>
get() = persistentListOfNotNull(
cardholderName,
number,
brand.takeIf { brand != VaultCardBrand.SELECT },
expiration,
securityCode,
)
/**
* A wrapper for the number data.
*
@@ -6,7 +6,7 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -17,10 +17,11 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.x8bit.bitwarden.ui.platform.base.util.toListItemCardStyle
import com.x8bit.bitwarden.ui.platform.components.card.BitwardenInfoCalloutCard
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenMasterPasswordDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
import com.x8bit.bitwarden.ui.platform.components.divider.BitwardenHorizontalDivider
import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText
import com.x8bit.bitwarden.ui.platform.components.listitem.BitwardenGroupItem
import com.x8bit.bitwarden.ui.platform.components.listitem.BitwardenListItem
@@ -101,25 +102,25 @@ fun VaultItemListingContent(
LazyColumn(
modifier = modifier,
) {
item {
if (showAddTotpBanner) {
if (showAddTotpBanner) {
item {
Spacer(modifier = Modifier.height(height = 12.dp))
BitwardenInfoCalloutCard(
text = stringResource(id = R.string.add_this_authenticator_key_to_a_login),
modifier = Modifier
.padding(horizontal = 16.dp)
.standardHorizontalMargin()
.fillMaxWidth(),
)
}
}
item {
if (policyDisablesSend) {
if (policyDisablesSend) {
item {
Spacer(modifier = Modifier.height(height = 12.dp))
BitwardenInfoCalloutCard(
text = stringResource(id = R.string.send_disabled_warning),
modifier = Modifier
.padding(horizontal = 16.dp)
.standardHorizontalMargin()
.fillMaxWidth(),
)
}
@@ -127,83 +128,80 @@ fun VaultItemListingContent(
if (state.displayCollectionList.isNotEmpty()) {
item {
Spacer(modifier = Modifier.height(height = 12.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.collections),
supportingLabel = state.displayCollectionList.count().toString(),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
item {
Spacer(modifier = Modifier.height(4.dp))
}
items(state.displayCollectionList) { collection ->
itemsIndexed(state.displayCollectionList) { index, collection ->
BitwardenGroupItem(
startIcon = rememberVectorPainter(id = R.drawable.ic_collections),
label = collection.name,
supportingLabel = collection.count.toString(),
onClick = { collectionClick(collection.id) },
showDivider = false,
cardStyle = state
.displayCollectionList
.toListItemCardStyle(index = index, dividerPadding = 56.dp),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
}
}
if (state.displayFolderList.isNotEmpty()) {
item {
Spacer(modifier = Modifier.height(height = 12.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.folders),
supportingLabel = state.displayFolderList.count().toString(),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
item {
Spacer(modifier = Modifier.height(4.dp))
}
items(state.displayFolderList) { folder ->
itemsIndexed(state.displayFolderList) { index, folder ->
BitwardenGroupItem(
startIcon = rememberVectorPainter(id = R.drawable.ic_folder),
label = folder.name,
supportingLabel = folder.count.toString(),
onClick = { folderClick(folder.id) },
showDivider = false,
cardStyle = state
.displayFolderList
.toListItemCardStyle(index = index, dividerPadding = 56.dp),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
}
}
if (state.shouldShowDivider) {
item {
BitwardenHorizontalDivider(
modifier = Modifier
.fillMaxWidth()
.padding(all = 16.dp),
.standardHorizontalMargin(),
)
}
}
if (state.displayItemList.isNotEmpty()) {
item {
Spacer(modifier = Modifier.height(height = 12.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.items),
supportingLabel = state.displayItemList.size.toString(),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
items(state.displayItemList) {
itemsIndexed(state.displayItemList) { index, it ->
BitwardenListItem(
startIcon = it.iconData,
startIconTestTag = it.iconTestTag,
@@ -263,14 +261,12 @@ fun VaultItemListingContent(
// Only show options if allowed
.filter { !policyDisablesSend }
.toPersistentList(),
cardStyle = state
.displayItemList
.toListItemCardStyle(index = index, dividerPadding = 56.dp),
modifier = Modifier
.fillMaxWidth()
.padding(
start = 16.dp,
// There is some built-in padding to the menu button that makes up
// the visual difference here.
end = 12.dp,
),
.standardHorizontalMargin(),
)
}
}
@@ -14,7 +14,9 @@ import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.x8bit.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectButton
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
import com.x8bit.bitwarden.ui.vault.components.collectionItemsSelector
import com.x8bit.bitwarden.ui.vault.model.VaultCollection
@@ -37,7 +39,7 @@ fun VaultMoveToOrganizationContent(
) {
if (!showOnlyCollections) {
item {
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.height(height = 12.dp))
BitwardenMultiSelectButton(
label = stringResource(id = R.string.organization),
options = state
@@ -52,9 +54,11 @@ fun VaultMoveToOrganizationContent(
.first { it.name == selectedString },
)
},
cardStyle = CardStyle.Full,
modifier = Modifier
.testTag("OrganizationListDropdown")
.padding(horizontal = 16.dp),
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
@@ -67,7 +71,8 @@ fun VaultMoveToOrganizationContent(
color = BitwardenTheme.colorScheme.text.secondary,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 32.dp),
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
}
}
@@ -3,18 +3,21 @@ package com.x8bit.bitwarden.ui.vault.feature.vault
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.components.divider.BitwardenHorizontalDivider
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.x8bit.bitwarden.ui.platform.base.util.toListItemCardStyle
import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText
import com.x8bit.bitwarden.ui.platform.components.listitem.BitwardenGroupItem
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.model.toIconResources
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.model.ListingItemOverflowAction
@@ -40,6 +43,9 @@ fun VaultContent(
LazyColumn(
modifier = modifier,
) {
item {
Spacer(modifier = Modifier.height(height = 12.dp))
}
if (state.totpItemsCount > 0) {
item {
BitwardenListHeaderText(
@@ -47,8 +53,10 @@ fun VaultContent(
supportingLabel = TOTP_TYPES_COUNT.toString(),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
item {
@@ -57,31 +65,31 @@ fun VaultContent(
label = stringResource(id = R.string.verification_codes),
supportingLabel = state.totpItemsCount.toString(),
onClick = vaultHandlers.verificationCodesClick,
showDivider = true,
showDivider = false,
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.testTag("VerificationCodesFilter")
.padding(16.dp),
.standardHorizontalMargin(),
)
}
}
if (state.favoriteItems.isNotEmpty()) {
item {
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.favorites),
supportingLabel = state.favoriteItems.count().toString(),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
item {
Spacer(modifier = Modifier.height(4.dp))
}
items(state.favoriteItems) { favoriteItem ->
itemsIndexed(state.favoriteItems) { index, favoriteItem ->
VaultEntryListItem(
startIcon = favoriteItem.startIcon,
startIconTestTag = favoriteItem.startIconTestTag,
@@ -102,39 +110,28 @@ fun VaultContent(
vaultHandlers.overflowOptionClick(action)
}
},
cardStyle = state
.favoriteItems
.toListItemCardStyle(index = index, dividerPadding = 56.dp),
modifier = Modifier
.fillMaxWidth()
.testTag("CipherCell")
.padding(
start = 16.dp,
// There is some built-in padding to the menu button that makes up
// the visual difference here.
end = 12.dp,
),
)
}
item {
BitwardenHorizontalDivider(
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp, start = 16.dp, bottom = 16.dp),
.standardHorizontalMargin(),
)
}
}
item {
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.types),
supportingLabel = state.itemTypesCount.toString(),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
}
item {
Spacer(modifier = Modifier.height(4.dp))
Spacer(modifier = Modifier.height(height = 8.dp))
}
item {
@@ -145,10 +142,11 @@ fun VaultContent(
supportingLabel = state.loginItemsCount.toString(),
onClick = vaultHandlers.loginGroupClick,
showDivider = false,
cardStyle = CardStyle.Top(dividerPadding = 56.dp),
modifier = Modifier
.fillMaxWidth()
.testTag("LoginFilter")
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
}
@@ -160,10 +158,11 @@ fun VaultContent(
supportingLabel = state.cardItemsCount.toString(),
onClick = vaultHandlers.cardGroupClick,
showDivider = false,
cardStyle = CardStyle.Middle(dividerPadding = 56.dp),
modifier = Modifier
.fillMaxWidth()
.testTag("CardFilter")
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
}
@@ -175,10 +174,11 @@ fun VaultContent(
supportingLabel = state.identityItemsCount.toString(),
onClick = vaultHandlers.identityGroupClick,
showDivider = false,
cardStyle = CardStyle.Middle(dividerPadding = 56.dp),
modifier = Modifier
.fillMaxWidth()
.testTag("IdentityFilter")
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
}
@@ -190,10 +190,15 @@ fun VaultContent(
supportingLabel = state.secureNoteItemsCount.toString(),
onClick = vaultHandlers.secureNoteGroupClick,
showDivider = false,
cardStyle = if (showSshKeys) {
CardStyle.Middle(dividerPadding = 56.dp)
} else {
CardStyle.Bottom
},
modifier = Modifier
.fillMaxWidth()
.testTag("SecureNoteFilter")
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
}
@@ -206,71 +211,61 @@ fun VaultContent(
supportingLabel = state.sshKeyItemsCount.toString(),
onClick = vaultHandlers.sshKeyGroupClick,
showDivider = false,
cardStyle = CardStyle.Bottom,
modifier = Modifier
.fillMaxWidth()
.testTag("SshKeyFilter")
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
}
}
if (state.folderItems.isNotEmpty()) {
item {
BitwardenHorizontalDivider(
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp, start = 16.dp, bottom = 16.dp),
)
}
item {
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.folders),
supportingLabel = state.folderItems.count().toString(),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
item {
Spacer(modifier = Modifier.height(4.dp))
}
items(state.folderItems) { folder ->
itemsIndexed(state.folderItems) { index, folder ->
BitwardenGroupItem(
startIcon = rememberVectorPainter(id = R.drawable.ic_folder),
label = folder.name(),
supportingLabel = folder.itemCount.toString(),
onClick = { vaultHandlers.folderClick(folder) },
showDivider = false,
cardStyle = state
.folderItems
.toListItemCardStyle(index = index, dividerPadding = 56.dp),
modifier = Modifier
.fillMaxWidth()
.testTag("FolderFilter")
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
}
}
if (state.noFolderItems.isNotEmpty()) {
item {
BitwardenHorizontalDivider(
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp, start = 16.dp, bottom = 16.dp),
)
}
item {
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.folder_none),
supportingLabel = state.noFolderItems.count().toString(),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
items(state.noFolderItems) { noFolderItem ->
itemsIndexed(state.noFolderItems) { index, noFolderItem ->
VaultEntryListItem(
startIcon = noFolderItem.startIcon,
startIconTestTag = noFolderItem.startIconTestTag,
@@ -291,68 +286,60 @@ fun VaultContent(
vaultHandlers.overflowOptionClick(action)
}
},
cardStyle = state
.noFolderItems
.toListItemCardStyle(index = index, dividerPadding = 56.dp),
modifier = Modifier
.fillMaxWidth()
.testTag("CipherCell")
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
}
}
if (state.collectionItems.isNotEmpty()) {
item {
BitwardenHorizontalDivider(
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp, start = 16.dp, bottom = 16.dp),
)
}
item {
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.collections),
supportingLabel = state.collectionItems.count().toString(),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
items(state.collectionItems) { collection ->
itemsIndexed(state.collectionItems) { index, collection ->
BitwardenGroupItem(
startIcon = rememberVectorPainter(id = R.drawable.ic_collections),
label = collection.name,
supportingLabel = collection.itemCount.toString(),
onClick = { vaultHandlers.collectionClick(collection) },
showDivider = false,
cardStyle = state
.collectionItems
.toListItemCardStyle(index = index, dividerPadding = 56.dp),
modifier = Modifier
.fillMaxWidth()
.testTag("CollectionFilter")
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
}
}
item {
BitwardenHorizontalDivider(
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp, start = 16.dp, bottom = 16.dp),
)
}
item {
Spacer(modifier = Modifier.height(height = 16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.trash),
supportingLabel = TRASH_TYPES_COUNT.toString(),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
}
item {
Spacer(modifier = Modifier.height(4.dp))
Spacer(modifier = Modifier.height(height = 8.dp))
}
item {
@@ -362,13 +349,17 @@ fun VaultContent(
supportingLabel = state.trashItemsCount.toString(),
onClick = vaultHandlers.trashClick,
showDivider = false,
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.testTag("TrashFilter")
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
}
item { Spacer(modifier = Modifier.height(88.dp)) }
item {
Spacer(modifier = Modifier.height(height = 88.dp))
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
}
@@ -6,6 +6,7 @@ import androidx.compose.ui.tooling.preview.Preview
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.components.listitem.BitwardenListItem
import com.x8bit.bitwarden.ui.platform.components.listitem.SelectionItemData
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.model.IconData
import com.x8bit.bitwarden.ui.platform.components.model.IconResource
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
@@ -24,6 +25,7 @@ import kotlinx.collections.immutable.toImmutableList
* @param onClick The lambda to be invoked when the item is clicked.
* @param overflowOptions List of options to display for the item.
* @param onOverflowOptionClick The lambda to be invoked when an overflow option is clicked.
* @param cardStyle Indicates the type of card style to be applied.
* @param modifier An optional [Modifier] for this Composable, defaulting to an empty Modifier.
* This allows the caller to specify things like padding, size, etc.
*/
@@ -35,6 +37,7 @@ fun VaultEntryListItem(
onClick: () -> Unit,
overflowOptions: ImmutableList<ListingItemOverflowAction.VaultAction>,
onOverflowOptionClick: (ListingItemOverflowAction.VaultAction) -> Unit,
cardStyle: CardStyle,
modifier: Modifier = Modifier,
trailingLabelIcons: ImmutableList<IconResource> = persistentListOf(),
supportingLabel: String? = null,
@@ -55,10 +58,11 @@ fun VaultEntryListItem(
)
}
.toImmutableList(),
cardStyle = cardStyle,
)
}
@Preview(showBackground = true)
@Preview
@Composable
private fun VaultEntryListItem_preview() {
BitwardenTheme {
@@ -70,6 +74,7 @@ private fun VaultEntryListItem_preview() {
onClick = {},
overflowOptions = persistentListOf(),
onOverflowOptionClick = {},
cardStyle = CardStyle.Full,
modifier = Modifier,
)
}
Loaded 100 of 110 files, more files were not shown because too many files have changed in this diff. Show more