Update Authenticator UI to match Password Manager style (#5969)

This commit is contained in:
David Perez 2025-10-06 09:59:46 -05:00 committed by GitHub
parent ca474b272a
commit 2636a4f93a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
73 changed files with 813 additions and 4888 deletions

View File

@ -9,17 +9,19 @@ import android.view.WindowManager
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.runtime.getValue
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController
import com.bitwarden.authenticator.data.platform.repository.SettingsRepository
import com.bitwarden.authenticator.ui.platform.composition.LocalManagerProvider
import com.bitwarden.authenticator.ui.platform.feature.debugmenu.manager.DebugMenuLaunchManager
import com.bitwarden.authenticator.ui.platform.feature.debugmenu.navigateToDebugMenuScreen
import com.bitwarden.authenticator.ui.platform.feature.rootnav.RootNavScreen
import com.bitwarden.authenticator.ui.platform.theme.AuthenticatorTheme
import com.bitwarden.ui.platform.theme.BitwardenTheme
import com.bitwarden.ui.platform.util.setupEdgeToEdge
import com.bitwarden.ui.platform.util.validate
import dagger.hilt.android.AndroidEntryPoint
@ -39,6 +41,9 @@ class MainActivity : AppCompatActivity() {
@Inject
lateinit var debugLaunchManager: DebugMenuLaunchManager
@Inject
lateinit var settingsRepository: SettingsRepository
override fun onCreate(savedInstanceState: Bundle?) {
intent = intent.validate()
var shouldShowSplashScreen = true
@ -53,13 +58,14 @@ class MainActivity : AppCompatActivity() {
)
}
AppCompatDelegate.setDefaultNightMode(settingsRepository.appTheme.osValue)
setupEdgeToEdge(appThemeFlow = mainViewModel.stateFlow.map { it.theme })
setContent {
val state by mainViewModel.stateFlow.collectAsStateWithLifecycle()
val navController = rememberNavController()
observeViewModelEvents(navController)
LocalManagerProvider {
AuthenticatorTheme(
BitwardenTheme(
theme = state.theme,
) {
RootNavScreen(
@ -94,6 +100,9 @@ class MainActivity : AppCompatActivity() {
}
MainEvent.NavigateToDebugMenu -> navController.navigateToDebugMenuScreen()
is MainEvent.UpdateAppTheme -> {
AppCompatDelegate.setDefaultNightMode(event.osTheme)
}
}
}
.launchIn(lifecycleScope)

View File

@ -60,6 +60,7 @@ class MainViewModel @Inject constructor(
private fun handleThemeUpdated(action: MainAction.Internal.ThemeUpdate) {
mutableStateFlow.update { it.copy(theme = action.theme) }
sendEvent(MainEvent.UpdateAppTheme(osTheme = action.theme.osValue))
}
private fun handleFirstIntentReceived(action: MainAction.ReceiveFirstIntent) {
@ -139,4 +140,9 @@ sealed class MainEvent {
* Event indicating a change in the screen capture setting.
*/
data class ScreenCaptureSettingChange(val isAllowed: Boolean) : MainEvent()
/**
* Indicates that the app theme has been updated.
*/
data class UpdateAppTheme(val osTheme: Int) : MainEvent()
}

View File

@ -2,16 +2,15 @@ package com.bitwarden.authenticator.ui.auth.unlock
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
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.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@ -25,18 +24,16 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.bitwarden.authenticator.ui.platform.components.button.AuthenticatorFilledTonalButton
import com.bitwarden.authenticator.ui.platform.components.dialog.BasicDialogState
import com.bitwarden.authenticator.ui.platform.components.dialog.BitwardenBasicDialog
import com.bitwarden.authenticator.ui.platform.components.dialog.BitwardenLoadingDialog
import com.bitwarden.authenticator.ui.platform.components.dialog.LoadingDialogState
import com.bitwarden.authenticator.ui.platform.components.scaffold.BitwardenScaffold
import com.bitwarden.authenticator.ui.platform.composition.LocalBiometricsManager
import com.bitwarden.authenticator.ui.platform.manager.biometrics.BiometricsManager
import com.bitwarden.ui.platform.base.util.EventsEffect
import com.bitwarden.ui.platform.components.button.BitwardenFilledButton
import com.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
import com.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.bitwarden.ui.platform.resource.BitwardenDrawable
import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.util.asText
import com.bitwarden.ui.platform.theme.BitwardenTheme
/**
* Top level composable for the unlock screen.
@ -60,10 +57,8 @@ fun UnlockScreen(
when (val dialog = state.dialog) {
is UnlockState.Dialog.Error -> BitwardenBasicDialog(
visibilityState = BasicDialogState.Shown(
title = BitwardenString.an_error_has_occurred.asText(),
message = dialog.message,
),
title = stringResource(id = BitwardenString.an_error_has_occurred),
message = dialog.message(),
onDismissRequest = remember(viewModel) {
{
viewModel.trySendAction(UnlockAction.DismissDialog)
@ -72,7 +67,7 @@ fun UnlockScreen(
)
UnlockState.Dialog.Loading -> BitwardenLoadingDialog(
visibilityState = LoadingDialogState.Shown(BitwardenString.loading.asText()),
text = stringResource(id = BitwardenString.loading),
)
null -> Unit
@ -107,12 +102,9 @@ fun UnlockScreen(
BitwardenScaffold(
modifier = Modifier
.fillMaxSize(),
) { innerPadding ->
Box {
) {
Column(
modifier = Modifier
.padding(innerPadding)
.fillMaxSize(),
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
@ -122,12 +114,12 @@ fun UnlockScreen(
.width(220.dp)
.height(74.dp)
.fillMaxWidth(),
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.primary),
colorFilter = ColorFilter.tint(BitwardenTheme.colorScheme.icon.secondary),
painter = painterResource(id = BitwardenDrawable.ic_logo_horizontal),
contentDescription = stringResource(BitwardenString.bitwarden_authenticator),
)
Spacer(modifier = Modifier.height(32.dp))
AuthenticatorFilledTonalButton(
BitwardenFilledButton(
label = stringResource(id = BitwardenString.use_biometrics_to_unlock),
onClick = {
biometricsManager.promptBiometrics(
@ -145,7 +137,9 @@ fun UnlockScreen(
.padding(horizontal = 16.dp)
.fillMaxWidth(),
)
}
Spacer(modifier = Modifier.height(height = 12.dp))
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
}

View File

@ -1,44 +1,31 @@
package com.bitwarden.authenticator.ui.authenticator.feature.edititem
import android.widget.Toast
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Row
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.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FabPosition
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
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.draw.clip
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTag
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
@ -46,24 +33,23 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.bitwarden.authenticator.data.authenticator.datasource.disk.entity.AuthenticatorItemAlgorithm
import com.bitwarden.authenticator.data.authenticator.datasource.disk.entity.AuthenticatorItemType
import com.bitwarden.authenticator.ui.authenticator.feature.edititem.model.EditItemData
import com.bitwarden.authenticator.ui.platform.components.appbar.AuthenticatorTopAppBar
import com.bitwarden.authenticator.ui.platform.components.button.AuthenticatorTextButton
import com.bitwarden.authenticator.ui.platform.components.content.AuthenticatorErrorContent
import com.bitwarden.authenticator.ui.platform.components.content.AuthenticatorLoadingContent
import com.bitwarden.authenticator.ui.platform.components.dialog.BasicDialogState
import com.bitwarden.authenticator.ui.platform.components.dialog.BitwardenBasicDialog
import com.bitwarden.authenticator.ui.platform.components.dialog.BitwardenLoadingDialog
import com.bitwarden.authenticator.ui.platform.components.dialog.LoadingDialogState
import com.bitwarden.authenticator.ui.platform.components.dropdown.BitwardenMultiSelectButton
import com.bitwarden.authenticator.ui.platform.components.field.BitwardenPasswordField
import com.bitwarden.authenticator.ui.platform.components.field.BitwardenTextField
import com.bitwarden.authenticator.ui.platform.components.header.BitwardenListHeaderText
import com.bitwarden.authenticator.ui.platform.components.scaffold.BitwardenScaffold
import com.bitwarden.authenticator.ui.platform.components.stepper.BitwardenStepper
import com.bitwarden.authenticator.ui.platform.components.toggle.BitwardenSwitch
import com.bitwarden.ui.platform.base.util.EventsEffect
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
import com.bitwarden.ui.platform.components.button.BitwardenTextButton
import com.bitwarden.ui.platform.components.content.BitwardenErrorContent
import com.bitwarden.ui.platform.components.content.BitwardenLoadingContent
import com.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
import com.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
import com.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectButton
import com.bitwarden.ui.platform.components.field.BitwardenPasswordField
import com.bitwarden.ui.platform.components.field.BitwardenTextField
import com.bitwarden.ui.platform.components.header.BitwardenExpandingHeader
import com.bitwarden.ui.platform.components.header.BitwardenListHeaderText
import com.bitwarden.ui.platform.components.model.CardStyle
import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.bitwarden.ui.platform.components.stepper.BitwardenStepper
import com.bitwarden.ui.platform.components.toggle.BitwardenSwitch
import com.bitwarden.ui.platform.resource.BitwardenDrawable
import com.bitwarden.ui.platform.resource.BitwardenPlurals
import com.bitwarden.ui.platform.resource.BitwardenString
@ -82,7 +68,7 @@ fun EditItemScreen(
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
val context = LocalContext.current
val resources = context.resources
val resources = LocalResources.current
EventsEffect(viewModel = viewModel) { event ->
when (event) {
@ -112,10 +98,8 @@ fun EditItemScreen(
.fillMaxSize()
.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
AuthenticatorTopAppBar(
title = stringResource(
id = BitwardenString.edit,
),
BitwardenTopAppBar(
title = stringResource(id = BitwardenString.edit),
scrollBehavior = scrollBehavior,
navigationIcon = painterResource(id = BitwardenDrawable.ic_close),
navigationIconContentDescription = stringResource(id = BitwardenString.close),
@ -125,7 +109,7 @@ fun EditItemScreen(
}
},
actions = {
AuthenticatorTextButton(
BitwardenTextButton(
label = stringResource(id = BitwardenString.save),
onClick = remember(viewModel) {
{ viewModel.trySendAction(EditItemAction.SaveClick) }
@ -136,12 +120,10 @@ fun EditItemScreen(
)
},
floatingActionButtonPosition = FabPosition.EndOverlay,
) { innerPadding ->
) {
when (val viewState = state.viewState) {
is EditItemState.ViewState.Content -> {
EditItemContent(
modifier = Modifier
.padding(innerPadding),
viewState = viewState,
onIssuerNameTextChange = remember(viewModel) {
{
@ -210,16 +192,11 @@ fun EditItemScreen(
}
is EditItemState.ViewState.Error -> {
AuthenticatorErrorContent(
message = viewState.message(),
modifier = Modifier.padding(innerPadding),
)
BitwardenErrorContent(message = viewState.message())
}
EditItemState.ViewState.Loading -> {
AuthenticatorLoadingContent(
modifier = Modifier.padding(innerPadding),
)
BitwardenLoadingContent()
}
}
}
@ -245,9 +222,11 @@ fun EditItemContent(
) {
LazyColumn(modifier = modifier) {
item {
Spacer(modifier = Modifier.height(height = 12.dp))
BitwardenListHeaderText(
modifier = Modifier
.standardHorizontalMargin()
.padding(horizontal = 16.dp)
.fillMaxWidth(),
label = stringResource(id = BitwardenString.information),
)
@ -264,11 +243,11 @@ fun EditItemContent(
value = viewState.itemData.issuer,
onValueChange = onIssuerNameTextChange,
singleLine = true,
cardStyle = CardStyle.Top(),
)
}
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenPasswordField(
modifier = Modifier
.testTag(tag = "KeyTextField")
@ -278,12 +257,11 @@ fun EditItemContent(
value = viewState.itemData.totpCode,
onValueChange = onTotpCodeTextChange,
singleLine = true,
capitalization = KeyboardCapitalization.Characters,
cardStyle = CardStyle.Middle(),
)
}
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
modifier = Modifier
.testTag(tag = "UsernameTextField")
@ -293,15 +271,16 @@ fun EditItemContent(
value = viewState.itemData.username.orEmpty(),
onValueChange = onUsernameTextChange,
singleLine = true,
cardStyle = CardStyle.Middle(),
)
}
item {
Spacer(modifier = Modifier.height(16.dp))
BitwardenSwitch(
label = stringResource(id = BitwardenString.favorite),
isChecked = viewState.itemData.favorite,
onCheckedChange = onToggleFavorite,
cardStyle = CardStyle.Bottom,
modifier = Modifier
.testTag(tag = "ItemFavoriteToggle")
.fillMaxWidth()
@ -310,48 +289,14 @@ fun EditItemContent(
}
item(key = "AdvancedOptions") {
val iconRotationDegrees = animateFloatAsState(
targetValue = if (viewState.isAdvancedOptionsExpanded) 180f else 0f,
label = "expanderIconRotationAnimation",
)
Spacer(modifier = Modifier.height(16.dp))
Row(
modifier = Modifier
.testTag(tag = "CollapseAdvancedOptions")
.standardHorizontalMargin()
.fillMaxWidth()
.clip(RoundedCornerShape(28.dp))
.clickable(
indication = ripple(
bounded = true,
color = MaterialTheme.colorScheme.primary,
),
interactionSource = remember { MutableInteractionSource() },
BitwardenExpandingHeader(
isExpanded = viewState.isAdvancedOptionsExpanded,
onClick = onExpandAdvancedOptionsClicked,
)
.padding(vertical = 12.dp)
.animateItem(),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = stringResource(BitwardenString.advanced),
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.primary,
)
Spacer(Modifier.width(8.dp))
Icon(
painter = rememberVectorPainter(id = BitwardenDrawable.ic_chevron_down),
contentDescription = if (viewState.isAdvancedOptionsExpanded) {
stringResource(BitwardenString.collapse_advanced_options)
} else {
stringResource(BitwardenString.expand_advanced_options)
},
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier
.rotate(degrees = iconRotationDegrees.value),
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
}
if (viewState.isAdvancedOptionsExpanded) {
advancedOptions(
@ -380,8 +325,7 @@ private fun LazyListScope.advancedOptions(
) {
item(key = "OtpItemTypeSelector") {
val possibleTypeOptions = AuthenticatorItemType.entries
val typeOptionsWithStrings =
possibleTypeOptions.associateWith { it.name }
val typeOptionsWithStrings = possibleTypeOptions.associateWith { it.name }
BitwardenMultiSelectButton(
modifier = Modifier
.testTag(tag = "OTPItemTypePicker")
@ -391,6 +335,7 @@ private fun LazyListScope.advancedOptions(
label = stringResource(id = BitwardenString.otp_type),
options = typeOptionsWithStrings.values.toImmutableList(),
selectedOption = viewState.itemData.type.name,
cardStyle = CardStyle.Top(),
onOptionSelected = { selectedOption ->
val selectedOptionName = typeOptionsWithStrings
.entries
@ -404,7 +349,6 @@ private fun LazyListScope.advancedOptions(
item(key = "AlgorithmItemTypeSelector") {
val possibleAlgorithmOptions = AuthenticatorItemAlgorithm.entries
val algorithmOptionsWithStrings = possibleAlgorithmOptions.associateWith { it.name }
Spacer(Modifier.height(8.dp))
BitwardenMultiSelectButton(
modifier = Modifier
.testTag(tag = "AlgorithmItemTypePicker")
@ -414,6 +358,7 @@ private fun LazyListScope.advancedOptions(
label = stringResource(id = BitwardenString.algorithm),
options = algorithmOptionsWithStrings.values.toImmutableList(),
selectedOption = viewState.itemData.algorithm.name,
cardStyle = CardStyle.Middle(),
onOptionSelected = { selectedOption ->
val selectedOptionName = algorithmOptionsWithStrings
.entries
@ -433,7 +378,6 @@ private fun LazyListScope.advancedOptions(
formatArgs = arrayOf(it.seconds),
)
}
Spacer(modifier = Modifier.height(8.dp))
BitwardenMultiSelectButton(
modifier = Modifier
.testTag(tag = "RefreshPeriodItemTypePicker")
@ -445,6 +389,7 @@ private fun LazyListScope.advancedOptions(
selectedOption = refreshPeriodOptionsWithStrings.getValue(
key = viewState.itemData.refreshPeriod,
),
cardStyle = CardStyle.Middle(),
onOptionSelected = remember(viewState) {
{ selectedOption ->
val selectedOptionName = refreshPeriodOptionsWithStrings
@ -458,7 +403,6 @@ private fun LazyListScope.advancedOptions(
}
item(key = "DigitsCounterItem") {
Spacer(modifier = Modifier.height(8.dp))
DigitsCounterItem(
modifier = Modifier
.fillMaxWidth()
@ -480,17 +424,15 @@ private fun EditItemDialogs(
when (dialogState) {
is EditItemState.DialogState.Generic -> {
BitwardenBasicDialog(
visibilityState = BasicDialogState.Shown(
title = dialogState.title,
message = dialogState.message,
),
title = dialogState.title(),
message = dialogState.message(),
onDismissRequest = onDismissRequest,
)
}
is EditItemState.DialogState.Loading -> {
BitwardenLoadingDialog(
visibilityState = LoadingDialogState.Shown(dialogState.message),
text = dialogState.message(),
)
}
@ -511,8 +453,7 @@ private fun DigitsCounterItem(
value = digits.coerceIn(minValue, maxValue),
range = minValue..maxValue,
onValueChange = onDigitsCounterChange,
increaseButtonTestTag = "DigitsIncreaseButton",
decreaseButtonTestTag = "DigitsDecreaseButton",
cardStyle = CardStyle.Bottom,
modifier = modifier.testTag(tag = "DigitsValueLabel"),
)
}

View File

@ -5,10 +5,8 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
@ -21,6 +19,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.bitwarden.ui.platform.resource.BitwardenDrawable
import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.platform.theme.BitwardenTheme
/**
* Show a snackbar that says "Account synced from Bitwarden app" with a close action.
@ -40,8 +39,8 @@ fun FirstTimeSyncSnackbarHost(
.fillMaxWidth()
.shadow(elevation = 6.dp)
.background(
color = MaterialTheme.colorScheme.inverseSurface,
shape = RoundedCornerShape(8.dp),
color = BitwardenTheme.colorScheme.background.alert,
shape = BitwardenTheme.shapes.snackbar,
),
verticalAlignment = Alignment.CenterVertically,
) {
@ -50,8 +49,8 @@ fun FirstTimeSyncSnackbarHost(
.padding(16.dp)
.weight(1f, fill = true),
text = stringResource(BitwardenString.account_synced_from_bitwarden_app),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.inverseOnSurface,
style = BitwardenTheme.typography.bodyLarge,
color = BitwardenTheme.colorScheme.text.reversed,
)
IconButton(
onClick = { state.currentSnackbarData?.dismiss() },
@ -59,7 +58,7 @@ fun FirstTimeSyncSnackbarHost(
Icon(
painter = painterResource(id = BitwardenDrawable.ic_close),
contentDescription = stringResource(id = BitwardenString.close),
tint = MaterialTheme.colorScheme.inverseOnSurface,
tint = BitwardenTheme.colorScheme.icon.reversed,
modifier = Modifier
.size(24.dp),
)

View File

@ -2,6 +2,7 @@ package com.bitwarden.authenticator.ui.authenticator.feature.itemlisting
import android.Manifest
import android.widget.Toast
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@ -10,22 +11,19 @@ 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.layout.size
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.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FabPosition
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
@ -43,10 +41,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTag
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@ -57,30 +54,29 @@ import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.model.It
import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.model.VaultDropdownMenuAction
import com.bitwarden.authenticator.ui.authenticator.feature.model.SharedCodesDisplayState
import com.bitwarden.authenticator.ui.authenticator.feature.model.VerificationCodeDisplayItem
import com.bitwarden.authenticator.ui.platform.components.appbar.AuthenticatorMediumTopAppBar
import com.bitwarden.authenticator.ui.platform.components.appbar.AuthenticatorTopAppBar
import com.bitwarden.authenticator.ui.platform.components.appbar.action.AuthenticatorSearchActionItem
import com.bitwarden.authenticator.ui.platform.components.button.AuthenticatorFilledButton
import com.bitwarden.authenticator.ui.platform.components.button.AuthenticatorFilledTonalButton
import com.bitwarden.authenticator.ui.platform.components.button.AuthenticatorTextButton
import com.bitwarden.authenticator.ui.platform.components.card.AuthenticatorActionCard
import com.bitwarden.authenticator.ui.platform.components.dialog.BasicDialogState
import com.bitwarden.authenticator.ui.platform.components.dialog.BitwardenBasicDialog
import com.bitwarden.authenticator.ui.platform.components.dialog.BitwardenLoadingDialog
import com.bitwarden.authenticator.ui.platform.components.dialog.BitwardenTwoButtonDialog
import com.bitwarden.authenticator.ui.platform.components.dialog.LoadingDialogState
import com.bitwarden.authenticator.ui.platform.components.fab.ExpandableFabIcon
import com.bitwarden.authenticator.ui.platform.components.fab.ExpandableFloatingActionButton
import com.bitwarden.authenticator.ui.platform.components.header.AuthenticatorExpandingHeader
import com.bitwarden.authenticator.ui.platform.components.header.BitwardenListHeaderTextWithSupportLabel
import com.bitwarden.authenticator.ui.platform.components.model.IconResource
import com.bitwarden.authenticator.ui.platform.components.scaffold.BitwardenScaffold
import com.bitwarden.authenticator.ui.platform.composition.LocalPermissionsManager
import com.bitwarden.authenticator.ui.platform.manager.permissions.PermissionsManager
import com.bitwarden.authenticator.ui.platform.theme.Typography
import com.bitwarden.authenticator.ui.platform.util.startAuthenticatorAppSettings
import com.bitwarden.authenticator.ui.platform.util.startBitwardenAccountSettings
import com.bitwarden.ui.platform.base.util.EventsEffect
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.bitwarden.ui.platform.base.util.toListItemCardStyle
import com.bitwarden.ui.platform.components.appbar.BitwardenMediumTopAppBar
import com.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
import com.bitwarden.ui.platform.components.appbar.action.BitwardenSearchActionItem
import com.bitwarden.ui.platform.components.button.BitwardenFilledButton
import com.bitwarden.ui.platform.components.button.BitwardenTextButton
import com.bitwarden.ui.platform.components.card.BitwardenActionCard
import com.bitwarden.ui.platform.components.card.color.bitwardenCardColors
import com.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
import com.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
import com.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
import com.bitwarden.ui.platform.components.fab.BitwardenExpandableFloatingActionButton
import com.bitwarden.ui.platform.components.fab.ExpandableFabIcon
import com.bitwarden.ui.platform.components.header.BitwardenListHeaderText
import com.bitwarden.ui.platform.components.icon.model.IconData
import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.bitwarden.ui.platform.composition.LocalIntentManager
import com.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
@ -89,6 +85,7 @@ import com.bitwarden.ui.platform.resource.BitwardenDrawable
import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.platform.theme.BitwardenTheme
import com.bitwarden.ui.util.asText
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.launch
/**
@ -310,18 +307,14 @@ private fun ItemListingDialogs(
when (dialog) {
ItemListingState.DialogState.Loading -> {
BitwardenLoadingDialog(
visibilityState = LoadingDialogState.Shown(
text = BitwardenString.syncing.asText(),
),
text = stringResource(id = BitwardenString.syncing),
)
}
is ItemListingState.DialogState.Error -> {
BitwardenBasicDialog(
visibilityState = BasicDialogState.Shown(
title = dialog.title,
message = dialog.message,
),
title = dialog.title(),
message = dialog.message(),
onDismissRequest = onDismissRequest,
)
}
@ -368,11 +361,11 @@ private fun ItemListingContent(
.fillMaxSize()
.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
AuthenticatorMediumTopAppBar(
BitwardenMediumTopAppBar(
title = stringResource(id = BitwardenString.verification_codes),
scrollBehavior = scrollBehavior,
actions = {
AuthenticatorSearchActionItem(
BitwardenSearchActionItem(
contentDescription = stringResource(id = BitwardenString.search_codes),
onClick = onNavigateToSearch,
)
@ -380,53 +373,43 @@ private fun ItemListingContent(
)
},
floatingActionButton = {
ExpandableFloatingActionButton(
modifier = Modifier
.semantics { testTag = "AddItemButton" }
.padding(bottom = 16.dp),
label = BitwardenString.add_item.asText(),
items = listOf(
BitwardenExpandableFloatingActionButton(
modifier = Modifier.testTag("AddItemButton"),
items = persistentListOf(
ItemListingExpandableFabAction.ScanQrCode(
label = BitwardenString.scan_a_qr_code.asText(),
icon = IconResource(
iconPainter = painterResource(id = BitwardenDrawable.ic_camera),
contentDescription = stringResource(
id = BitwardenString.scan_a_qr_code,
),
icon = IconData.Local(
iconRes = BitwardenDrawable.ic_camera_small,
contentDescription = BitwardenString.scan_a_qr_code.asText(),
testTag = "ScanQRCodeButton",
),
onScanQrCodeClick = onScanQrCodeClick,
),
ItemListingExpandableFabAction.EnterSetupKey(
label = BitwardenString.enter_key_manually.asText(),
icon = IconResource(
iconPainter = painterResource(id = BitwardenDrawable.ic_keyboard),
contentDescription = stringResource(
id = BitwardenString.enter_key_manually,
),
icon = IconData.Local(
iconRes = BitwardenDrawable.ic_lock_encrypted_small,
contentDescription = BitwardenString.enter_key_manually.asText(),
testTag = "EnterSetupKeyButton",
),
onEnterSetupKeyClick = onEnterSetupKeyClick,
),
),
expandableFabIcon = ExpandableFabIcon(
iconData = IconResource(
iconPainter = painterResource(id = BitwardenDrawable.ic_plus),
contentDescription = stringResource(id = BitwardenString.add_item),
icon = IconData.Local(
iconRes = BitwardenDrawable.ic_plus,
contentDescription = BitwardenString.add_item.asText(),
testTag = "AddItemButton",
),
iconRotation = 45f,
),
)
},
floatingActionButtonPosition = FabPosition.EndOverlay,
snackbarHost = { FirstTimeSyncSnackbarHost(state = snackbarHostState) },
) { paddingValues ->
) {
var isLocalHeaderExpanded by rememberSaveable { mutableStateOf(true) }
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
modifier = Modifier.fillMaxSize(),
) {
item(key = "action_card") {
ActionCard(
@ -437,30 +420,29 @@ private fun ItemListingContent(
onSyncWithBitwardenDismissClick = onDismissSyncWithBitwardenClick,
onSyncLearnMoreClick = onSyncLearnMoreClick,
modifier = Modifier
.padding(all = 16.dp)
.standardHorizontalMargin()
.padding(top = 12.dp, bottom = 16.dp)
.animateItem(),
)
}
if (state.favoriteItems.isNotEmpty()) {
item(key = "favorites_header") {
BitwardenListHeaderTextWithSupportLabel(
BitwardenListHeaderText(
label = stringResource(id = BitwardenString.favorites),
supportingLabel = state.favoriteItems.count().toString(),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp)
.animateItem(),
)
Spacer(modifier = Modifier.height(height = 8.dp))
}
item {
Spacer(modifier = Modifier.height(4.dp))
}
items(
itemsIndexed(
items = state.favoriteItems,
key = { "favorite_item_${it.id}" },
) {
key = { _, it -> "favorite_item_${it.id}" },
) { index, it ->
VaultVerificationCodeItem(
authCode = it.authCode,
primaryLabel = it.title,
@ -475,18 +457,10 @@ private fun ItemListingContent(
},
showMoveToBitwarden = it.showMoveToBitwarden,
allowLongPress = it.allowLongPressActions,
modifier = Modifier.fillMaxWidth(),
)
}
item(key = "favorites_divider") {
HorizontalDivider(
thickness = 1.dp,
color = MaterialTheme.colorScheme.outlineVariant,
cardStyle = state.favoriteItems.toListItemCardStyle(index = index),
modifier = Modifier
.fillMaxWidth()
.padding(all = 16.dp)
.animateItem(),
.standardHorizontalMargin()
.fillMaxWidth(),
)
}
}
@ -511,16 +485,17 @@ private fun ItemListingContent(
},
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.animateItem(),
)
}
}
if (isLocalHeaderExpanded) {
items(
itemsIndexed(
items = state.itemList,
key = { "local_item_${it.id}" },
) {
key = { _, it -> "local_item_${it.id}" },
) { index, it ->
VaultVerificationCodeItem(
authCode = it.authCode,
primaryLabel = it.title,
@ -535,7 +510,9 @@ private fun ItemListingContent(
},
showMoveToBitwarden = it.showMoveToBitwarden,
allowLongPress = it.allowLongPressActions,
cardStyle = state.itemList.toListItemCardStyle(index = index),
modifier = Modifier
.standardHorizontalMargin()
.fillMaxWidth()
.animateItem(),
)
@ -561,14 +538,15 @@ private fun ItemListingContent(
},
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.animateItem(),
)
}
if (section.isExpanded) {
items(
itemsIndexed(
items = section.codes,
key = { code -> "code_${code.id}" },
) {
key = { _, code -> "code_${code.id}" },
) { index, it ->
VaultVerificationCodeItem(
authCode = it.authCode,
primaryLabel = it.title,
@ -583,7 +561,9 @@ private fun ItemListingContent(
},
showMoveToBitwarden = it.showMoveToBitwarden,
allowLongPress = it.allowLongPressActions,
cardStyle = section.codes.toListItemCardStyle(index = index),
modifier = Modifier
.standardHorizontalMargin()
.fillMaxWidth()
.animateItem(),
)
@ -596,9 +576,10 @@ private fun ItemListingContent(
item(key = "shared_codes_error") {
Text(
text = stringResource(BitwardenString.shared_codes_error),
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodySmall,
color = BitwardenTheme.colorScheme.text.secondary,
style = BitwardenTheme.typography.bodySmall,
modifier = Modifier
.standardHorizontalMargin()
.padding(horizontal = 16.dp)
.animateItem(),
)
@ -609,7 +590,8 @@ private fun ItemListingContent(
// Add a spacer item to prevent the FAB from hiding verification codes at the
// bottom of the list
item {
Spacer(Modifier.height(72.dp))
Spacer(modifier = Modifier.height(height = 88.dp))
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
}
@ -642,59 +624,49 @@ fun EmptyItemListingContent(
.fillMaxSize()
.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
AuthenticatorTopAppBar(
BitwardenTopAppBar(
title = stringResource(id = BitwardenString.verification_codes),
scrollBehavior = scrollBehavior,
navigationIcon = null,
actions = { },
)
},
floatingActionButton = {
ExpandableFloatingActionButton(
modifier = Modifier
.semantics { testTag = "AddItemButton" }
.padding(bottom = 16.dp),
label = BitwardenString.add_item.asText(),
items = listOf(
BitwardenExpandableFloatingActionButton(
modifier = Modifier.testTag("AddItemButton"),
items = persistentListOf(
ItemListingExpandableFabAction.ScanQrCode(
label = BitwardenString.scan_a_qr_code.asText(),
icon = IconResource(
iconPainter = painterResource(id = BitwardenDrawable.ic_camera),
contentDescription = stringResource(
id = BitwardenString.scan_a_qr_code,
),
icon = IconData.Local(
iconRes = BitwardenDrawable.ic_camera_small,
contentDescription = BitwardenString.scan_a_qr_code.asText(),
testTag = "ScanQRCodeButton",
),
onScanQrCodeClick = onScanQrCodeClick,
),
ItemListingExpandableFabAction.EnterSetupKey(
label = BitwardenString.enter_key_manually.asText(),
icon = IconResource(
iconPainter = painterResource(id = BitwardenDrawable.ic_keyboard),
contentDescription = stringResource(
id = BitwardenString.enter_key_manually,
),
icon = IconData.Local(
iconRes = BitwardenDrawable.ic_lock_encrypted_small,
contentDescription = BitwardenString.enter_key_manually.asText(),
testTag = "EnterSetupKeyButton",
),
onEnterSetupKeyClick = onEnterSetupKeyClick,
),
),
expandableFabIcon = ExpandableFabIcon(
iconData = IconResource(
iconPainter = painterResource(id = BitwardenDrawable.ic_plus),
contentDescription = stringResource(id = BitwardenString.add_item),
icon = IconData.Local(
iconRes = BitwardenDrawable.ic_plus,
contentDescription = BitwardenString.add_item.asText(),
testTag = "AddItemButton",
),
iconRotation = 45f,
),
)
},
floatingActionButtonPosition = FabPosition.EndOverlay,
) { innerPadding ->
) {
Column(
modifier = modifier
.fillMaxSize()
.padding(paddingValues = innerPadding)
.verticalScroll(rememberScrollState()),
verticalArrangement = when (actionCardState) {
ItemListingState.ActionCardState.None -> Arrangement.Center
@ -709,19 +681,15 @@ fun EmptyItemListingContent(
onSyncWithBitwardenClick = onSyncWithBitwardenClick,
onSyncWithBitwardenDismissClick = onDismissSyncWithBitwardenClick,
onSyncLearnMoreClick = onSyncLearnMoreClick,
modifier = Modifier
.standardHorizontalMargin()
.padding(top = 12.dp, bottom = 16.dp),
)
// Add a spacer if an action card is showing:
when (actionCardState) {
ItemListingState.ActionCardState.None -> Unit
ItemListingState.ActionCardState.DownloadBitwardenApp,
ItemListingState.ActionCardState.SyncWithBitwarden,
-> Spacer(Modifier.height(16.dp))
}
Column(
modifier = modifier
.fillMaxSize()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
@ -744,7 +712,7 @@ fun EmptyItemListingContent(
Spacer(modifier = Modifier.height(16.dp))
Text(
text = stringResource(id = BitwardenString.you_dont_have_items_to_display),
style = Typography.titleMedium,
style = BitwardenTheme.typography.titleMedium,
)
Spacer(modifier = Modifier.height(16.dp))
@ -754,13 +722,16 @@ fun EmptyItemListingContent(
)
Spacer(modifier = Modifier.height(16.dp))
AuthenticatorFilledTonalButton(
BitwardenFilledButton(
modifier = Modifier
.semantics { testTag = "AddCodeButton" }
.testTag("AddCodeButton")
.fillMaxWidth(),
label = stringResource(BitwardenString.add_code),
onClick = onAddCodeClick,
)
Spacer(modifier = Modifier.height(height = 12.dp))
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
}
@ -771,26 +742,20 @@ private fun DownloadBitwardenActionCard(
modifier: Modifier = Modifier,
onDismissClick: () -> Unit,
onDownloadBitwardenClick: () -> Unit,
) = AuthenticatorActionCard(
) = BitwardenActionCard(
modifier = modifier,
actionIcon = rememberVectorPainter(BitwardenDrawable.ic_shield),
actionText = stringResource(BitwardenString.download_bitwarden_card_message),
callToActionText = stringResource(BitwardenString.download_now),
titleText = stringResource(BitwardenString.download_bitwarden_card_title),
onCardClicked = onDownloadBitwardenClick,
trailingContent = {
IconButton(
onClick = onDismissClick,
) {
cardSubtitle = stringResource(BitwardenString.download_bitwarden_card_message),
actionText = stringResource(BitwardenString.download_now),
cardTitle = stringResource(BitwardenString.download_bitwarden_card_title),
onActionClick = onDownloadBitwardenClick,
leadingContent = {
Icon(
painter = painterResource(id = BitwardenDrawable.ic_close),
contentDescription = stringResource(id = BitwardenString.close),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier
.size(24.dp),
painter = rememberVectorPainter(BitwardenDrawable.ic_shield),
contentDescription = null,
tint = BitwardenTheme.colorScheme.icon.secondary,
)
}
},
onDismissClick = onDismissClick,
)
@Suppress("LongMethod")
@ -803,12 +768,10 @@ private fun SyncWithBitwardenActionCard(
) {
Card(
modifier = modifier,
shape = RoundedCornerShape(size = 16.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceContainer,
disabledContainerColor = MaterialTheme.colorScheme.surfaceContainer,
),
shape = BitwardenTheme.shapes.actionCard,
colors = bitwardenCardColors(),
elevation = CardDefaults.elevatedCardElevation(),
border = BorderStroke(width = 1.dp, color = BitwardenTheme.colorScheme.stroke.border),
) {
Spacer(Modifier.height(height = 4.dp))
Row(modifier = Modifier.fillMaxWidth()) {
@ -820,14 +783,14 @@ private fun SyncWithBitwardenActionCard(
Icon(
painter = rememberVectorPainter(id = BitwardenDrawable.ic_shield),
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
tint = BitwardenTheme.colorScheme.icon.secondary,
modifier = Modifier.size(size = 20.dp),
)
Spacer(Modifier.width(width = 16.dp))
Text(
text = stringResource(id = BitwardenString.sync_with_the_bitwarden_app),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
style = BitwardenTheme.typography.bodyLarge,
color = BitwardenTheme.colorScheme.text.primary,
)
}
Spacer(Modifier.weight(weight = 1f))
@ -836,7 +799,7 @@ private fun SyncWithBitwardenActionCard(
Icon(
painter = painterResource(id = BitwardenDrawable.ic_close),
contentDescription = stringResource(id = BitwardenString.close),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
tint = BitwardenTheme.colorScheme.icon.primary,
modifier = Modifier.size(size = 24.dp),
)
}
@ -844,22 +807,22 @@ private fun SyncWithBitwardenActionCard(
}
Text(
text = stringResource(id = BitwardenString.sync_with_bitwarden_action_card_message),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = BitwardenTheme.typography.bodyMedium,
color = BitwardenTheme.colorScheme.text.primary,
modifier = Modifier
.padding(horizontal = 16.dp)
.padding(start = 36.dp, end = 48.dp)
.fillMaxWidth(),
)
Spacer(Modifier.height(height = 16.dp))
AuthenticatorFilledButton(
BitwardenFilledButton(
label = stringResource(id = BitwardenString.take_me_to_app_settings),
onClick = onAppSettingsClick,
modifier = Modifier
.padding(horizontal = 16.dp)
.fillMaxWidth(),
)
AuthenticatorTextButton(
BitwardenTextButton(
label = stringResource(id = BitwardenString.learn_more),
onClick = onLearnMoreClick,
modifier = Modifier

View File

@ -6,6 +6,7 @@ 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.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.padding
@ -14,7 +15,6 @@ import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
@ -24,20 +24,22 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTag
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.model.VaultDropdownMenuAction
import com.bitwarden.authenticator.ui.platform.components.indicator.AuthenticatorCircularCountdownIndicator
import com.bitwarden.authenticator.ui.platform.theme.AuthenticatorTheme
import com.bitwarden.ui.platform.base.util.cardBackground
import com.bitwarden.ui.platform.base.util.cardPadding
import com.bitwarden.ui.platform.components.icon.BitwardenIcon
import com.bitwarden.ui.platform.components.icon.model.IconData
import com.bitwarden.ui.platform.components.indicator.BitwardenCircularCountdownIndicator
import com.bitwarden.ui.platform.components.model.CardStyle
import com.bitwarden.ui.platform.resource.BitwardenDrawable
import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.platform.theme.BitwardenTheme
/**
* The verification code item displayed to the user.
@ -69,41 +71,46 @@ fun VaultVerificationCodeItem(
onDropdownMenuClick: (VaultDropdownMenuAction) -> Unit,
allowLongPress: Boolean,
showMoveToBitwarden: Boolean,
cardStyle: CardStyle,
modifier: Modifier = Modifier,
) {
var shouldShowDropdownMenu by remember { mutableStateOf(value = false) }
Box(modifier = modifier) {
Row(
modifier = Modifier
.semantics { testTag = "Item" }
.testTag(tag = "Item")
.defaultMinSize(minHeight = 60.dp)
.cardBackground(cardStyle = cardStyle)
.then(
if (allowLongPress) {
Modifier.combinedClickable(
interactionSource = remember { MutableInteractionSource() },
indication = ripple(color = MaterialTheme.colorScheme.primary),
indication = ripple(
color = BitwardenTheme.colorScheme.background.pressed,
),
onClick = onItemClick,
onLongClick = { shouldShowDropdownMenu = true },
)
} else {
Modifier.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = ripple(color = MaterialTheme.colorScheme.primary),
indication = ripple(
color = BitwardenTheme.colorScheme.background.pressed,
),
onClick = onItemClick,
)
},
)
.defaultMinSize(minHeight = 72.dp)
.padding(
vertical = 8.dp,
horizontal = 16.dp,
)
.then(modifier),
.cardPadding(
cardStyle = cardStyle,
paddingValues = PaddingValues(vertical = 8.dp, horizontal = 16.dp),
),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp),
) {
BitwardenIcon(
iconData = startIcon,
tint = MaterialTheme.colorScheme.onSurface,
tint = BitwardenTheme.colorScheme.icon.primary,
modifier = Modifier.size(24.dp),
)
@ -114,10 +121,10 @@ fun VaultVerificationCodeItem(
) {
if (!primaryLabel.isNullOrEmpty()) {
Text(
modifier = Modifier.semantics { testTag = "Name" },
modifier = Modifier.testTag("Name"),
text = primaryLabel,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
style = BitwardenTheme.typography.bodyLarge,
color = BitwardenTheme.colorScheme.text.primary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
@ -125,34 +132,36 @@ fun VaultVerificationCodeItem(
if (!secondaryLabel.isNullOrEmpty()) {
Text(
modifier = Modifier.semantics { testTag = "Username" },
modifier = Modifier.testTag("Username"),
text = secondaryLabel,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = BitwardenTheme.typography.bodyMedium,
color = BitwardenTheme.colorScheme.text.secondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
AuthenticatorCircularCountdownIndicator(
modifier = Modifier.semantics { testTag = "CircularCountDown" },
BitwardenCircularCountdownIndicator(
modifier = Modifier.testTag("CircularCountDown"),
timeLeftSeconds = timeLeftSeconds,
periodSeconds = periodSeconds,
alertThresholdSeconds = alertThresholdSeconds,
)
Text(
modifier = Modifier.semantics { testTag = "AuthCode" },
modifier = Modifier.testTag("AuthCode"),
text = authCode.chunked(3).joinToString(" "),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = BitwardenTheme.typography.sensitiveInfoSmall,
color = BitwardenTheme.colorScheme.text.primary,
)
}
DropdownMenu(
expanded = shouldShowDropdownMenu,
onDismissRequest = { shouldShowDropdownMenu = false },
shape = BitwardenTheme.shapes.menu,
containerColor = BitwardenTheme.colorScheme.background.primary,
) {
DropdownMenuItem(
text = {
@ -229,7 +238,7 @@ fun VaultVerificationCodeItem(
@Preview(showBackground = true)
@Composable
private fun VerificationCodeItem_preview() {
AuthenticatorTheme {
BitwardenTheme {
VaultVerificationCodeItem(
authCode = "1234567890".chunked(3).joinToString(" "),
primaryLabel = "Issuer, AKA Name",
@ -243,6 +252,7 @@ private fun VerificationCodeItem_preview() {
allowLongPress = true,
modifier = Modifier.padding(horizontal = 16.dp),
showMoveToBitwarden = true,
cardStyle = CardStyle.Full,
)
}
}

View File

@ -1,16 +1,16 @@
package com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.model
import androidx.compose.material3.ExtendedFloatingActionButton
import com.bitwarden.authenticator.ui.platform.components.fab.ExpandableFabOption
import com.bitwarden.authenticator.ui.platform.components.model.IconResource
import com.bitwarden.ui.platform.components.fab.ExpandableFabOption
import com.bitwarden.ui.platform.components.icon.model.IconData
import com.bitwarden.ui.util.Text
/**
* Models [ExpandableFabOption]s that can be triggered by the [ExtendedFloatingActionButton].
*/
sealed class ItemListingExpandableFabAction(
label: Text?,
icon: IconResource,
label: Text,
icon: IconData.Local,
onFabOptionClick: () -> Unit,
) : ExpandableFabOption(label, icon, onFabOptionClick) {
@ -18,25 +18,25 @@ sealed class ItemListingExpandableFabAction(
* Indicates the Scan QR code button was clicked.
*/
class ScanQrCode(
label: Text?,
icon: IconResource,
label: Text,
icon: IconData.Local,
onScanQrCodeClick: () -> Unit,
) : ItemListingExpandableFabAction(
label,
icon,
onScanQrCodeClick,
label = label,
icon = icon,
onFabOptionClick = onScanQrCodeClick,
)
/**
* Indicates the Enter Key button was clicked.
*/
class EnterSetupKey(
label: Text?,
icon: IconResource,
label: Text,
icon: IconData.Local,
onEnterSetupKeyClick: () -> Unit,
) : ItemListingExpandableFabAction(
label,
icon,
onEnterSetupKeyClick,
label = label,
icon = icon,
onFabOptionClick = onEnterSetupKeyClick,
)
}

View File

@ -10,11 +10,10 @@ 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.wrapContentWidth
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
@ -24,38 +23,35 @@ 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.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.CustomAccessibilityAction
import androidx.compose.ui.semantics.customActions
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.bitwarden.authenticator.ui.platform.components.appbar.AuthenticatorTopAppBar
import com.bitwarden.authenticator.ui.platform.components.dialog.BasicDialogState
import com.bitwarden.authenticator.ui.platform.components.dialog.BitwardenBasicDialog
import com.bitwarden.authenticator.ui.platform.components.dialog.BitwardenLoadingDialog
import com.bitwarden.authenticator.ui.platform.components.dialog.BitwardenTwoButtonDialog
import com.bitwarden.authenticator.ui.platform.components.dialog.LoadingDialogState
import com.bitwarden.authenticator.ui.platform.components.field.BitwardenPasswordField
import com.bitwarden.authenticator.ui.platform.components.field.BitwardenTextField
import com.bitwarden.authenticator.ui.platform.components.scaffold.BitwardenScaffold
import com.bitwarden.authenticator.ui.platform.composition.LocalPermissionsManager
import com.bitwarden.authenticator.ui.platform.manager.permissions.PermissionsManager
import com.bitwarden.ui.platform.base.util.EventsEffect
import com.bitwarden.ui.platform.base.util.annotatedStringResource
import com.bitwarden.ui.platform.base.util.spanStyleOf
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
import com.bitwarden.ui.platform.components.button.BitwardenTextButton
import com.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
import com.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
import com.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
import com.bitwarden.ui.platform.components.field.BitwardenPasswordField
import com.bitwarden.ui.platform.components.field.BitwardenTextField
import com.bitwarden.ui.platform.components.model.CardStyle
import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.bitwarden.ui.platform.composition.LocalIntentManager
import com.bitwarden.ui.platform.manager.IntentManager
import com.bitwarden.ui.platform.resource.BitwardenDrawable
import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.platform.theme.BitwardenTheme
/**
* The screen to manually add a totp code.
@ -134,7 +130,7 @@ fun ManualCodeEntryScreen(
BitwardenScaffold(
modifier = Modifier.fillMaxSize(),
topBar = {
AuthenticatorTopAppBar(
BitwardenTopAppBar(
title = stringResource(id = BitwardenString.create_verification_code),
navigationIcon = painterResource(id = BitwardenDrawable.ic_close),
navigationIconContentDescription = stringResource(id = BitwardenString.close),
@ -144,7 +140,7 @@ fun ManualCodeEntryScreen(
scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()),
)
},
) { paddingValues ->
) {
ManualCodeEntryContent(
state = state,
onNameChange = remember(viewModel) {
@ -168,11 +164,11 @@ fun ManualCodeEntryScreen(
}
}
},
modifier = Modifier.padding(paddingValues = paddingValues),
)
}
}
@Suppress("LongMethod")
@Composable
private fun ManualCodeEntryContent(
state: ManualCodeEntryState,
@ -184,38 +180,40 @@ private fun ManualCodeEntryContent(
modifier: Modifier = Modifier,
) {
Column(modifier = modifier.verticalScroll(state = rememberScrollState())) {
Spacer(modifier = Modifier.height(height = 24.dp))
Text(
text = stringResource(id = BitwardenString.enter_key_manually),
style = MaterialTheme.typography.titleMedium,
style = BitwardenTheme.typography.titleMedium,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(height = 8.dp))
Spacer(modifier = Modifier.height(height = 24.dp))
BitwardenTextField(
label = stringResource(id = BitwardenString.name),
value = state.issuer,
onValueChange = onNameChange,
cardStyle = CardStyle.Top(),
modifier = Modifier
.testTag(tag = "NameTextField")
.fillMaxWidth()
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(height = 8.dp))
BitwardenPasswordField(
singleLine = false,
label = stringResource(id = BitwardenString.key),
value = state.code,
onValueChange = onKeyChange,
capitalization = KeyboardCapitalization.Characters,
cardStyle = CardStyle.Bottom,
modifier = Modifier
.testTag(tag = "KeyTextField")
.fillMaxWidth()
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(24.dp))
SaveManualCodeButtons(
state = state.buttonState,
onSaveLocallyClick = onSaveLocallyClick,
@ -224,57 +222,29 @@ private fun ManualCodeEntryContent(
.standardHorizontalMargin()
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(height = 8.dp))
Spacer(modifier = Modifier.height(height = 24.dp))
Text(
text = stringResource(id = BitwardenString.cannot_add_authenticator_key),
style = MaterialTheme.typography.bodyMedium,
color = BitwardenTheme.colorScheme.text.secondary,
style = BitwardenTheme.typography.bodyMedium,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(height = 8.dp))
ScanQrCodeText(
BitwardenTextButton(
label = stringResource(id = BitwardenString.scan_qr_code),
onClick = onScanQrCodeClick,
modifier = Modifier.standardHorizontalMargin(),
modifier = Modifier
.wrapContentWidth()
.align(alignment = Alignment.CenterHorizontally)
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(height = 16.dp))
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
@Composable
private fun ScanQrCodeText(
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val accessibilityString = stringResource(id = BitwardenString.scan_qr_code)
Text(
text = annotatedStringResource(
id = BitwardenString.scan_qr_code,
emphasisHighlightStyle = spanStyleOf(
color = MaterialTheme.colorScheme.primary,
textStyle = MaterialTheme.typography.bodyMedium,
),
onAnnotationClick = {
when (it) {
"scanQrCode" -> onClick()
}
},
),
modifier = modifier.semantics {
customActions = listOf(
CustomAccessibilityAction(
label = accessibilityString,
action = {
onClick()
true
},
),
)
},
)
}
@Composable
private fun ManualCodeEntryDialogs(
dialog: ManualCodeEntryState.DialogState?,
@ -283,16 +253,14 @@ private fun ManualCodeEntryDialogs(
when (val dialogString = dialog) {
is ManualCodeEntryState.DialogState.Error -> {
BitwardenBasicDialog(
visibilityState = BasicDialogState.Shown(
title = dialogString.title,
message = dialogString.message,
),
title = dialogString.title?.invoke(),
message = dialogString.message(),
onDismissRequest = onDismissRequest,
)
}
is ManualCodeEntryState.DialogState.Loading -> {
BitwardenLoadingDialog(visibilityState = LoadingDialogState.Shown(dialog.message))
BitwardenLoadingDialog(text = dialog.message())
}
null -> Unit

View File

@ -1,14 +1,16 @@
package com.bitwarden.authenticator.ui.authenticator.feature.manualcodeentry
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import com.bitwarden.authenticator.ui.platform.components.button.AuthenticatorFilledButton
import com.bitwarden.authenticator.ui.platform.components.button.AuthenticatorFilledTonalButton
import com.bitwarden.authenticator.ui.platform.components.button.AuthenticatorOutlinedButton
import androidx.compose.ui.unit.dp
import com.bitwarden.ui.platform.components.button.BitwardenFilledButton
import com.bitwarden.ui.platform.components.button.BitwardenOutlinedButton
import com.bitwarden.ui.platform.resource.BitwardenString
/**
@ -28,7 +30,7 @@ fun SaveManualCodeButtons(
) {
when (state) {
ManualCodeEntryState.ButtonState.LocalOnly -> {
AuthenticatorFilledTonalButton(
BitwardenFilledButton(
label = stringResource(id = BitwardenString.add_code),
onClick = onSaveLocallyClick,
modifier = modifier.testTag(tag = "AddCodeButton"),
@ -37,12 +39,13 @@ fun SaveManualCodeButtons(
ManualCodeEntryState.ButtonState.SaveLocallyPrimary -> {
Column(modifier = modifier) {
AuthenticatorFilledButton(
BitwardenFilledButton(
label = stringResource(id = BitwardenString.save_here),
onClick = onSaveLocallyClick,
modifier = Modifier.fillMaxWidth(),
)
AuthenticatorOutlinedButton(
Spacer(modifier = Modifier.height(height = 8.dp))
BitwardenOutlinedButton(
label = stringResource(BitwardenString.save_to_bitwarden),
onClick = onSaveToBitwardenClick,
modifier = Modifier.fillMaxWidth(),
@ -52,12 +55,13 @@ fun SaveManualCodeButtons(
ManualCodeEntryState.ButtonState.SaveToBitwardenPrimary -> {
Column(modifier = modifier) {
AuthenticatorFilledButton(
BitwardenFilledButton(
label = stringResource(id = BitwardenString.save_to_bitwarden),
onClick = onSaveToBitwardenClick,
modifier = Modifier.fillMaxWidth(),
)
AuthenticatorOutlinedButton(
Spacer(modifier = Modifier.height(height = 8.dp))
BitwardenOutlinedButton(
label = stringResource(BitwardenString.save_here),
onClick = onSaveLocallyClick,
modifier = Modifier.fillMaxWidth(),

View File

@ -1,40 +1,17 @@
package com.bitwarden.authenticator.ui.authenticator.feature.navbar
import android.os.Parcelable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.exclude
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBars
import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.NavigationBarItemDefaults
import androidx.compose.material3.ScaffoldDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTag
import androidx.compose.ui.text.style.TextOverflow
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavController
import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavHostController
import androidx.navigation.NavOptions
@ -46,17 +23,19 @@ import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.ItemList
import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.ItemListingRoute
import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.itemListingGraph
import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.navigateToItemListGraph
import com.bitwarden.authenticator.ui.platform.components.scaffold.BitwardenScaffold
import com.bitwarden.authenticator.ui.platform.components.scrim.BitwardenAnimatedScrim
import com.bitwarden.authenticator.ui.platform.feature.settings.SettingsGraphRoute
import com.bitwarden.authenticator.ui.platform.feature.settings.SettingsRoute
import com.bitwarden.authenticator.ui.platform.feature.settings.navigateToSettingsGraph
import com.bitwarden.ui.platform.base.util.EventsEffect
import com.bitwarden.ui.platform.base.util.max
import com.bitwarden.ui.platform.base.util.toDp
import com.bitwarden.ui.platform.components.navigation.model.NavigationItem
import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.bitwarden.ui.platform.components.scaffold.model.ScaffoldNavigationData
import com.bitwarden.ui.platform.resource.BitwardenDrawable
import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.platform.theme.RootTransitionProviders
import com.bitwarden.ui.platform.util.toObjectNavigationRoute
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.parcelize.Parcelize
@ -135,39 +114,30 @@ private fun AuthenticatorNavBarScaffold(
navigateToImport: () -> Unit,
navigateToTutorial: () -> Unit,
) {
var shouldDimNavBar by rememberSaveable { mutableStateOf(value = false) }
// This scaffold will host screens that contain top bars while not hosting one itself.
// We need to ignore the all insets here and let the content screens handle it themselves.
val navBackStackEntry by navController.currentBackStackEntryAsState()
BitwardenScaffold(
contentWindowInsets = ScaffoldDefaults.contentWindowInsets.exclude(WindowInsets.statusBars),
bottomBar = {
Box {
var appBarHeightPx by remember { mutableIntStateOf(0) }
AuthenticatorBottomAppBar(
modifier = Modifier
.onGloballyPositioned {
appBarHeightPx = it.size.height
},
navController = navController,
verificationCodesTabClickedAction = verificationTabClickedAction,
settingsTabClickedAction = settingsTabClickedAction,
)
BitwardenAnimatedScrim(
isVisible = false,
onClick = {
// Do nothing
},
modifier = Modifier
.fillMaxWidth()
.height(appBarHeightPx.toDp()),
)
contentWindowInsets = WindowInsets(),
navigationData = ScaffoldNavigationData(
navigationItems = AuthenticatorNavBarTab.navigationItems,
selectedNavigationItem = AuthenticatorNavBarTab
.navigationItems
.find { navBackStackEntry.isCurrentRoute(route = it.graphRoute) },
onNavigationClick = { navigationItem ->
when (navigationItem) {
AuthenticatorNavBarTab.VerificationCodes -> verificationTabClickedAction()
AuthenticatorNavBarTab.Settings -> settingsTabClickedAction()
}
},
) { innerPadding ->
shouldDimNavigation = shouldDimNavBar,
),
) {
NavHost(
navController = navController,
startDestination = ItemListingGraphRoute,
modifier = Modifier
.consumeWindowInsets(WindowInsets.navigationBars)
.consumeWindowInsets(WindowInsets.ime)
.padding(innerPadding.max(WindowInsets.ime)),
enterTransition = RootTransitionProviders.Enter.fadeIn,
exitTransition = RootTransitionProviders.Exit.fadeOut,
popEnterTransition = RootTransitionProviders.Enter.fadeIn,
@ -188,76 +158,6 @@ private fun AuthenticatorNavBarScaffold(
}
}
@Suppress("LongMethod")
@Composable
private fun AuthenticatorBottomAppBar(
navController: NavController,
verificationCodesTabClickedAction: () -> Unit,
settingsTabClickedAction: () -> Unit,
modifier: Modifier = Modifier,
) {
BottomAppBar(
containerColor = MaterialTheme.colorScheme.surfaceContainer,
modifier = modifier,
) {
val destinations = listOf(
AuthenticatorNavBarTab.VerificationCodes,
AuthenticatorNavBarTab.Settings,
)
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
destinations.forEach { destination ->
val isSelected = currentDestination?.hierarchy?.any {
it.route == destination.route
} == true
NavigationBarItem(
icon = {
Icon(
painter = painterResource(
id = if (isSelected) {
destination.iconResSelected
} else {
destination.iconRes
},
),
contentDescription = stringResource(
id = destination.contentDescriptionRes,
),
)
},
label = {
Text(
text = stringResource(id = destination.labelRes),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
},
selected = isSelected,
onClick = {
when (destination) {
AuthenticatorNavBarTab.VerificationCodes -> {
verificationCodesTabClickedAction()
}
AuthenticatorNavBarTab.Settings -> {
settingsTabClickedAction()
}
}
},
colors = NavigationBarItemDefaults.colors(
indicatorColor = MaterialTheme.colorScheme.secondaryContainer,
selectedIconColor = MaterialTheme.colorScheme.onSecondaryContainer,
unselectedIconColor = MaterialTheme.colorScheme.onSurface,
selectedTextColor = MaterialTheme.colorScheme.onSecondaryContainer,
unselectedTextColor = MaterialTheme.colorScheme.onSurface,
),
modifier = Modifier.semantics { testTag = destination.testTag },
)
}
}
}
/**
* Represents the different tabs available in the navigation bar
* for the authenticator screens.
@ -271,36 +171,17 @@ private fun AuthenticatorBottomAppBar(
* @property iconResSelected The resource ID for the icon representing the tab when it's selected.
*/
@Parcelize
private sealed class AuthenticatorNavBarTab : Parcelable {
/**
* The resource ID for the icon representing the tab when it is selected.
*/
abstract val iconResSelected: Int
private sealed class AuthenticatorNavBarTab : NavigationItem, Parcelable {
companion object {
/**
* Resource id for the icon representing the tab.
* The list of navigation tabs available in the authenticator.
*/
abstract val iconRes: Int
/**
* Resource id for the label describing the tab.
*/
abstract val labelRes: Int
/**
* Resource id for the content description describing the tab.
*/
abstract val contentDescriptionRes: Int
/**
* Route of the tab.
*/
abstract val route: String
/**
* The test tag of the tab.
*/
abstract val testTag: String
val navigationItems: ImmutableList<AuthenticatorNavBarTab> = persistentListOf(
VerificationCodes,
Settings,
)
}
/**
* Show the Verification Codes screen.
@ -311,8 +192,10 @@ private sealed class AuthenticatorNavBarTab : Parcelable {
override val iconRes get() = BitwardenDrawable.ic_verification_codes
override val labelRes get() = BitwardenString.verification_codes
override val contentDescriptionRes get() = BitwardenString.verification_codes
override val route get() = ItemListingRoute.toObjectNavigationRoute()
override val graphRoute get() = ItemListingGraphRoute
override val startDestinationRoute get() = ItemListingRoute
override val testTag get() = "VerificationCodesTab"
override val notificationCount: Int get() = 0
}
/**
@ -320,12 +203,14 @@ private sealed class AuthenticatorNavBarTab : Parcelable {
*/
@Parcelize
data object Settings : AuthenticatorNavBarTab() {
override val iconResSelected get() = BitwardenDrawable.ic_settings_solid
override val iconResSelected get() = BitwardenDrawable.ic_settings_filled
override val iconRes get() = BitwardenDrawable.ic_settings
override val labelRes get() = BitwardenString.settings
override val contentDescriptionRes get() = BitwardenString.settings
override val route get() = SettingsGraphRoute.toObjectNavigationRoute()
override val graphRoute get() = SettingsGraphRoute
override val startDestinationRoute get() = SettingsRoute
override val testTag get() = "SettingsTab"
override val notificationCount: Int get() = 0
}
}
@ -340,3 +225,12 @@ private fun NavController.authenticatorNavBarScreenNavOptions(): NavOptions =
launchSingleTop = true
restoreState = true
}
/**
* Determine if the current destination is the same as the given tab.
*/
private fun NavBackStackEntry?.isCurrentRoute(route: Any): Boolean =
this
?.destination
?.parent
?.route == route.toObjectNavigationRoute()

View File

@ -10,8 +10,6 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredHeightIn
import androidx.compose.foundation.layout.requiredWidthIn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@ -25,11 +23,14 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.bitwarden.authenticator.ui.platform.components.button.AuthenticatorTextButton
import com.bitwarden.authenticator.ui.platform.components.toggle.BitwardenWideSwitch
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.bitwarden.ui.platform.components.button.BitwardenTextButton
import com.bitwarden.ui.platform.components.dialog.util.maxDialogHeight
import com.bitwarden.ui.platform.components.dialog.util.maxDialogWidth
import com.bitwarden.ui.platform.components.model.CardStyle
import com.bitwarden.ui.platform.components.toggle.BitwardenSwitch
import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.platform.theme.BitwardenTheme
/**
* Displays a dialog asking the user where they would like to save a new QR code.
@ -60,8 +61,8 @@ fun ChooseSaveLocationDialog(
max = configuration.maxDialogWidth,
)
.background(
color = MaterialTheme.colorScheme.surfaceContainerHigh,
shape = RoundedCornerShape(28.dp),
color = BitwardenTheme.colorScheme.background.primary,
shape = BitwardenTheme.shapes.dialog,
),
horizontalAlignment = Alignment.End,
) {
@ -71,8 +72,8 @@ fun ChooseSaveLocationDialog(
.padding(horizontal = 24.dp)
.fillMaxWidth(),
text = stringResource(BitwardenString.verification_code_created),
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.headlineSmall,
color = BitwardenTheme.colorScheme.text.primary,
style = BitwardenTheme.typography.headlineSmall,
)
Spacer(modifier = Modifier.height(16.dp))
Text(
@ -81,33 +82,34 @@ fun ChooseSaveLocationDialog(
.padding(horizontal = 24.dp)
.fillMaxWidth(),
text = stringResource(BitwardenString.choose_save_location_message),
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodyMedium,
color = BitwardenTheme.colorScheme.text.primary,
style = BitwardenTheme.typography.bodyMedium,
)
Spacer(Modifier.height(16.dp))
BitwardenWideSwitch(
modifier = Modifier.padding(horizontal = 16.dp),
BitwardenSwitch(
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
label = stringResource(BitwardenString.save_option_as_default),
isChecked = isSaveAsDefaultChecked,
onCheckedChange = { isSaveAsDefaultChecked = !isSaveAsDefaultChecked },
cardStyle = CardStyle.Full,
)
Spacer(Modifier.height(16.dp))
FlowRow(
horizontalArrangement = Arrangement.End,
modifier = Modifier.padding(horizontal = 8.dp),
) {
AuthenticatorTextButton(
BitwardenTextButton(
modifier = Modifier
.padding(horizontal = 4.dp),
label = stringResource(BitwardenString.save_here),
labelTextColor = MaterialTheme.colorScheme.primary,
onClick = { onSaveHereClick.invoke(isSaveAsDefaultChecked) },
)
AuthenticatorTextButton(
BitwardenTextButton(
modifier = Modifier
.padding(horizontal = 4.dp),
label = stringResource(BitwardenString.save_to_bitwarden),
labelTextColor = MaterialTheme.colorScheme.primary,
onClick = { onTakeMeToBitwardenClick.invoke(isSaveAsDefaultChecked) },
)
}

View File

@ -21,7 +21,6 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
@ -53,19 +52,17 @@ import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.bitwarden.authenticator.ui.authenticator.feature.qrcodescan.util.QrCodeAnalyzer
import com.bitwarden.authenticator.ui.authenticator.feature.qrcodescan.util.QrCodeAnalyzerImpl
import com.bitwarden.authenticator.ui.platform.components.appbar.AuthenticatorTopAppBar
import com.bitwarden.authenticator.ui.platform.components.dialog.BasicDialogState
import com.bitwarden.authenticator.ui.platform.components.dialog.BitwardenBasicDialog
import com.bitwarden.authenticator.ui.platform.components.scaffold.BitwardenScaffold
import com.bitwarden.authenticator.ui.platform.theme.LocalNonMaterialColors
import com.bitwarden.authenticator.ui.platform.util.isPortrait
import com.bitwarden.ui.platform.base.util.EventsEffect
import com.bitwarden.ui.platform.base.util.annotatedStringResource
import com.bitwarden.ui.platform.base.util.spanStyleOf
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
import com.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.bitwarden.ui.platform.resource.BitwardenDrawable
import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.util.asText
import com.bitwarden.ui.platform.theme.BitwardenTheme
import java.util.concurrent.Executors
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
@ -117,7 +114,7 @@ fun QrCodeScanScreen(
BitwardenScaffold(
modifier = Modifier.fillMaxSize(),
topBar = {
AuthenticatorTopAppBar(
BitwardenTopAppBar(
title = stringResource(id = BitwardenString.scan_qr_code),
navigationIcon = painterResource(id = BitwardenDrawable.ic_close),
navigationIconContentDescription = stringResource(id = BitwardenString.close),
@ -127,24 +124,21 @@ fun QrCodeScanScreen(
scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()),
)
},
) { innerPadding ->
) {
CameraPreview(
cameraErrorReceive = remember(viewModel) {
{ viewModel.trySendAction(QrCodeScanAction.CameraSetupErrorReceive) }
},
qrCodeAnalyzer = qrCodeAnalyzer,
modifier = Modifier.padding(innerPadding),
)
if (LocalConfiguration.current.isPortrait) {
PortraitQRCodeContent(
onEnterCodeManuallyClick = onEnterCodeManuallyClick,
modifier = Modifier.padding(paddingValues = innerPadding),
)
} else {
LandscapeQRCodeContent(
onEnterCodeManuallyClick = onEnterCodeManuallyClick,
modifier = Modifier.padding(paddingValues = innerPadding),
)
}
}
@ -167,10 +161,8 @@ private fun QrCodeScanDialogs(
QrCodeScanState.DialogState.SaveToBitwardenError -> {
BitwardenBasicDialog(
visibilityState = BasicDialogState.Shown(
title = BitwardenString.something_went_wrong.asText(),
message = BitwardenString.please_try_again.asText(),
),
title = stringResource(id = BitwardenString.something_went_wrong),
message = stringResource(id = BitwardenString.please_try_again),
onDismissRequest = onDismissRequest,
)
}
@ -207,7 +199,7 @@ private fun PortraitQRCodeContent(
text = stringResource(id = BitwardenString.point_your_camera_at_the_qr_code),
textAlign = TextAlign.Center,
color = Color.White,
style = MaterialTheme.typography.bodyMedium,
style = BitwardenTheme.typography.bodyMedium,
modifier = Modifier.padding(horizontal = 16.dp),
)
@ -248,7 +240,7 @@ private fun LandscapeQRCodeContent(
text = stringResource(id = BitwardenString.point_your_camera_at_the_qr_code),
textAlign = TextAlign.Center,
color = Color.White,
style = MaterialTheme.typography.bodySmall,
style = BitwardenTheme.typography.bodySmall,
)
BottomClickableText(
@ -345,7 +337,7 @@ private fun QrCodeSquare(
modifier: Modifier = Modifier,
squareOutlineSize: Dp,
) {
val color = MaterialTheme.colorScheme.primary
val color = BitwardenTheme.colorScheme.text.primary
Box(
contentAlignment = Alignment.Center,
@ -443,12 +435,12 @@ private fun BottomClickableText(
text = annotatedStringResource(
id = BitwardenString.cannot_scan_qr_code_enter_key_manually,
linkHighlightStyle = spanStyleOf(
color = LocalNonMaterialColors.current.qrCodeClickableText,
textStyle = MaterialTheme.typography.bodyMedium,
color = BitwardenTheme.colorScheme.text.interaction,
textStyle = BitwardenTheme.typography.bodyMedium,
),
style = spanStyleOf(
color = Color.White,
textStyle = MaterialTheme.typography.bodyMedium,
textStyle = BitwardenTheme.typography.bodyMedium,
),
onAnnotationClick = {
when (it) {

View File

@ -7,8 +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.LazyListScope
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.MaterialTheme
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@ -17,8 +16,12 @@ import androidx.compose.ui.unit.dp
import com.bitwarden.authenticator.ui.authenticator.feature.model.SharedCodesDisplayState
import com.bitwarden.authenticator.ui.authenticator.feature.model.VerificationCodeDisplayItem
import com.bitwarden.authenticator.ui.authenticator.feature.search.handlers.SearchHandlers
import com.bitwarden.authenticator.ui.platform.components.header.BitwardenListHeaderText
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.bitwarden.ui.platform.base.util.toListItemCardStyle
import com.bitwarden.ui.platform.components.header.BitwardenListHeaderText
import com.bitwarden.ui.platform.components.model.CardStyle
import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.platform.theme.BitwardenTheme
/**
* The content state for the item search screen.
@ -30,32 +33,37 @@ fun ItemSearchContent(
modifier: Modifier = Modifier,
) {
LazyColumn(modifier = modifier) {
item {
Spacer(Modifier.height(height = 12.dp))
}
if (viewState.hasLocalAndSharedItems) {
item {
BitwardenListHeaderText(
label = viewState.localListHeader(),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(Modifier.height(height = 8.dp))
}
}
items(viewState.itemList) {
itemsIndexed(viewState.itemList) { index, it ->
VaultVerificationCodeItem(
displayItem = it,
onCopyClick = searchHandlers.onItemClick,
cardStyle = viewState.itemList.toListItemCardStyle(index = index),
modifier = Modifier
.fillMaxWidth()
// There is some built-in padding to the menu button that
// makes up the visual difference here.
.padding(start = 16.dp, end = 12.dp),
.standardHorizontalMargin(),
)
}
if (viewState.hasLocalAndSharedItems) {
item {
Spacer(Modifier.height(height = 8.dp))
Spacer(Modifier.height(height = 12.dp))
}
}
@ -83,14 +91,17 @@ private fun LazyListScope.sharedCodes(
label = section.label(),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(Modifier.height(height = 12.dp))
}
items(section.codes) {
itemsIndexed(section.codes) { index, it ->
VaultVerificationCodeItem(
displayItem = it,
onCopyClick = onCopyClick,
cardStyle = section.codes.toListItemCardStyle(index = index),
modifier = Modifier
.fillMaxWidth()
// There is some built-in padding to the menu button that
@ -105,8 +116,8 @@ private fun LazyListScope.sharedCodes(
item {
Text(
text = stringResource(BitwardenString.shared_codes_error),
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodySmall,
color = BitwardenTheme.colorScheme.text.secondary,
style = BitwardenTheme.typography.bodySmall,
modifier = Modifier.padding(horizontal = 16.dp),
)
}
@ -117,6 +128,7 @@ private fun LazyListScope.sharedCodes(
@Composable
private fun VaultVerificationCodeItem(
displayItem: VerificationCodeDisplayItem,
cardStyle: CardStyle,
onCopyClick: (authCode: String) -> Unit,
modifier: Modifier = Modifier,
) {
@ -130,6 +142,7 @@ private fun VaultVerificationCodeItem(
startIcon = displayItem.startIcon,
onCopyClick = { onCopyClick(displayItem.authCode) },
onItemClick = { onCopyClick(displayItem.authCode) },
cardStyle = cardStyle,
modifier = modifier,
)
}

View File

@ -6,20 +6,19 @@ 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.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTag
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.bitwarden.ui.platform.resource.BitwardenDrawable
import com.bitwarden.ui.platform.theme.BitwardenTheme
/**
* The empty state for the item search screen.
@ -37,10 +36,10 @@ fun ItemSearchEmptyContent(
Icon(
painter = painterResource(id = BitwardenDrawable.ic_search_wide),
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
tint = BitwardenTheme.colorScheme.icon.primary,
modifier = Modifier
.size(74.dp)
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(24.dp))
@ -49,11 +48,11 @@ fun ItemSearchEmptyContent(
Text(
textAlign = TextAlign.Center,
modifier = Modifier
.semantics { testTag = "NoSearchResultsLabel" }
.testTag("NoSearchResultsLabel")
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
text = it(),
style = MaterialTheme.typography.bodyMedium,
style = BitwardenTheme.typography.bodyMedium,
)
}

View File

@ -2,7 +2,6 @@ package com.bitwarden.authenticator.ui.authenticator.feature.search
import android.widget.Toast
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
@ -12,18 +11,18 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTag
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.bitwarden.authenticator.ui.authenticator.feature.search.handlers.SearchHandlers
import com.bitwarden.authenticator.ui.platform.components.appbar.AuthenticatorSearchTopAppBar
import com.bitwarden.authenticator.ui.platform.components.scaffold.BitwardenScaffold
import com.bitwarden.ui.platform.base.util.EventsEffect
import com.bitwarden.ui.platform.base.util.bottomDivider
import com.bitwarden.ui.platform.components.appbar.BitwardenSearchTopAppBar
import com.bitwarden.ui.platform.components.appbar.NavigationIcon
import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.bitwarden.ui.platform.resource.BitwardenDrawable
import com.bitwarden.ui.platform.resource.BitwardenString
@ -40,7 +39,7 @@ fun ItemSearchScreen(
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
val searchHandlers = remember(viewModel) { SearchHandlers.create(viewModel) }
val context = LocalContext.current
val resources = context.resources
val resources = LocalResources.current
EventsEffect(viewModel = viewModel) { event ->
when (event) {
@ -58,14 +57,15 @@ fun ItemSearchScreen(
modifier = Modifier
.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
AuthenticatorSearchTopAppBar(
BitwardenSearchTopAppBar(
modifier = Modifier
.semantics { testTag = "SearchFieldEntry" }
.testTag("SearchFieldEntry")
.bottomDivider(),
searchTerm = state.searchTerm,
placeholder = stringResource(id = BitwardenString.search_codes),
onSearchTermChange = searchHandlers.onSearchTermChange,
scrollBehavior = scrollBehavior,
clearIconContentDescription = stringResource(id = BitwardenString.clear),
navigationIcon = NavigationIcon(
navigationIcon = painterResource(id = BitwardenDrawable.ic_back),
navigationIconContentDescription = stringResource(id = BitwardenString.back),
@ -73,24 +73,20 @@ fun ItemSearchScreen(
),
)
},
) { innerPadding ->
) {
when (val viewState = state.viewState) {
is ItemSearchState.ViewState.Content -> {
ItemSearchContent(
viewState = viewState,
searchHandlers = searchHandlers,
modifier = Modifier
.fillMaxSize()
.padding(paddingValues = innerPadding),
modifier = Modifier.fillMaxSize(),
)
}
is ItemSearchState.ViewState.Empty -> {
ItemSearchEmptyContent(
viewState = viewState,
modifier = Modifier
.fillMaxSize()
.padding(paddingValues = innerPadding),
modifier = Modifier.fillMaxSize(),
)
}
}

View File

@ -1,7 +1,5 @@
package com.bitwarden.authenticator.ui.authenticator.feature.search
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
@ -10,11 +8,8 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
@ -22,12 +17,14 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.bitwarden.authenticator.ui.platform.components.indicator.AuthenticatorCircularCountdownIndicator
import com.bitwarden.authenticator.ui.platform.theme.AuthenticatorTheme
import com.bitwarden.ui.platform.base.util.cardStyle
import com.bitwarden.ui.platform.components.icon.BitwardenIcon
import com.bitwarden.ui.platform.components.icon.model.IconData
import com.bitwarden.ui.platform.components.indicator.BitwardenCircularCountdownIndicator
import com.bitwarden.ui.platform.components.model.CardStyle
import com.bitwarden.ui.platform.resource.BitwardenDrawable
import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.platform.theme.BitwardenTheme
/**
* The verification code item displayed to the user.
@ -53,25 +50,24 @@ fun VaultVerificationCodeItem(
startIcon: IconData,
onCopyClick: () -> Unit,
onItemClick: () -> Unit,
cardStyle: CardStyle,
modifier: Modifier = Modifier,
supportingLabel: String? = null,
) {
Row(
modifier = Modifier
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = ripple(color = MaterialTheme.colorScheme.primary),
modifier = modifier
.defaultMinSize(minHeight = 60.dp)
.cardStyle(
cardStyle = cardStyle,
onClick = onItemClick,
)
.defaultMinSize(minHeight = 72.dp)
.padding(vertical = 8.dp)
.then(modifier),
paddingStart = 16.dp,
),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp),
) {
BitwardenIcon(
iconData = startIcon,
tint = MaterialTheme.colorScheme.onSurface,
tint = BitwardenTheme.colorScheme.icon.primary,
modifier = Modifier.size(24.dp),
)
@ -83,8 +79,8 @@ fun VaultVerificationCodeItem(
if (!issuer.isNullOrEmpty()) {
Text(
text = issuer,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
style = BitwardenTheme.typography.bodyLarge,
color = BitwardenTheme.colorScheme.text.primary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
@ -92,15 +88,15 @@ fun VaultVerificationCodeItem(
if (!supportingLabel.isNullOrEmpty()) {
Text(
text = supportingLabel,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = BitwardenTheme.typography.bodyMedium,
color = BitwardenTheme.colorScheme.text.secondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
AuthenticatorCircularCountdownIndicator(
BitwardenCircularCountdownIndicator(
timeLeftSeconds = timeLeftSeconds,
periodSeconds = periodSeconds,
alertThresholdSeconds = alertThresholdSeconds,
@ -108,8 +104,8 @@ fun VaultVerificationCodeItem(
Text(
text = authCode.chunked(3).joinToString(" "),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = BitwardenTheme.typography.sensitiveInfoSmall,
color = BitwardenTheme.colorScheme.text.primary,
)
IconButton(
@ -118,7 +114,7 @@ fun VaultVerificationCodeItem(
Icon(
painter = painterResource(id = BitwardenDrawable.ic_copy),
contentDescription = stringResource(id = BitwardenString.copy),
tint = MaterialTheme.colorScheme.primary,
tint = BitwardenTheme.colorScheme.icon.primary,
modifier = Modifier.size(24.dp),
)
}
@ -129,7 +125,7 @@ fun VaultVerificationCodeItem(
@Preview(showBackground = true)
@Composable
private fun VerificationCodeItem_preview() {
AuthenticatorTheme {
BitwardenTheme {
VaultVerificationCodeItem(
startIcon = IconData.Local(BitwardenDrawable.ic_login_item),
issuer = "Sample Label",
@ -141,6 +137,7 @@ private fun VerificationCodeItem_preview() {
onItemClick = {},
modifier = Modifier.padding(horizontal = 16.dp),
alertThresholdSeconds = 7,
cardStyle = CardStyle.Full,
)
}
}

View File

@ -1,87 +0,0 @@
package com.bitwarden.authenticator.ui.platform.components.appbar
import androidx.compose.foundation.layout.RowScope
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MediumTopAppBar
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTag
import androidx.compose.ui.tooling.preview.Preview
import com.bitwarden.ui.platform.resource.BitwardenDrawable
/**
* A custom Bitwarden-themed medium top app bar with support for actions.
*
* This app bar wraps around Material 3's [MediumTopAppBar] and customizes its appearance
* and behavior according to the app theme.
* It provides a title and an optional set of actions on the trailing side.
* These actions are arranged within a custom action row tailored to the app's design requirements.
*
* @param title The text to be displayed as the title of the app bar.
* @param scrollBehavior Defines the scrolling behavior of the app bar. It controls how the app bar
* behaves in conjunction with scrolling content.
* @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.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AuthenticatorMediumTopAppBar(
title: String,
scrollBehavior: TopAppBarScrollBehavior,
modifier: Modifier = Modifier,
actions: @Composable RowScope.() -> Unit = {},
) {
MediumTopAppBar(
colors = TopAppBarDefaults.largeTopAppBarColors(
containerColor = MaterialTheme.colorScheme.surface,
scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer,
navigationIconContentColor = MaterialTheme.colorScheme.onSurface,
titleContentColor = MaterialTheme.colorScheme.onSurface,
actionIconContentColor = MaterialTheme.colorScheme.onSurfaceVariant,
),
scrollBehavior = scrollBehavior,
title = {
Text(
text = title,
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.semantics { testTag = "PageTitleLabel" },
)
},
modifier = modifier.semantics { testTag = "HeaderBarComponent" },
actions = actions,
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Preview(showBackground = true)
@Composable
private fun AuthenticatorMediumTopAppBar_preview() {
MaterialTheme {
AuthenticatorMediumTopAppBar(
title = "Preview Title",
scrollBehavior = TopAppBarDefaults
.exitUntilCollapsedScrollBehavior(
rememberTopAppBarState(),
),
actions = {
IconButton(onClick = { }) {
Icon(
painter = painterResource(id = BitwardenDrawable.ic_ellipsis_vertical),
contentDescription = "",
tint = MaterialTheme.colorScheme.onSurface,
)
}
},
)
}
}

View File

@ -1,105 +0,0 @@
package com.bitwarden.authenticator.ui.platform.components.appbar
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTag
import androidx.compose.ui.text.input.ImeAction
import com.bitwarden.ui.platform.base.util.mirrorIfRtl
import com.bitwarden.ui.platform.components.appbar.NavigationIcon
import com.bitwarden.ui.platform.resource.BitwardenDrawable
import com.bitwarden.ui.platform.resource.BitwardenString
/**
* Represents a Bitwarden styled [TopAppBar] that assumes the following components:
*
* - an optional single navigation control in the upper-left defined by [navigationIcon].
* - an editable [TextField] populated by a [searchTerm] in the middle.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AuthenticatorSearchTopAppBar(
searchTerm: String,
placeholder: String,
onSearchTermChange: (String) -> Unit,
scrollBehavior: TopAppBarScrollBehavior,
navigationIcon: NavigationIcon?,
modifier: Modifier = Modifier,
autoFocus: Boolean = true,
) {
val focusRequester = remember { FocusRequester() }
TopAppBar(
modifier = modifier.semantics { testTag = "HeaderBarComponent" },
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surface,
scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer,
navigationIconContentColor = MaterialTheme.colorScheme.onSurface,
titleContentColor = MaterialTheme.colorScheme.onSurface,
actionIconContentColor = MaterialTheme.colorScheme.onSurfaceVariant,
),
scrollBehavior = scrollBehavior,
navigationIcon = {
navigationIcon?.let {
IconButton(
onClick = it.onNavigationIconClick,
modifier = Modifier.semantics { testTag = "CloseButton" },
) {
Icon(
modifier = Modifier.mirrorIfRtl(),
painter = it.navigationIcon,
contentDescription = it.navigationIconContentDescription,
)
}
}
},
title = {
TextField(
colors = TextFieldDefaults.colors(
focusedContainerColor = Color.Transparent,
unfocusedContainerColor = Color.Transparent,
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
),
placeholder = { Text(text = placeholder) },
value = searchTerm,
onValueChange = onSearchTermChange,
trailingIcon = {
IconButton(
onClick = { onSearchTermChange("") },
) {
Icon(
painter = painterResource(id = BitwardenDrawable.ic_close),
contentDescription = stringResource(id = BitwardenString.clear),
)
}
},
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
modifier = Modifier
.focusRequester(focusRequester)
.fillMaxWidth(),
)
},
)
if (autoFocus) {
LaunchedEffect(Unit) { focusRequester.requestFocus() }
}
}

View File

@ -1,136 +0,0 @@
package com.bitwarden.authenticator.ui.platform.components.appbar
import androidx.compose.foundation.layout.RowScope
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTag
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import com.bitwarden.authenticator.ui.platform.theme.AuthenticatorTheme
import com.bitwarden.ui.platform.base.util.mirrorIfRtl
import com.bitwarden.ui.platform.components.appbar.NavigationIcon
import com.bitwarden.ui.platform.resource.BitwardenDrawable
import com.bitwarden.ui.platform.resource.BitwardenString
/**
* Represents a Bitwarden styled [TopAppBar] that assumes the following components:
*
* - a single navigation control in the upper-left defined by [navigationIcon],
* [navigationIconContentDescription], and [onNavigationIconClick].
* - a [title] in the middle.
* - a [actions] 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.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AuthenticatorTopAppBar(
title: String,
scrollBehavior: TopAppBarScrollBehavior,
navigationIcon: Painter,
navigationIconContentDescription: String,
onNavigationIconClick: () -> Unit,
modifier: Modifier = Modifier,
actions: @Composable RowScope.() -> Unit = { },
) {
AuthenticatorTopAppBar(
title = title,
scrollBehavior = scrollBehavior,
navigationIcon = NavigationIcon(
navigationIcon = navigationIcon,
navigationIconContentDescription = navigationIconContentDescription,
onNavigationIconClick = onNavigationIconClick,
),
modifier = modifier,
actions = actions,
)
}
/**
* Represents a Bitwarden styled [TopAppBar] that assumes the following components:
*
* - an optional single navigation control in the upper-left defined by [navigationIcon].
* - a [title] in the middle.
* - a [actions] 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.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AuthenticatorTopAppBar(
title: String,
scrollBehavior: TopAppBarScrollBehavior,
navigationIcon: NavigationIcon?,
modifier: Modifier = Modifier,
actions: @Composable RowScope.() -> Unit = {},
) {
TopAppBar(
colors = TopAppBarDefaults.largeTopAppBarColors(
containerColor = MaterialTheme.colorScheme.surface,
scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer,
navigationIconContentColor = MaterialTheme.colorScheme.onSurface,
titleContentColor = MaterialTheme.colorScheme.onSurface,
actionIconContentColor = MaterialTheme.colorScheme.onSurfaceVariant,
),
scrollBehavior = scrollBehavior,
navigationIcon = {
navigationIcon?.let {
IconButton(
onClick = it.onNavigationIconClick,
modifier = Modifier.semantics { testTag = "CloseButton" },
) {
Icon(
modifier = Modifier.mirrorIfRtl(),
painter = it.navigationIcon,
contentDescription = it.navigationIconContentDescription,
)
}
}
},
title = {
Text(
text = title,
style = MaterialTheme.typography.titleLarge,
maxLines = 1,
softWrap = false,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.semantics { testTag = "PageTitleLabel" },
)
},
modifier = modifier.semantics { testTag = "HeaderBarComponent" },
actions = actions,
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Composable
private fun AuthenticatorTopAppBar_preview() {
AuthenticatorTheme {
AuthenticatorTopAppBar(
title = "Title",
scrollBehavior = TopAppBarDefaults
.exitUntilCollapsedScrollBehavior(
rememberTopAppBarState(),
),
navigationIcon = NavigationIcon(
navigationIcon = painterResource(id = BitwardenDrawable.ic_close),
navigationIconContentDescription = stringResource(id = BitwardenString.close),
onNavigationIconClick = { },
),
)
}
}

View File

@ -1,45 +0,0 @@
package com.bitwarden.authenticator.ui.platform.components.appbar.action
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.tooling.preview.Preview
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.bitwarden.ui.platform.resource.BitwardenDrawable
/**
* Represents the Authenticator search action item.
*
* This is an [Icon] composable tailored specifically for the search functionality
* in the Authenticator app.
* It presents the search icon and offers an `onClick` callback for when the icon is tapped.
*
* @param contentDescription A description of the UI element, used for accessibility purposes.
* @param onClick A callback to be invoked when this action item is clicked.
*/
@Composable
fun AuthenticatorSearchActionItem(
contentDescription: String,
onClick: () -> Unit,
) {
IconButton(
onClick = onClick,
modifier = Modifier.testTag("SearchButton"),
) {
Icon(
painter = rememberVectorPainter(id = BitwardenDrawable.ic_search_wide),
contentDescription = contentDescription,
)
}
}
@Preview(showBackground = true)
@Composable
private fun AuthenticatorSearchActionItem_preview() {
AuthenticatorSearchActionItem(
contentDescription = "Search",
onClick = {},
)
}

View File

@ -1,64 +0,0 @@
package com.bitwarden.authenticator.ui.platform.components.button
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
/**
* Represents a Bitwarden Authenticator-styled filled [Button].
*
* @param label The label for the button.
* @param onClick The callback when the button is clicked.
* @param modifier The [Modifier] to be applied to the button.
* @param isEnabled Whether or not the button is enabled.
*/
@Composable
fun AuthenticatorFilledButton(
label: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
isEnabled: Boolean = true,
) {
Button(
onClick = onClick,
modifier = modifier.semantics(mergeDescendants = true) {},
enabled = isEnabled,
contentPadding = PaddingValues(
vertical = 10.dp,
horizontal = 24.dp,
),
colors = ButtonDefaults.buttonColors(),
) {
Text(
text = label,
style = MaterialTheme.typography.labelLarge,
)
}
}
@Preview
@Composable
private fun AuthenticatorFilledButton_preview_isEnabled() {
AuthenticatorFilledButton(
label = "Label",
onClick = {},
isEnabled = true,
)
}
@Preview
@Composable
private fun BitwardenFilledButton_preview_isNotEnabled() {
AuthenticatorFilledButton(
label = "Label",
onClick = {},
isEnabled = false,
)
}

View File

@ -1,61 +0,0 @@
package com.bitwarden.authenticator.ui.platform.components.button
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme
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.bitwarden.authenticator.ui.platform.theme.AuthenticatorTheme
/**
* A filled tonal button for the Bitwarden Authenticator UI with a customized appearance.
*
* This button uses the `secondaryContainer` color from the current [MaterialTheme.colorScheme]
* for its background and the `onSecondaryContainer` color for its label text.
*
* @param label The text to be displayed on the button.
* @param onClick A lambda which will be invoked when the button is clicked.
* @param isEnabled Whether or not the button is enabled.
* @param modifier A [Modifier] for this composable, allowing for adjustments to its appearance
* or behavior. This can be used to apply padding, layout, and other Modifiers.
*/
@Composable
fun AuthenticatorFilledTonalButton(
label: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
isEnabled: Boolean = true,
) {
Button(
onClick = onClick,
contentPadding = PaddingValues(
vertical = 10.dp,
horizontal = 24.dp,
),
enabled = isEnabled,
colors = ButtonDefaults.filledTonalButtonColors(),
modifier = modifier,
) {
Text(
text = label,
style = MaterialTheme.typography.labelLarge,
)
}
}
@Preview(showBackground = true)
@Composable
private fun AuthenticatorFilledTonalButton_preview() {
AuthenticatorTheme {
AuthenticatorFilledTonalButton(
label = "Sample Text",
onClick = {},
modifier = Modifier.padding(horizontal = 16.dp),
)
}
}

View File

@ -1,65 +0,0 @@
package com.bitwarden.authenticator.ui.platform.components.button
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
/**
* Represents a Bitwarden Authenticator-styled filled [OutlinedButton].
*
* @param label The label for the button.
* @param onClick The callback when the button is clicked.
* @param modifier The [Modifier] to be applied to the button.
* @param isEnabled Whether or not the button is enabled.
*/
@Composable
fun AuthenticatorOutlinedButton(
label: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
isEnabled: Boolean = true,
) {
OutlinedButton(
onClick = onClick,
modifier = modifier
.semantics(mergeDescendants = true) { },
enabled = isEnabled,
contentPadding = PaddingValues(
vertical = 10.dp,
horizontal = 24.dp,
),
colors = ButtonDefaults.outlinedButtonColors(),
) {
Text(
text = label,
style = MaterialTheme.typography.labelLarge,
)
}
}
@Preview
@Composable
private fun AuthenticatorOutlinedButton_preview_isEnabled() {
AuthenticatorOutlinedButton(
label = "Label",
onClick = {},
isEnabled = true,
)
}
@Preview
@Composable
private fun AuthenticatorOutlinedButton_preview_isNotEnabled() {
AuthenticatorOutlinedButton(
label = "Label",
onClick = {},
isEnabled = false,
)
}

View File

@ -1,63 +0,0 @@
package com.bitwarden.authenticator.ui.platform.components.button
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
/**
* Represents a Bitwarden Authenticator-styled [TextButton].
*
* @param label The label for the button.
* @param onClick The callback when the button is clicked.
* @param modifier The [Modifier] to be applied to the button.
* @param labelTextColor The color for the label text.
*/
@Composable
fun AuthenticatorTextButton(
label: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
isEnabled: Boolean = true,
labelTextColor: Color? = null,
) {
val defaultColors = if (labelTextColor != null) {
ButtonDefaults.textButtonColors(
contentColor = labelTextColor,
)
} else {
ButtonDefaults.textButtonColors()
}
TextButton(
onClick = onClick,
modifier = modifier,
enabled = isEnabled,
colors = defaultColors,
) {
Text(
text = label,
style = MaterialTheme.typography.labelLarge,
modifier = Modifier
.padding(
vertical = 10.dp,
horizontal = 12.dp,
),
)
}
}
@Preview
@Composable
private fun AuthenticatorTextButton_preview() {
AuthenticatorTextButton(
label = "Label",
onClick = {},
)
}

View File

@ -1,132 +0,0 @@
package com.bitwarden.authenticator.ui.platform.components.card
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
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.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.VectorPainter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.bitwarden.ui.platform.resource.BitwardenDrawable
import com.bitwarden.ui.platform.resource.BitwardenString
/**
* A reusable card for displaying actions to the user.
*/
@Composable
fun AuthenticatorActionCard(
actionIcon: VectorPainter,
titleText: String,
actionText: String,
callToActionText: String,
onCardClicked: () -> Unit,
modifier: Modifier = Modifier,
trailingContent: (@Composable BoxScope.() -> Unit)? = null,
) {
Card(
onClick = onCardClicked,
shape = RoundedCornerShape(size = 16.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceContainer,
disabledContainerColor = MaterialTheme.colorScheme.surfaceContainer,
),
modifier = modifier,
elevation = CardDefaults.elevatedCardElevation(),
) {
Row(
modifier = Modifier
.fillMaxWidth(),
) {
Icon(
painter = actionIcon,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier
.padding(start = 16.dp, top = 16.dp)
.size(24.dp),
)
Spacer(modifier = Modifier.width(16.dp))
Column(
modifier = Modifier
.weight(weight = 1f)
.padding(vertical = 16.dp),
) {
Text(
text = titleText,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
)
Text(
text = actionText,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = callToActionText,
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.primary,
)
}
Spacer(modifier = Modifier.width(16.dp))
Box {
trailingContent?.invoke(this)
}
}
}
}
@Preview
@Composable
private fun ActionCardPreview() {
AuthenticatorActionCard(
actionIcon = rememberVectorPainter(id = BitwardenDrawable.ic_close),
actionText = "This is an action.",
callToActionText = "Take action",
titleText = "This is a title",
onCardClicked = { },
)
}
@Preview
@Composable
private fun ActionCardWithTrailingPreview() {
AuthenticatorActionCard(
actionIcon = rememberVectorPainter(id = BitwardenDrawable.ic_shield),
actionText = "An action with trailing content",
titleText = "This is a title",
callToActionText = "Take action",
onCardClicked = {},
trailingContent = {
IconButton(
onClick = {},
) {
Icon(
painter = painterResource(id = BitwardenDrawable.ic_close),
contentDescription = stringResource(id = BitwardenString.close),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier
.size(24.dp),
)
}
},
)
}

View File

@ -1,58 +0,0 @@
package com.bitwarden.authenticator.ui.platform.components.content
import androidx.compose.foundation.layout.Column
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.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.bitwarden.authenticator.ui.platform.components.button.AuthenticatorTextButton
import com.bitwarden.ui.platform.resource.BitwardenString
/**
* A Bitwarden-themed, re-usable error state.
*
* Note that when [onTryAgainClick] is absent, there will be no "Try again" button displayed.
*/
@Composable
fun AuthenticatorErrorContent(
message: String,
modifier: Modifier = Modifier,
onTryAgainClick: (() -> Unit)? = null,
) {
Column(
modifier = modifier.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Spacer(modifier = Modifier.weight(1f))
Text(
text = message,
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center,
modifier = Modifier
.padding(horizontal = 16.dp)
.fillMaxWidth(),
)
onTryAgainClick?.let {
Spacer(modifier = Modifier.height(16.dp))
AuthenticatorTextButton(
label = stringResource(id = BitwardenString.try_again),
onClick = it,
modifier = Modifier.padding(horizontal = 16.dp),
)
}
Spacer(modifier = Modifier.weight(1f))
Spacer(modifier = Modifier.navigationBarsPadding())
}
}

View File

@ -1,27 +0,0 @@
package com.bitwarden.authenticator.ui.platform.components.content
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
/**
* A Bitwarden-themed, re-usable loading state.
*/
@Composable
fun AuthenticatorLoadingContent(
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier,
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
CircularProgressIndicator()
Spacer(modifier = Modifier.navigationBarsPadding())
}
}

View File

@ -1,91 +0,0 @@
package com.bitwarden.authenticator.ui.platform.components.dialog
import android.os.Parcelable
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.bitwarden.authenticator.ui.platform.components.button.AuthenticatorTextButton
import com.bitwarden.authenticator.ui.platform.theme.AuthenticatorTheme
import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.util.Text
import com.bitwarden.ui.util.asText
import kotlinx.parcelize.Parcelize
/**
* Represents a Bitwarden-styled dialog that is hidden or shown based on [visibilityState].
*
* @param visibilityState the [BasicDialogState] used to populate the dialog.
* @param onDismissRequest called when the user has requested to dismiss the dialog, whether by
* tapping "OK", tapping outside the dialog, or pressing the back button.
*/
@Composable
fun BitwardenBasicDialog(
visibilityState: BasicDialogState,
onDismissRequest: () -> Unit,
): Unit = when (visibilityState) {
BasicDialogState.Hidden -> Unit
is BasicDialogState.Shown -> {
AlertDialog(
onDismissRequest = onDismissRequest,
confirmButton = {
AuthenticatorTextButton(
label = stringResource(id = BitwardenString.okay),
onClick = onDismissRequest,
)
},
title = visibilityState.title?.let {
{
Text(
text = it(),
style = MaterialTheme.typography.headlineSmall,
)
}
},
text = {
Text(
text = visibilityState.message(),
style = MaterialTheme.typography.bodyMedium,
)
},
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
)
}
}
@Preview
@Composable
private fun BitwardenBasicDialog_preview() {
AuthenticatorTheme {
BitwardenBasicDialog(
visibilityState = BasicDialogState.Shown(
title = "An error has occurred".asText(),
message = "Username or password is incorrect. Try again.".asText(),
),
onDismissRequest = {},
)
}
}
/**
* Models display of a [BitwardenBasicDialog].
*/
sealed class BasicDialogState : Parcelable {
/**
* Hide the dialog.
*/
@Parcelize
data object Hidden : BasicDialogState()
/**
* Show the dialog with the given values.
*/
@Parcelize
data class Shown(
val title: Text?,
val message: Text,
) : BasicDialogState()
}

View File

@ -1,107 +0,0 @@
package com.bitwarden.authenticator.ui.platform.components.dialog
import android.os.Parcelable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.bitwarden.authenticator.ui.platform.theme.AuthenticatorTheme
import com.bitwarden.ui.util.Text
import com.bitwarden.ui.util.asText
import kotlinx.parcelize.Parcelize
/**
* Represents a Bitwarden-styled loading dialog that shows text and a circular progress indicator.
*
* @param visibilityState the [LoadingDialogState] used to populate the dialog.
*/
@Composable
fun BitwardenLoadingDialog(
visibilityState: LoadingDialogState,
) {
when (visibilityState) {
is LoadingDialogState.Hidden -> Unit
is LoadingDialogState.Shown -> {
Dialog(
onDismissRequest = {},
properties = DialogProperties(
dismissOnBackPress = false,
dismissOnClickOutside = false,
),
) {
Card(
shape = RoundedCornerShape(28.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
),
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(),
) {
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = visibilityState.text(),
modifier = Modifier.padding(
top = 24.dp,
bottom = 8.dp,
),
)
CircularProgressIndicator(
modifier = Modifier.padding(
top = 8.dp,
bottom = 24.dp,
),
)
}
}
}
}
}
}
@Preview
@Composable
private fun BitwardenLoadingDialog_preview() {
AuthenticatorTheme {
BitwardenLoadingDialog(
visibilityState = LoadingDialogState.Shown(
text = "Loading...".asText(),
),
)
}
}
/**
* Models display of a [BitwardenLoadingDialog].
*/
sealed class LoadingDialogState : Parcelable {
/**
* Hide the dialog.
*/
@Parcelize
data object Hidden : LoadingDialogState()
/**
* Show the dialog with the given values.
*/
@Parcelize
data class Shown(val text: Text) : LoadingDialogState()
}

View File

@ -1,120 +0,0 @@
package com.bitwarden.authenticator.ui.platform.components.dialog
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredHeightIn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTagsAsResourceId
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import com.bitwarden.authenticator.ui.platform.components.button.AuthenticatorTextButton
import com.bitwarden.ui.platform.components.dialog.util.maxDialogHeight
import com.bitwarden.ui.platform.resource.BitwardenString
/**
* Displays a dialog with a title and "Cancel" button.
*
* @param title Title to display.
* @param subtitle Optional subtitle to display below the title.
* @param dismissLabel Label to show on the dismiss button at the bottom of the dialog.
* @param onDismissRequest Invoked when the user dismisses the dialog.
* @param onDismissActionClick Invoked when the user dismisses the via the dismiss action button.
* By default, this just defers to onDismissRequest.
* @param selectionItems Lambda containing selection items to show to the user. See
* [BitwardenSelectionRow].
*/
@Suppress("LongMethod")
@Composable
fun BitwardenSelectionDialog(
title: String,
subtitle: String? = null,
dismissLabel: String = stringResource(BitwardenString.cancel),
onDismissRequest: () -> Unit,
onDismissActionClick: () -> Unit = onDismissRequest,
selectionItems: @Composable ColumnScope.() -> Unit = {},
) {
Dialog(
onDismissRequest = onDismissRequest,
) {
val configuration = LocalConfiguration.current
val scrollState = rememberScrollState()
Column(
modifier = Modifier
.semantics { testTagsAsResourceId = true }
.requiredHeightIn(
max = configuration.maxDialogHeight,
)
// This background is necessary for the dialog to not be transparent.
.background(
color = MaterialTheme.colorScheme.surfaceContainerHigh,
shape = RoundedCornerShape(28.dp),
),
horizontalAlignment = Alignment.End,
) {
Text(
modifier = Modifier
.padding(24.dp)
.fillMaxWidth(),
text = title,
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.headlineSmall,
)
subtitle?.let {
Text(
modifier = Modifier
.padding(
start = 24.dp,
end = 24.dp,
bottom = 24.dp,
)
.fillMaxWidth(),
text = subtitle,
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodyMedium,
)
}
if (scrollState.canScrollBackward) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(1.dp)
.background(MaterialTheme.colorScheme.outlineVariant),
)
}
Column(
modifier = Modifier
.weight(1f, fill = false)
.verticalScroll(scrollState),
content = selectionItems,
)
if (scrollState.canScrollForward) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(1.dp)
.background(MaterialTheme.colorScheme.outlineVariant),
)
}
AuthenticatorTextButton(
modifier = Modifier.padding(24.dp),
label = dismissLabel,
onClick = onDismissActionClick,
)
}
}
}

View File

@ -1,52 +0,0 @@
package com.bitwarden.authenticator.ui.platform.components.dialog
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.selected
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import com.bitwarden.ui.util.Text
/**
* A clickable item that displays a radio button and text.
*
* @param text The text to display.
* @param onClick Invoked when either the radio button or text is clicked.
* @param isSelected Whether or not the radio button should be checked.
*/
@Composable
fun BitwardenSelectionRow(
text: Text,
onClick: () -> Unit,
isSelected: Boolean,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier
.fillMaxWidth()
.clickable(onClick = onClick)
.semantics(mergeDescendants = true) {
selected = isSelected
},
verticalAlignment = Alignment.CenterVertically,
) {
RadioButton(
modifier = Modifier.padding(16.dp),
selected = isSelected,
onClick = null,
)
Text(
text = text(),
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.bodyLarge,
)
}
}

