mirror of
https://github.com/bitwarden/android.git
synced 2026-05-10 16:45:43 -05:00
Compare commits
2 Commits
cron-sync-
...
BWA-99/sho
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1337f1f432 | ||
|
|
03849f38da |
@@ -106,16 +106,21 @@ class TotpCodeManagerImpl @Inject constructor(
|
||||
authenticatorSdkSource
|
||||
.generateTotp(item.otpUri, dateTime)
|
||||
.onSuccess { response ->
|
||||
val period = response.period.toInt()
|
||||
val nextCode = authenticatorSdkSource
|
||||
.generateTotp(item.otpUri, dateTime.plusSeconds(period.toLong()))
|
||||
.getOrNull()
|
||||
?.code
|
||||
verificationCodeItem = VerificationCodeItem(
|
||||
code = response.code,
|
||||
periodSeconds = response.period.toInt(),
|
||||
timeLeftSeconds = response.period.toInt() -
|
||||
(time % response.period.toInt()),
|
||||
periodSeconds = period,
|
||||
timeLeftSeconds = period - (time % period),
|
||||
issueTime = clock.millis(),
|
||||
id = item.cipherId,
|
||||
issuer = item.issuer,
|
||||
label = item.label,
|
||||
source = item.source,
|
||||
nextCode = nextCode,
|
||||
)
|
||||
}
|
||||
.onFailure {
|
||||
|
||||
@@ -12,6 +12,8 @@ import com.bitwarden.authenticator.data.authenticator.repository.model.Authentic
|
||||
* @property issueTime The time the verification code was issued.
|
||||
* @property id The cipher id of the item.
|
||||
* @property username The username associated with the item.
|
||||
* @property nextCode The next verification code that will become active after the current code
|
||||
* expires, or null if not yet computed.
|
||||
*/
|
||||
data class VerificationCodeItem(
|
||||
val code: String,
|
||||
@@ -22,6 +24,7 @@ data class VerificationCodeItem(
|
||||
val issuer: String?,
|
||||
val label: String?,
|
||||
val source: AuthenticatorItem.Source,
|
||||
val nextCode: String? = null,
|
||||
) {
|
||||
/**
|
||||
* The composite label of the authenticator item. Used for constructing an OTPAuth URI.
|
||||
|
||||
@@ -25,6 +25,7 @@ fun VerificationCodeItem.toDisplayItem(
|
||||
periodSeconds = periodSeconds,
|
||||
alertThresholdSeconds = alertThresholdSeconds,
|
||||
authCode = code,
|
||||
nextAuthCode = nextCode,
|
||||
showOverflow = showOverflow,
|
||||
favorite = (source as? AuthenticatorItem.Source.Local)?.isFavorite ?: false,
|
||||
showMoveToBitwarden = when (source) {
|
||||
|
||||
@@ -33,7 +33,7 @@ import com.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
/**
|
||||
* The verification code item displayed to the user.
|
||||
*
|
||||
* @param displayItem he model containing all relevant data to be displayed.
|
||||
* @param displayItem The model containing all relevant data to be displayed.
|
||||
* @param onItemClick The lambda function to be invoked when the item is clicked.
|
||||
* @param onDropdownMenuClick A lambda function invoked when a dropdown menu action is clicked.
|
||||
* @param cardStyle The card style to be applied to this item.
|
||||
@@ -61,9 +61,12 @@ fun VaultVerificationCodeItem(
|
||||
showMoveToBitwarden = displayItem.showMoveToBitwarden,
|
||||
cardStyle = cardStyle,
|
||||
modifier = modifier,
|
||||
nextAuthCode = displayItem.nextAuthCode,
|
||||
)
|
||||
}
|
||||
|
||||
private const val NEXT_CODE_THRESHOLD_SECONDS = 5
|
||||
|
||||
/**
|
||||
* The verification code item displayed to the user.
|
||||
*
|
||||
@@ -80,6 +83,8 @@ fun VaultVerificationCodeItem(
|
||||
* @param showMoveToBitwarden Whether the option to move the item to Bitwarden is displayed.
|
||||
* @param cardStyle The card style to be applied to this item.
|
||||
* @param modifier The modifier for the item.
|
||||
* @param nextAuthCode The next verification code to display when the current code is near expiry,
|
||||
* or null if not yet available.
|
||||
*/
|
||||
@Suppress("LongMethod", "MagicNumber")
|
||||
@Composable
|
||||
@@ -97,6 +102,7 @@ fun VaultVerificationCodeItem(
|
||||
showMoveToBitwarden: Boolean,
|
||||
cardStyle: CardStyle,
|
||||
modifier: Modifier = Modifier,
|
||||
nextAuthCode: String? = null,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
@@ -145,21 +151,40 @@ fun VaultVerificationCodeItem(
|
||||
}
|
||||
}
|
||||
|
||||
BitwardenCircularCountdownIndicator(
|
||||
modifier = Modifier.testTag(tag = "CircularCountDown"),
|
||||
timeLeftSeconds = timeLeftSeconds,
|
||||
periodSeconds = periodSeconds,
|
||||
alertThresholdSeconds = alertThresholdSeconds,
|
||||
)
|
||||
Column(horizontalAlignment = Alignment.End) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(space = 16.dp),
|
||||
) {
|
||||
BitwardenCircularCountdownIndicator(
|
||||
modifier = Modifier.testTag(tag = "CircularCountDown"),
|
||||
timeLeftSeconds = timeLeftSeconds,
|
||||
periodSeconds = periodSeconds,
|
||||
alertThresholdSeconds = alertThresholdSeconds,
|
||||
)
|
||||
|
||||
Text(
|
||||
modifier = Modifier.testTag(tag = "AuthCode"),
|
||||
text = authCode
|
||||
.chunked(size = 3) { it.padEnd(length = 3, padChar = ' ') }
|
||||
.joinToString(separator = " "),
|
||||
style = BitwardenTheme.typography.sensitiveInfoSmall,
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.testTag(tag = "AuthCode"),
|
||||
text = authCode
|
||||
.chunked(size = 3) { it.padEnd(length = 3, padChar = ' ') }
|
||||
.joinToString(separator = " "),
|
||||
style = BitwardenTheme.typography.sensitiveInfoSmall,
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
)
|
||||
}
|
||||
|
||||
if (nextAuthCode != null && timeLeftSeconds <= NEXT_CODE_THRESHOLD_SECONDS) {
|
||||
Text(
|
||||
modifier = Modifier.testTag(tag = "NextAuthCode"),
|
||||
text = stringResource(
|
||||
id = BitwardenString.next_verification_code,
|
||||
nextAuthCode.chunked(size = 3).joinToString(separator = " "),
|
||||
),
|
||||
style = BitwardenTheme.typography.bodySmall,
|
||||
color = BitwardenTheme.colorScheme.text.secondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (showOverflow) {
|
||||
BitwardenOverflowActionItem(
|
||||
@@ -210,7 +235,7 @@ private fun VerificationCodeItem_preview() {
|
||||
primaryLabel = "Issuer, AKA Name",
|
||||
secondaryLabel = "username@bitwarden.com",
|
||||
periodSeconds = 30,
|
||||
timeLeftSeconds = 15,
|
||||
timeLeftSeconds = 3,
|
||||
alertThresholdSeconds = 7,
|
||||
startIcon = IconData.Local(BitwardenDrawable.ic_login_item),
|
||||
onItemClick = {},
|
||||
@@ -219,6 +244,7 @@ private fun VerificationCodeItem_preview() {
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
showMoveToBitwarden = true,
|
||||
cardStyle = CardStyle.Full,
|
||||
nextAuthCode = "987654321",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,4 +24,5 @@ data class VerificationCodeDisplayItem(
|
||||
val favorite: Boolean,
|
||||
val showOverflow: Boolean,
|
||||
val showMoveToBitwarden: Boolean,
|
||||
val nextAuthCode: String? = null,
|
||||
) : Parcelable
|
||||
|
||||
@@ -48,16 +48,56 @@ class TotpCodeManagerTest {
|
||||
createMockAuthenticatorItem(number = 1, otpUri = totp),
|
||||
)
|
||||
val code = "123456"
|
||||
val totpResponse = TotpResponse(code = code, period = 30u)
|
||||
val nextCode = "789012"
|
||||
val period = 30u
|
||||
coEvery {
|
||||
authenticatorSdkSource.generateTotp(totp = totp, time = clock.instant())
|
||||
} returns totpResponse.asSuccess()
|
||||
} returns TotpResponse(code = code, period = period).asSuccess()
|
||||
coEvery {
|
||||
authenticatorSdkSource.generateTotp(
|
||||
totp = totp,
|
||||
time = clock.instant().plusSeconds(period.toLong()),
|
||||
)
|
||||
} returns TotpResponse(code = nextCode, period = period).asSuccess()
|
||||
|
||||
val expected = createMockVerificationCodeItem(
|
||||
number = 1,
|
||||
code = code,
|
||||
issueTime = clock.instant().toEpochMilli(),
|
||||
timeLeftSeconds = 30,
|
||||
nextCode = nextCode,
|
||||
)
|
||||
|
||||
manager.getTotpCodesFlow(authenticatorItems).test {
|
||||
assertEquals(listOf(expected), awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getTotpCodesFlow should emit item with null nextCode if next code generation fails`() =
|
||||
runTest {
|
||||
val totp = "otpUri"
|
||||
val authenticatorItems = listOf(
|
||||
createMockAuthenticatorItem(number = 1, otpUri = totp),
|
||||
)
|
||||
val code = "123456"
|
||||
val period = 30u
|
||||
coEvery {
|
||||
authenticatorSdkSource.generateTotp(totp = totp, time = clock.instant())
|
||||
} returns TotpResponse(code = code, period = period).asSuccess()
|
||||
coEvery {
|
||||
authenticatorSdkSource.generateTotp(
|
||||
totp = totp,
|
||||
time = clock.instant().plusSeconds(period.toLong()),
|
||||
)
|
||||
} returns Exception().asFailure()
|
||||
|
||||
val expected = createMockVerificationCodeItem(
|
||||
number = 1,
|
||||
code = code,
|
||||
issueTime = clock.instant().toEpochMilli(),
|
||||
timeLeftSeconds = 30,
|
||||
nextCode = null,
|
||||
)
|
||||
|
||||
manager.getTotpCodesFlow(authenticatorItems).test {
|
||||
|
||||
@@ -43,6 +43,7 @@ fun createMockVerificationCodeItem(
|
||||
label: String = "mockLabel-$number",
|
||||
issuer: String = "mockIssuer-$number",
|
||||
source: AuthenticatorItem.Source = createMockLocalAuthenticatorItemSource(),
|
||||
nextCode: String? = null,
|
||||
): VerificationCodeItem =
|
||||
VerificationCodeItem(
|
||||
code = code,
|
||||
@@ -53,6 +54,7 @@ fun createMockVerificationCodeItem(
|
||||
label = label,
|
||||
issuer = issuer,
|
||||
source = source,
|
||||
nextCode = nextCode,
|
||||
)
|
||||
|
||||
/**
|
||||
|
||||
@@ -197,6 +197,7 @@
|
||||
<string name="point_your_camera_at_the_qr_code">Point your camera at the QR Code.
|
||||
Scanning will happen automatically.</string>
|
||||
<string name="copy_totp">Copy TOTP</string>
|
||||
<string name="next_verification_code">Next: %1$s</string>
|
||||
<string name="copy_totp_automatically_description">If a login has an authenticator key, copy the TOTP verification code to your clipboard when you autofill the login.</string>
|
||||
<string name="copy_totp_automatically">Copy TOTP automatically</string>
|
||||
<string name="premium_required">A Premium membership is required to use this feature.</string>
|
||||
|
||||
Reference in New Issue
Block a user