-
Notifications
You must be signed in to change notification settings - Fork 904
PM-27597: Update Yubikey illustration to match the rest of the app #6087
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,24 +7,19 @@ 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.shape.RoundedCornerShape | ||
| import androidx.compose.foundation.text.KeyboardActions | ||
| import androidx.compose.foundation.verticalScroll | ||
| import androidx.compose.material3.ExperimentalMaterial3Api | ||
| import androidx.compose.material3.Text | ||
| import androidx.compose.material3.TopAppBarDefaults | ||
| import androidx.compose.material3.rememberTopAppBarState | ||
| import androidx.compose.runtime.Composable | ||
| 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.input.nestedscroll.nestedScroll | ||
| import androidx.compose.ui.layout.ContentScale | ||
| import androidx.compose.ui.res.painterResource | ||
| import androidx.compose.ui.res.stringResource | ||
| import androidx.compose.ui.text.input.ImeAction | ||
|
|
@@ -58,7 +53,6 @@ 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.util.asText | ||
| import com.x8bit.bitwarden.ui.auth.feature.twofactorlogin.util.description | ||
| import com.x8bit.bitwarden.ui.auth.feature.twofactorlogin.util.title | ||
| import com.x8bit.bitwarden.ui.platform.composition.LocalAuthTabLaunchers | ||
|
|
@@ -126,19 +120,18 @@ fun TwoFactorLoginScreen( | |
| }, | ||
| ) | ||
|
|
||
| val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) | ||
| val title = if (state.isNewDeviceVerification) { | ||
| BitwardenString.verify_your_identity.asText() | ||
| } else { | ||
| state.authMethod.title | ||
| } | ||
| val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() | ||
| BitwardenScaffold( | ||
| modifier = Modifier | ||
| .fillMaxSize() | ||
| .nestedScroll(scrollBehavior.nestedScrollConnection), | ||
| topBar = { | ||
| BitwardenTopAppBar( | ||
| title = title(), | ||
| title = if (state.isNewDeviceVerification) { | ||
| stringResource(id = BitwardenString.verify_your_identity) | ||
| } else { | ||
| state.authMethod.title() | ||
| }, | ||
| scrollBehavior = scrollBehavior, | ||
| navigationIcon = rememberVectorPainter(id = BitwardenDrawable.ic_close), | ||
| navigationIconContentDescription = stringResource(id = BitwardenString.close), | ||
|
|
@@ -230,55 +223,45 @@ private fun TwoFactorLoginScreenContent( | |
| modifier = modifier | ||
| .verticalScroll(rememberScrollState()), | ||
| ) { | ||
| if (state.authMethod != TwoFactorAuthMethod.YUBI_KEY) { | ||
| state.imageRes?.let { | ||
| Spacer(modifier = Modifier.height(12.dp)) | ||
| Image( | ||
| painter = painterResource(id = it), | ||
| contentDescription = null, | ||
| modifier = Modifier | ||
| .standardHorizontalMargin() | ||
| .size(124.dp), | ||
| ) | ||
| Spacer(modifier = Modifier.height(12.dp)) | ||
| } | ||
| state.imageRes?.let { | ||
| Spacer(modifier = Modifier.height(12.dp)) | ||
| Image( | ||
| painter = painterResource(id = it), | ||
| contentDescription = null, | ||
| modifier = Modifier | ||
| .standardHorizontalMargin() | ||
| .size(124.dp), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ๐ Improved Image RenderingGood migration from raster to vector drawable:
The streamlined implementation improves both performance and maintainability. |
||
| ) | ||
| Spacer(modifier = Modifier.height(12.dp)) | ||
| } | ||
| Spacer(modifier = Modifier.height(height = 12.dp)) | ||
| Text( | ||
| text = if (state.isNewDeviceVerification) { | ||
| stringResource(BitwardenString.enter_verification_code_new_device) | ||
| } else { | ||
| state.authMethod.description( | ||
| state.displayEmail, | ||
| ).invoke() | ||
| }, | ||
| textAlign = TextAlign.Center, | ||
| style = BitwardenTheme.typography.bodyMedium, | ||
| color = BitwardenTheme.colorScheme.text.primary, | ||
| modifier = Modifier | ||
| .standardHorizontalMargin() | ||
| .fillMaxWidth(), | ||
| ) | ||
|
|
||
| Spacer(modifier = Modifier.height(12.dp)) | ||
|
|
||
| if (state.authMethod == TwoFactorAuthMethod.YUBI_KEY) { | ||
| state.imageRes?.let { | ||
| Spacer(modifier = Modifier.height(12.dp)) | ||
| Image( | ||
| painter = painterResource(id = it), | ||
| contentDescription = null, | ||
| alignment = Alignment.Center, | ||
| contentScale = ContentScale.FillWidth, | ||
| if (state.isNewDeviceVerification) { | ||
| Spacer(modifier = Modifier.height(height = 12.dp)) | ||
| Text( | ||
| text = stringResource(id = BitwardenString.enter_verification_code_new_device), | ||
| textAlign = TextAlign.Center, | ||
| style = BitwardenTheme.typography.bodyMedium, | ||
| color = BitwardenTheme.colorScheme.text.primary, | ||
| modifier = Modifier | ||
| .standardHorizontalMargin() | ||
| .fillMaxWidth(), | ||
| ) | ||
| } else { | ||
| state.authMethod.description(email = state.displayEmail)?.let { text -> | ||
| Spacer(modifier = Modifier.height(height = 12.dp)) | ||
| Text( | ||
| text = text(), | ||
| textAlign = TextAlign.Center, | ||
| style = BitwardenTheme.typography.bodyMedium, | ||
| color = BitwardenTheme.colorScheme.text.primary, | ||
| modifier = Modifier | ||
| .padding(horizontal = 24.dp) | ||
| .clip(RoundedCornerShape(4.dp)) | ||
| .standardHorizontalMargin() | ||
| .fillMaxWidth(), | ||
| ) | ||
| Spacer(modifier = Modifier.height(24.dp)) | ||
| } | ||
| } | ||
|
|
||
| Spacer(modifier = Modifier.height(12.dp)) | ||
| if (state.shouldShowCodeInput) { | ||
| BitwardenPasswordField( | ||
| value = state.codeInput, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -29,11 +29,12 @@ val TwoFactorAuthMethod.title: Text | |
| /** | ||
| * Get the description for the given auth method. | ||
| */ | ||
| fun TwoFactorAuthMethod.description(email: String): Text = when (this) { | ||
| fun TwoFactorAuthMethod.description(email: String): Text? = when (this) { | ||
| TwoFactorAuthMethod.AUTHENTICATOR_APP -> BitwardenString.enter_verification_code_app.asText() | ||
| TwoFactorAuthMethod.DUO -> { | ||
| BitwardenString.follow_the_steps_from_duo_to_finish_logging_in.asText() | ||
| } | ||
|
|
||
| TwoFactorAuthMethod.DUO_ORGANIZATION -> { | ||
| BitwardenString.duo_two_step_login_is_required_for_your_account | ||
| .asText() | ||
|
|
@@ -45,8 +46,12 @@ fun TwoFactorAuthMethod.description(email: String): Text = when (this) { | |
| TwoFactorAuthMethod.WEB_AUTH -> { | ||
| BitwardenString.continue_to_complete_web_authn_verification.asText() | ||
| } | ||
|
|
||
| TwoFactorAuthMethod.YUBI_KEY -> BitwardenString.yubi_key_instruction.asText() | ||
| else -> "".asText() | ||
| TwoFactorAuthMethod.U2F, | ||
| TwoFactorAuthMethod.REMEMBER, | ||
| TwoFactorAuthMethod.RECOVERY_CODE, | ||
| -> null | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -122,7 +127,7 @@ val TwoFactorAuthMethod.shouldUseNfc: Boolean | |
| @get:DrawableRes | ||
| val TwoFactorAuthMethod.imageRes: Int? | ||
| get() = when (this) { | ||
| TwoFactorAuthMethod.YUBI_KEY -> BitwardenDrawable.img_yubi_key | ||
| TwoFactorAuthMethod.YUBI_KEY -> BitwardenDrawable.ill_yubi_key | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ๐ Correct Resource Update The reference has been properly updated from the raster image resources (
The vector drawable provides:
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ๐ Correct Resource UpdateProperly updated from raster (
The vector approach provides better scalability and smaller binary size. |
||
| TwoFactorAuthMethod.EMAIL -> BitwardenDrawable.ill_new_device_verification | ||
| TwoFactorAuthMethod.AUTHENTICATOR_APP -> BitwardenDrawable.ill_authenticator | ||
| else -> null | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,21 +1,14 @@ | ||
| package com.x8bit.bitwarden.ui.vault.feature.qrcodescan | ||
|
|
||
| import android.net.Uri | ||
| import com.bitwarden.ui.platform.base.BaseViewModel | ||
| import com.bitwarden.ui.platform.util.getTotpDataOrNull | ||
| import com.x8bit.bitwarden.data.vault.repository.VaultRepository | ||
| import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult | ||
| import dagger.hilt.android.lifecycle.HiltViewModel | ||
| import javax.inject.Inject | ||
|
|
||
| private const val ALGORITHM = "algorithm" | ||
| private const val DIGITS = "digits" | ||
| private const val PERIOD = "period" | ||
| private const val SECRET = "secret" | ||
| private const val TOTP_CODE_PREFIX = "otpauth://totp" | ||
|
|
||
| /** | ||
| * Handles [QrCodeScanAction], | ||
| * and launches [QrCodeScanEvent] for the [QrCodeScanScreen]. | ||
| * Handles [QrCodeScanAction] and launches [QrCodeScanEvent] for the [QrCodeScanScreen]. | ||
| */ | ||
| @HiltViewModel | ||
| class QrCodeScanViewModel @Inject constructor( | ||
|
|
@@ -33,83 +26,25 @@ class QrCodeScanViewModel @Inject constructor( | |
| } | ||
|
|
||
| private fun handleCloseClick() { | ||
| sendEvent( | ||
| QrCodeScanEvent.NavigateBack, | ||
| ) | ||
| sendEvent(QrCodeScanEvent.NavigateBack) | ||
| } | ||
|
|
||
| private fun handleManualEntryTextClick() { | ||
| sendEvent( | ||
| QrCodeScanEvent.NavigateToManualCodeEntry, | ||
| ) | ||
| sendEvent(QrCodeScanEvent.NavigateToManualCodeEntry) | ||
| } | ||
|
|
||
| // For more information: https://bitwarden.com/help/authenticator-keys/#support-for-more-parameters | ||
| private fun handleQrCodeScanReceive(action: QrCodeScanAction.QrCodeScanReceive) { | ||
| var result: TotpCodeResult = TotpCodeResult.Success(action.qrCode) | ||
| val scannedCode = action.qrCode | ||
|
|
||
| if (scannedCode.isBlank() || !scannedCode.startsWith(TOTP_CODE_PREFIX)) { | ||
| vaultRepository.emitTotpCodeResult(TotpCodeResult.CodeScanningError()) | ||
| sendEvent(QrCodeScanEvent.NavigateBack) | ||
| return | ||
| } | ||
|
|
||
| val scannedCodeUri = Uri.parse(scannedCode) | ||
| val secretValue = scannedCodeUri.getQueryParameter(SECRET) | ||
| if (secretValue == null || !secretValue.isBase32()) { | ||
| vaultRepository.emitTotpCodeResult(TotpCodeResult.CodeScanningError()) | ||
| sendEvent(QrCodeScanEvent.NavigateBack) | ||
| return | ||
| } | ||
|
|
||
| val values = scannedCodeUri.queryParameterNames | ||
| if (!areParametersValid(scannedCode, values)) { | ||
| result = TotpCodeResult.CodeScanningError() | ||
| } | ||
|
|
||
| vaultRepository.emitTotpCodeResult(result) | ||
| val qrCode = action.qrCode | ||
| qrCode | ||
| .getTotpDataOrNull() | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ๐ Simplified ViewModel LogicExcellent refactoring using the new Before: ~80 lines of URI parsing logic duplicated in this ViewModel This aligns with MVVM architecture guidelines:
The error handling remains clear with the elvis operator handling invalid TOTP data. |
||
| ?.let { vaultRepository.emitTotpCodeResult(TotpCodeResult.Success(code = qrCode)) } | ||
| ?: run { vaultRepository.emitTotpCodeResult(TotpCodeResult.CodeScanningError()) } | ||
| sendEvent(QrCodeScanEvent.NavigateBack) | ||
| } | ||
|
|
||
| private fun handleCameraErrorReceive() { | ||
| sendEvent( | ||
| QrCodeScanEvent.NavigateToManualCodeEntry, | ||
| ) | ||
| } | ||
|
|
||
| @Suppress("NestedBlockDepth", "MagicNumber") | ||
| private fun areParametersValid(scannedCode: String, parameters: Set<String>): Boolean { | ||
| parameters.forEach { parameter -> | ||
| Uri.parse(scannedCode).getQueryParameter(parameter)?.let { value -> | ||
| when (parameter) { | ||
| DIGITS -> { | ||
| val digit = value.toInt() | ||
| if (digit > 10 || digit < 1) { | ||
| return false | ||
| } | ||
| } | ||
|
|
||
| PERIOD -> { | ||
| val period = value.toInt() | ||
| if (period < 1) { | ||
| return false | ||
| } | ||
| } | ||
|
|
||
| ALGORITHM -> { | ||
| val lowercaseAlgo = value.lowercase() | ||
| if (lowercaseAlgo != "sha1" && | ||
| lowercaseAlgo != "sha256" && | ||
| lowercaseAlgo != "sha512" | ||
| ) { | ||
| return false | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| return true | ||
| sendEvent(QrCodeScanEvent.NavigateToManualCodeEntry) | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -154,11 +89,3 @@ sealed class QrCodeScanAction { | |
| */ | ||
| data object CameraSetupErrorReceive : QrCodeScanAction() | ||
| } | ||
|
|
||
| /** | ||
| * Checks if a string is using base32 digits. | ||
| */ | ||
| private fun String.isBase32(): Boolean { | ||
| val regex = ("^[A-Za-z2-7]+=*$").toRegex() | ||
| return regex.matches(this) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -43,8 +43,8 @@ class TwoFactorAuthMethodExtensionTest { | |
| TwoFactorAuthMethod.DUO to | ||
| BitwardenString.follow_the_steps_from_duo_to_finish_logging_in.asText(), | ||
| TwoFactorAuthMethod.YUBI_KEY to BitwardenString.yubi_key_instruction.asText(), | ||
| TwoFactorAuthMethod.U2F to "".asText(), | ||
| TwoFactorAuthMethod.REMEMBER to "".asText(), | ||
| TwoFactorAuthMethod.U2F to null, | ||
| TwoFactorAuthMethod.REMEMBER to null, | ||
| TwoFactorAuthMethod.DUO_ORGANIZATION to | ||
| BitwardenString.duo_two_step_login_is_required_for_your_account | ||
| .asText() | ||
|
|
@@ -54,7 +54,7 @@ class TwoFactorAuthMethodExtensionTest { | |
| ), | ||
| TwoFactorAuthMethod.WEB_AUTH to | ||
| BitwardenString.continue_to_complete_web_authn_verification.asText(), | ||
| TwoFactorAuthMethod.RECOVERY_CODE to "".asText(), | ||
| TwoFactorAuthMethod.RECOVERY_CODE to null, | ||
| ) | ||
| .forEach { (type, title) -> | ||
| assertEquals( | ||
|
|
@@ -142,7 +142,7 @@ class TwoFactorAuthMethodExtensionTest { | |
| TwoFactorAuthMethod.AUTHENTICATOR_APP to BitwardenDrawable.ill_authenticator, | ||
| TwoFactorAuthMethod.EMAIL to BitwardenDrawable.ill_new_device_verification, | ||
| TwoFactorAuthMethod.DUO to null, | ||
| TwoFactorAuthMethod.YUBI_KEY to BitwardenDrawable.img_yubi_key, | ||
| TwoFactorAuthMethod.YUBI_KEY to BitwardenDrawable.ill_yubi_key, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. โ Test Coverage Updated The test has been correctly updated to verify that
All tests are passing according to CI, confirming the changes work as expected.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. โ Test Coverage MaintainedTest correctly updated to verify
All tests passing per CI confirms the changes work as expected. |
||
| TwoFactorAuthMethod.U2F to null, | ||
| TwoFactorAuthMethod.REMEMBER to null, | ||
| TwoFactorAuthMethod.DUO_ORGANIZATION to null, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
๐ Good Improvement
The migration from
painterResource(used for raster images) to using it with the new vector drawable is correct. The image is now properly sized at 124.dp, which matches the vector drawable's viewport dimensions and provides consistent sizing with other 2FA method illustrations like the authenticator app icon.The removal of unnecessary layout complexity (previous version had more nested layouts) also improves performance.