View File

@ -1,68 +0,0 @@
package com.bitwarden.authenticator.ui.platform.components.dialog
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import com.bitwarden.authenticator.ui.platform.components.button.AuthenticatorTextButton
/**
* Represents a Bitwarden-styled dialog with two buttons.
*
* @param title the optional title to show.
* @param message message to show.
* @param confirmButtonText text to show on confirm button.
* @param dismissButtonText text to show on dismiss button.
* @param onConfirmClick called when the confirm button is clicked.
* @param onDismissClick called when the dismiss button is clicked.
* @param onDismissRequest called when the user attempts to dismiss the dialog (for example by
* tapping outside of it).
* @param confirmTextColor The color of the confirm text.
* @param dismissTextColor The color of the dismiss text.
*/
@Composable
fun BitwardenTwoButtonDialog(
title: String?,
message: String,
confirmButtonText: String,
dismissButtonText: String,
onConfirmClick: () -> Unit,
onDismissClick: () -> Unit,
onDismissRequest: () -> Unit,
confirmTextColor: Color? = null,
dismissTextColor: Color? = null,
) {
AlertDialog(
onDismissRequest = onDismissRequest,
dismissButton = {
AuthenticatorTextButton(
label = dismissButtonText,
labelTextColor = dismissTextColor,
onClick = onDismissClick,
)
},
confirmButton = {
AuthenticatorTextButton(
label = confirmButtonText,
labelTextColor = confirmTextColor,
onClick = onConfirmClick,
)
},
title = title?.let {
{
Text(
text = it,
style = MaterialTheme.typography.headlineSmall,
)
}
},
text = {
Text(
text = message,
style = MaterialTheme.typography.bodyMedium,
)
},
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
)
}

