diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/field/BitwardenTextField.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/field/BitwardenTextField.kt index 5a3ffd58c1..3202f82045 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/field/BitwardenTextField.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/field/BitwardenTextField.kt @@ -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 @@ -26,14 +30,19 @@ 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 @@ -41,11 +50,13 @@ 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 @@ -53,6 +64,7 @@ import com.x8bit.bitwarden.ui.platform.components.field.toolbar.BitwardenEmptyTe 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 @@ -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, @@ -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 -> { @@ -298,7 +351,10 @@ fun BitwardenTextField( visualTransformation = visualTransformation, modifier = Modifier .nullableTestTag(tag = textFieldTestTag) - .fillMaxWidth(), + .fillMaxWidth() + .onFocusChanged { focusState -> + focused = focusState.isFocused + }, ) supportingContent ?.let { content -> diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/text/BitwardenClickableText.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/text/BitwardenClickableText.kt index d67bd15489..4c407aaf13 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/text/BitwardenClickableText.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/text/BitwardenClickableText.kt @@ -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)) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditLoginItems.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditLoginItems.kt index 185129e477..3e1cc3d25c 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditLoginItems.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditLoginItems.kt @@ -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 @@ -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 @@ -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 @@ -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 @@ -281,7 +280,6 @@ private fun CoachMarkScope.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 @@ -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 diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreen.kt index 14067b8644..bbbd41ff4b 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreen.kt @@ -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) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt index 21cc047cb0..7decdaa710 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt @@ -991,6 +991,10 @@ class VaultAddEditViewModel @Inject constructor( VaultAddEditAction.ItemType.LoginType.StartLearnAboutLogins -> { handleStartLearnAboutLogins() } + + VaultAddEditAction.ItemType.LoginType.AuthenticatorHelpToolTipClick -> { + handleAuthenticatorHelpToolTipClick() + } } } @@ -1823,6 +1827,10 @@ class VaultAddEditViewModel @Inject constructor( getRequestAndRegisterCredential() } + + private fun handleAuthenticatorHelpToolTipClick() { + sendEvent(VaultAddEditEvent.NavigateToAuthenticatorKeyTooltipUri) + } //endregion Internal Type Handlers //region Utility Functions @@ -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() } /** @@ -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() } /** diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/handlers/VaultAddEditLoginTypeHandlers.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/handlers/VaultAddEditLoginTypeHandlers.kt index 2096801811..ef2ccfd26e 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/handlers/VaultAddEditLoginTypeHandlers.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/handlers/VaultAddEditLoginTypeHandlers.kt @@ -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( @@ -44,6 +46,7 @@ data class VaultAddEditLoginTypeHandlers( val onClearFido2CredentialClick: () -> Unit, val onStartLoginCoachMarkTour: () -> Unit, val onDismissLearnAboutLoginsCard: () -> Unit, + val onAuthenticatorHelpToolTipClick: () -> Unit, ) { @Suppress("UndocumentedPublicClass") companion object { @@ -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( diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/qrcodescan/QrCodeScanScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/qrcodescan/QrCodeScanScreen.kt index eb3d65512b..12c3ec126c 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/qrcodescan/QrCodeScanScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/qrcodescan/QrCodeScanScreen.kt @@ -13,9 +13,6 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.FlowRow -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -44,8 +41,10 @@ import androidx.compose.ui.graphics.drawscope.drawIntoCanvas import androidx.compose.ui.graphics.nativeCanvas import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.testTag 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.style.TextAlign import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -55,9 +54,9 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.LocalLifecycleOwner import com.x8bit.bitwarden.R import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect +import com.x8bit.bitwarden.ui.platform.base.util.toAnnotatedString import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold -import com.x8bit.bitwarden.ui.platform.components.text.BitwardenClickableText import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme import com.x8bit.bitwarden.ui.platform.theme.LocalBitwardenColorScheme @@ -87,7 +86,7 @@ fun QrCodeScanScreen( val context = LocalContext.current - val onEnterCodeManuallyClick = remember(viewModel) { + val onEnterKeyManuallyClick = remember(viewModel) { { viewModel.trySendAction(QrCodeScanAction.ManualEntryTextClick) } } @@ -135,11 +134,11 @@ fun QrCodeScanScreen( if (LocalConfiguration.current.isPortrait) { PortraitQRCodeContent( - onEnterCodeManuallyClick = onEnterCodeManuallyClick, + onEnterKeyManuallyClick = onEnterKeyManuallyClick, ) } else { LandscapeQRCodeContent( - onEnterCodeManuallyClick = onEnterCodeManuallyClick, + onEnterKeyManuallyClick = onEnterKeyManuallyClick, ) } } @@ -148,7 +147,7 @@ fun QrCodeScanScreen( @Composable private fun PortraitQRCodeContent( - onEnterCodeManuallyClick: () -> Unit, + onEnterKeyManuallyClick: () -> Unit, modifier: Modifier = Modifier, ) { Column( @@ -178,8 +177,8 @@ private fun PortraitQRCodeContent( modifier = Modifier.padding(horizontal = 16.dp), ) - BottomClickableText( - onEnterCodeManuallyClick = onEnterCodeManuallyClick, + EnterKeyManuallyText( + onEnterKeyManuallyClick = onEnterKeyManuallyClick, ) Spacer(modifier = Modifier.navigationBarsPadding()) } @@ -188,7 +187,7 @@ private fun PortraitQRCodeContent( @Composable private fun LandscapeQRCodeContent( - onEnterCodeManuallyClick: () -> Unit, + onEnterKeyManuallyClick: () -> Unit, modifier: Modifier = Modifier, ) { Row( @@ -218,8 +217,8 @@ private fun LandscapeQRCodeContent( style = BitwardenTheme.typography.bodySmall, ) - BottomClickableText( - onEnterCodeManuallyClick = onEnterCodeManuallyClick, + EnterKeyManuallyText( + onEnterKeyManuallyClick = onEnterKeyManuallyClick, ) } } @@ -403,28 +402,33 @@ private fun QrCodeSquare( } } -@OptIn(ExperimentalLayoutApi::class) @Composable -private fun BottomClickableText( - onEnterCodeManuallyClick: () -> Unit, +private fun EnterKeyManuallyText( + onEnterKeyManuallyClick: () -> Unit, modifier: Modifier = Modifier, ) { - FlowRow( - modifier = modifier, - verticalArrangement = Arrangement.SpaceEvenly, - ) { - Text( - modifier = Modifier.padding(vertical = 4.dp), - text = stringResource(id = R.string.cannot_scan_qr_code), - color = Color.White, - style = BitwardenTheme.typography.bodyMedium, - ) - BitwardenClickableText( - label = stringResource(id = R.string.enter_key_manually), - style = BitwardenTheme.typography.labelLarge, - innerPadding = PaddingValues(vertical = 4.dp, horizontal = 12.dp), - onClick = onEnterCodeManuallyClick, - modifier = Modifier.testTag("EnterKeyManuallyButton"), - ) + val enterKeyManuallyString = stringResource(R.string.enter_key_manually) + val annotatedLinkString = R.string.cannot_scan_qr_code_enter_key_manually.toAnnotatedString { + when (it) { + "enterKeyManually" -> onEnterKeyManuallyClick() + } } + Text( + text = annotatedLinkString, + style = BitwardenTheme.typography.bodySmall, + color = Color.White, + textAlign = TextAlign.Center, + modifier = modifier + .semantics { + customActions = listOf( + CustomAccessibilityAction( + label = enterKeyManuallyString, + action = { + onEnterKeyManuallyClick() + true + }, + ), + ) + }, + ) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 14f1cf57fe..9251759ecf 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -709,8 +709,9 @@ Scanning will happen automatically. Cannot add authenticator key? Cannot add authenticator key? Scan QR Code Scan QR Code - Cannot scan QR Code? + Cannot scan QR code? Enter key manually Authenticator key + Authenticator key help Enter key manually Add TOTP Set up TOTP diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreenTest.kt index dd88422c36..b26cd7ba11 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreenTest.kt @@ -154,6 +154,16 @@ class VaultAddEditScreenTest : BaseComposeTest() { } } + @Test + fun `on NavigateToAuthenticatorKeyTooltipUri Event should invoke IntentManager`() { + mutableEventFlow.tryEmit(VaultAddEditEvent.NavigateToAuthenticatorKeyTooltipUri) + verify { + intentManager.launchUri( + "https://bitwarden.com/help/integrated-authenticator".toUri(), + ) + } + } + @Test fun `on NavigateToQrCodeScan event should invoke NavigateToQrCodeScan`() { mutableEventFlow.tryEmit(VaultAddEditEvent.NavigateToQrCodeScan) @@ -1020,10 +1030,6 @@ class VaultAddEditScreenTest : BaseComposeTest() { mutableStateFlow.update { currentState -> updateLoginType(currentState) { copy(totp = null) } } - - composeTestRule - .onNodeWithText("Authenticator key") - .assertDoesNotExist() } @Suppress("MaxLineLength") @@ -1090,13 +1096,13 @@ class VaultAddEditScreenTest : BaseComposeTest() { } @Test - fun `in ItemType_Login state SetupTOTP button should be present based on state`() { + fun `in ItemType_Login state Set up authenticator key button should always be present`() { mutableStateFlow.update { currentState -> updateLoginType(currentState) { copy(totp = null) } } composeTestRule - .onNodeWithTextAfterScroll(text = "Set up TOTP") + .onNodeWithTextAfterScroll(text = "Set up authenticator key") .assertIsDisplayed() mutableStateFlow.update { currentState -> @@ -1104,8 +1110,8 @@ class VaultAddEditScreenTest : BaseComposeTest() { } composeTestRule - .onNodeWithText(text = "Set up TOTP") - .assertIsNotDisplayed() + .onNodeWithText(text = "Set up authenticator key") + .assertIsDisplayed() } @Suppress("MaxLineLength") @@ -1114,7 +1120,7 @@ class VaultAddEditScreenTest : BaseComposeTest() { fakePermissionManager.checkPermissionResult = true composeTestRule - .onNodeWithTextAfterScroll(text = "Set up TOTP") + .onNodeWithTextAfterScroll(text = "Set up authenticator key") .performClick() verify { @@ -1131,7 +1137,7 @@ class VaultAddEditScreenTest : BaseComposeTest() { fakePermissionManager.getPermissionsResult = true composeTestRule - .onNodeWithTextAfterScroll(text = "Set up TOTP") + .onNodeWithTextAfterScroll(text = "Set up authenticator key") .performClick() verify { @@ -1145,12 +1151,12 @@ class VaultAddEditScreenTest : BaseComposeTest() { @Suppress("MaxLineLength") @Test - fun `in ItemType_Login state clicking Set up TOTP button with a negative result should send false`() { + fun `in ItemType_Login state clicking Set up authenticator key button with a negative result should send false`() { fakePermissionManager.checkPermissionResult = false fakePermissionManager.getPermissionsResult = false composeTestRule - .onNodeWithTextAfterScroll(text = "Set up TOTP") + .onNodeWithTextAfterScroll(text = "Set up authenticator key") .performClick() verify { @@ -1164,7 +1170,7 @@ class VaultAddEditScreenTest : BaseComposeTest() { @Suppress("MaxLineLength") @Test - fun `in ItemType_Login state the SetupTOTP button should be visible but disabled based on state`() { + fun `in ItemType_Login state the SetupAuthenticatorKey button should be visible but disabled based on state`() { mutableStateFlow.update { currentState -> updateLoginType(currentState) { copy( @@ -1175,7 +1181,7 @@ class VaultAddEditScreenTest : BaseComposeTest() { } composeTestRule - .onNodeWithTextAfterScroll(text = "Set up TOTP") + .onNodeWithTextAfterScroll(text = "Set up authenticator key") .assertIsDisplayed() .assertIsNotEnabled() } @@ -1193,7 +1199,7 @@ class VaultAddEditScreenTest : BaseComposeTest() { } composeTestRule - .onNodeWithTextAfterScroll(text = "Authenticator key") + .onNodeWithTextAfterScroll(text = "Set up authenticator key") .assertIsDisplayed() .assertIsNotEnabled() @@ -1202,6 +1208,19 @@ class VaultAddEditScreenTest : BaseComposeTest() { composeTestRule.assertScrollableNodeDoesNotExist("Camera") } + @Test + fun `Clicking the Authenticator key tooltip sends AuthenticatorHelpToolTipClick action`() { + composeTestRule + .onNodeWithContentDescriptionAfterScroll(label = "Authenticator key help") + .performClick() + + verify { + viewModel.trySendAction( + VaultAddEditAction.ItemType.LoginType.AuthenticatorHelpToolTipClick, + ) + } + } + @Test fun `in ItemType_Login state changing URI text field should trigger UriValueChange`() { mutableStateFlow.update { currentState -> diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt index 246183d289..fb18ce3b42 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt @@ -2421,6 +2421,19 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { } } + @Test + fun `Authenticator TooltipCLick emits NavigateToAuthenticatorKeyTooltipUri`() = runTest { + viewModel.eventFlow.test { + viewModel.trySendAction( + VaultAddEditAction.ItemType.LoginType.AuthenticatorHelpToolTipClick, + ) + assertEquals( + VaultAddEditEvent.NavigateToAuthenticatorKeyTooltipUri, + awaitItem(), + ) + } + } + @Test fun `ClearTotpKeyClick call should clear the totp code`() { val viewModel = createAddVaultItemViewModel( diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/qrcodescan/QrCodeScanScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/qrcodescan/QrCodeScanScreenTest.kt index 707d58f0c2..3ffe40381e 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/qrcodescan/QrCodeScanScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/qrcodescan/QrCodeScanScreenTest.kt @@ -4,6 +4,7 @@ import androidx.camera.core.ImageProxy import androidx.compose.ui.test.onNodeWithText import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest +import com.x8bit.bitwarden.ui.util.performCustomAccessibilityAction import com.x8bit.bitwarden.ui.vault.feature.qrcodescan.util.FakeQrCodeAnalyzer import io.mockk.every import io.mockk.mockk @@ -89,17 +90,23 @@ class QrCodeScanScreenTest : BaseComposeTest() { @Config(qualifiers = "land") @Test fun `clicking on manual text should send ManualEntryTextClick in landscape mode`() = runTest { - // TODO Update the tests once clickable text issue is resolved (BIT-1357) composeTestRule - .onNodeWithText("Enter key manually", substring = true) - .assertExists() + .onNodeWithText(text = "Cannot scan QR code? Enter key manually") + .performCustomAccessibilityAction(label = "Enter key manually") + + verify { + viewModel.trySendAction(QrCodeScanAction.ManualEntryTextClick) + } } @Test fun `clicking on manual text should send ManualEntryTextClick`() = runTest { - // TODO Update the tests once clickable text issue is resolved (BIT-1357) composeTestRule - .onNodeWithText("Enter key manually", substring = true) - .assertExists() + .onNodeWithText(text = "Cannot scan QR code? Enter key manually") + .performCustomAccessibilityAction(label = "Enter key manually") + + verify { + viewModel.trySendAction(QrCodeScanAction.ManualEntryTextClick) + } } }