Skip to content

Commit

Permalink
PM-17838 - Add help button for authenticator key (#4697)
Browse files Browse the repository at this point in the history
  • Loading branch information
phil-livefront authored Feb 10, 2025
1 parent 30e882d commit eb3bc83
Show file tree
Hide file tree
Showing 11 changed files with 239 additions and 123 deletions.
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
package com.x8bit.bitwarden.ui.platform.components.field

import androidx.compose.animation.core.animateDpAsState
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.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
Expand All @@ -26,33 +30,41 @@ import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.focus.onFocusEvent
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalTextToolbar
import androidx.compose.ui.platform.TextToolbar
import androidx.compose.ui.semantics.CustomAccessibilityAction
import androidx.compose.ui.semantics.customActions
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.PopupProperties
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.cardStyle
import com.x8bit.bitwarden.ui.platform.base.util.nullableTestTag
import com.x8bit.bitwarden.ui.platform.base.util.toPx
import com.x8bit.bitwarden.ui.platform.base.util.withLineBreaksAtWidth
import com.x8bit.bitwarden.ui.platform.components.appbar.color.bitwardenMenuItemColors
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenStandardIconButton
import com.x8bit.bitwarden.ui.platform.components.divider.BitwardenHorizontalDivider
import com.x8bit.bitwarden.ui.platform.components.field.color.bitwardenTextFieldColors
import com.x8bit.bitwarden.ui.platform.components.field.toolbar.BitwardenCutCopyTextToolbar
import com.x8bit.bitwarden.ui.platform.components.field.toolbar.BitwardenEmptyTextToolbar
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.model.IconResource
import com.x8bit.bitwarden.ui.platform.components.model.TextToolbarType
import com.x8bit.bitwarden.ui.platform.components.model.TooltipData
import com.x8bit.bitwarden.ui.platform.components.row.BitwardenRowOfActions
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
import kotlinx.collections.immutable.ImmutableList
Expand Down Expand Up @@ -192,6 +204,7 @@ fun BitwardenTextField(
supportingContent: (@Composable ColumnScope.() -> Unit)?,
cardStyle: CardStyle,
modifier: Modifier = Modifier,
tooltip: TooltipData? = null,
supportingContentPadding: PaddingValues = PaddingValues(vertical = 12.dp, horizontal = 16.dp),
placeholder: String? = null,
leadingIconResource: IconResource? = null,
Expand Down Expand Up @@ -255,12 +268,52 @@ fun BitwardenTextField(
paddingTop = 6.dp,
paddingBottom = 0.dp,
)
.fillMaxWidth(),
.fillMaxWidth()
.semantics {
customActions = listOfNotNull(
tooltip?.let {
CustomAccessibilityAction(
label = it.contentDescription,
action = {
it.onClick()
true
},
)
},
)
},
) {
var focused by remember { mutableStateOf(false) }

TextField(
colors = bitwardenTextFieldColors(),
enabled = enabled,
label = label?.let { { Text(text = it) } },
label = label?.let {
{
Row(verticalAlignment = Alignment.CenterVertically) {
Text(text = it)
tooltip?.let {
val targetSize = if (textFieldValue.text.isEmpty() || focused) {
16.dp
} else {
12.dp
}
val size by animateDpAsState(
targetValue = targetSize,
label = "${it.contentDescription}_animation",
)
Spacer(modifier = Modifier.width(16.dp))
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_question_circle_small,
contentDescription = it.contentDescription,
onClick = it.onClick,
contentColor = BitwardenTheme.colorScheme.icon.secondary,
modifier = Modifier.size(size),
)
}
}
}
},
value = textFieldValue,
leadingIcon = leadingIconResource?.let { iconResource ->
{
Expand Down Expand Up @@ -298,7 +351,10 @@ fun BitwardenTextField(
visualTransformation = visualTransformation,
modifier = Modifier
.nullableTestTag(tag = textFieldTestTag)
.fillMaxWidth(),
.fillMaxWidth()
.onFocusChanged { focusState ->
focused = focusState.isFocused
},
)
supportingContent
?.let { content ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,11 @@ fun BitwardenClickableText(
Icon(
painter = leadingIcon,
contentDescription = null,
tint = color,
tint = if (isEnabled) {
color
} else {
BitwardenTheme.colorScheme.filledButton.foregroundDisabled
},
modifier = Modifier.size(size = 16.dp),
)
Spacer(modifier = Modifier.width(width = 8.dp))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.x8bit.bitwarden.ui.vault.feature.addedit

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
Expand All @@ -14,6 +13,7 @@ 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.focusProperties
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
Expand All @@ -23,7 +23,6 @@ import com.x8bit.bitwarden.ui.platform.base.util.Text
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.base.util.cardStyle
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenOutlinedButton
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenStandardIconButton
import com.x8bit.bitwarden.ui.platform.components.coachmark.CoachMarkActionText
import com.x8bit.bitwarden.ui.platform.components.coachmark.CoachMarkScope
Expand All @@ -34,8 +33,8 @@ import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordField
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.model.TooltipData
import com.x8bit.bitwarden.ui.platform.components.text.BitwardenClickableText
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditLoginTypeHandlers

Expand Down Expand Up @@ -281,7 +280,6 @@ private fun CoachMarkScope<AddEditItemCoachMark>.PasswordRow(
label = stringResource(id = R.string.check_password_for_data_breaches),
style = BitwardenTheme.typography.labelMedium,
onClick = loginItemTypeHandlers.onPasswordCheckerClick,
leadingIcon = painterResource(id = R.drawable.ic_camera_small),
innerPadding = PaddingValues(all = 16.dp),
cornerSize = 0.dp,
modifier = Modifier
Expand Down Expand Up @@ -364,69 +362,51 @@ private fun TotpRow(
onTotpSetupClick: () -> Unit,
modifier: Modifier = Modifier,
) {
if (totpKey != null) {
if (canViewTotp) {
BitwardenTextField(
label = stringResource(id = R.string.authenticator_key),
value = totpKey,
onValueChange = {},
readOnly = true,
singleLine = true,
actions = {
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_clear,
contentDescription = stringResource(id = R.string.delete),
onClick = loginItemTypeHandlers.onClearTotpKeyClick,
)
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_copy,
contentDescription = stringResource(id = R.string.copy_totp),
onClick = { loginItemTypeHandlers.onCopyTotpKeyClick(totpKey) },
)
},
supportingContentPadding = PaddingValues(),
supportingContent = {
BitwardenClickableText(
label = stringResource(id = R.string.set_up_authenticator_key),
onClick = onTotpSetupClick,
leadingIcon = painterResource(id = R.drawable.ic_plus_small),
style = BitwardenTheme.typography.labelMedium,
innerPadding = PaddingValues(all = 16.dp),
cornerSize = 0.dp,
modifier = Modifier.fillMaxWidth(),
)
},
textFieldTestTag = "LoginTotpEntry",
cardStyle = CardStyle.Full,
modifier = modifier.fillMaxWidth(),
)
} else {
BitwardenTextField(
label = stringResource(id = R.string.authenticator_key),
value = totpKey,
cardStyle = CardStyle.Full,
textFieldTestTag = "LoginTotpEntry",
onValueChange = {},
readOnly = true,
enabled = false,
singleLine = true,
modifier = modifier.fillMaxWidth(),
)
}
} else {
Column(modifier = modifier) {
Spacer(modifier = Modifier.height(8.dp))
BitwardenOutlinedButton(
label = stringResource(id = R.string.setup_totp),
icon = rememberVectorPainter(id = R.drawable.ic_light_bulb),
BitwardenTextField(
label = stringResource(id = R.string.authenticator_key),
value = totpKey.orEmpty(),
onValueChange = {},
readOnly = true,
singleLine = true,
actions = {
totpKey?.let {
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_clear,
contentDescription = stringResource(id = R.string.delete),
onClick = loginItemTypeHandlers.onClearTotpKeyClick,
)
BitwardenStandardIconButton(
vectorIconRes = R.drawable.ic_copy,
contentDescription = stringResource(id = R.string.copy_totp),
onClick = { loginItemTypeHandlers.onCopyTotpKeyClick(totpKey) },
)
}
},
tooltip = TooltipData(
onClick = loginItemTypeHandlers.onAuthenticatorHelpToolTipClick,
contentDescription = stringResource(id = R.string.authenticator_key_help),
),
supportingContentPadding = PaddingValues(),
supportingContent = {
BitwardenClickableText(
label = stringResource(id = R.string.set_up_authenticator_key),
onClick = onTotpSetupClick,
leadingIcon = painterResource(id = R.drawable.ic_camera_small),
style = BitwardenTheme.typography.labelMedium,
innerPadding = PaddingValues(all = 16.dp),
isEnabled = canViewTotp,
cornerSize = 0.dp,
modifier = Modifier
.testTag("SetupTotpButton")
.fillMaxWidth(),
.fillMaxWidth()
.testTag("SetupTotpButton"),
)
}
}
},
textFieldTestTag = "LoginTotpEntry",
cardStyle = CardStyle.Full,
modifier = modifier
.fillMaxWidth()
.focusProperties { canFocus = false },
)
}

@Composable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,12 @@ fun VaultAddEditScreen(
)
}

is VaultAddEditEvent.NavigateToAuthenticatorKeyTooltipUri -> {
intentManager.launchUri(
"https://bitwarden.com/help/integrated-authenticator".toUri(),
)
}

is VaultAddEditEvent.CompleteFido2Registration -> {
fido2CompletionManager.completeFido2Registration(event.result)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -991,6 +991,10 @@ class VaultAddEditViewModel @Inject constructor(
VaultAddEditAction.ItemType.LoginType.StartLearnAboutLogins -> {
handleStartLearnAboutLogins()
}

VaultAddEditAction.ItemType.LoginType.AuthenticatorHelpToolTipClick -> {
handleAuthenticatorHelpToolTipClick()
}
}
}

Expand Down Expand Up @@ -1823,6 +1827,10 @@ class VaultAddEditViewModel @Inject constructor(

getRequestAndRegisterCredential()
}

private fun handleAuthenticatorHelpToolTipClick() {
sendEvent(VaultAddEditEvent.NavigateToAuthenticatorKeyTooltipUri)
}
//endregion Internal Type Handlers

//region Utility Functions
Expand Down Expand Up @@ -2649,6 +2657,11 @@ sealed class VaultAddEditEvent {
* Start the coach mark guided tour of the add login content.
*/
data object StartAddLoginItemCoachMarkTour : VaultAddEditEvent()

/**
* Navigate the user to the tooltip URI for Authenticator key help.
*/
data object NavigateToAuthenticatorKeyTooltipUri : VaultAddEditEvent()
}

/**
Expand Down Expand Up @@ -2969,6 +2982,11 @@ sealed class VaultAddEditAction {
* User has dismissed the learn about logins card.
*/
data object LearnAboutLoginsDismissed : LoginType()

/**
* User has clicked the call to action on the authenticator help tooltip.
*/
data object AuthenticatorHelpToolTipClick : LoginType()
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import com.x8bit.bitwarden.ui.vault.feature.addedit.model.UriItem
* clicked.
* @property onClearFido2CredentialClick Handles the action when the clear Fido2 credential button
* is clicked.
* @property onAuthenticatorHelpToolTipClick Handles the action when the authenticator help tooltip
* is clicked.
*/
@Suppress("LongParameterList")
data class VaultAddEditLoginTypeHandlers(
Expand All @@ -44,6 +46,7 @@ data class VaultAddEditLoginTypeHandlers(
val onClearFido2CredentialClick: () -> Unit,
val onStartLoginCoachMarkTour: () -> Unit,
val onDismissLearnAboutLoginsCard: () -> Unit,
val onAuthenticatorHelpToolTipClick: () -> Unit,
) {
@Suppress("UndocumentedPublicClass")
companion object {
Expand Down Expand Up @@ -103,6 +106,11 @@ data class VaultAddEditLoginTypeHandlers(
onAddNewUriClick = {
viewModel.trySendAction(VaultAddEditAction.ItemType.LoginType.AddNewUriClick)
},
onAuthenticatorHelpToolTipClick = {
viewModel.trySendAction(
VaultAddEditAction.ItemType.LoginType.AuthenticatorHelpToolTipClick,
)
},
onCopyTotpKeyClick = { totpKey ->
viewModel.trySendAction(
VaultAddEditAction.ItemType.LoginType.CopyTotpKeyClick(
Expand Down
Loading

0 comments on commit eb3bc83

Please sign in to comment.