View File

@ -1,45 +0,0 @@
package com.bitwarden.authenticator.ui.platform.components.dialog.row
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.bitwarden.authenticator.ui.platform.components.dialog.BitwardenSelectionDialog
/**
* A simple clickable row for use within a [BitwardenSelectionDialog] as an alternative to a
* [BitwardenSelectionRow].
*
* @param text The text to display in the row.
* @param onClick A callback to be invoked when the row is clicked.
* @param modifier A [Modifier] for the composable.
*/
@Composable
fun BitwardenBasicDialogRow(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Text(
text = text,
style = MaterialTheme.typography.bodyLarge,
modifier = modifier
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = ripple(color = MaterialTheme.colorScheme.primary),
onClick = onClick,
)
.padding(
vertical = 16.dp,
horizontal = 24.dp,
)
.fillMaxWidth(),
)
}

View File

@ -1,52 +0,0 @@
package com.bitwarden.authenticator.ui.platform.components.dialog.row
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.selected
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import com.bitwarden.ui.util.Text
/**
* A clickable item that displays a radio button and text.
*
* @param text The text to display.
* @param onClick Invoked when either the radio button or text is clicked.
* @param isSelected Whether or not the radio button should be checked.
*/
@Composable
fun BitwardenSelectionRow(
text: Text,
onClick: () -> Unit,
isSelected: Boolean,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier
.fillMaxWidth()
.clickable(onClick = onClick)
.semantics(mergeDescendants = true) {
selected = isSelected
},
verticalAlignment = Alignment.CenterVertically,
) {
RadioButton(
modifier = Modifier.padding(16.dp),
selected = isSelected,
onClick = null,
)
Text(
text = text(),
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.bodyLarge,
)
}
}

