Skip to content

Commit eb3bc83

Browse files
PM-17838 - Add help button for authenticator key (#4697)
1 parent 30e882d commit eb3bc83

File tree

11 files changed

+239
-123
lines changed

11 files changed

+239
-123
lines changed

app/src/main/java/com/x8bit/bitwarden/ui/platform/components/field/BitwardenTextField.kt

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
package com.x8bit.bitwarden.ui.platform.components.field
22

3+
import androidx.compose.animation.core.animateDpAsState
34
import androidx.compose.foundation.layout.Arrangement
45
import androidx.compose.foundation.layout.Box
56
import androidx.compose.foundation.layout.Column
67
import androidx.compose.foundation.layout.ColumnScope
78
import androidx.compose.foundation.layout.PaddingValues
9+
import androidx.compose.foundation.layout.Row
810
import androidx.compose.foundation.layout.RowScope
911
import androidx.compose.foundation.layout.Spacer
1012
import androidx.compose.foundation.layout.defaultMinSize
1113
import androidx.compose.foundation.layout.fillMaxWidth
1214
import androidx.compose.foundation.layout.height
1315
import androidx.compose.foundation.layout.padding
16+
import androidx.compose.foundation.layout.size
17+
import androidx.compose.foundation.layout.width
1418
import androidx.compose.foundation.text.KeyboardOptions
1519
import androidx.compose.material3.DropdownMenu
1620
import androidx.compose.material3.DropdownMenuItem
@@ -26,33 +30,41 @@ import androidx.compose.runtime.mutableIntStateOf
2630
import androidx.compose.runtime.mutableStateOf
2731
import androidx.compose.runtime.remember
2832
import androidx.compose.runtime.setValue
33+
import androidx.compose.ui.Alignment
2934
import androidx.compose.ui.Modifier
3035
import androidx.compose.ui.focus.FocusRequester
3136
import androidx.compose.ui.focus.focusRequester
37+
import androidx.compose.ui.focus.onFocusChanged
3238
import androidx.compose.ui.focus.onFocusEvent
3339
import androidx.compose.ui.layout.onGloballyPositioned
3440
import androidx.compose.ui.platform.LocalClipboardManager
3541
import androidx.compose.ui.platform.LocalTextToolbar
3642
import androidx.compose.ui.platform.TextToolbar
43+
import androidx.compose.ui.semantics.CustomAccessibilityAction
44+
import androidx.compose.ui.semantics.customActions
45+
import androidx.compose.ui.semantics.semantics
3746
import androidx.compose.ui.text.TextStyle
3847
import androidx.compose.ui.text.input.KeyboardType
3948
import androidx.compose.ui.text.input.TextFieldValue
4049
import androidx.compose.ui.text.input.VisualTransformation
4150
import androidx.compose.ui.tooling.preview.Preview
4251
import androidx.compose.ui.unit.dp
4352
import androidx.compose.ui.window.PopupProperties
53+
import com.x8bit.bitwarden.R
4454
import com.x8bit.bitwarden.ui.platform.base.util.cardStyle
4555
import com.x8bit.bitwarden.ui.platform.base.util.nullableTestTag
4656
import com.x8bit.bitwarden.ui.platform.base.util.toPx
4757
import com.x8bit.bitwarden.ui.platform.base.util.withLineBreaksAtWidth
4858
import com.x8bit.bitwarden.ui.platform.components.appbar.color.bitwardenMenuItemColors
59+
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenStandardIconButton
4960
import com.x8bit.bitwarden.ui.platform.components.divider.BitwardenHorizontalDivider
5061
import com.x8bit.bitwarden.ui.platform.components.field.color.bitwardenTextFieldColors
5162
import com.x8bit.bitwarden.ui.platform.components.field.toolbar.BitwardenCutCopyTextToolbar
5263
import com.x8bit.bitwarden.ui.platform.components.field.toolbar.BitwardenEmptyTextToolbar
5364
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
5465
import com.x8bit.bitwarden.ui.platform.components.model.IconResource
5566
import com.x8bit.bitwarden.ui.platform.components.model.TextToolbarType
67+
import com.x8bit.bitwarden.ui.platform.components.model.TooltipData
5668
import com.x8bit.bitwarden.ui.platform.components.row.BitwardenRowOfActions
5769
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
5870
import kotlinx.collections.immutable.ImmutableList
@@ -192,6 +204,7 @@ fun BitwardenTextField(
192204
supportingContent: (@Composable ColumnScope.() -> Unit)?,
193205
cardStyle: CardStyle,
194206
modifier: Modifier = Modifier,
207+
tooltip: TooltipData? = null,
195208
supportingContentPadding: PaddingValues = PaddingValues(vertical = 12.dp, horizontal = 16.dp),
196209
placeholder: String? = null,
197210
leadingIconResource: IconResource? = null,
@@ -255,12 +268,52 @@ fun BitwardenTextField(
255268
paddingTop = 6.dp,
256269
paddingBottom = 0.dp,
257270
)
258-
.fillMaxWidth(),
271+
.fillMaxWidth()
272+
.semantics {
273+
customActions = listOfNotNull(
274+
tooltip?.let {
275+
CustomAccessibilityAction(
276+
label = it.contentDescription,
277+
action = {
278+
it.onClick()
279+
true
280+
},
281+
)
282+
},
283+
)
284+
},
259285
) {
286+
var focused by remember { mutableStateOf(false) }
287+
260288
TextField(
261289
colors = bitwardenTextFieldColors(),
262290
enabled = enabled,
263-
label = label?.let { { Text(text = it) } },
291+
label = label?.let {
292+
{
293+
Row(verticalAlignment = Alignment.CenterVertically) {
294+
Text(text = it)
295+
tooltip?.let {
296+
val targetSize = if (textFieldValue.text.isEmpty() || focused) {
297+
16.dp
298+
} else {
299+
12.dp
300+
}
301+
val size by animateDpAsState(
302+
targetValue = targetSize,
303+
label = "${it.contentDescription}_animation",
304+
)
305+
Spacer(modifier = Modifier.width(16.dp))
306+
BitwardenStandardIconButton(
307+
vectorIconRes = R.drawable.ic_question_circle_small,
308+
contentDescription = it.contentDescription,
309+
onClick = it.onClick,
310+
contentColor = BitwardenTheme.colorScheme.icon.secondary,
311+
modifier = Modifier.size(size),
312+
)
313+
}
314+
}
315+
}
316+
},
264317
value = textFieldValue,
265318
leadingIcon = leadingIconResource?.let { iconResource ->
266319
{
@@ -298,7 +351,10 @@ fun BitwardenTextField(
298351
visualTransformation = visualTransformation,
299352
modifier = Modifier
300353
.nullableTestTag(tag = textFieldTestTag)
301-
.fillMaxWidth(),
354+
.fillMaxWidth()
355+
.onFocusChanged { focusState ->
356+
focused = focusState.isFocused
357+
},
302358
)
303359
supportingContent
304360
?.let { content ->

app/src/main/java/com/x8bit/bitwarden/ui/platform/components/text/BitwardenClickableText.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,11 @@ fun BitwardenClickableText(
6464
Icon(
6565
painter = leadingIcon,
6666
contentDescription = null,
67-
tint = color,
67+
tint = if (isEnabled) {
68+
color
69+
} else {
70+
BitwardenTheme.colorScheme.filledButton.foregroundDisabled
71+
},
6872
modifier = Modifier.size(size = 16.dp),
6973
)
7074
Spacer(modifier = Modifier.width(width = 8.dp))

app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditLoginItems.kt

Lines changed: 43 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package com.x8bit.bitwarden.ui.vault.feature.addedit
22

3-
import androidx.compose.foundation.layout.Column
43
import androidx.compose.foundation.layout.PaddingValues
54
import androidx.compose.foundation.layout.Spacer
65
import androidx.compose.foundation.layout.fillMaxWidth
@@ -14,6 +13,7 @@ import androidx.compose.runtime.remember
1413
import androidx.compose.runtime.saveable.rememberSaveable
1514
import androidx.compose.runtime.setValue
1615
import androidx.compose.ui.Modifier
16+
import androidx.compose.ui.focus.focusProperties
1717
import androidx.compose.ui.platform.testTag
1818
import androidx.compose.ui.res.painterResource
1919
import androidx.compose.ui.res.stringResource
@@ -23,7 +23,6 @@ import com.x8bit.bitwarden.ui.platform.base.util.Text
2323
import com.x8bit.bitwarden.ui.platform.base.util.asText
2424
import com.x8bit.bitwarden.ui.platform.base.util.cardStyle
2525
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
26-
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenOutlinedButton
2726
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenStandardIconButton
2827
import com.x8bit.bitwarden.ui.platform.components.coachmark.CoachMarkActionText
2928
import com.x8bit.bitwarden.ui.platform.components.coachmark.CoachMarkScope
@@ -34,8 +33,8 @@ import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordField
3433
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
3534
import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText
3635
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
36+
import com.x8bit.bitwarden.ui.platform.components.model.TooltipData
3737
import com.x8bit.bitwarden.ui.platform.components.text.BitwardenClickableText
38-
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
3938
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
4039
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditLoginTypeHandlers
4140

@@ -281,7 +280,6 @@ private fun CoachMarkScope<AddEditItemCoachMark>.PasswordRow(
281280
label = stringResource(id = R.string.check_password_for_data_breaches),
282281
style = BitwardenTheme.typography.labelMedium,
283282
onClick = loginItemTypeHandlers.onPasswordCheckerClick,
284-
leadingIcon = painterResource(id = R.drawable.ic_camera_small),
285283
innerPadding = PaddingValues(all = 16.dp),
286284
cornerSize = 0.dp,
287285
modifier = Modifier
@@ -364,69 +362,51 @@ private fun TotpRow(
364362
onTotpSetupClick: () -> Unit,
365363
modifier: Modifier = Modifier,
366364
) {
367-
if (totpKey != null) {
368-
if (canViewTotp) {
369-
BitwardenTextField(
370-
label = stringResource(id = R.string.authenticator_key),
371-
value = totpKey,
372-
onValueChange = {},
373-
readOnly = true,
374-
singleLine = true,
375-
actions = {
376-
BitwardenStandardIconButton(
377-
vectorIconRes = R.drawable.ic_clear,
378-
contentDescription = stringResource(id = R.string.delete),
379-
onClick = loginItemTypeHandlers.onClearTotpKeyClick,
380-
)
381-
BitwardenStandardIconButton(
382-
vectorIconRes = R.drawable.ic_copy,
383-
contentDescription = stringResource(id = R.string.copy_totp),
384-
onClick = { loginItemTypeHandlers.onCopyTotpKeyClick(totpKey) },
385-
)
386-
},
387-
supportingContentPadding = PaddingValues(),
388-
supportingContent = {
389-
BitwardenClickableText(
390-
label = stringResource(id = R.string.set_up_authenticator_key),
391-
onClick = onTotpSetupClick,
392-
leadingIcon = painterResource(id = R.drawable.ic_plus_small),
393-
style = BitwardenTheme.typography.labelMedium,
394-
innerPadding = PaddingValues(all = 16.dp),
395-
cornerSize = 0.dp,
396-
modifier = Modifier.fillMaxWidth(),
397-
)
398-
},
399-
textFieldTestTag = "LoginTotpEntry",
400-
cardStyle = CardStyle.Full,
401-
modifier = modifier.fillMaxWidth(),
402-
)
403-
} else {
404-
BitwardenTextField(
405-
label = stringResource(id = R.string.authenticator_key),
406-
value = totpKey,
407-
cardStyle = CardStyle.Full,
408-
textFieldTestTag = "LoginTotpEntry",
409-
onValueChange = {},
410-
readOnly = true,
411-
enabled = false,
412-
singleLine = true,
413-
modifier = modifier.fillMaxWidth(),
414-
)
415-
}
416-
} else {
417-
Column(modifier = modifier) {
418-
Spacer(modifier = Modifier.height(8.dp))
419-
BitwardenOutlinedButton(
420-
label = stringResource(id = R.string.setup_totp),
421-
icon = rememberVectorPainter(id = R.drawable.ic_light_bulb),
365+
BitwardenTextField(
366+
label = stringResource(id = R.string.authenticator_key),
367+
value = totpKey.orEmpty(),
368+
onValueChange = {},
369+
readOnly = true,
370+
singleLine = true,
371+
actions = {
372+
totpKey?.let {
373+
BitwardenStandardIconButton(
374+
vectorIconRes = R.drawable.ic_clear,
375+
contentDescription = stringResource(id = R.string.delete),
376+
onClick = loginItemTypeHandlers.onClearTotpKeyClick,
377+
)
378+
BitwardenStandardIconButton(
379+
vectorIconRes = R.drawable.ic_copy,
380+
contentDescription = stringResource(id = R.string.copy_totp),
381+
onClick = { loginItemTypeHandlers.onCopyTotpKeyClick(totpKey) },
382+
)
383+
}
384+
},
385+
tooltip = TooltipData(
386+
onClick = loginItemTypeHandlers.onAuthenticatorHelpToolTipClick,
387+
contentDescription = stringResource(id = R.string.authenticator_key_help),
388+
),
389+
supportingContentPadding = PaddingValues(),
390+
supportingContent = {
391+
BitwardenClickableText(
392+
label = stringResource(id = R.string.set_up_authenticator_key),
422393
onClick = onTotpSetupClick,
394+
leadingIcon = painterResource(id = R.drawable.ic_camera_small),
395+
style = BitwardenTheme.typography.labelMedium,
396+
innerPadding = PaddingValues(all = 16.dp),
423397
isEnabled = canViewTotp,
398+
cornerSize = 0.dp,
424399
modifier = Modifier
425-
.testTag("SetupTotpButton")
426-
.fillMaxWidth(),
400+
.fillMaxWidth()
401+
.testTag("SetupTotpButton"),
427402
)
428-
}
429-
}
403+
},
404+
textFieldTestTag = "LoginTotpEntry",
405+
cardStyle = CardStyle.Full,
406+
modifier = modifier
407+
.fillMaxWidth()
408+
.focusProperties { canFocus = false },
409+
)
430410
}
431411

432412
@Composable

app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreen.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,12 @@ fun VaultAddEditScreen(
130130
)
131131
}
132132

133+
is VaultAddEditEvent.NavigateToAuthenticatorKeyTooltipUri -> {
134+
intentManager.launchUri(
135+
"https://bitwarden.com/help/integrated-authenticator".toUri(),
136+
)
137+
}
138+
133139
is VaultAddEditEvent.CompleteFido2Registration -> {
134140
fido2CompletionManager.completeFido2Registration(event.result)
135141
}

app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -991,6 +991,10 @@ class VaultAddEditViewModel @Inject constructor(
991991
VaultAddEditAction.ItemType.LoginType.StartLearnAboutLogins -> {
992992
handleStartLearnAboutLogins()
993993
}
994+
995+
VaultAddEditAction.ItemType.LoginType.AuthenticatorHelpToolTipClick -> {
996+
handleAuthenticatorHelpToolTipClick()
997+
}
994998
}
995999
}
9961000

@@ -1823,6 +1827,10 @@ class VaultAddEditViewModel @Inject constructor(
18231827

18241828
getRequestAndRegisterCredential()
18251829
}
1830+
1831+
private fun handleAuthenticatorHelpToolTipClick() {
1832+
sendEvent(VaultAddEditEvent.NavigateToAuthenticatorKeyTooltipUri)
1833+
}
18261834
//endregion Internal Type Handlers
18271835

18281836
//region Utility Functions
@@ -2649,6 +2657,11 @@ sealed class VaultAddEditEvent {
26492657
* Start the coach mark guided tour of the add login content.
26502658
*/
26512659
data object StartAddLoginItemCoachMarkTour : VaultAddEditEvent()
2660+
2661+
/**
2662+
* Navigate the user to the tooltip URI for Authenticator key help.
2663+
*/
2664+
data object NavigateToAuthenticatorKeyTooltipUri : VaultAddEditEvent()
26522665
}
26532666

26542667
/**
@@ -2969,6 +2982,11 @@ sealed class VaultAddEditAction {
29692982
* User has dismissed the learn about logins card.
29702983
*/
29712984
data object LearnAboutLoginsDismissed : LoginType()
2985+
2986+
/**
2987+
* User has clicked the call to action on the authenticator help tooltip.
2988+
*/
2989+
data object AuthenticatorHelpToolTipClick : LoginType()
29722990
}
29732991

29742992
/**

app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/handlers/VaultAddEditLoginTypeHandlers.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ import com.x8bit.bitwarden.ui.vault.feature.addedit.model.UriItem
2626
* clicked.
2727
* @property onClearFido2CredentialClick Handles the action when the clear Fido2 credential button
2828
* is clicked.
29+
* @property onAuthenticatorHelpToolTipClick Handles the action when the authenticator help tooltip
30+
* is clicked.
2931
*/
3032
@Suppress("LongParameterList")
3133
data class VaultAddEditLoginTypeHandlers(
@@ -44,6 +46,7 @@ data class VaultAddEditLoginTypeHandlers(
4446
val onClearFido2CredentialClick: () -> Unit,
4547
val onStartLoginCoachMarkTour: () -> Unit,
4648
val onDismissLearnAboutLoginsCard: () -> Unit,
49+
val onAuthenticatorHelpToolTipClick: () -> Unit,
4750
) {
4851
@Suppress("UndocumentedPublicClass")
4952
companion object {
@@ -103,6 +106,11 @@ data class VaultAddEditLoginTypeHandlers(
103106
onAddNewUriClick = {
104107
viewModel.trySendAction(VaultAddEditAction.ItemType.LoginType.AddNewUriClick)
105108
},
109+
onAuthenticatorHelpToolTipClick = {
110+
viewModel.trySendAction(
111+
VaultAddEditAction.ItemType.LoginType.AuthenticatorHelpToolTipClick,
112+
)
113+
},
106114
onCopyTotpKeyClick = { totpKey ->
107115
viewModel.trySendAction(
108116
VaultAddEditAction.ItemType.LoginType.CopyTotpKeyClick(

0 commit comments

Comments
 (0)