View File

@ -1,186 +0,0 @@
package com.bitwarden.authenticator.ui.platform.components.dropdown
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
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.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.semantics.CustomAccessibilityAction
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.customActions
import androidx.compose.ui.semantics.role
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.bitwarden.authenticator.ui.platform.components.dialog.BitwardenSelectionDialog
import com.bitwarden.authenticator.ui.platform.components.dialog.BitwardenSelectionRow
import com.bitwarden.authenticator.ui.platform.theme.AuthenticatorTheme
import com.bitwarden.ui.platform.components.model.TooltipData
import com.bitwarden.ui.platform.resource.BitwardenDrawable
import com.bitwarden.ui.util.asText
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
/**
* A custom composable representing a multi-select button.
*
* This composable displays an [OutlinedTextField] with a dropdown icon as a trailing icon.
* When the field is clicked, a dropdown menu appears with a list of options to select from.
*
* @param label The descriptive text label for the [OutlinedTextField].
* @param options A list of strings representing the available options in the dialog.
* @param selectedOption The currently selected option that is displayed in the [OutlinedTextField]
* (or `null` if no option is selected).
* @param onOptionSelected A lambda that is invoked when an option
* is selected from the dropdown menu.
* @param isEnabled Whether or not the button is enabled.
* @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.
*/
@Suppress("LongMethod")
@Composable
fun BitwardenMultiSelectButton(
label: String,
options: ImmutableList<String>,
selectedOption: String?,
onOptionSelected: (String) -> Unit,
modifier: Modifier = Modifier,
isEnabled: Boolean = true,
supportingText: String? = null,
tooltip: TooltipData? = null,
) {
var shouldShowDialog by rememberSaveable { mutableStateOf(false) }
OutlinedTextField(
modifier = modifier
.clearAndSetSemantics {
role = Role.DropdownList
contentDescription = supportingText
?.let { "$selectedOption. $label. $it" }
?: "$selectedOption. $label"
customActions = listOfNotNull(
tooltip?.let {
CustomAccessibilityAction(
label = it.contentDescription,
action = {
it.onClick()
true
},
)
},
)
}
.fillMaxWidth()
.clickable(
indication = null,
enabled = isEnabled,
interactionSource = remember { MutableInteractionSource() },
) {
shouldShowDialog = !shouldShowDialog
},
textStyle = MaterialTheme.typography.bodyLarge,
readOnly = true,
label = {
Row {
Text(
text = label,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
tooltip?.let {
Spacer(modifier = Modifier.width(3.dp))
IconButton(
onClick = it.onClick,
enabled = isEnabled,
colors = IconButtonDefaults.iconButtonColors(
contentColor = MaterialTheme.colorScheme.primary,
),
modifier = Modifier.size(16.dp),
) {
Icon(
painter = painterResource(id = BitwardenDrawable.ic_tooltip_small),
contentDescription = it.contentDescription,
)
}
}
}
},
value = selectedOption.orEmpty(),
onValueChange = onOptionSelected,
enabled = shouldShowDialog,
trailingIcon = {
Icon(
painter = painterResource(id = BitwardenDrawable.ic_region_select_dropdown),
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
},
colors = OutlinedTextFieldDefaults.colors(
disabledTextColor = MaterialTheme.colorScheme.onSurface,
disabledBorderColor = MaterialTheme.colorScheme.outline,
disabledLeadingIconColor = MaterialTheme.colorScheme.onSurfaceVariant,
disabledTrailingIconColor = MaterialTheme.colorScheme.onSurfaceVariant,
disabledLabelColor = MaterialTheme.colorScheme.onSurfaceVariant,
disabledPlaceholderColor = MaterialTheme.colorScheme.onSurfaceVariant,
disabledSupportingTextColor = MaterialTheme.colorScheme.onSurfaceVariant,
),
supportingText = supportingText?.let {
{
Text(
text = supportingText,
style = MaterialTheme.typography.bodySmall,
)
}
},
)
if (shouldShowDialog) {
BitwardenSelectionDialog(
title = label,
onDismissRequest = { shouldShowDialog = false },
) {
options.forEach { optionString ->
BitwardenSelectionRow(
text = optionString.asText(),
isSelected = optionString == selectedOption,
onClick = {
shouldShowDialog = false
onOptionSelected(optionString)
},
)
}
}
}
}
@Preview
@Composable
private fun BitwardenMultiSelectButton_preview() {
AuthenticatorTheme {
BitwardenMultiSelectButton(
label = "Label",
options = persistentListOf("a", "b"),
selectedOption = "",
onOptionSelected = {},
)
}
}

View File

@ -1,210 +0,0 @@
package com.bitwarden.authenticator.ui.platform.components.fab
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.expandHorizontally
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkHorizontally
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SmallFloatingActionButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import com.bitwarden.authenticator.ui.platform.components.model.IconResource
import com.bitwarden.authenticator.ui.platform.theme.Typography
import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.util.Text
/**
* A FAB that expands, when clicked, to display a collection of options that can be clicked.
*
* @param label [Text] displayed when the FAB is expanded.
* @param items [ExpandableFabOption] buttons displayed when the FAB is expanded.
* @param expandableFabState [ExpandableFabIcon] displayed in the FAB.
* @param onStateChange Lambda invoked when the FAB expanded state changes.
*/
@Suppress("LongMethod")
@Composable
fun <T : ExpandableFabOption> ExpandableFloatingActionButton(
modifier: Modifier = Modifier,
label: Text?,
items: List<T>,
expandableFabState: MutableState<ExpandableFabState> = rememberExpandableFabState(),
expandableFabIcon: ExpandableFabIcon,
onStateChange: (expandableFabState: ExpandableFabState) -> Unit = { },
) {
val rotation by animateFloatAsState(
targetValue = if (expandableFabState.value == ExpandableFabState.Expanded) {
expandableFabIcon.iconRotation ?: 0f
} else {
0f
},
label = stringResource(BitwardenString.add_item_rotation),
)
Column(
modifier = modifier.wrapContentSize(),
horizontalAlignment = Alignment.End,
) {
AnimatedVisibility(
visible = expandableFabState.value.isExpanded(),
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically(),
) {
LazyColumn(
modifier = Modifier
.wrapContentSize()
.padding(bottom = 16.dp),
horizontalAlignment = Alignment.End,
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
items(items) { expandableFabOption ->
ExpandableFabOption(
onFabOptionClick = {
expandableFabState.value = expandableFabState.value.toggleValue()
onStateChange(expandableFabState.value)
expandableFabOption.onFabOptionClick()
},
expandableFabOption = expandableFabOption,
)
}
}
}
ExtendedFloatingActionButton(
onClick = {
expandableFabState.value = expandableFabState.value.toggleValue()
onStateChange(expandableFabState.value)
},
containerColor = MaterialTheme.colorScheme.primaryContainer,
) {
if (label != null) {
AnimatedVisibility(
visible = expandableFabState.value.isExpanded(),
enter = fadeIn() + expandHorizontally(),
exit = fadeOut() + shrinkHorizontally(),
) {
Text(
modifier = Modifier.padding(end = 8.dp),
text = label(),
)
}
}
Icon(
modifier = Modifier
.rotate(rotation)
.semantics { expandableFabIcon.iconData.testTag },
painter = expandableFabIcon.iconData.iconPainter,
contentDescription = expandableFabIcon.iconData.contentDescription,
)
}
}
}
@Composable
private fun <T : ExpandableFabOption> ExpandableFabOption(
expandableFabOption: T,
onFabOptionClick: (option: T) -> Unit,
) {
SmallFloatingActionButton(
onClick = { onFabOptionClick(expandableFabOption) },
) {
Row(
modifier = Modifier
.wrapContentSize()
.padding(end = 8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
expandableFabOption.label?.let { label ->
Text(
modifier = Modifier
.clip(RoundedCornerShape(size = 8.dp))
.padding(all = 8.dp),
text = label(),
style = Typography.labelSmall,
)
}
Icon(
painter = expandableFabOption.iconData.iconPainter,
contentDescription = expandableFabOption.iconData.contentDescription,
)
}
}
}
@Composable
private fun rememberExpandableFabState() =
remember { mutableStateOf<ExpandableFabState>(ExpandableFabState.Collapsed) }
/**
* Represents options displayed when the FAB is expanded.
*/
abstract class ExpandableFabOption(
val label: Text?,
val iconData: IconResource,
val onFabOptionClick: () -> Unit,
)
/**
* Models data for an expandable FAB icon.
*/
data class ExpandableFabIcon(
val iconData: IconResource,
val iconRotation: Float?,
)
/**
* Models the state of the expandable FAB.
*/
sealed class ExpandableFabState {
/**
* Indicates if the FAB is expanded.
*/
fun isExpanded() = this is Expanded
/**
* Invert the state of the FAB.
*/
fun toggleValue() = if (isExpanded()) {
Collapsed
} else {
Expanded
}
/**
* Indicates the FAB is collapsed.
*/
data object Collapsed : ExpandableFabState()
/**
* Indicates the FAB is expanded.
*/
data object Expanded : ExpandableFabState()
}

View File

@ -1,235 +0,0 @@
package com.bitwarden.authenticator.ui.platform.components.field
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
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.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTag
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.tooling.preview.Preview
import com.bitwarden.ui.platform.components.util.nonLetterColorVisualTransformation
import com.bitwarden.ui.platform.resource.BitwardenDrawable
import com.bitwarden.ui.platform.resource.BitwardenString
/**
* 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 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.
* @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.
*/
@Composable
fun BitwardenPasswordField(
label: String,
value: String,
showPassword: Boolean,
showPasswordChange: (Boolean) -> Unit,
onValueChange: (String) -> 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,
capitalization: KeyboardCapitalization = KeyboardCapitalization.None,
) {
val focusRequester = remember { FocusRequester() }
OutlinedTextField(
modifier = modifier.focusRequester(focusRequester),
textStyle = MaterialTheme.typography.bodyLarge,
label = { Text(text = label) },
value = value,
onValueChange = onValueChange,
visualTransformation = when {
!showPassword -> PasswordVisualTransformation()
readOnly -> nonLetterColorVisualTransformation(
digitColor = MaterialTheme.colorScheme.primary,
specialCharacterColor = MaterialTheme.colorScheme.error,
)
else -> VisualTransformation.None
},
singleLine = singleLine,
readOnly = readOnly,
keyboardOptions = KeyboardOptions(
capitalization = capitalization,
keyboardType = keyboardType,
imeAction = imeAction,
),
supportingText = hint?.let {
{
Text(
text = hint,
style = MaterialTheme.typography.bodySmall,
)
}
},
trailingIcon = {
IconButton(
onClick = { showPasswordChange.invoke(!showPassword) },
) {
@DrawableRes
val painterRes = if (showPassword) {
BitwardenDrawable.ic_visibility_off
} else {
BitwardenDrawable.ic_visibility
}
@StringRes
val contentDescriptionRes =
if (showPassword) BitwardenString.hide else BitwardenString.show
Icon(
modifier = Modifier.semantics { showPasswordTestTag?.let { testTag = it } },
painter = painterResource(id = painterRes),
contentDescription = stringResource(id = contentDescriptionRes),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
},
)
if (autoFocus) {
LaunchedEffect(Unit) { focusRequester.requestFocus() }
}
}
/**
* Represents a Bitwarden-styled password field that manages the state of a show/hide indicator
* internally.
*
* @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 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 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).
* @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.
*/
@Composable
fun BitwardenPasswordField(
label: String,
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
readOnly: Boolean = false,
singleLine: Boolean = true,
hint: String? = null,
initialShowPassword: Boolean = false,
showPasswordTestTag: String? = null,
autoFocus: Boolean = false,
keyboardType: KeyboardType = KeyboardType.Password,
imeAction: ImeAction = ImeAction.Default,
capitalization: KeyboardCapitalization = KeyboardCapitalization.None,
) {
var showPassword by rememberSaveable { mutableStateOf(initialShowPassword) }
BitwardenPasswordField(
modifier = modifier,
label = label,
value = value,
showPassword = showPassword,
showPasswordChange = { showPassword = !showPassword },
onValueChange = onValueChange,
readOnly = readOnly,
singleLine = singleLine,
hint = hint,
showPasswordTestTag = showPasswordTestTag,
autoFocus = autoFocus,
keyboardType = keyboardType,
imeAction = imeAction,
capitalization = capitalization,
)
}
@Preview(showBackground = true)
@Composable
private fun BitwardenPasswordField_preview_withInput_hidePassword() {
BitwardenPasswordField(
label = "Label",
value = "Password",
onValueChange = {},
initialShowPassword = false,
hint = "Hint",
)
}
@Preview(showBackground = true)
@Composable
private fun BitwardenPasswordField_preview_withInput_showPassword() {
BitwardenPasswordField(
label = "Label",
value = "Password",
onValueChange = {},
initialShowPassword = true,
hint = "Hint",
)
}
@Preview(showBackground = true)
@Composable
private fun BitwardenPasswordField_preview_withoutInput_hidePassword() {
BitwardenPasswordField(
label = "Label",
value = "",
onValueChange = {},
initialShowPassword = false,
hint = "Hint",
)
}
@Preview(showBackground = true)
@Composable
private fun BitwardenPasswordField_preview_withoutInput_showPassword() {
BitwardenPasswordField(
label = "Label",
value = "",
onValueChange = {},
initialShowPassword = true,
hint = "Hint",
)
}

View File

@ -1,138 +0,0 @@
package com.bitwarden.authenticator.ui.platform.components.field
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onGloballyPositioned
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.bitwarden.authenticator.ui.platform.components.model.IconResource
import com.bitwarden.ui.platform.base.util.toPx
import com.bitwarden.ui.platform.base.util.withLineBreaksAtWidth
/**
* 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 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 hint optional hint 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.
* @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.
*/
@Composable
fun BitwardenTextField(
label: String,
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
placeholder: String? = null,
leadingIconResource: IconResource? = null,
trailingIconContent: (@Composable () -> Unit)? = null,
hint: String? = null,
singleLine: Boolean = true,
readOnly: Boolean = false,
enabled: Boolean = true,
textStyle: TextStyle? = null,
shouldAddCustomLineBreaks: Boolean = false,
keyboardType: KeyboardType = KeyboardType.Text,
isError: Boolean = false,
visualTransformation: VisualTransformation = VisualTransformation.None,
) {
var widthPx by remember { mutableIntStateOf(0) }
val currentTextStyle = textStyle ?: LocalTextStyle.current
val formattedText = if (shouldAddCustomLineBreaks) {
value.withLineBreaksAtWidth(
// Adjust for built in padding
widthPx = widthPx - 16.dp.toPx(),
monospacedTextStyle = currentTextStyle,
)
} else {
value
}
OutlinedTextField(
modifier = modifier
.onGloballyPositioned { widthPx = it.size.width },
enabled = enabled,
label = { Text(text = label) },
value = formattedText,
leadingIcon = leadingIconResource?.let { iconResource ->
{
Icon(
painter = iconResource.iconPainter,
contentDescription = iconResource.contentDescription,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
},
trailingIcon = trailingIconContent?.let {
trailingIconContent
},
placeholder = placeholder?.let {
{ Text(text = it) }
},
supportingText = hint?.let {
{
Text(
text = hint,
style = MaterialTheme.typography.bodySmall,
)
}
},
onValueChange = onValueChange,
singleLine = singleLine,
readOnly = readOnly,
textStyle = currentTextStyle,
keyboardOptions = KeyboardOptions.Default.copy(keyboardType = keyboardType),
isError = isError,
visualTransformation = visualTransformation,
)
}
@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",
)
}

View File

@ -1,100 +0,0 @@
package com.bitwarden.authenticator.ui.platform.components.field
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTag
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 com.bitwarden.authenticator.ui.platform.theme.AuthenticatorTheme
import com.bitwarden.ui.platform.components.row.BitwardenRowOfActions
import com.bitwarden.ui.platform.resource.BitwardenDrawable
/**
* Represents a Bitwarden-styled text field accompanied by a series of actions.
* This component allows for a more versatile design by accepting
* icons or actions next to the text field.
*
* @param label Label for the text field.
* @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 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 trailingIconContent the content for the trailing icon in the text field.
* @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 textFieldTestTag The test tag to be used on the text field.
*/
@Composable
fun BitwardenTextFieldWithActions(
label: String,
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
textStyle: TextStyle? = null,
shouldAddCustomLineBreaks: Boolean = false,
visualTransformation: VisualTransformation = VisualTransformation.None,
readOnly: Boolean = false,
singleLine: Boolean = true,
keyboardType: KeyboardType = KeyboardType.Text,
trailingIconContent: (@Composable () -> Unit)? = null,
actions: @Composable RowScope.() -> Unit = {},
actionsTestTag: String? = null,
textFieldTestTag: String? = null,
) {
Row(
modifier = modifier
.fillMaxWidth()
.semantics(mergeDescendants = true) { },
verticalAlignment = Alignment.CenterVertically,
) {
BitwardenTextField(
modifier = Modifier
.semantics { textFieldTestTag?.let { testTag = it } }
.weight(1f),
label = label,
value = value,
readOnly = readOnly,
singleLine = singleLine,
onValueChange = onValueChange,
keyboardType = keyboardType,
trailingIconContent = trailingIconContent,
textStyle = textStyle,
shouldAddCustomLineBreaks = shouldAddCustomLineBreaks,
visualTransformation = visualTransformation,
)
BitwardenRowOfActions(
modifier = Modifier.run { actionsTestTag?.let { semantics { testTag = it } } ?: this },
actions = actions,
)
}
}
@Preview(showBackground = true)
@Composable
private fun BitwardenTextFieldWithActions_preview() {
AuthenticatorTheme {
BitwardenTextFieldWithActions(
label = "Username",
value = "user@example.com",
onValueChange = {},
actions = {
Icon(
painter = painterResource(id = BitwardenDrawable.ic_tooltip),
contentDescription = "Action 1",
)
},
)
}
}

View File

@ -10,7 +10,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.minimumInteractiveComponentSize
import androidx.compose.runtime.Composable
@ -22,6 +21,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.bitwarden.ui.platform.resource.BitwardenDrawable
import com.bitwarden.ui.platform.theme.BitwardenTheme
/**
* A header that can be expanded and collapsed.
@ -54,15 +54,15 @@ fun AuthenticatorExpandingHeader(
)
Text(
text = label,
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = BitwardenTheme.typography.labelMedium,
color = BitwardenTheme.colorScheme.text.secondary,
modifier = Modifier.weight(1f, fill = false),
)
Spacer(modifier = Modifier.width(width = 8.dp))
Icon(
painter = rememberVectorPainter(id = BitwardenDrawable.ic_chevron_up_small),
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
tint = BitwardenTheme.colorScheme.icon.primary,
modifier = Modifier.rotate(degrees = iconRotationDegrees.value),
)
}

View File

@ -1,43 +0,0 @@
package com.bitwarden.authenticator.ui.platform.components.header
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
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.bitwarden.authenticator.ui.platform.theme.AuthenticatorTheme
/**
* Represents a Bitwarden-styled label text.
*
* @param label The text content for the label.
* @param modifier The [Modifier] to be applied to the label.
*/
@Composable
fun BitwardenListHeaderText(
label: String,
modifier: Modifier = Modifier,
) {
Text(
text = label,
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = modifier.padding(
top = 12.dp,
bottom = 4.dp,
),
)
}
@Preview(showBackground = true)
@Composable
private fun BitwardenListHeaderText_preview() {
AuthenticatorTheme {
BitwardenListHeaderText(
label = "Sample Label",
modifier = Modifier,
)
}
}

View File

@ -1,62 +0,0 @@
package com.bitwarden.authenticator.ui.platform.components.header
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.bitwarden.authenticator.ui.platform.theme.AuthenticatorTheme
/**
* Represents a Bitwarden-styled label text.
*
* @param label The text content for the label.
* @param supportingLabel The text for the supporting label.
* @param modifier The [Modifier] to be applied to the label.
*/
@Composable
fun BitwardenListHeaderTextWithSupportLabel(
label: String,
supportingLabel: String,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier
.padding(
top = 12.dp,
bottom = 4.dp,
end = 8.dp,
)
.semantics(mergeDescendants = true) { },
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(
text = label,
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Text(
text = supportingLabel,
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
@Preview(showBackground = true)
@Composable
private fun BitwardenListHeaderTextWithSupportLabel_preview() {
AuthenticatorTheme {
BitwardenListHeaderTextWithSupportLabel(
label = "Sample Label",
supportingLabel = "0",
modifier = Modifier,
)
}
}

View File

@ -1,61 +0,0 @@
package com.bitwarden.authenticator.ui.platform.components.icon
import androidx.compose.material3.FilledIconButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButtonColors
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.tooling.preview.Preview
import com.bitwarden.authenticator.ui.platform.components.model.IconResource
import com.bitwarden.authenticator.ui.platform.theme.AuthenticatorTheme
import com.bitwarden.ui.platform.resource.BitwardenDrawable
/**
* An icon button that displays an icon from the provided [IconResource].
*
* @param iconRes Icon to display on the button.
* @param onClick Callback for when the icon button is clicked.
* @param isEnabled Whether or not the button should be enabled.
* @param modifier A [Modifier] for the composable.
*/
@Composable
fun BitwardenIconButtonWithResource(
iconRes: IconResource,
onClick: () -> Unit,
modifier: Modifier = Modifier,
isEnabled: Boolean = true,
) {
FilledIconButton(
modifier = modifier.semantics(mergeDescendants = true) {},
onClick = onClick,
colors = IconButtonColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer,
contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
disabledContainerColor = MaterialTheme.colorScheme.onSurface.copy(alpha = .12f),
disabledContentColor = MaterialTheme.colorScheme.onSecondaryContainer,
),
enabled = isEnabled,
) {
Icon(
painter = iconRes.iconPainter,
contentDescription = iconRes.contentDescription,
)
}
}
@Preview(showBackground = true)
@Composable
private fun BitwardenIconButtonWithResource_preview() {
AuthenticatorTheme {
BitwardenIconButtonWithResource(
iconRes = IconResource(
iconPainter = painterResource(id = BitwardenDrawable.ic_tooltip),
contentDescription = "Sample Icon",
),
onClick = {},
)
}
}

View File

@ -1,70 +0,0 @@
package com.bitwarden.authenticator.ui.platform.components.indicator
import androidx.compose.animation.core.LinearOutSlowInEasing
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.unit.dp
/**
* A countdown timer displayed to the user.
*
* @param timeLeftSeconds The seconds left on the timer.
* @param periodSeconds The period for the timer countdown.
* @param modifier A [Modifier] for the composable.
*/
@Composable
fun AuthenticatorCircularCountdownIndicator(
modifier: Modifier = Modifier,
timeLeftSeconds: Int,
periodSeconds: Int,
alertThresholdSeconds: Int = -1,
alertIndicatorColor: Color = MaterialTheme.colorScheme.error,
) {
val progressAnimate by animateFloatAsState(
targetValue = timeLeftSeconds.toFloat() / periodSeconds,
animationSpec = tween(
durationMillis = periodSeconds,
delayMillis = 0,
easing = LinearOutSlowInEasing,
),
label = "CircularCountDownAnimation",
)
Box(
contentAlignment = Alignment.Center,
modifier = modifier,
) {
CircularProgressIndicator(
progress = { progressAnimate },
modifier = Modifier.size(size = 30.dp),
color = if (timeLeftSeconds > alertThresholdSeconds) {
MaterialTheme.colorScheme.primary
} else {
alertIndicatorColor
},
strokeWidth = 3.dp,
strokeCap = StrokeCap.Round,
)
Text(
text = timeLeftSeconds.toString(),
style = MaterialTheme.typography.bodySmall,
color = if (timeLeftSeconds > alertThresholdSeconds) {
MaterialTheme.colorScheme.onSurfaceVariant
} else {
alertIndicatorColor
},
)
}
}

View File

@ -1,189 +0,0 @@
package com.bitwarden.authenticator.ui.platform.components.listitem
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.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
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
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.graphics.painter.Painter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTag
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.bitwarden.authenticator.ui.platform.components.dialog.BitwardenSelectionDialog
import com.bitwarden.authenticator.ui.platform.components.dialog.row.BitwardenBasicDialogRow
import com.bitwarden.authenticator.ui.platform.components.model.IconResource
import com.bitwarden.authenticator.ui.platform.theme.AuthenticatorTheme
import com.bitwarden.ui.platform.components.icon.BitwardenIcon
import com.bitwarden.ui.platform.components.icon.model.IconData
import com.bitwarden.ui.platform.resource.BitwardenDrawable
import com.bitwarden.ui.platform.resource.BitwardenString
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
/**
* A Composable function that displays a row item.
*
* @param label The primary text label to display for the item.
* @param startIcon The [Painter] object used to draw the icon at the start of the item.
* @param onClick The lambda to be invoked when the item is clicked.
* @param selectionDataList A list of all the selection items to be displayed in the overflow
* dialog.
* @param modifier An optional [Modifier] for this Composable, defaulting to an empty Modifier.
* This allows the caller to specify things like padding, size, etc.
* @param labelTestTag The optional test tag for the [label].
* @param optionsTestTag The optional test tag for the options button.
* @param supportingLabel An optional secondary text label to display beneath the label.
* @param supportingLabelTestTag The optional test tag for the [supportingLabel].
* @param trailingLabelIcons An optional list of small icons to be displayed after the [label].
*/
@Suppress("LongMethod")
@Composable
fun BitwardenListItem(
label: String,
startIcon: IconData,
onClick: () -> Unit,
selectionDataList: ImmutableList<SelectionItemData>,
modifier: Modifier = Modifier,
labelTestTag: String? = null,
optionsTestTag: String? = null,
supportingLabel: String? = null,
supportingLabelTestTag: String? = null,
trailingLabelIcons: ImmutableList<IconResource> = persistentListOf(),
) {
var shouldShowDialog by rememberSaveable { mutableStateOf(false) }
Row(
modifier = Modifier
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = ripple(color = MaterialTheme.colorScheme.primary),
onClick = onClick,
)
.defaultMinSize(minHeight = 72.dp)
.padding(vertical = 8.dp)
.then(modifier),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp),
) {
BitwardenIcon(
iconData = startIcon,
tint = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.size(24.dp),
)
Column(modifier = Modifier.weight(1f)) {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = label,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.semantics { labelTestTag?.let { testTag = it } }
.weight(weight = 1f, fill = false),
)
trailingLabelIcons.forEach { iconResource ->
Spacer(modifier = Modifier.width(8.dp))
Icon(
painter = iconResource.iconPainter,
contentDescription = iconResource.contentDescription,
tint = MaterialTheme.colorScheme.secondary,
modifier = Modifier
.semantics { iconResource.testTag?.let { testTag = it } }
.size(16.dp),
)
}
}
supportingLabel?.let { supportLabel ->
Text(
text = supportLabel,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.semantics { supportingLabelTestTag?.let { testTag = it } },
)
}
}
if (selectionDataList.isNotEmpty()) {
IconButton(
onClick = { shouldShowDialog = true },
modifier = Modifier.semantics { optionsTestTag?.let { testTag = it } },
) {
Icon(
painter = painterResource(id = BitwardenDrawable.ic_more_horizontal),
contentDescription = stringResource(id = BitwardenString.options),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(24.dp),
)
}
}
}
if (shouldShowDialog) {
BitwardenSelectionDialog(
title = label,
onDismissRequest = { shouldShowDialog = false },
selectionItems = {
selectionDataList.forEach { itemData ->
BitwardenBasicDialogRow(
modifier = Modifier.semantics { itemData.testTag?.let { testTag = it } },
text = itemData.text,
onClick = {
shouldShowDialog = false
itemData.onClick()
},
)
}
},
)
}
}
/**
* Wrapper for the an individual selection item's data.
*/
data class SelectionItemData(
val text: String,
val onClick: () -> Unit,
val testTag: String? = null,
)
@Preview(showBackground = true)
@Composable
private fun BitwardenListItem_preview() {
AuthenticatorTheme {
BitwardenListItem(
label = "Sample Label",
supportingLabel = "Jan 3, 2024, 10:35 AM",
startIcon = IconData.Local(BitwardenDrawable.ic_login_item),
onClick = {},
selectionDataList = persistentListOf(),
)
}
}

View File

@ -1,16 +0,0 @@
package com.bitwarden.authenticator.ui.platform.components.model
import androidx.compose.ui.graphics.painter.Painter
/**
* Data class representing the resources required for an icon.
*
* @property iconPainter Painter for the icon.
* @property contentDescription String for the icon's content description.
* @property testTag The optional test tag to associate with this icon.
*/
data class IconResource(
val iconPainter: Painter,
val contentDescription: String,
val testTag: String? = null,
)

View File

@ -1,89 +0,0 @@
package com.bitwarden.authenticator.ui.platform.components.row
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.bitwarden.authenticator.ui.platform.components.dialog.BitwardenTwoButtonDialog
import com.bitwarden.authenticator.ui.platform.theme.AuthenticatorTheme
import com.bitwarden.ui.platform.base.util.mirrorIfRtl
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.bitwarden.ui.platform.resource.BitwardenDrawable
import com.bitwarden.ui.platform.resource.BitwardenString
/**
* Represents a row of text that can be clicked on and contains an external link.
* A confirmation dialog will always be displayed before [onConfirmClick] is invoked.
*
* @param text The label for the row as a [String].
* @param onConfirmClick The callback when the confirm button of the dialog is clicked.
* @param modifier The modifier to be applied to the layout.
* @param withDivider Indicates if a divider should be drawn on the bottom of the row, defaults
* to `true`.
* @param dialogTitle The title of the dialog displayed when the user clicks this item.
* @param dialogMessage The message of the dialog displayed when the user clicks this item.
* @param dialogConfirmButtonText The text on the confirm button of the dialog displayed when the
* user clicks this item.
* @param dialogDismissButtonText The text on the dismiss button of the dialog displayed when the
* user clicks this item.
*/
@Composable
fun BitwardenExternalLinkRow(
text: String,
onConfirmClick: () -> Unit,
modifier: Modifier = Modifier,
withDivider: Boolean = true,
dialogTitle: String,
dialogMessage: String,
dialogConfirmButtonText: String = stringResource(id = BitwardenString.continue_text),
dialogDismissButtonText: String = stringResource(id = BitwardenString.cancel),
) {
var shouldShowDialog by rememberSaveable { mutableStateOf(false) }
BitwardenTextRow(
text = text,
onClick = { shouldShowDialog = true },
modifier = modifier,
withDivider = withDivider,
) {
Icon(
modifier = Modifier.mirrorIfRtl(),
painter = rememberVectorPainter(id = BitwardenDrawable.ic_external_link),
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurface,
)
}
if (shouldShowDialog) {
BitwardenTwoButtonDialog(
title = dialogTitle,
message = dialogMessage,
confirmButtonText = dialogConfirmButtonText,
dismissButtonText = dialogDismissButtonText,
onConfirmClick = {
shouldShowDialog = false
onConfirmClick()
},
onDismissClick = { shouldShowDialog = false },
onDismissRequest = { shouldShowDialog = false },
)
}
}
@Preview
@Composable
private fun BitwardenExternalLinkRow_preview() {
AuthenticatorTheme {
BitwardenExternalLinkRow(
text = "Linked Text",
onConfirmClick = { },
dialogTitle = "",
dialogMessage = "",
)
}
}

View File

@ -1,91 +0,0 @@
package com.bitwarden.authenticator.ui.platform.components.row
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.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.unit.dp
/**
* Represents a clickable row of text and can contains an optional [content] that appears to the
* right of the [text].
*
* @param text The label for the row as a [String].
* @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 withDivider Indicates if a divider should be drawn on the bottom of the row, defaults
* to `false`.
* @param content The content of the [BitwardenTextRow].
*/
@Composable
fun BitwardenTextRow(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
description: AnnotatedString? = null,
withDivider: Boolean = false,
content: (@Composable () -> Unit)? = null,
) {
Box(
contentAlignment = Alignment.BottomCenter,
modifier = modifier
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = ripple(color = MaterialTheme.colorScheme.primary),
onClick = onClick,
)
.semantics(mergeDescendants = true) { },
) {
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,
) {
Column(
modifier = Modifier
.padding(end = 16.dp)
.weight(1f),
) {
Text(
text = text,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
)
description?.let {
Text(
text = it,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
content?.invoke()
}
if (withDivider) {
HorizontalDivider(
modifier = Modifier.padding(start = 16.dp),
thickness = 1.dp,
color = MaterialTheme.colorScheme.outlineVariant,
)
}
}
}

View File

@ -1,56 +0,0 @@
package com.bitwarden.authenticator.ui.platform.components.scaffold
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.exclude
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.material3.FabPosition
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.ScaffoldDefaults
import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTagsAsResourceId
/**
* Direct passthrough to [Scaffold] but contains a few specific override values. Everything is
* still overridable if necessary.
*/
@Composable
fun BitwardenScaffold(
modifier: Modifier = Modifier,
topBar: @Composable () -> Unit = { },
bottomBar: @Composable () -> Unit = { },
snackbarHost: @Composable () -> Unit = { },
floatingActionButton: @Composable () -> Unit = { },
floatingActionButtonPosition: FabPosition = FabPosition.End,
containerColor: Color = MaterialTheme.colorScheme.surface,
contentColor: Color = contentColorFor(containerColor),
contentWindowInsets: WindowInsets = ScaffoldDefaults
.contentWindowInsets
.exclude(WindowInsets.navigationBars),
content: @Composable (PaddingValues) -> Unit,
) {
Scaffold(
modifier = Modifier
.semantics { testTagsAsResourceId = true }
.then(modifier),
topBar = topBar,
bottomBar = bottomBar,
snackbarHost = snackbarHost,
floatingActionButton = floatingActionButton,
floatingActionButtonPosition = floatingActionButtonPosition,
containerColor = containerColor,
contentColor = contentColor,
contentWindowInsets = contentWindowInsets,
content = { paddingValues ->
Box {
content(paddingValues)
}
},
)
}

View File

@ -1,45 +0,0 @@
package com.bitwarden.authenticator.ui.platform.components.scrim
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
/**
* A scrim that animates its visibility.
*
* @param isVisible Whether or not the scrim should be visible. This controls the animation.
* @param onClick A callback that is triggered when the scrim is clicked. No ripple will be
* performed.
* @param modifier A [Modifier] for the scrim's content.
*/
@Composable
fun BitwardenAnimatedScrim(
isVisible: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
AnimatedVisibility(
visible = isVisible,
enter = fadeIn(),
exit = fadeOut(),
) {
Box(
modifier = modifier
.background(Color.Black.copy(alpha = 0.40f))
.clickable(
interactionSource = remember { MutableInteractionSource() },
// Clear the ripple
indication = null,
onClick = onClick,
),
)
}
}

View File

@ -1,110 +0,0 @@
package com.bitwarden.authenticator.ui.platform.components.stepper
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTag
import androidx.compose.ui.text.input.KeyboardType
import com.bitwarden.authenticator.ui.platform.components.field.BitwardenTextFieldWithActions
import com.bitwarden.authenticator.ui.platform.components.icon.BitwardenIconButtonWithResource
import com.bitwarden.authenticator.ui.platform.components.model.IconResource
import com.bitwarden.ui.platform.base.util.ZERO_WIDTH_CHARACTER
import com.bitwarden.ui.platform.base.util.orNullIfBlank
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.bitwarden.ui.platform.resource.BitwardenDrawable
/**
* 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 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.
*/
@Suppress("LongMethod")
@Composable
fun BitwardenStepper(
label: String,
value: Int?,
onValueChange: (Int) -> Unit,
modifier: Modifier = Modifier,
range: ClosedRange<Int> = 1..Int.MAX_VALUE,
isIncrementEnabled: Boolean = true,
isDecrementEnabled: Boolean = true,
textFieldReadOnly: Boolean = true,
stepperActionsTestTag: String? = null,
increaseButtonTestTag: String? = null,
decreaseButtonTestTag: String? = null,
) {
val clampedValue = value?.coerceIn(range)
if (clampedValue != value && clampedValue != null) {
onValueChange(clampedValue)
}
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 = {
BitwardenIconButtonWithResource(
iconRes = IconResource(
iconPainter = rememberVectorPainter(id = BitwardenDrawable.ic_minus),
contentDescription = "\u2212",
),
onClick = {
val decrementedValue = ((value ?: 0) - 1).coerceIn(range)
if (decrementedValue != value) {
onValueChange(decrementedValue)
}
},
isEnabled = isDecrementEnabled,
modifier = Modifier.semantics {
if (decreaseButtonTestTag != null) {
testTag = decreaseButtonTestTag
}
},
)
BitwardenIconButtonWithResource(
iconRes = IconResource(
iconPainter = rememberVectorPainter(id = BitwardenDrawable.ic_plus),
contentDescription = "+",
),
onClick = {
val incrementedValue = ((value ?: 0) + 1).coerceIn(range)
if (incrementedValue != value) {
onValueChange(incrementedValue)
}
},
isEnabled = isIncrementEnabled,
modifier = Modifier.semantics {
if (increaseButtonTestTag != null) {
testTag = increaseButtonTestTag
}
},
)
},
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) ?: value }
?: range.start,
)
},
modifier = modifier,
)
}

View File

@ -1,118 +0,0 @@
package com.bitwarden.authenticator.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.Row
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.width
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.toggleableState
import androidx.compose.ui.state.ToggleableState
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
/**
* Represents a Bitwarden-styled [Switch].
*
* @param label The label for the switch.
* @param isChecked Whether or not the switch is currently checked.
* @param onCheckedChange A callback for when the checked state changes.
* @param modifier The [Modifier] to be applied to the button.
* @param description The description of the switch to be displayed below the [label].
*/
@Composable
fun BitwardenSwitch(
label: String,
isChecked: Boolean,
onCheckedChange: ((Boolean) -> Unit)?,
modifier: Modifier = Modifier,
description: String? = null,
) {
Row(
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.run {
if (onCheckedChange != null) {
this.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = ripple(color = MaterialTheme.colorScheme.primary),
onClick = { onCheckedChange.invoke(!isChecked) },
)
} else {
this
}
}
.semantics(mergeDescendants = true) {
toggleableState = ToggleableState(isChecked)
}
.then(modifier),
) {
Switch(
modifier = Modifier
.padding(vertical = 8.dp)
.height(32.dp)
.width(52.dp),
checked = isChecked,
onCheckedChange = null,
)
Spacer(modifier = Modifier.width(16.dp))
Column {
Spacer(modifier = Modifier.height(4.dp))
Text(
text = label,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
)
description?.let {
Text(
text = it,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
Spacer(modifier = Modifier.height(4.dp))
}
}
}
@Preview(showBackground = true)
@Composable
private fun BitwardenSwitch_preview_isChecked() {
BitwardenSwitch(
label = "Label",
description = "Description",
isChecked = true,
onCheckedChange = {},
modifier = Modifier.fillMaxWidth(),
)
}
@Preview(showBackground = true)
@Composable
private fun BitwardenSwitch_preview_isNotChecked() {
BitwardenSwitch(
label = "Label",
isChecked = false,
onCheckedChange = {},
modifier = Modifier.fillMaxWidth(),
)
}

View File

@ -1,130 +0,0 @@
package com.bitwarden.authenticator.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.Row
import androidx.compose.foundation.layout.Spacer
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.MaterialTheme
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.toggleableState
import androidx.compose.ui.state.ToggleableState
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.bitwarden.authenticator.ui.platform.theme.AuthenticatorTheme
/**
* A wide custom switch composable
*
* @param label The descriptive text label to be displayed adjacent to the switch.
* @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 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.
*/
@Composable
fun BitwardenWideSwitch(
label: String,
isChecked: Boolean,
onCheckedChange: ((Boolean) -> Unit)?,
modifier: Modifier = Modifier,
description: String? = null,
contentDescription: String? = null,
readOnly: Boolean = false,
enabled: Boolean = true,
) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.wrapContentHeight()
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = ripple(color = MaterialTheme.colorScheme.primary),
onClick = { onCheckedChange?.invoke(!isChecked) },
enabled = !readOnly && enabled,
)
.semantics(mergeDescendants = true) {
toggleableState = ToggleableState(isChecked)
contentDescription?.let { this.contentDescription = it }
}
.then(modifier),
) {
Column(
modifier = Modifier
.weight(1f)
.padding(vertical = 8.dp),
) {
Text(
text = label,
style = MaterialTheme.typography.bodyLarge,
color = if (enabled) {
MaterialTheme.colorScheme.onSurface
} else {
MaterialTheme.colorScheme.outline
},
)
description?.let {
Text(
text = it,
style = MaterialTheme.typography.bodyMedium,
color = if (enabled) {
MaterialTheme.colorScheme.onSurfaceVariant
} else {
MaterialTheme.colorScheme.outline
},
)
}
}
Spacer(modifier = Modifier.width(16.dp))
Switch(
modifier = Modifier
.height(56.dp),
checked = isChecked,
onCheckedChange = null,
)
}
}
@Preview
@Composable
private fun BitwardenWideSwitch_preview_isChecked() {
AuthenticatorTheme {
BitwardenWideSwitch(
label = "Label",
isChecked = true,
onCheckedChange = {},
)
}
}
@Preview
@Composable
private fun BitwardenWideSwitch_preview_isNotChecked() {
AuthenticatorTheme {
BitwardenWideSwitch(
label = "Label",
isChecked = false,
onCheckedChange = {},
)
}
}

View File

@ -5,11 +5,11 @@ 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
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@ -21,21 +21,21 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.bitwarden.authenticator.ui.platform.components.appbar.AuthenticatorTopAppBar
import com.bitwarden.authenticator.ui.platform.components.button.AuthenticatorFilledButton
import com.bitwarden.authenticator.ui.platform.components.content.AuthenticatorErrorContent
import com.bitwarden.authenticator.ui.platform.components.header.BitwardenListHeaderText
import com.bitwarden.authenticator.ui.platform.components.scaffold.BitwardenScaffold
import com.bitwarden.authenticator.ui.platform.feature.debugmenu.components.ListItemContent
import com.bitwarden.authenticator.ui.platform.theme.AuthenticatorTheme
import com.bitwarden.core.data.manager.model.FlagKey
import com.bitwarden.ui.platform.base.util.EventsEffect
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.bitwarden.ui.platform.base.util.toListItemCardStyle
import com.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
import com.bitwarden.ui.platform.components.appbar.NavigationIcon
import com.bitwarden.ui.platform.components.divider.BitwardenHorizontalDivider
import com.bitwarden.ui.platform.components.button.BitwardenFilledButton
import com.bitwarden.ui.platform.components.content.BitwardenErrorContent
import com.bitwarden.ui.platform.components.debug.ListItemContent
import com.bitwarden.ui.platform.components.header.BitwardenListHeaderText
import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.bitwarden.ui.platform.resource.BitwardenDrawable
import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.platform.theme.BitwardenTheme
/**
* Top level screen for the debug menu.
@ -60,7 +60,7 @@ fun DebugMenuScreen(
.fillMaxSize()
.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
AuthenticatorTopAppBar(
BitwardenTopAppBar(
title = stringResource(BitwardenString.debug_menu),
scrollBehavior = scrollBehavior,
navigationIcon = NavigationIcon(
@ -74,13 +74,11 @@ fun DebugMenuScreen(
),
)
},
) { innerPadding ->
) {
if (state.featureFlags.isEmpty()) {
AuthenticatorErrorContent(
BitwardenErrorContent(
message = stringResource(id = BitwardenString.empty_item_list),
modifier = Modifier
.padding(paddingValues = innerPadding)
.fillMaxSize(),
modifier = Modifier.fillMaxSize(),
)
} else {
FeatureFlagContent(
@ -94,8 +92,7 @@ fun DebugMenuScreen(
{ viewModel.trySendAction(DebugMenuAction.ResetFeatureFlagValues) }
},
modifier = Modifier
.verticalScroll(rememberScrollState())
.padding(paddingValues = innerPadding),
.verticalScroll(rememberScrollState()),
)
}
}
@ -111,29 +108,26 @@ private fun FeatureFlagContent(
Column(
modifier = modifier,
) {
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.height(12.dp))
BitwardenListHeaderText(
label = stringResource(BitwardenString.feature_flags),
modifier = Modifier.standardHorizontalMargin(),
modifier = Modifier
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(8.dp))
BitwardenHorizontalDivider(
color = MaterialTheme.colorScheme.outline,
thickness = 1.dp,
)
featureFlagMap.forEach { featureFlag ->
featureFlagMap.onEach { featureFlag ->
featureFlag.key.ListItemContent(
currentValue = featureFlag.value,
onValueChange = onValueChange,
cardStyle = featureFlagMap.keys.toListItemCardStyle(
index = featureFlagMap.keys.indexOf(element = featureFlag.key),
),
modifier = Modifier.standardHorizontalMargin(),
)
BitwardenHorizontalDivider(
color = MaterialTheme.colorScheme.outline,
thickness = 1.dp,
)
}
Spacer(modifier = Modifier.height(12.dp))
AuthenticatorFilledButton(
BitwardenFilledButton(
label = stringResource(BitwardenString.reset_values),
onClick = onResetValues,
modifier = Modifier
@ -141,13 +135,14 @@ private fun FeatureFlagContent(
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
@Preview(showBackground = true)
@Composable
private fun FeatureFlagContent_preview() {
AuthenticatorTheme {
BitwardenTheme {
FeatureFlagContent(
featureFlagMap = mapOf(
FlagKey.BitwardenAuthenticationEnabled to true,

View File

@ -1,72 +0,0 @@
package com.bitwarden.authenticator.ui.platform.feature.debugmenu.components
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.bitwarden.authenticator.ui.platform.components.toggle.BitwardenWideSwitch
import com.bitwarden.core.data.manager.model.FlagKey
import com.bitwarden.ui.platform.resource.BitwardenString
/**
* Creates a list item for a [FlagKey].
*/
@Suppress("UNCHECKED_CAST")
@Composable
fun <T : Any> FlagKey<T>.ListItemContent(
currentValue: T,
onValueChange: (key: FlagKey<T>, value: T) -> Unit,
modifier: Modifier = Modifier,
) = when (val flagKey = this) {
is FlagKey.DummyInt,
FlagKey.DummyString,
-> Unit
FlagKey.DummyBoolean,
FlagKey.BitwardenAuthenticationEnabled,
FlagKey.CipherKeyEncryption,
FlagKey.CredentialExchangeProtocolExport,
FlagKey.CredentialExchangeProtocolImport,
-> BooleanFlagItem(
label = flagKey.getDisplayLabel(),
key = flagKey as FlagKey<Boolean>,
currentValue = currentValue as Boolean,
onValueChange = onValueChange as (FlagKey<Boolean>, Boolean) -> Unit,
modifier = modifier,
)
}
/**
* The UI layout for a boolean backed flag key.
*/
@Composable
private fun BooleanFlagItem(
label: String,
key: FlagKey<Boolean>,
currentValue: Boolean,
onValueChange: (key: FlagKey<Boolean>, value: Boolean) -> Unit,
modifier: Modifier = Modifier,
) {
BitwardenWideSwitch(
label = label,
isChecked = currentValue,
onCheckedChange = {
onValueChange(key, it)
},
modifier = modifier,
)
}
@Composable
private fun <T : Any> FlagKey<T>.getDisplayLabel(): String = when (this) {
FlagKey.DummyBoolean,
is FlagKey.DummyInt,
FlagKey.DummyString,
-> this.keyName
FlagKey.CredentialExchangeProtocolImport -> stringResource(BitwardenString.cxp_import)
FlagKey.CredentialExchangeProtocolExport -> stringResource(BitwardenString.cxp_export)
FlagKey.CipherKeyEncryption -> stringResource(BitwardenString.cipher_key_encryption)
FlagKey.BitwardenAuthenticationEnabled -> {
stringResource(BitwardenString.bitwarden_authentication_enabled)
}
}

View File

@ -4,29 +4,24 @@ package com.bitwarden.authenticator.ui.platform.feature.settings
import android.content.Intent
import android.content.res.Resources
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.ColumnScope
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.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
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.mutableStateOf
@ -42,38 +37,39 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTag
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.bitwarden.authenticator.ui.platform.components.appbar.AuthenticatorMediumTopAppBar
import com.bitwarden.authenticator.ui.platform.components.dialog.BitwardenSelectionDialog
import com.bitwarden.authenticator.ui.platform.components.dialog.BitwardenSelectionRow
import com.bitwarden.authenticator.ui.platform.components.header.BitwardenListHeaderText
import com.bitwarden.authenticator.ui.platform.components.row.BitwardenExternalLinkRow
import com.bitwarden.authenticator.ui.platform.components.row.BitwardenTextRow
import com.bitwarden.authenticator.ui.platform.components.scaffold.BitwardenScaffold
import com.bitwarden.authenticator.ui.platform.components.toggle.BitwardenWideSwitch
import com.bitwarden.authenticator.ui.platform.composition.LocalBiometricsManager
import com.bitwarden.authenticator.ui.platform.feature.settings.data.model.DefaultSaveOption
import com.bitwarden.authenticator.ui.platform.manager.biometrics.BiometricsManager
import com.bitwarden.authenticator.ui.platform.theme.AuthenticatorTheme
import com.bitwarden.authenticator.ui.platform.util.displayLabel
import com.bitwarden.ui.platform.base.util.EventsEffect
import com.bitwarden.ui.platform.base.util.annotatedStringResource
import com.bitwarden.ui.platform.base.util.cardStyle
import com.bitwarden.ui.platform.base.util.mirrorIfRtl
import com.bitwarden.ui.platform.base.util.spanStyleOf
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.bitwarden.ui.platform.components.appbar.BitwardenMediumTopAppBar
import com.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectButton
import com.bitwarden.ui.platform.components.header.BitwardenListHeaderText
import com.bitwarden.ui.platform.components.model.CardStyle
import com.bitwarden.ui.platform.components.row.BitwardenExternalLinkRow
import com.bitwarden.ui.platform.components.row.BitwardenTextRow
import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.bitwarden.ui.platform.components.toggle.BitwardenSwitch
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.bitwarden.ui.platform.composition.LocalIntentManager
import com.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
import com.bitwarden.ui.platform.manager.IntentManager
import com.bitwarden.ui.platform.resource.BitwardenDrawable
import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.platform.theme.BitwardenTheme
import com.bitwarden.ui.platform.util.displayLabel
import com.bitwarden.ui.util.Text
import com.bitwarden.ui.util.asText
import kotlinx.collections.immutable.toImmutableList
/**
* Display the settings screen.
@ -90,8 +86,7 @@ fun SettingsScreen(
onNavigateToImport: () -> Unit,
) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
val scrollBehavior =
TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState())
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
EventsEffect(viewModel = viewModel) { event ->
when (event) {
@ -141,15 +136,14 @@ fun SettingsScreen(
.fillMaxSize()
.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
AuthenticatorMediumTopAppBar(
BitwardenMediumTopAppBar(
title = stringResource(id = BitwardenString.settings),
scrollBehavior = scrollBehavior,
)
},
) { innerPadding ->
) {
Column(
modifier = Modifier
.padding(innerPadding)
.fillMaxSize()
.verticalScroll(state = rememberScrollState()),
) {
@ -245,10 +239,12 @@ fun SettingsScreen(
Text(
modifier = Modifier.padding(end = 16.dp),
text = state.copyrightInfo.invoke(),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurface,
style = BitwardenTheme.typography.bodySmall,
color = BitwardenTheme.colorScheme.text.primary,
)
}
Spacer(modifier = Modifier.height(height = 12.dp))
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
}
@ -262,9 +258,11 @@ private fun SecuritySettings(
onBiometricToggle: (Boolean) -> Unit,
) {
if (!biometricsManager.isBiometricsSupported) return
Spacer(modifier = Modifier.height(height = 12.dp))
BitwardenListHeaderText(
modifier = Modifier.padding(horizontal = 16.dp),
modifier = Modifier
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
label = stringResource(id = BitwardenString.security),
)
Spacer(modifier = Modifier.height(8.dp))
@ -272,7 +270,7 @@ private fun SecuritySettings(
modifier = Modifier
.testTag("UnlockWithBiometricsSwitch")
.fillMaxWidth()
.padding(horizontal = 16.dp),
.standardHorizontalMargin(),
isChecked = state.isUnlockWithBiometricsEnabled,
onBiometricToggle = { onBiometricToggle(it) },
biometricsManager = biometricsManager,
@ -285,8 +283,7 @@ private fun SecuritySettings(
@Composable
@Suppress("LongMethod")
private fun VaultSettings(
modifier: Modifier = Modifier,
private fun ColumnScope.VaultSettings(
defaultSaveOption: DefaultSaveOption,
onExportClick: () -> Unit,
onImportClick: () -> Unit,
@ -298,70 +295,66 @@ private fun VaultSettings(
shouldShowDefaultSaveOptions: Boolean,
) {
BitwardenListHeaderText(
modifier = Modifier.padding(horizontal = 16.dp),
modifier = Modifier
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
label = stringResource(id = BitwardenString.data),
)
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.height(height = 8.dp))
BitwardenTextRow(
text = stringResource(id = BitwardenString.import_vault),
onClick = onImportClick,
modifier = modifier
.semantics { testTag = "Import" },
withDivider = true,
modifier = Modifier
.standardHorizontalMargin()
.testTag("Import"),
cardStyle = CardStyle.Top(),
content = {
Icon(
modifier = Modifier
.mirrorIfRtl()
.size(24.dp),
painter = painterResource(id = BitwardenDrawable.ic_navigate_next),
modifier = Modifier.mirrorIfRtl(),
painter = painterResource(id = BitwardenDrawable.ic_chevron_right),
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurface,
tint = BitwardenTheme.colorScheme.icon.primary,
)
},
)
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextRow(
text = stringResource(id = BitwardenString.export),
onClick = onExportClick,
modifier = modifier
.semantics { testTag = "Export" },
withDivider = true,
modifier = Modifier
.standardHorizontalMargin()
.testTag("Export"),
cardStyle = CardStyle.Middle(),
content = {
Icon(
modifier = Modifier
.mirrorIfRtl()
.size(24.dp),
painter = painterResource(id = BitwardenDrawable.ic_navigate_next),
modifier = Modifier.mirrorIfRtl(),
painter = painterResource(id = BitwardenDrawable.ic_chevron_right),
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurface,
tint = BitwardenTheme.colorScheme.icon.primary,
)
},
)
Spacer(modifier = Modifier.height(8.dp))
BitwardenExternalLinkRow(
text = stringResource(BitwardenString.backup),
onConfirmClick = onBackupClick,
modifier = modifier
.semantics { testTag = "Backup" },
modifier = Modifier
.standardHorizontalMargin()
.testTag("Backup"),
withDivider = false,
dialogTitle = stringResource(BitwardenString.data_backup_title),
dialogMessage = stringResource(BitwardenString.data_backup_message),
dialogConfirmButtonText = stringResource(BitwardenString.learn_more),
dialogDismissButtonText = stringResource(BitwardenString.okay),
cardStyle = if (shouldShowSyncWithBitwardenApp || shouldShowDefaultSaveOptions) {
CardStyle.Middle()
} else {
CardStyle.Bottom
},
)
if (shouldShowSyncWithBitwardenApp) {
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextRow(
text = stringResource(id = BitwardenString.sync_with_bitwarden_app),
description = annotatedStringResource(
id = BitwardenString.learn_more_link,
style = spanStyleOf(
color = MaterialTheme.colorScheme.onSurfaceVariant,
textStyle = MaterialTheme.typography.bodyMedium,
),
linkHighlightStyle = spanStyleOf(
color = MaterialTheme.colorScheme.primary,
textStyle = MaterialTheme.typography.labelLarge,
),
onAnnotationClick = {
when (it) {
"learnMore" -> onSyncLearnMoreClick()
@ -369,14 +362,18 @@ private fun VaultSettings(
},
),
onClick = onSyncWithBitwardenClick,
modifier = modifier,
withDivider = true,
modifier = Modifier.standardHorizontalMargin(),
cardStyle = if (shouldShowDefaultSaveOptions) {
CardStyle.Middle()
} else {
CardStyle.Bottom
},
content = {
Icon(
modifier = Modifier.mirrorIfRtl(),
painter = painterResource(id = BitwardenDrawable.ic_external_link),
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurface,
tint = BitwardenTheme.colorScheme.icon.primary,
)
},
)
@ -385,6 +382,7 @@ private fun VaultSettings(
DefaultSaveOptionSelectionRow(
currentSelection = defaultSaveOption,
onSaveOptionUpdated = onDefaultSaveOptionUpdated,
modifier = Modifier.standardHorizontalMargin(),
)
}
}
@ -394,46 +392,23 @@ private fun DefaultSaveOptionSelectionRow(
currentSelection: DefaultSaveOption,
onSaveOptionUpdated: (DefaultSaveOption) -> Unit,
modifier: Modifier = Modifier,
resources: Resources = LocalResources.current,
) {
var shouldShowDefaultSaveOptionDialog by remember { mutableStateOf(false) }
BitwardenTextRow(
text = stringResource(id = BitwardenString.default_save_option),
onClick = { shouldShowDefaultSaveOptionDialog = true },
BitwardenMultiSelectButton(
label = stringResource(id = BitwardenString.default_save_option),
dialogSubtitle = stringResource(id = BitwardenString.default_save_options_subtitle),
options = DefaultSaveOption.entries.map { it.displayLabel() }.toImmutableList(),
selectedOption = currentSelection.displayLabel(),
onOptionSelected = { selectedOptionLabel ->
val selectedOption = DefaultSaveOption
.entries
.first { it.displayLabel(resources) == selectedOptionLabel }
onSaveOptionUpdated(selectedOption)
},
cardStyle = CardStyle.Bottom,
modifier = modifier,
withDivider = true,
) {
Text(
modifier = Modifier.padding(vertical = 20.dp),
text = currentSelection.displayLabel(),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
var dialogSelection by remember { mutableStateOf(currentSelection) }
if (shouldShowDefaultSaveOptionDialog) {
BitwardenSelectionDialog(
title = stringResource(id = BitwardenString.default_save_option),
subtitle = stringResource(id = BitwardenString.default_save_options_subtitle),
dismissLabel = stringResource(id = BitwardenString.confirm),
onDismissRequest = { shouldShowDefaultSaveOptionDialog = false },
onDismissActionClick = {
onSaveOptionUpdated(dialogSelection)
shouldShowDefaultSaveOptionDialog = false
},
) {
DefaultSaveOption.entries.forEach { option ->
BitwardenSelectionRow(
text = option.displayLabel,
isSelected = option == dialogSelection,
onClick = {
dialogSelection = DefaultSaveOption.entries.first { it == option }
},
)
}
}
}
}
@Composable
private fun UnlockWithBiometricsRow(
@ -444,8 +419,9 @@ private fun UnlockWithBiometricsRow(
) {
if (!biometricsManager.isBiometricsSupported) return
var showBiometricsPrompt by rememberSaveable { mutableStateOf(false) }
BitwardenWideSwitch(
BitwardenSwitch(
modifier = modifier,
cardStyle = CardStyle.Full,
label = stringResource(BitwardenString.unlock_with_biometrics),
isChecked = isChecked || showBiometricsPrompt,
onCheckedChange = { toggled ->
@ -472,19 +448,23 @@ private fun UnlockWithBiometricsRow(
//region Appearance settings
@Composable
private fun AppearanceSettings(
private fun ColumnScope.AppearanceSettings(
state: SettingsState,
onThemeSelection: (theme: AppTheme) -> Unit,
) {
BitwardenListHeaderText(
modifier = Modifier.padding(horizontal = 16.dp),
modifier = Modifier
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
label = stringResource(id = BitwardenString.appearance),
)
Spacer(modifier = Modifier.height(height = 8.dp))
ThemeSelectionRow(
currentSelection = state.appearance.theme,
onThemeSelection = onThemeSelection,
modifier = Modifier
.semantics { testTag = "ThemeChooser" }
.testTag("ThemeChooser")
.standardHorizontalMargin()
.fillMaxWidth(),
)
}
@ -494,79 +474,59 @@ private fun ThemeSelectionRow(
currentSelection: AppTheme,
onThemeSelection: (AppTheme) -> Unit,
modifier: Modifier = Modifier,
resources: Resources = LocalResources.current,
) {
var shouldShowThemeSelectionDialog by remember { mutableStateOf(false) }
BitwardenTextRow(
text = stringResource(id = BitwardenString.theme),
onClick = { shouldShowThemeSelectionDialog = true },
modifier = modifier,
withDivider = true,
) {
Icon(
modifier = Modifier
.mirrorIfRtl()
.size(24.dp),
painter = painterResource(
id = BitwardenDrawable.ic_navigate_next,
),
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurface,
)
}
if (shouldShowThemeSelectionDialog) {
BitwardenSelectionDialog(
title = stringResource(id = BitwardenString.theme),
onDismissRequest = { shouldShowThemeSelectionDialog = false },
) {
AppTheme.entries.forEach { option ->
BitwardenSelectionRow(
text = option.displayLabel,
isSelected = option == currentSelection,
onClick = {
shouldShowThemeSelectionDialog = false
onThemeSelection(
AppTheme.entries.first { it == option },
)
BitwardenMultiSelectButton(
label = stringResource(id = BitwardenString.theme),
options = AppTheme.entries.map { it.displayLabel() }.toImmutableList(),
selectedOption = currentSelection.displayLabel(),
onOptionSelected = { selectedOptionLabel ->
val selectedOption = AppTheme
.entries
.first { it.displayLabel(resources) == selectedOptionLabel }
onThemeSelection(selectedOption)
},
cardStyle = CardStyle.Full,
modifier = modifier,
)
}
}
}
}
//endregion Appearance settings
//region Help settings
@Composable
private fun HelpSettings(
modifier: Modifier = Modifier,
private fun ColumnScope.HelpSettings(
onTutorialClick: () -> Unit,
onHelpCenterClick: () -> Unit,
) {
BitwardenListHeaderText(
modifier = Modifier.padding(horizontal = 16.dp),
modifier = Modifier
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
label = stringResource(id = BitwardenString.help),
)
Spacer(modifier = Modifier.height(height = 8.dp))
BitwardenTextRow(
text = stringResource(id = BitwardenString.launch_tutorial),
onClick = onTutorialClick,
modifier = modifier
.semantics { testTag = "LaunchTutorial" },
withDivider = true,
modifier = Modifier
.testTag("LaunchTutorial")
.standardHorizontalMargin(),
cardStyle = CardStyle.Top(),
)
Spacer(modifier = Modifier.height(8.dp))
BitwardenExternalLinkRow(
text = stringResource(id = BitwardenString.bitwarden_help_center),
onConfirmClick = onHelpCenterClick,
modifier = modifier
.semantics { testTag = "BitwardenHelpCenter" },
modifier = Modifier
.standardHorizontalMargin()
.testTag("BitwardenHelpCenter"),
withDivider = false,
dialogTitle = stringResource(id = BitwardenString.continue_to_help_center),
dialogMessage = stringResource(
BitwardenString.learn_more_about_how_to_use_bitwarden_authenticator_on_the_help_center,
),
cardStyle = CardStyle.Bottom,
)
}
@ -574,38 +534,45 @@ private fun HelpSettings(
//region About settings
@Composable
private fun AboutSettings(
modifier: Modifier = Modifier,
private fun ColumnScope.AboutSettings(
state: SettingsState,
onSubmitCrashLogsCheckedChange: (Boolean) -> Unit,
onPrivacyPolicyClick: () -> Unit,
onVersionClick: () -> Unit,
) {
BitwardenListHeaderText(
modifier = modifier.padding(horizontal = 16.dp),
modifier = Modifier
.standardHorizontalMargin()
.padding(horizontal = 16.dp),
label = stringResource(id = BitwardenString.about),
)
BitwardenWideSwitch(
modifier = modifier
.padding(horizontal = 16.dp)
.semantics { testTag = "SubmitCrashLogs" },
Spacer(modifier = Modifier.height(height = 8.dp))
BitwardenSwitch(
modifier = Modifier
.standardHorizontalMargin()
.testTag("SubmitCrashLogs"),
label = stringResource(id = BitwardenString.submit_crash_logs),
isChecked = state.isSubmitCrashLogsEnabled,
onCheckedChange = onSubmitCrashLogsCheckedChange,
cardStyle = CardStyle.Top(),
)
BitwardenExternalLinkRow(
text = stringResource(id = BitwardenString.privacy_policy),
modifier = modifier
.semantics { testTag = "PrivacyPolicy" },
modifier = Modifier
.standardHorizontalMargin()
.testTag("PrivacyPolicy"),
withDivider = false,
onConfirmClick = onPrivacyPolicyClick,
dialogTitle = stringResource(id = BitwardenString.continue_to_privacy_policy),
dialogMessage = stringResource(
id = BitwardenString.privacy_policy_description_long,
),
cardStyle = CardStyle.Middle(),
)
CopyRow(
text = state.version,
onClick = onVersionClick,
modifier = Modifier.standardHorizontalMargin(),
)
}
@ -617,21 +584,17 @@ private fun CopyRow(
resources: Resources = LocalResources.current,
) {
Box(
contentAlignment = Alignment.BottomCenter,
contentAlignment = Alignment.Center,
modifier = modifier
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = ripple(color = MaterialTheme.colorScheme.primary),
onClick = onClick,
)
.defaultMinSize(minHeight = 60.dp)
.cardStyle(cardStyle = CardStyle.Bottom, onClick = onClick)
.semantics(mergeDescendants = true) {
contentDescription = text.toString(resources)
},
) {
Row(
modifier = Modifier
.defaultMinSize(minHeight = 56.dp)
.padding(start = 16.dp, end = 24.dp, top = 8.dp, bottom = 8.dp)
.padding(horizontal = 16.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
@ -641,20 +604,15 @@ private fun CopyRow(
.padding(end = 16.dp)
.weight(1f),
text = text(),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurface,
style = BitwardenTheme.typography.bodyLarge,
color = BitwardenTheme.colorScheme.text.primary,
)
Icon(
painter = rememberVectorPainter(id = BitwardenDrawable.ic_copy),
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurface,
tint = BitwardenTheme.colorScheme.icon.primary,
)
}
HorizontalDivider(
modifier = Modifier.padding(start = 16.dp),
thickness = 1.dp,
color = MaterialTheme.colorScheme.outlineVariant,
)
}
}
@ -663,7 +621,7 @@ private fun CopyRow(
@Preview
@Composable
private fun CopyRow_preview() {
AuthenticatorTheme {
BitwardenTheme {
CopyRow(
text = "Copyable Text".asText(),
onClick = { },

View File

@ -7,7 +7,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.padding
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
@ -22,25 +22,24 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTag
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.bitwarden.authenticator.ui.platform.components.appbar.AuthenticatorTopAppBar
import com.bitwarden.authenticator.ui.platform.components.button.AuthenticatorFilledTonalButton
import com.bitwarden.authenticator.ui.platform.components.dialog.BasicDialogState
import com.bitwarden.authenticator.ui.platform.components.dialog.BitwardenBasicDialog
import com.bitwarden.authenticator.ui.platform.components.dialog.BitwardenLoadingDialog
import com.bitwarden.authenticator.ui.platform.components.dialog.BitwardenTwoButtonDialog
import com.bitwarden.authenticator.ui.platform.components.dialog.LoadingDialogState
import com.bitwarden.authenticator.ui.platform.components.dropdown.BitwardenMultiSelectButton
import com.bitwarden.authenticator.ui.platform.components.scaffold.BitwardenScaffold
import com.bitwarden.authenticator.ui.platform.feature.settings.export.model.ExportVaultFormat
import com.bitwarden.authenticator.ui.platform.util.displayLabel
import com.bitwarden.ui.platform.base.util.EventsEffect
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
import com.bitwarden.ui.platform.components.button.BitwardenFilledButton
import com.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
import com.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
import com.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
import com.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectButton
import com.bitwarden.ui.platform.components.model.CardStyle
import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.bitwarden.ui.platform.composition.LocalIntentManager
import com.bitwarden.ui.platform.manager.IntentManager
import com.bitwarden.ui.platform.resource.BitwardenDrawable
@ -114,10 +113,8 @@ fun ExportScreen(
when (val dialog = state.dialogState) {
is ExportState.DialogState.Error -> {
BitwardenBasicDialog(
visibilityState = BasicDialogState.Shown(
title = dialog.title,
message = dialog.message,
),
title = dialog.title?.invoke(),
message = dialog.message(),
onDismissRequest = remember(viewModel) {
{
viewModel.trySendAction(ExportAction.DialogDismiss)
@ -127,11 +124,7 @@ fun ExportScreen(
}
is ExportState.DialogState.Loading -> {
BitwardenLoadingDialog(
visibilityState = LoadingDialogState.Shown(
text = dialog.message,
),
)
BitwardenLoadingDialog(text = dialog.message())
}
null -> Unit
@ -143,7 +136,7 @@ fun ExportScreen(
.fillMaxSize()
.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
AuthenticatorTopAppBar(
BitwardenTopAppBar(
title = stringResource(id = BitwardenString.export),
scrollBehavior = scrollBehavior,
navigationIcon = painterResource(id = BitwardenDrawable.ic_close),
@ -155,11 +148,9 @@ fun ExportScreen(
},
)
},
) { paddingValues ->
) {
ExportScreenContent(
modifier = Modifier
.padding(paddingValues)
.fillMaxSize(),
modifier = Modifier.fillMaxSize(),
state = state,
onExportFormatOptionSelected = remember(viewModel) {
{
@ -183,6 +174,7 @@ private fun ExportScreenContent(
.verticalScroll(rememberScrollState()),
) {
val resources = LocalResources.current
Spacer(modifier = Modifier.height(height = 12.dp))
BitwardenMultiSelectButton(
label = stringResource(id = BitwardenString.file_format),
options = ExportVaultFormat.entries.map { it.displayLabel() }.toImmutableList(),
@ -193,21 +185,25 @@ private fun ExportScreenContent(
.first { it.displayLabel(resources) == selectedOptionLabel }
onExportFormatOptionSelected(selectedOption)
},
cardStyle = CardStyle.Full,
modifier = Modifier
.semantics { testTag = "FileFormatPicker" }
.padding(horizontal = 16.dp)
.testTag("FileFormatPicker")
.standardHorizontalMargin()
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.height(height = 24.dp))
AuthenticatorFilledTonalButton(
BitwardenFilledButton(
label = stringResource(id = BitwardenString.export),
onClick = onExportClick,
modifier = Modifier
.semantics { testTag = "ExportVaultButton" }
.padding(horizontal = 16.dp)
.testTag("ExportVaultButton")
.standardHorizontalMargin()
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(height = 12.dp))
Spacer(modifier = Modifier.navigationBarsPadding())
}
}

View File

@ -6,7 +6,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.padding
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
@ -19,24 +19,24 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTag
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.bitwarden.authenticator.data.platform.manager.imports.model.ImportFileFormat
import com.bitwarden.authenticator.ui.platform.components.appbar.AuthenticatorTopAppBar
import com.bitwarden.authenticator.ui.platform.components.button.AuthenticatorFilledTonalButton
import com.bitwarden.authenticator.ui.platform.components.dialog.BitwardenLoadingDialog
import com.bitwarden.authenticator.ui.platform.components.dialog.BitwardenTwoButtonDialog
import com.bitwarden.authenticator.ui.platform.components.dialog.LoadingDialogState
import com.bitwarden.authenticator.ui.platform.components.dropdown.BitwardenMultiSelectButton
import com.bitwarden.authenticator.ui.platform.components.scaffold.BitwardenScaffold
import com.bitwarden.authenticator.ui.platform.util.displayLabel
import com.bitwarden.ui.platform.base.util.EventsEffect
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
import com.bitwarden.ui.platform.components.button.BitwardenFilledButton
import com.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
import com.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
import com.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectButton
import com.bitwarden.ui.platform.components.model.CardStyle
import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.bitwarden.ui.platform.composition.LocalIntentManager
import com.bitwarden.ui.platform.manager.IntentManager
import com.bitwarden.ui.platform.model.FileData
@ -108,11 +108,7 @@ fun ImportingScreen(
}
is ImportState.DialogState.Loading -> {
BitwardenLoadingDialog(
visibilityState = LoadingDialogState.Shown(
text = dialog.message,
),
)
BitwardenLoadingDialog(text = dialog.message())
}
null -> Unit
@ -124,7 +120,7 @@ fun ImportingScreen(
.fillMaxSize()
.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
AuthenticatorTopAppBar(
BitwardenTopAppBar(
title = stringResource(id = BitwardenString.import_vault),
scrollBehavior = scrollBehavior,
navigationIcon = painterResource(id = BitwardenDrawable.ic_close),
@ -136,11 +132,9 @@ fun ImportingScreen(
},
)
},
) { paddingValues ->
) {
ImportScreenContent(
modifier = Modifier
.padding(paddingValues)
.fillMaxSize(),
modifier = Modifier.fillMaxSize(),
state = state,
onImportFormatOptionSelected = remember(viewModel) {
{
@ -168,6 +162,7 @@ private fun ImportScreenContent(
.verticalScroll(rememberScrollState()),
) {
val resources = LocalResources.current
Spacer(modifier = Modifier.height(height = 12.dp))
BitwardenMultiSelectButton(
label = stringResource(id = BitwardenString.file_format),
options = ImportFileFormat.entries.map { it.displayLabel() }.toImmutableList(),
@ -178,21 +173,25 @@ private fun ImportScreenContent(
.first { it.displayLabel(resources) == selectedOptionLabel }
onImportFormatOptionSelected(selectedOption)
},
cardStyle = CardStyle.Full,
modifier = Modifier
.semantics { testTag = "FileFormatPicker" }
.padding(horizontal = 16.dp)
.testTag("FileFormatPicker")
.standardHorizontalMargin()
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.height(height = 24.dp))
AuthenticatorFilledTonalButton(
BitwardenFilledButton(
label = stringResource(id = BitwardenString.import_vault),
onClick = onImportClick,
modifier = Modifier
.semantics { testTag = "ImportVaultButton" }
.padding(horizontal = 16.dp)
.testTag("ImportVaultButton")
.standardHorizontalMargin()
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(height = 12.dp))
Spacer(modifier = Modifier.navigationBarsPadding())
}
}

View File

@ -23,7 +23,6 @@ import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@ -41,14 +40,15 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.bitwarden.authenticator.ui.platform.components.button.AuthenticatorFilledTonalButton
import com.bitwarden.authenticator.ui.platform.components.button.AuthenticatorTextButton
import com.bitwarden.authenticator.ui.platform.components.scaffold.BitwardenScaffold
import com.bitwarden.authenticator.ui.platform.util.isPortrait
import com.bitwarden.ui.platform.base.util.EventsEffect
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.bitwarden.ui.platform.components.button.BitwardenFilledButton
import com.bitwarden.ui.platform.components.button.BitwardenTextButton
import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.platform.theme.BitwardenTheme
import kotlinx.coroutines.launch
/**
@ -155,7 +155,7 @@ private fun TutorialScreenContent(
.height(44.dp),
)
AuthenticatorFilledTonalButton(
BitwardenFilledButton(
label = state.actionButtonText,
onClick = { continueClick(state.index) },
modifier = Modifier
@ -163,7 +163,7 @@ private fun TutorialScreenContent(
.fillMaxWidth(),
)
AuthenticatorTextButton(
BitwardenTextButton(
isEnabled = !state.isLastPage,
label = stringResource(id = BitwardenString.skip),
onClick = skipClick,
@ -196,7 +196,7 @@ private fun TutorialScreenPortrait(
Text(
text = stringResource(id = state.title),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.headlineMedium,
style = BitwardenTheme.typography.headlineMedium,
modifier = Modifier
.padding(
top = 48.dp,
@ -206,7 +206,7 @@ private fun TutorialScreenPortrait(
Text(
text = stringResource(id = state.message),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodyLarge,
style = BitwardenTheme.typography.bodyLarge,
)
}
}
@ -235,14 +235,14 @@ private fun TutorialScreenLandscape(
Text(
text = stringResource(id = state.title),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.headlineMedium,
style = BitwardenTheme.typography.headlineMedium,
modifier = Modifier.padding(bottom = 16.dp),
)
Text(
text = stringResource(id = state.message),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodyLarge,
style = BitwardenTheme.typography.bodyLarge,
)
}
@ -264,7 +264,7 @@ private fun IndicatorDots(
) {
items(totalCount) { index ->
val color = animateColorAsState(
targetValue = MaterialTheme.colorScheme.primary.copy(
targetValue = BitwardenTheme.colorScheme.icon.secondary.copy(
alpha = if (index == selectedIndexProvider()) 1.0f else 0.3f,
),
label = "dotColor",

View File

@ -1,176 +0,0 @@
package com.bitwarden.authenticator.ui.platform.theme
import android.content.Context
import android.os.Build
import androidx.annotation.ColorRes
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.ProvidableCompositionLocal
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import com.bitwarden.authenticator.R
import com.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
import com.bitwarden.ui.platform.util.isDarkMode
/**
* The overall application theme. This can be configured to support a [theme] and [dynamicColor].
*/
@Composable
fun AuthenticatorTheme(
theme: AppTheme = AppTheme.DEFAULT,
dynamicColor: Boolean = false,
content: @Composable () -> Unit,
) {
val darkTheme = theme.isDarkMode(isSystemDarkMode = isSystemInDarkTheme())
// Get the current scheme
val context = LocalContext.current
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> darkColorScheme(context)
else -> lightColorScheme(context)
}
val nonMaterialColors = if (darkTheme) {
darkNonMaterialColors(context)
} else {
lightNonMaterialColors(context)
}
CompositionLocalProvider(
LocalNonMaterialColors provides nonMaterialColors,
LocalNonMaterialTypography provides nonMaterialTypography,
) {
// Set overall theme based on color scheme and typography settings
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content,
)
}
}
private fun darkColorScheme(context: Context): ColorScheme =
androidx.compose.material3.darkColorScheme(
primary = R.color.dark_primary.toColor(context),
onPrimary = R.color.dark_on_primary.toColor(context),
primaryContainer = R.color.dark_primary_container.toColor(context),
onPrimaryContainer = R.color.dark_on_primary_container.toColor(context),
secondary = R.color.dark_secondary.toColor(context),
onSecondary = R.color.dark_on_secondary.toColor(context),
secondaryContainer = R.color.dark_secondary_container.toColor(context),
onSecondaryContainer = R.color.dark_on_secondary_container.toColor(context),
tertiary = R.color.dark_tertiary.toColor(context),
onTertiary = R.color.dark_on_tertiary.toColor(context),
tertiaryContainer = R.color.dark_tertiary_container.toColor(context),
onTertiaryContainer = R.color.dark_on_tertiary_container.toColor(context),
error = R.color.dark_error.toColor(context),
onError = R.color.dark_on_error.toColor(context),
errorContainer = R.color.dark_error_container.toColor(context),
onErrorContainer = R.color.dark_on_error_container.toColor(context),
surface = R.color.dark_surface.toColor(context),
surfaceBright = R.color.dark_surface_bright.toColor(context),
surfaceContainer = R.color.dark_surface_container.toColor(context),
surfaceContainerHigh = R.color.dark_surface_container_high.toColor(context),
surfaceContainerHighest = R.color.dark_surface_container_highest.toColor(context),
surfaceContainerLow = R.color.dark_surface_container_low.toColor(context),
surfaceContainerLowest = R.color.dark_surface_container_lowest.toColor(context),
surfaceVariant = R.color.dark_surface_variant.toColor(context),
surfaceDim = R.color.dark_surface_dim.toColor(context),
onSurface = R.color.dark_on_surface.toColor(context),
onSurfaceVariant = R.color.dark_on_surface_variant.toColor(context),
outline = R.color.dark_outline.toColor(context),
outlineVariant = R.color.dark_outline_variant.toColor(context),
inverseSurface = R.color.dark_inverse_surface.toColor(context),
inverseOnSurface = R.color.dark_inverse_on_surface.toColor(context),
inversePrimary = R.color.dark_inverse_primary.toColor(context),
scrim = R.color.dark_scrim.toColor(context),
)
private fun lightColorScheme(context: Context): ColorScheme =
androidx.compose.material3.lightColorScheme(
primary = R.color.primary.toColor(context),
onPrimary = R.color.on_primary.toColor(context),
primaryContainer = R.color.primary_container.toColor(context),
onPrimaryContainer = R.color.on_primary_container.toColor(context),
secondary = R.color.secondary.toColor(context),
onSecondary = R.color.on_secondary.toColor(context),
secondaryContainer = R.color.secondary_container.toColor(context),
onSecondaryContainer = R.color.on_secondary_container.toColor(context),
tertiary = R.color.tertiary.toColor(context),
onTertiary = R.color.on_tertiary.toColor(context),
tertiaryContainer = R.color.tertiary_container.toColor(context),
onTertiaryContainer = R.color.on_tertiary_container.toColor(context),
error = R.color.error.toColor(context),
onError = R.color.on_error.toColor(context),
errorContainer = R.color.error_container.toColor(context),
onErrorContainer = R.color.on_error_container.toColor(context),
surface = R.color.surface.toColor(context),
surfaceBright = R.color.surface_bright.toColor(context),
surfaceContainer = R.color.surface_container.toColor(context),
surfaceContainerHigh = R.color.surface_container_high.toColor(context),
surfaceContainerHighest = R.color.surface_container_highest.toColor(context),
surfaceContainerLow = R.color.surface_container_low.toColor(context),
surfaceContainerLowest = R.color.surface_container_lowest.toColor(context),
surfaceVariant = R.color.surface_variant.toColor(context),
surfaceDim = R.color.surface_dim.toColor(context),
onSurface = R.color.on_surface.toColor(context),
onSurfaceVariant = R.color.on_surface_variant.toColor(context),
outline = R.color.outline.toColor(context),
outlineVariant = R.color.outline_variant.toColor(context),
inverseSurface = R.color.inverse_surface.toColor(context),
inverseOnSurface = R.color.inverse_on_surface.toColor(context),
inversePrimary = R.color.inverse_primary.toColor(context),
scrim = R.color.scrim.toColor(context),
)
@ColorRes
private fun Int.toColor(context: Context): Color =
Color(context.getColor(this))
/**
* Provides access to non material theme typography throughout the app.
*/
val LocalNonMaterialTypography: ProvidableCompositionLocal<NonMaterialTypography> =
compositionLocalOf { nonMaterialTypography }
/**
* Provides access to non material theme colors throughout the app.
*/
val LocalNonMaterialColors: ProvidableCompositionLocal<NonMaterialColors> =
compositionLocalOf {
// Default value here will immediately be overridden in BitwardenTheme, similar
// to how MaterialTheme works.
NonMaterialColors(
fingerprint = Color.Transparent,
qrCodeClickableText = Color.Transparent,
)
}
/**
* Models colors that live outside of the Material Theme spec.
*/
data class NonMaterialColors(
val fingerprint: Color,
val qrCodeClickableText: Color,
)
private fun lightNonMaterialColors(context: Context): NonMaterialColors =
NonMaterialColors(
fingerprint = R.color.light_fingerprint.toColor(context),
qrCodeClickableText = R.color.qr_code_clickable_text.toColor(context),
)
private fun darkNonMaterialColors(context: Context): NonMaterialColors =
NonMaterialColors(
fingerprint = R.color.dark_fingerprint.toColor(context),
qrCodeClickableText = R.color.qr_code_clickable_text.toColor(context),
)

View File

@ -1,255 +0,0 @@
package com.bitwarden.authenticator.ui.platform.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.PlatformTextStyle
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.LineHeightStyle
import androidx.compose.ui.unit.sp
import com.bitwarden.authenticator.R
val Typography: Typography = Typography(
displayLarge = TextStyle(
fontSize = 57.sp,
lineHeight = 64.sp,
fontFamily = FontFamily(Font(R.font.roboto_regular)),
fontWeight = FontWeight.W400,
letterSpacing = (-0.25).sp,
lineHeightStyle = LineHeightStyle(
alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None,
),
platformStyle = PlatformTextStyle(includeFontPadding = false),
),
displayMedium = TextStyle(
fontSize = 45.sp,
lineHeight = 52.sp,
fontFamily = FontFamily(Font(R.font.roboto_regular)),
fontWeight = FontWeight.W400,
letterSpacing = (0).sp,
lineHeightStyle = LineHeightStyle(
alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None,
),
platformStyle = PlatformTextStyle(includeFontPadding = false),
),
displaySmall = TextStyle(
fontSize = 36.sp,
lineHeight = 44.sp,
fontFamily = FontFamily(Font(R.font.roboto_regular)),
fontWeight = FontWeight.W400,
letterSpacing = 0.sp,
lineHeightStyle = LineHeightStyle(
alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None,
),
platformStyle = PlatformTextStyle(includeFontPadding = false),
),
headlineLarge = TextStyle(
fontSize = 32.sp,
lineHeight = 40.sp,
fontFamily = FontFamily(Font(R.font.roboto_regular)),
fontWeight = FontWeight.W400,
letterSpacing = 0.sp,
lineHeightStyle = LineHeightStyle(
alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None,
),
platformStyle = PlatformTextStyle(includeFontPadding = false),
),
headlineMedium = TextStyle(
fontSize = 28.sp,
lineHeight = 36.sp,
fontFamily = FontFamily(Font(R.font.roboto_regular)),
fontWeight = FontWeight.W400,
letterSpacing = 0.sp,
lineHeightStyle = LineHeightStyle(
alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None,
),
platformStyle = PlatformTextStyle(includeFontPadding = false),
),
headlineSmall = TextStyle(
fontSize = 24.sp,
lineHeight = 32.sp,
fontFamily = FontFamily(Font(R.font.roboto_regular)),
fontWeight = FontWeight.W400,
letterSpacing = 0.sp,
lineHeightStyle = LineHeightStyle(
alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None,
),
platformStyle = PlatformTextStyle(includeFontPadding = false),
),
titleLarge = TextStyle(
fontSize = 22.sp,
lineHeight = 28.sp,
fontFamily = FontFamily(Font(R.font.roboto_regular)),
fontWeight = FontWeight.W400,
letterSpacing = 0.sp,
lineHeightStyle = LineHeightStyle(
alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None,
),
platformStyle = PlatformTextStyle(includeFontPadding = false),
),
titleMedium = TextStyle(
fontSize = 16.sp,
lineHeight = 24.sp,
fontFamily = FontFamily(Font(R.font.roboto_medium)),
fontWeight = FontWeight.W500,
letterSpacing = 0.15.sp,
lineHeightStyle = LineHeightStyle(
alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None,
),
platformStyle = PlatformTextStyle(includeFontPadding = false),
),
titleSmall = TextStyle(
fontSize = 14.sp,
lineHeight = 20.sp,
fontFamily = FontFamily(Font(R.font.roboto_medium)),
fontWeight = FontWeight.W500,
letterSpacing = 0.1.sp,
lineHeightStyle = LineHeightStyle(
alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None,
),
platformStyle = PlatformTextStyle(includeFontPadding = false),
),
bodyLarge = TextStyle(
fontSize = 16.sp,
lineHeight = 24.sp,
fontFamily = FontFamily(Font(R.font.roboto_regular)),
fontWeight = FontWeight.W400,
letterSpacing = 0.5.sp,
lineHeightStyle = LineHeightStyle(
alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None,
),
platformStyle = PlatformTextStyle(includeFontPadding = false),
),
bodyMedium = TextStyle(
fontSize = 14.sp,
lineHeight = 20.sp,
fontFamily = FontFamily(Font(R.font.roboto_regular)),
fontWeight = FontWeight.W400,
letterSpacing = 0.25.sp,
lineHeightStyle = LineHeightStyle(
alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None,
),
platformStyle = PlatformTextStyle(includeFontPadding = false),
),
bodySmall = TextStyle(
fontSize = 12.sp,
lineHeight = 16.sp,
fontFamily = FontFamily(Font(R.font.roboto_regular)),
fontWeight = FontWeight.W400,
letterSpacing = 0.4.sp,
lineHeightStyle = LineHeightStyle(
alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None,
),
platformStyle = PlatformTextStyle(includeFontPadding = false),
),
labelLarge = TextStyle(
fontSize = 14.sp,
lineHeight = 20.sp,
fontFamily = FontFamily(Font(R.font.roboto_medium)),
fontWeight = FontWeight.W500,
letterSpacing = 0.1.sp,
lineHeightStyle = LineHeightStyle(
alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None,
),
platformStyle = PlatformTextStyle(includeFontPadding = false),
),
labelMedium = TextStyle(
fontSize = 12.sp,
lineHeight = 16.sp,
fontFamily = FontFamily(Font(R.font.roboto_medium)),
fontWeight = FontWeight.W500,
letterSpacing = 0.5.sp,
lineHeightStyle = LineHeightStyle(
alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None,
),
platformStyle = PlatformTextStyle(includeFontPadding = false),
),
labelSmall = TextStyle(
fontSize = 11.sp,
lineHeight = 16.sp,
fontFamily = FontFamily(Font(R.font.roboto_medium)),
fontWeight = FontWeight.W500,
letterSpacing = 0.5.sp,
lineHeightStyle = LineHeightStyle(
alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None,
),
platformStyle = PlatformTextStyle(includeFontPadding = false),
),
)
val nonMaterialTypography: NonMaterialTypography = NonMaterialTypography(
sensitiveInfoSmall = TextStyle(
fontSize = 14.sp,
lineHeight = 20.sp,
fontFamily = FontFamily(Font(R.font.roboto_regular_mono)),
fontWeight = FontWeight.W400,
letterSpacing = 0.5.sp,
lineHeightStyle = LineHeightStyle(
alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None,
),
platformStyle = PlatformTextStyle(includeFontPadding = false),
),
sensitiveInfoMedium = TextStyle(
fontSize = 16.sp,
lineHeight = 24.sp,
fontFamily = FontFamily(Font(R.font.roboto_regular_mono)),
fontWeight = FontWeight.W400,
letterSpacing = 0.5.sp,
lineHeightStyle = LineHeightStyle(
alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None,
),
platformStyle = PlatformTextStyle(includeFontPadding = false),
),
bodySmallProminent = TextStyle(
fontSize = 12.sp,
lineHeight = 16.sp,
fontFamily = FontFamily(Font(R.font.roboto_regular)),
fontWeight = FontWeight.W700,
letterSpacing = 0.4.sp,
lineHeightStyle = LineHeightStyle(
alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None,
),
platformStyle = PlatformTextStyle(includeFontPadding = false),
),
labelMediumProminent = TextStyle(
fontSize = 12.sp,
lineHeight = 16.sp,
fontFamily = FontFamily(Font(R.font.roboto_regular)),
fontWeight = FontWeight.W600,
letterSpacing = 0.5.sp,
lineHeightStyle = LineHeightStyle(
alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None,
),
platformStyle = PlatformTextStyle(includeFontPadding = false),
),
)
/**
* Models typography that live outside of the Material Theme spec.
*/
data class NonMaterialTypography(
val bodySmallProminent: TextStyle,
val labelMediumProminent: TextStyle,
val sensitiveInfoSmall: TextStyle,
val sensitiveInfoMedium: TextStyle,
)

View File

@ -11,7 +11,6 @@ import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
class MainViewModelTest : BaseViewModelTest() {
@ -24,35 +23,28 @@ class MainViewModelTest : BaseViewModelTest() {
every { isScreenCaptureAllowedStateFlow } returns mutableScreenCaptureAllowedFlow
}
private val fakeServerConfigRepository = FakeServerConfigRepository()
private lateinit var mainViewModel: MainViewModel
@BeforeEach
fun setUp() {
mainViewModel = MainViewModel(
settingsRepository,
fakeServerConfigRepository,
private val mainViewModel: MainViewModel = MainViewModel(
settingsRepository = settingsRepository,
configRepository = fakeServerConfigRepository,
)
}
@Test
fun `on AppThemeChanged should update state`() {
fun `on AppThemeChanged should update state`() = runTest {
mainViewModel.stateEventFlow(backgroundScope) { stateFlow, eventFlow ->
assertEquals(
MainState(
theme = AppTheme.DEFAULT,
),
mainViewModel.stateFlow.value,
MainState(theme = AppTheme.DEFAULT),
stateFlow.awaitItem(),
)
mainViewModel.trySendAction(
MainAction.Internal.ThemeUpdate(
theme = AppTheme.DARK,
),
mainViewModel.trySendAction(MainAction.Internal.ThemeUpdate(theme = AppTheme.DARK))
assertEquals(
MainState(theme = AppTheme.DARK),
stateFlow.awaitItem(),
)
assertEquals(
MainState(
theme = AppTheme.DARK,
),
mainViewModel.stateFlow.value,
MainEvent.UpdateAppTheme(osTheme = AppTheme.DARK.osValue),
eventFlow.expectMostRecentItem(),
)
}
verify {
settingsRepository.appTheme
@ -62,10 +54,10 @@ class MainViewModelTest : BaseViewModelTest() {
@Test
fun `send NavigateToDebugMenu action when OpenDebugMenu action is sent`() = runTest {
mainViewModel.trySendAction(MainAction.OpenDebugMenu)
mainViewModel.eventFlow.test {
awaitItem() // ignore first event
// Ignore the events that are fired off by flows in the ViewModel init
skipItems(2)
mainViewModel.trySendAction(MainAction.OpenDebugMenu)
assertEquals(MainEvent.NavigateToDebugMenu, awaitItem())
}
}

View File

@ -7,9 +7,9 @@ import androidx.compose.ui.test.hasAnyAncestor
import androidx.compose.ui.test.hasContentDescription
import androidx.compose.ui.test.isDialog
import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onChildren
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.onSiblings
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextInput
import com.bitwarden.authenticator.data.authenticator.datasource.disk.entity.AuthenticatorItemAlgorithm
@ -152,12 +152,12 @@ class EditItemScreenTest : AuthenticatorComposeTest() {
mutableStateFlow.update { it.copy(viewState = DEFAULT_CONTENT) }
composeTestRule.onNodeWithTextAfterScroll(text = "Information").assertIsDisplayed()
composeTestRule.onNodeWithTextAfterScroll(text = "INFORMATION").assertIsDisplayed()
composeTestRule.onNodeWithTextAfterScroll(text = "Name").assertIsDisplayed()
composeTestRule.onNodeWithTextAfterScroll(text = "Key").assertIsDisplayed()
composeTestRule.onNodeWithTextAfterScroll(text = "Username").assertIsDisplayed()
composeTestRule.onNodeWithTextAfterScroll(text = "Favorite").assertIsDisplayed()
composeTestRule.onNodeWithTextAfterScroll(text = "Advanced").assertIsDisplayed()
composeTestRule.onNodeWithTextAfterScroll(text = "Additional options").assertIsDisplayed()
}
@Test
@ -211,7 +211,7 @@ class EditItemScreenTest : AuthenticatorComposeTest() {
fun `advanced click should send ExpandAdvancedOptionsClick`() {
mutableStateFlow.update { it.copy(viewState = DEFAULT_CONTENT) }
composeTestRule
.onNodeWithTextAfterScroll(text = "Advanced")
.onNodeWithTextAfterScroll(text = "Additional options")
.performClick()
verify(exactly = 1) {
viewModel.trySendAction(EditItemAction.ExpandAdvancedOptionsClick)
@ -356,7 +356,7 @@ class EditItemScreenTest : AuthenticatorComposeTest() {
}
composeTestRule
.onNodeWithTextAfterScroll(text = "Number of digits")
.onSiblings()
.onChildren()
.filterToOne(hasContentDescription("+"))
.performClick()
@ -372,7 +372,7 @@ class EditItemScreenTest : AuthenticatorComposeTest() {
}
composeTestRule
.onNodeWithTextAfterScroll(text = "Number of digits")
.onSiblings()
.onChildren()
.filterToOne(hasContentDescription("\u2212"))
.performClick()

View File

@ -14,7 +14,6 @@ import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
import com.bitwarden.ui.platform.manager.IntentManager
import com.bitwarden.ui.util.asText
import com.bitwarden.ui.util.assertNoDialogExists
import com.bitwarden.ui.util.performCustomAccessibilityAction
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
@ -194,7 +193,7 @@ class ManualCodeEntryScreenTest : AuthenticatorComposeTest() {
}
@Test
fun `on permission dialog Settings clock should emit SettingsClick`() {
fun `on permission dialog Settings click should emit SettingsClick`() {
permissionsManager.checkPermissionResult = false
permissionsManager.getPermissionsResult = false
composeTestRule
@ -227,21 +226,6 @@ class ManualCodeEntryScreenTest : AuthenticatorComposeTest() {
}
}
@Suppress("MaxLineLength")
@Test
fun `on Scan QR code accessibility action without permission and permission is granted should emit ScanQrCodeTextClick`() {
permissionsManager.checkPermissionResult = false
permissionsManager.getPermissionsResult = true
composeTestRule
.onNodeWithText(text = "Scan QR code")
.performScrollTo()
.performCustomAccessibilityAction(label = "Scan QR code")
verify(exactly = 1) {
viewModel.trySendAction(ManualCodeEntryAction.ScanQrCodeTextClick)
}
}
@Test
fun `on dialog should updates according to state`() {
composeTestRule.assertNoDialogExists()

View File

@ -5,10 +5,10 @@ import com.bitwarden.authenticator.ui.platform.composition.LocalManagerProvider
import com.bitwarden.authenticator.ui.platform.manager.biometrics.BiometricsManager
import com.bitwarden.authenticator.ui.platform.manager.exit.ExitManager
import com.bitwarden.authenticator.ui.platform.manager.permissions.PermissionsManager
import com.bitwarden.authenticator.ui.platform.theme.AuthenticatorTheme
import com.bitwarden.ui.platform.base.BaseComposeTest
import com.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
import com.bitwarden.ui.platform.manager.IntentManager
import com.bitwarden.ui.platform.theme.BitwardenTheme
import io.mockk.mockk
/**
@ -19,7 +19,7 @@ abstract class AuthenticatorComposeTest : BaseComposeTest() {
/**
* Helper for testing a basic Composable function that only requires a Composable environment
* with the [AuthenticatorTheme].
* with the [BitwardenTheme].
*/
@Suppress("LongParameterList")
protected fun setContent(
@ -31,7 +31,7 @@ abstract class AuthenticatorComposeTest : BaseComposeTest() {
test: @Composable () -> Unit,
) {
setTestContent {
AuthenticatorTheme(theme = theme) {
BitwardenTheme(theme = theme) {
LocalManagerProvider(
permissionsManager = permissionsManager,
intentManager = intentManager,

View File

@ -143,7 +143,7 @@ class SettingsScreenTest : AuthenticatorComposeTest() {
@Test
@Suppress("MaxLineLength")
fun `Default Save Option dialog should send DefaultSaveOptionUpdated when confirm is clicked`() =
fun `Default Save Option dialog should send DefaultSaveOptionUpdated when selection is made`() =
runTest {
val expectedSaveOption = DefaultSaveOption.BITWARDEN_APP
mutableStateFlow.value = DEFAULT_STATE
@ -164,12 +164,6 @@ class SettingsScreenTest : AuthenticatorComposeTest() {
.assertIsDisplayed()
.performClick()
// Click confirm:
composeTestRule
.onNodeWithText("Confirm")
.assertIsDisplayed()
.performClick()
verify {
viewModel.trySendAction(
SettingsAction.DataClick.DefaultSaveOptionUpdated(expectedSaveOption),

View File

@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.Typography
import androidx.compose.runtime.Composable
@ -20,6 +19,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.bitwarden.ui.R
import com.bitwarden.ui.platform.theme.BitwardenTheme
/**
* The default [BitwardenTypography] for the app.
@ -279,7 +279,7 @@ fun BitwardenTypography.toMaterialTypography(): Typography = Typography(
@Preview(showBackground = true)
@Composable
private fun BitwardenTypography_preview() {
MaterialTheme {
BitwardenTheme {
Column(
modifier = Modifier
.padding(8.dp)

View File

@ -0,0 +1,18 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="25dp"
android:viewportWidth="24"
android:viewportHeight="25">
<path
android:fillColor="#96A3BB"
android:fillType="evenOdd"
android:pathData="M16.96,9.146C17.349,9.538 17.346,10.171 16.954,10.56L10.902,16.56C10.512,16.946 9.884,16.946 9.494,16.56L6.546,13.637C6.154,13.248 6.151,12.615 6.54,12.222C6.929,11.83 7.562,11.828 7.954,12.216L10.198,14.441L15.546,9.139C15.938,8.751 16.571,8.753 16.96,9.146Z" />
<path
android:fillColor="#96A3BB"
android:fillType="evenOdd"
android:pathData="M12.001,0.851C11.448,0.851 11.001,1.298 11.001,1.851C11.001,2.403 11.448,2.851 12.001,2.851C17.524,2.851 22.001,7.328 22.001,12.851C22.001,18.373 17.524,22.851 12.001,22.851C7.666,22.851 3.974,20.093 2.587,16.233C2.4,15.713 1.827,15.443 1.308,15.63C0.788,15.817 0.518,16.389 0.705,16.909C2.368,21.538 6.796,24.851 12.001,24.851C18.628,24.851 24.001,19.478 24.001,12.851C24.001,6.223 18.628,0.851 12.001,0.851Z" />
<path
android:fillColor="#96A3BB"
android:fillType="evenOdd"
android:pathData="M12.001,2.851C11.836,2.851 11.671,2.854 11.508,2.862C10.957,2.889 10.488,2.463 10.461,1.912C10.434,1.36 10.86,0.891 11.412,0.865C11.607,0.855 11.804,0.851 12.001,0.851C12.553,0.851 13,1.298 13,1.85C13,2.403 12.553,2.851 12.001,2.851ZM9.235,2.154C9.421,2.674 9.15,3.246 8.63,3.432C8.321,3.543 8.018,3.668 7.724,3.808C7.225,4.044 6.629,3.831 6.392,3.332C6.156,2.833 6.369,2.237 6.868,2C7.221,1.833 7.584,1.682 7.956,1.549C8.476,1.363 9.049,1.634 9.235,2.154ZM5.354,4.028C5.725,4.437 5.694,5.07 5.285,5.441C5.042,5.661 4.81,5.892 4.59,6.135C4.219,6.544 3.587,6.575 3.178,6.204C2.769,5.833 2.738,5.201 3.109,4.791C3.372,4.501 3.65,4.223 3.941,3.959C4.35,3.588 4.983,3.619 5.354,4.028ZM2.482,7.243C2.981,7.479 3.194,8.075 2.957,8.574C2.818,8.869 2.692,9.171 2.582,9.48C2.396,10 1.823,10.271 1.303,10.085C0.783,9.899 0.513,9.327 0.699,8.807C0.832,8.435 0.983,8.072 1.15,7.718C1.387,7.219 1.983,7.006 2.482,7.243ZM1.061,11.312C1.613,11.338 2.039,11.807 2.012,12.359C2.004,12.522 2,12.686 2,12.85C2,13.056 2.006,13.26 2.018,13.462C2.052,14.013 1.632,14.487 1.08,14.52C0.529,14.554 0.055,14.134 0.022,13.582C0.007,13.34 0,13.096 0,12.85C0,12.653 0.005,12.457 0.014,12.262C0.041,11.71 0.51,11.285 1.061,11.312ZM1.573,16.503C2.075,16.274 2.668,16.496 2.898,16.998C2.982,17.184 3.073,17.366 3.168,17.545C3.428,18.033 3.243,18.638 2.756,18.898C2.268,19.158 1.663,18.973 1.403,18.486C1.288,18.271 1.18,18.051 1.078,17.828C0.849,17.326 1.07,16.733 1.573,16.503Z" />
</vector>

View File

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="17dp"
android:viewportWidth="16"
android:viewportHeight="17">
<path
android:pathData="M4,4.333C4,2.124 5.791,0.333 8,0.333C10.209,0.333 12,2.124 12,4.333V5.333H13C14.105,5.333 15,6.228 15,7.333C15,7.747 14.664,8.083 14.25,8.083C13.836,8.083 13.5,7.747 13.5,7.333C13.5,7.057 13.276,6.833 13,6.833H3C2.724,6.833 2.5,7.057 2.5,7.333V14.333C2.5,14.609 2.724,14.833 3,14.833H7.25C7.664,14.833 8,15.169 8,15.583C8,15.997 7.664,16.333 7.25,16.333H3C1.895,16.333 1,15.438 1,14.333V7.333C1,6.228 1.895,5.333 3,5.333H4V4.333ZM10.5,4.333V5.333H5.5V4.333C5.5,2.952 6.619,1.833 8,1.833C9.381,1.833 10.5,2.952 10.5,4.333Z"
android:fillColor="#000000"
android:fillType="evenOdd"/>
<path
android:pathData="M12.754,9.547C12.754,9.133 12.418,8.797 12.004,8.797C11.59,8.797 11.254,9.133 11.254,9.547V11.687L9.242,11.027C8.848,10.899 8.424,11.113 8.295,11.507C8.167,11.9 8.381,12.324 8.775,12.453L10.801,13.116L9.54,14.883C9.299,15.22 9.377,15.688 9.714,15.929C10.051,16.17 10.52,16.091 10.76,15.754L12.004,14.012L13.248,15.755C13.489,16.091 13.958,16.17 14.295,15.929C14.632,15.688 14.71,15.22 14.469,14.883L13.208,13.116L15.233,12.453C15.627,12.324 15.842,11.9 15.713,11.507C15.584,11.113 15.16,10.899 14.767,11.027L12.754,11.687V9.547Z"
android:fillColor="#000000"/>
</vector>

View File

@ -1,13 +1,18 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24">
<group>
<clip-path android:pathData="M0,0h24v24h-24z" />
android:height="25dp"
android:viewportWidth="24"
android:viewportHeight="25">
<path
android:fillColor="#001550"
android:fillColor="#5A6D91"
android:fillType="evenOdd"
android:pathData="M6.355,5.259C10.078,1.536 16.114,1.536 19.837,5.259C23.559,8.982 23.559,15.018 19.837,18.741C17.65,20.927 14.666,21.83 11.819,21.448C11.444,21.397 11.099,21.661 11.048,22.036C10.998,22.411 11.261,22.756 11.636,22.807C14.889,23.244 18.305,22.212 20.806,19.711C25.065,15.452 25.065,8.548 20.806,4.29C16.548,0.031 9.644,0.031 5.385,4.29C4.14,5.535 3.258,7.009 2.741,8.577L1.615,6.576C1.43,6.246 1.012,6.129 0.682,6.315C0.352,6.5 0.235,6.918 0.42,7.248L2.355,10.69C2.541,11.02 2.959,11.137 3.289,10.951L6.73,9.016C7.06,8.83 7.177,8.412 6.992,8.082C6.806,7.752 6.388,7.635 6.058,7.821L4.064,8.942C4.519,7.597 5.282,6.333 6.355,5.259ZM15.372,6.408C16.315,6.408 17.084,7.178 17.084,8.121V9.394H13.659V8.121C13.659,7.174 14.428,6.408 15.372,6.408ZM12.836,9.394V8.121C12.836,6.718 13.975,5.586 15.372,5.586C16.769,5.586 17.907,6.724 17.907,8.121V9.394H18.324C18.702,9.394 19.01,9.701 19.01,10.08V15.016C19.01,15.395 18.702,15.701 18.324,15.701H15.834V18.421C15.834,18.58 15.794,18.744 15.705,18.881C15.618,19.014 15.451,19.166 15.207,19.166H0.627C0.388,19.166 0.219,19.019 0.129,18.885C0.038,18.748 -0.005,18.581 0,18.414V13.314C0,13.155 0.041,12.991 0.129,12.854C0.216,12.72 0.383,12.569 0.627,12.569H11.836V10.08C11.836,9.701 12.143,9.394 12.521,9.394H12.836ZM12.658,12.569H15.207C15.451,12.569 15.618,12.72 15.705,12.854C15.794,12.991 15.834,13.155 15.834,13.314V14.879H18.187V10.217H12.658V12.569ZM15.011,14.879V13.392H0.823V18.343H15.011V15.701H14.993V14.879H15.011ZM8.065,16.623C8.097,16.456 8.113,16.258 8.113,16.027C8.113,15.601 8.052,15.272 7.93,15.039C7.86,14.905 7.773,14.793 7.67,14.703C7.569,14.611 7.452,14.542 7.317,14.496C7.182,14.448 7.032,14.424 6.867,14.424C6.617,14.424 6.402,14.478 6.22,14.585C6.041,14.69 5.904,14.845 5.809,15.048C5.755,15.17 5.715,15.317 5.689,15.487C5.663,15.658 5.65,15.85 5.65,16.063C5.65,16.227 5.662,16.381 5.684,16.523C5.708,16.664 5.745,16.793 5.795,16.91C5.893,17.125 6.038,17.292 6.23,17.413C6.424,17.535 6.643,17.595 6.887,17.595C7.098,17.595 7.291,17.55 7.464,17.46C7.637,17.37 7.778,17.242 7.887,17.075C7.973,16.94 8.033,16.789 8.065,16.623ZM7.401,15.352C7.436,15.516 7.454,15.726 7.454,15.984C7.454,16.256 7.437,16.476 7.404,16.644C7.37,16.812 7.311,16.941 7.228,17.031C7.145,17.121 7.029,17.166 6.882,17.166C6.739,17.166 6.626,17.123 6.543,17.037C6.459,16.95 6.4,16.822 6.365,16.652C6.329,16.483 6.312,16.266 6.312,16.001C6.312,15.611 6.353,15.322 6.434,15.134C6.518,14.947 6.665,14.853 6.877,14.853C7.024,14.853 7.14,14.896 7.223,14.98C7.307,15.063 7.366,15.187 7.401,15.352ZM3.223,15.333V17.257C3.223,17.37 3.253,17.455 3.312,17.513C3.371,17.571 3.45,17.6 3.548,17.6C3.767,17.6 3.877,17.458 3.877,17.174V14.754C3.877,14.652 3.852,14.573 3.8,14.515C3.749,14.457 3.681,14.428 3.596,14.428C3.52,14.428 3.469,14.441 3.442,14.466C3.415,14.492 3.356,14.559 3.266,14.669C3.177,14.779 3.073,14.878 2.956,14.967C2.841,15.056 2.686,15.141 2.492,15.221C2.362,15.275 2.272,15.318 2.22,15.352C2.169,15.386 2.143,15.439 2.143,15.511C2.143,15.573 2.169,15.628 2.22,15.676C2.273,15.722 2.333,15.745 2.401,15.745C2.542,15.745 2.816,15.608 3.223,15.333ZM10.396,17.257V15.333C9.989,15.608 9.715,15.745 9.574,15.745C9.506,15.745 9.446,15.722 9.393,15.676C9.342,15.628 9.316,15.573 9.316,15.511C9.316,15.439 9.342,15.386 9.393,15.352C9.445,15.318 9.535,15.275 9.665,15.221C9.859,15.141 10.014,15.056 10.129,14.967C10.246,14.878 10.35,14.779 10.439,14.669C10.529,14.559 10.588,14.492 10.615,14.466C10.642,14.441 10.693,14.428 10.769,14.428C10.854,14.428 10.922,14.457 10.973,14.515C11.025,14.573 11.05,14.652 11.05,14.754V17.174C11.05,17.458 10.94,17.6 10.721,17.6C10.623,17.6 10.544,17.571 10.485,17.513C10.426,17.455 10.396,17.37 10.396,17.257ZM13.325,15.333V17.257C13.325,17.37 13.355,17.455 13.414,17.513C13.473,17.571 13.552,17.6 13.65,17.6C13.869,17.6 13.979,17.458 13.979,17.174V14.754C13.979,14.652 13.954,14.573 13.902,14.515C13.851,14.457 13.783,14.428 13.698,14.428C13.623,14.428 13.571,14.441 13.544,14.466C13.517,14.492 13.458,14.559 13.368,14.669C13.279,14.779 13.175,14.878 13.058,14.967C12.943,15.056 12.788,15.141 12.594,15.221C12.464,15.275 12.374,15.318 12.322,15.352C12.271,15.386 12.245,15.439 12.245,15.511C12.245,15.573 12.271,15.628 12.322,15.676C12.375,15.722 12.435,15.745 12.503,15.745C12.644,15.745 12.918,15.608 13.325,15.333Z" />
</group>
android:pathData="M16.96,9.146C17.349,9.538 17.346,10.171 16.954,10.56L10.902,16.56C10.512,16.946 9.884,16.946 9.494,16.56L6.546,13.637C6.154,13.248 6.151,12.615 6.54,12.222C6.929,11.83 7.562,11.828 7.954,12.216L10.198,14.441L15.546,9.139C15.938,8.751 16.571,8.753 16.96,9.146Z" />
<path
android:fillColor="#5A6D91"
android:fillType="evenOdd"
android:pathData="M12.001,0.851C11.448,0.851 11.001,1.298 11.001,1.851C11.001,2.403 11.448,2.851 12.001,2.851C17.524,2.851 22.001,7.328 22.001,12.851C22.001,18.373 17.524,22.851 12.001,22.851C7.666,22.851 3.974,20.093 2.587,16.233C2.4,15.713 1.827,15.443 1.308,15.63C0.788,15.817 0.518,16.389 0.705,16.909C2.368,21.538 6.796,24.851 12.001,24.851C18.628,24.851 24.001,19.478 24.001,12.851C24.001,6.223 18.628,0.851 12.001,0.851Z" />
<path
android:fillColor="#5A6D91"
android:fillType="evenOdd"
android:pathData="M12.001,2.851C11.836,2.851 11.671,2.854 11.508,2.862C10.957,2.889 10.488,2.463 10.461,1.912C10.434,1.36 10.86,0.891 11.412,0.865C11.607,0.855 11.804,0.851 12.001,0.851C12.553,0.851 13,1.298 13,1.85C13,2.403 12.553,2.851 12.001,2.851ZM9.235,2.154C9.421,2.674 9.15,3.246 8.63,3.432C8.321,3.543 8.018,3.668 7.724,3.808C7.225,4.044 6.629,3.831 6.392,3.332C6.156,2.833 6.369,2.237 6.868,2C7.221,1.833 7.584,1.682 7.956,1.549C8.476,1.363 9.049,1.634 9.235,2.154ZM5.354,4.028C5.725,4.437 5.694,5.07 5.285,5.441C5.042,5.661 4.81,5.892 4.59,6.135C4.219,6.544 3.587,6.575 3.178,6.204C2.769,5.833 2.738,5.201 3.109,4.791C3.372,4.501 3.65,4.223 3.941,3.959C4.35,3.588 4.983,3.619 5.354,4.028ZM2.482,7.243C2.981,7.479 3.194,8.075 2.957,8.574C2.818,8.869 2.692,9.171 2.582,9.48C2.396,10 1.823,10.271 1.303,10.085C0.783,9.899 0.513,9.327 0.699,8.807C0.832,8.435 0.983,8.072 1.15,7.718C1.387,7.219 1.983,7.006 2.482,7.243ZM1.061,11.312C1.613,11.338 2.039,11.807 2.012,12.359C2.004,12.522 2,12.686 2,12.85C2,13.056 2.006,13.26 2.018,13.462C2.052,14.013 1.632,14.487 1.08,14.52C0.529,14.554 0.055,14.134 0.022,13.582C0.007,13.34 0,13.096 0,12.85C0,12.653 0.005,12.457 0.014,12.262C0.041,11.71 0.51,11.285 1.061,11.312ZM1.573,16.503C2.075,16.274 2.668,16.496 2.898,16.998C2.982,17.184 3.073,17.366 3.168,17.545C3.428,18.033 3.243,18.638 2.756,18.898C2.268,19.158 1.663,18.973 1.403,18.486C1.288,18.271 1.18,18.051 1.078,17.828C0.849,17.326 1.07,16.733 1.573,16.503Z" />
</vector>

View File

@ -1,14 +1,29 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:height="25dp"
android:viewportWidth="24"
android:viewportHeight="24">
<group>
<clip-path
android:pathData="M0,0h24v24h-24z"/>
android:viewportHeight="25">
<path
android:pathData="M6.355,5.259C10.078,1.536 16.114,1.536 19.837,5.259C23.559,8.982 23.559,15.018 19.837,18.741C17.65,20.927 14.666,21.83 11.819,21.448C11.444,21.397 11.099,21.661 11.048,22.036C10.998,22.411 11.261,22.756 11.636,22.807C14.889,23.244 18.305,22.212 20.806,19.711C25.065,15.452 25.065,8.548 20.806,4.29C16.548,0.031 9.644,0.031 5.385,4.29C4.14,5.535 3.258,7.009 2.741,8.577L1.615,6.576C1.43,6.246 1.012,6.129 0.682,6.315C0.352,6.5 0.235,6.918 0.42,7.248L2.355,10.69C2.541,11.02 2.959,11.137 3.289,10.951L6.73,9.016C7.06,8.83 7.177,8.412 6.992,8.082C6.806,7.752 6.388,7.635 6.058,7.821L4.064,8.942C4.519,7.597 5.282,6.333 6.355,5.259ZM15.372,6.408C16.315,6.408 17.084,7.178 17.084,8.121V9.394H13.659V8.121C13.659,7.174 14.428,6.408 15.372,6.408ZM12.836,9.394V8.121C12.836,6.718 13.975,5.586 15.372,5.586C16.769,5.586 17.907,6.724 17.907,8.121V9.394H18.324C18.702,9.394 19.01,9.701 19.01,10.08V15.016C19.01,15.395 18.702,15.701 18.324,15.701H15.834V18.421C15.834,18.58 15.794,18.744 15.705,18.881C15.618,19.014 15.451,19.166 15.207,19.166H0.627C0.388,19.166 0.219,19.019 0.129,18.885C0.038,18.748 -0.005,18.581 0,18.414V13.314C0,13.155 0.041,12.991 0.129,12.854C0.216,12.72 0.383,12.569 0.627,12.569H11.836V10.08C11.836,9.701 12.143,9.394 12.521,9.394H12.836ZM12.658,12.569H15.207C15.451,12.569 15.618,12.72 15.705,12.854C15.794,12.991 15.834,13.155 15.834,13.314V14.879H18.187V10.217H12.658V12.569ZM15.011,14.879V13.392H0.823V18.343H15.011V15.701H14.993V14.879H15.011ZM8.065,16.623C8.097,16.456 8.113,16.258 8.113,16.027C8.113,15.601 8.052,15.272 7.93,15.039C7.86,14.905 7.773,14.793 7.67,14.703C7.569,14.611 7.452,14.542 7.317,14.496C7.182,14.448 7.032,14.424 6.867,14.424C6.617,14.424 6.402,14.478 6.22,14.585C6.041,14.69 5.904,14.845 5.809,15.048C5.755,15.17 5.715,15.317 5.689,15.487C5.663,15.658 5.65,15.85 5.65,16.063C5.65,16.227 5.662,16.381 5.684,16.523C5.708,16.664 5.745,16.793 5.795,16.91C5.893,17.125 6.038,17.292 6.23,17.413C6.424,17.535 6.643,17.595 6.887,17.595C7.098,17.595 7.291,17.55 7.464,17.46C7.637,17.37 7.778,17.242 7.887,17.075C7.973,16.94 8.033,16.789 8.065,16.623ZM7.401,15.352C7.436,15.516 7.454,15.726 7.454,15.984C7.454,16.256 7.437,16.476 7.404,16.644C7.37,16.812 7.311,16.941 7.228,17.031C7.145,17.121 7.029,17.166 6.882,17.166C6.739,17.166 6.626,17.123 6.543,17.037C6.459,16.95 6.4,16.822 6.365,16.652C6.329,16.483 6.312,16.266 6.312,16.001C6.312,15.611 6.353,15.322 6.434,15.134C6.518,14.947 6.665,14.853 6.877,14.853C7.024,14.853 7.14,14.896 7.223,14.98C7.307,15.063 7.366,15.187 7.401,15.352ZM3.223,15.333V17.257C3.223,17.37 3.253,17.455 3.312,17.513C3.371,17.571 3.45,17.6 3.548,17.6C3.767,17.6 3.877,17.458 3.877,17.174V14.754C3.877,14.652 3.852,14.573 3.8,14.515C3.749,14.457 3.681,14.428 3.596,14.428C3.52,14.428 3.469,14.441 3.442,14.466C3.415,14.492 3.356,14.559 3.266,14.669C3.177,14.779 3.073,14.878 2.956,14.967C2.841,15.056 2.686,15.141 2.492,15.221C2.362,15.275 2.272,15.318 2.22,15.352C2.169,15.386 2.143,15.439 2.143,15.511C2.143,15.573 2.169,15.628 2.22,15.676C2.273,15.722 2.333,15.745 2.401,15.745C2.542,15.745 2.816,15.608 3.223,15.333ZM10.396,17.257V15.333C9.989,15.608 9.715,15.745 9.574,15.745C9.506,15.745 9.446,15.722 9.393,15.676C9.342,15.628 9.316,15.573 9.316,15.511C9.316,15.439 9.342,15.386 9.393,15.352C9.445,15.318 9.535,15.275 9.665,15.221C9.859,15.141 10.014,15.056 10.129,14.967C10.246,14.878 10.35,14.779 10.439,14.669C10.529,14.559 10.588,14.492 10.615,14.466C10.642,14.441 10.693,14.428 10.769,14.428C10.854,14.428 10.922,14.457 10.973,14.515C11.025,14.573 11.05,14.652 11.05,14.754V17.174C11.05,17.458 10.94,17.6 10.721,17.6C10.623,17.6 10.544,17.571 10.485,17.513C10.426,17.455 10.396,17.37 10.396,17.257ZM13.325,15.333V17.257C13.325,17.37 13.355,17.455 13.414,17.513C13.473,17.571 13.552,17.6 13.65,17.6C13.869,17.6 13.979,17.458 13.979,17.174V14.754C13.979,14.652 13.954,14.573 13.902,14.515C13.851,14.457 13.783,14.428 13.698,14.428C13.623,14.428 13.571,14.441 13.544,14.466C13.517,14.492 13.458,14.559 13.368,14.669C13.279,14.779 13.175,14.878 13.058,14.967C12.943,15.056 12.788,15.141 12.594,15.221C12.464,15.275 12.374,15.318 12.322,15.352C12.271,15.386 12.245,15.439 12.245,15.511C12.245,15.573 12.271,15.628 12.322,15.676C12.375,15.722 12.435,15.745 12.503,15.745C12.644,15.745 12.918,15.608 13.325,15.333Z"
android:fillColor="#001550"
android:fillType="evenOdd"/>
</group>
android:name="navigationActiveAccent"
android:fillColor="#DBE5F6"
android:pathData="M21,12C21,16.971 16.971,21 12,21C7.029,21 3,16.971 3,12C3,7.029 7.029,3 12,3C16.971,3 21,7.029 21,12Z" />
<path
android:name="navigationActiveAccent"
android:fillColor="#DBE5F6"
android:pathData="M20,12.25C20,16.668 16.418,20.25 12,20.25C7.582,20.25 4,16.668 4,12.25C4,7.832 7.582,4.25 12,4.25C16.418,4.25 20,7.832 20,12.25Z" />
<path
android:name="navigation"
android:fillColor="#175DDC"
android:fillType="evenOdd"
android:pathData="M16.96,8.546C17.349,8.938 17.346,9.571 16.954,9.96L10.902,15.96C10.512,16.347 9.884,16.347 9.494,15.96L6.546,13.037C6.154,12.648 6.151,12.015 6.54,11.623C6.929,11.231 7.562,11.228 7.954,11.617L10.198,13.842L15.546,8.54C15.938,8.151 16.571,8.154 16.96,8.546Z" />
<path
android:name="navigation"
android:fillColor="#175DDC"
android:fillType="evenOdd"
android:pathData="M12.001,0.25C11.448,0.25 11.001,0.698 11.001,1.25C11.001,1.802 11.448,2.25 12.001,2.25C17.524,2.25 22.001,6.727 22.001,12.25C22.001,17.773 17.524,22.25 12.001,22.25C7.666,22.25 3.974,19.492 2.587,15.632C2.4,15.112 1.827,14.842 1.308,15.029C0.788,15.216 0.518,15.789 0.705,16.309C2.368,20.937 6.796,24.25 12.001,24.25C18.628,24.25 24.001,18.877 24.001,12.25C24.001,5.623 18.628,0.25 12.001,0.25Z" />
<path
android:name="navigation"
android:fillColor="#175DDC"
android:fillType="evenOdd"
android:pathData="M12.001,2.25C11.836,2.25 11.671,2.254 11.508,2.262C10.957,2.288 10.488,1.863 10.461,1.311C10.434,0.759 10.86,0.291 11.412,0.264C11.607,0.255 11.804,0.25 12.001,0.25C12.553,0.25 13,0.698 13,1.25C13,1.802 12.553,2.25 12.001,2.25ZM9.235,1.553C9.421,2.073 9.15,2.646 8.63,2.832C8.321,2.942 8.018,3.068 7.724,3.207C7.225,3.444 6.629,3.231 6.392,2.732C6.156,2.233 6.369,1.636 6.868,1.4C7.221,1.232 7.584,1.082 7.956,0.949C8.476,0.762 9.049,1.033 9.235,1.553ZM5.354,3.427C5.725,3.837 5.694,4.469 5.285,4.84C5.042,5.06 4.81,5.292 4.59,5.534C4.219,5.944 3.587,5.974 3.178,5.603C2.769,5.232 2.738,4.6 3.109,4.191C3.372,3.9 3.65,3.622 3.941,3.359C4.35,2.987 4.983,3.018 5.354,3.427ZM2.482,6.642C2.981,6.878 3.194,7.475 2.957,7.974C2.818,8.268 2.692,8.571 2.582,8.88C2.396,9.4 1.823,9.67 1.303,9.484C0.783,9.298 0.513,8.726 0.699,8.206C0.832,7.834 0.983,7.471 1.15,7.117C1.387,6.618 1.983,6.405 2.482,6.642ZM1.061,10.711C1.613,10.738 2.039,11.207 2.012,11.758C2.004,11.921 2,12.085 2,12.25C2,12.455 2.006,12.659 2.018,12.861C2.052,13.413 1.632,13.887 1.08,13.92C0.529,13.953 0.055,13.533 0.022,12.982C0.007,12.74 0,12.495 0,12.25C0,12.053 0.005,11.857 0.014,11.662C0.041,11.11 0.51,10.684 1.061,10.711ZM1.573,15.903C2.075,15.674 2.668,15.895 2.898,16.397C2.982,16.583 3.073,16.765 3.168,16.945C3.428,17.432 3.243,18.038 2.756,18.298C2.268,18.557 1.663,18.373 1.403,17.885C1.288,17.67 1.18,17.451 1.078,17.228C0.849,16.725 1.07,16.132 1.573,15.903Z" />
</vector>

View File

@ -993,7 +993,6 @@ Do you want to switch to this account?</string>
<string name="delete_x">Delete %s</string>
<string name="failed_to_decrypt_cipher_contact_support">Failed to decrypt cipher. Contact support.</string>
<string name="decryption_error">Decryption error</string>
<string name="add_item_rotation">Add Item Rotation</string>
<string name="scan_a_qr_code">Scan a QR code</string>
<string name="cannot_add_authenticator_key">Cannot add authenticator key?</string>
<string name="enable_camera_permission_to_use_the_scanner">Enable camera permission to use the scanner</string>