1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-12-18 15:47:57 +01:00

PM-5550 Implement on-page autofil for single line TOTP (#12058)

* PM-5550 initial commit -Initial render
-Edit tests
-Clean up styling
-New method to validate totpfields

* add refresh overlay

* localize and clean up

* - Clean up code
- Remove unnecessary data from buildtotpelement
- Add feature flag
- Add aria labels to buildtotpelement
- Add tests and update relevant snapshots

* Add and translate aria labels

* add aria labels

* implement feature flag

* address totp tests

* clean up totpfield function

* fix styling and tests, update snapshots

* Update apps/browser/src/_locales/en/messages.json

Formatting suggestion

Co-authored-by: Jonathan Prusik <jprusik@users.noreply.github.com>

* Update apps/browser/src/_locales/en/messages.json

Formatting suggestion

Co-authored-by: Jonathan Prusik <jprusik@users.noreply.github.com>

* remove group tag

* update snapshots

* adress feedback

---------

Co-authored-by: Jonathan Prusik <jprusik@users.noreply.github.com>
This commit is contained in:
Daniel Riera 2024-12-13 12:37:16 -05:00 committed by GitHub
parent 649590ad62
commit 6383048197
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 944 additions and 106 deletions

View File

@ -192,6 +192,13 @@
"autoFillIdentity": { "autoFillIdentity": {
"message": "Autofill identity" "message": "Autofill identity"
}, },
"fillVerificationCode": {
"message": "Fill verification code"
},
"fillVerificationCodeAria": {
"message": "Fill Verification Code",
"description": "Aria label for the heading displayed the inline menu for totp code autofill"
},
"generatePasswordCopied": { "generatePasswordCopied": {
"message": "Generate password (copied)" "message": "Generate password (copied)"
}, },
@ -3580,6 +3587,14 @@
"message": "Unlock your account, opens in a new window", "message": "Unlock your account, opens in a new window",
"description": "Screen reader text (aria-label) for unlock account button in overlay" "description": "Screen reader text (aria-label) for unlock account button in overlay"
}, },
"totpCodeAria": {
"message": "Time-based One-Time Password Verification Code",
"description": "Aria label for the totp code displayed in the inline menu for autofill"
},
"totpSecondsSpanAria": {
"message": "Time remaining before current TOTP expires",
"description": "Aria label for the totp seconds displayed in the inline menu for autofill"
},
"fillCredentialsFor": { "fillCredentialsFor": {
"message": "Fill credentials for", "message": "Fill credentials for",
"description": "Screen reader text for when overlay item is in focused" "description": "Screen reader text for when overlay item is in focused"

View File

@ -160,6 +160,9 @@ export type InlineMenuCipherData = {
icon: WebsiteIconData; icon: WebsiteIconData;
accountCreationFieldType?: string; accountCreationFieldType?: string;
login?: { login?: {
totp?: string;
totpField?: boolean;
totpCodeTimeInterval?: number;
username: string; username: string;
passkey: { passkey: {
rpName: string; rpName: string;
@ -262,6 +265,7 @@ export type InlineMenuListPortMessageHandlers = {
updateAutofillInlineMenuListHeight: ({ message, port }: PortOnMessageHandlerParams) => void; updateAutofillInlineMenuListHeight: ({ message, port }: PortOnMessageHandlerParams) => void;
refreshGeneratedPassword: () => Promise<void>; refreshGeneratedPassword: () => Promise<void>;
fillGeneratedPassword: ({ port }: PortConnectionParam) => Promise<void>; fillGeneratedPassword: ({ port }: PortConnectionParam) => Promise<void>;
refreshOverlayCiphers: () => Promise<void>;
}; };
export interface OverlayBackground { export interface OverlayBackground {

View File

@ -928,6 +928,7 @@ describe("OverlayBackground", () => {
login: { login: {
username: "username-1", username: "username-1",
passkey: null, passkey: null,
totpField: false,
}, },
name: "name-1", name: "name-1",
reprompt: loginCipher1.reprompt, reprompt: loginCipher1.reprompt,
@ -1065,6 +1066,7 @@ describe("OverlayBackground", () => {
login: { login: {
username: loginCipher1.login.username, username: loginCipher1.login.username,
passkey: null, passkey: null,
totpField: false,
}, },
name: loginCipher1.name, name: loginCipher1.name,
reprompt: loginCipher1.reprompt, reprompt: loginCipher1.reprompt,
@ -1189,6 +1191,7 @@ describe("OverlayBackground", () => {
rpName: passkeyCipher.login.fido2Credentials[0].rpName, rpName: passkeyCipher.login.fido2Credentials[0].rpName,
userName: passkeyCipher.login.fido2Credentials[0].userName, userName: passkeyCipher.login.fido2Credentials[0].userName,
}, },
totpField: false,
}, },
}, },
{ {
@ -1207,6 +1210,7 @@ describe("OverlayBackground", () => {
login: { login: {
username: passkeyCipher.login.username, username: passkeyCipher.login.username,
passkey: null, passkey: null,
totpField: false,
}, },
}, },
{ {
@ -1225,6 +1229,7 @@ describe("OverlayBackground", () => {
login: { login: {
username: loginCipher1.login.username, username: loginCipher1.login.username,
passkey: null, passkey: null,
totpField: false,
}, },
}, },
], ],
@ -1272,6 +1277,7 @@ describe("OverlayBackground", () => {
login: { login: {
username: passkeyCipher.login.username, username: passkeyCipher.login.username,
passkey: null, passkey: null,
totpField: false,
}, },
}, },
{ {
@ -1290,6 +1296,7 @@ describe("OverlayBackground", () => {
login: { login: {
username: loginCipher1.login.username, username: loginCipher1.login.username,
passkey: null, passkey: null,
totpField: false,
}, },
}, },
], ],
@ -1337,6 +1344,7 @@ describe("OverlayBackground", () => {
login: { login: {
username: passkeyCipher.login.username, username: passkeyCipher.login.username,
passkey: null, passkey: null,
totpField: false,
}, },
}, },
{ {
@ -1355,6 +1363,7 @@ describe("OverlayBackground", () => {
login: { login: {
username: loginCipher1.login.username, username: loginCipher1.login.username,
passkey: null, passkey: null,
totpField: false,
}, },
}, },
], ],
@ -1400,6 +1409,7 @@ describe("OverlayBackground", () => {
login: { login: {
username: loginCipher1.login.username, username: loginCipher1.login.username,
passkey: null, passkey: null,
totpField: false,
}, },
}, },
{ {
@ -1418,6 +1428,7 @@ describe("OverlayBackground", () => {
login: { login: {
username: loginCipher2.login.username, username: loginCipher2.login.username,
passkey: null, passkey: null,
totpField: false,
}, },
}, },
], ],

View File

@ -204,6 +204,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
updateAutofillInlineMenuListHeight: ({ message }) => this.updateInlineMenuListHeight(message), updateAutofillInlineMenuListHeight: ({ message }) => this.updateInlineMenuListHeight(message),
refreshGeneratedPassword: () => this.updateGeneratedPassword(true), refreshGeneratedPassword: () => this.updateGeneratedPassword(true),
fillGeneratedPassword: ({ port }) => this.fillGeneratedPassword(port), fillGeneratedPassword: ({ port }) => this.fillGeneratedPassword(port),
refreshOverlayCiphers: () => this.updateOverlayCiphers(false),
}; };
constructor( constructor(
@ -464,7 +465,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
this.showPasskeysLabelsWithinInlineMenu = false; this.showPasskeysLabelsWithinInlineMenu = false;
if (this.shouldShowInlineMenuAccountCreation()) { if (this.shouldShowInlineMenuAccountCreation()) {
inlineMenuCipherData = this.buildInlineMenuAccountCreationCiphers( inlineMenuCipherData = await this.buildInlineMenuAccountCreationCiphers(
inlineMenuCiphersArray, inlineMenuCiphersArray,
true, true,
); );
@ -485,7 +486,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
* @param inlineMenuCiphersArray - Array of inline menu ciphers * @param inlineMenuCiphersArray - Array of inline menu ciphers
* @param showFavicons - Identifies whether favicons should be shown * @param showFavicons - Identifies whether favicons should be shown
*/ */
private buildInlineMenuAccountCreationCiphers( private async buildInlineMenuAccountCreationCiphers(
inlineMenuCiphersArray: [string, CipherView][], inlineMenuCiphersArray: [string, CipherView][],
showFavicons: boolean, showFavicons: boolean,
) { ) {
@ -497,7 +498,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
if (cipher.type === CipherType.Login) { if (cipher.type === CipherType.Login) {
accountCreationLoginCiphers.push( accountCreationLoginCiphers.push(
this.buildCipherData({ await this.buildCipherData({
inlineMenuCipherId, inlineMenuCipherId,
cipher, cipher,
showFavicons, showFavicons,
@ -517,7 +518,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
} }
inlineMenuCipherData.push( inlineMenuCipherData.push(
this.buildCipherData({ await this.buildCipherData({
inlineMenuCipherId, inlineMenuCipherId,
cipher, cipher,
showFavicons, showFavicons,
@ -561,13 +562,13 @@ export class OverlayBackground implements OverlayBackgroundInterface {
if (!passkeysEnabled || !(await this.showCipherAsPasskey(cipher, domainExclusionsSet))) { if (!passkeysEnabled || !(await this.showCipherAsPasskey(cipher, domainExclusionsSet))) {
inlineMenuCipherData.push( inlineMenuCipherData.push(
this.buildCipherData({ inlineMenuCipherId, cipher, showFavicons }), await this.buildCipherData({ inlineMenuCipherId, cipher, showFavicons }),
); );
continue; continue;
} }
passkeyCipherData.push( passkeyCipherData.push(
this.buildCipherData({ await this.buildCipherData({
inlineMenuCipherId, inlineMenuCipherId,
cipher, cipher,
showFavicons, showFavicons,
@ -577,7 +578,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
if (cipher.login?.password && cipher.login.username) { if (cipher.login?.password && cipher.login.username) {
inlineMenuCipherData.push( inlineMenuCipherData.push(
this.buildCipherData({ inlineMenuCipherId, cipher, showFavicons }), await this.buildCipherData({ inlineMenuCipherId, cipher, showFavicons }),
); );
} }
} }
@ -620,6 +621,23 @@ export class OverlayBackground implements OverlayBackgroundInterface {
return this.inlineMenuFido2Credentials.has(credentialId); return this.inlineMenuFido2Credentials.has(credentialId);
} }
private isTotpFieldForCurrentField(): boolean {
if (!this.focusedFieldData) {
return false;
}
const { tabId, frameId } = this.focusedFieldData;
const pageDetailsMap = this.pageDetailsForTab[tabId];
if (!pageDetailsMap || !pageDetailsMap.has(frameId)) {
return false;
}
const pageDetail = pageDetailsMap.get(frameId);
return (
pageDetail?.details?.fields?.every((field) =>
this.inlineMenuFieldQualificationService.isTotpField(field),
) || false
);
}
/** /**
* Builds the cipher data for the inline menu list. * Builds the cipher data for the inline menu list.
* *
@ -630,14 +648,14 @@ export class OverlayBackground implements OverlayBackgroundInterface {
* @param hasPasskey - Identifies whether the cipher has a FIDO2 credential * @param hasPasskey - Identifies whether the cipher has a FIDO2 credential
* @param identityData - Pre-created identity data * @param identityData - Pre-created identity data
*/ */
private buildCipherData({ private async buildCipherData({
inlineMenuCipherId, inlineMenuCipherId,
cipher, cipher,
showFavicons, showFavicons,
showInlineMenuAccountCreation, showInlineMenuAccountCreation,
hasPasskey, hasPasskey,
identityData, identityData,
}: BuildCipherDataParams): InlineMenuCipherData { }: BuildCipherDataParams): Promise<InlineMenuCipherData> {
const inlineMenuData: InlineMenuCipherData = { const inlineMenuData: InlineMenuCipherData = {
id: inlineMenuCipherId, id: inlineMenuCipherId,
name: cipher.name, name: cipher.name,
@ -649,8 +667,13 @@ export class OverlayBackground implements OverlayBackgroundInterface {
}; };
if (cipher.type === CipherType.Login) { if (cipher.type === CipherType.Login) {
const totpCode = await this.totpService.getCode(cipher.login?.totp);
const totpCodeTimeInterval = this.totpService.getTimeInterval(cipher.login?.totp);
inlineMenuData.login = { inlineMenuData.login = {
username: cipher.login.username, username: cipher.login.username,
totp: totpCode,
totpField: this.isTotpFieldForCurrentField(),
totpCodeTimeInterval: totpCodeTimeInterval,
passkey: hasPasskey passkey: hasPasskey
? { ? {
rpName: cipher.login.fido2Credentials[0].rpName, rpName: cipher.login.fido2Credentials[0].rpName,
@ -1980,35 +2003,39 @@ export class OverlayBackground implements OverlayBackgroundInterface {
private getInlineMenuTranslations() { private getInlineMenuTranslations() {
if (!this.inlineMenuPageTranslations) { if (!this.inlineMenuPageTranslations) {
const translationKeys = [ const translationKeys = [
"addNewCardItemAria",
"addNewIdentityItemAria",
"addNewLoginItemAria",
"addNewVaultItem",
"authenticating",
"cardNumberEndsWith",
"fillCredentialsFor",
"fillGeneratedPassword",
"fillVerificationCode",
"fillVerificationCodeAria",
"generatedPassword",
"lowercaseAriaLabel",
"logInWithPasskeyAriaLabel",
"newCard",
"newIdentity",
"newItem",
"newLogin",
"noItemsToShow",
"opensInANewWindow", "opensInANewWindow",
"passkeys",
"passwordRegenerated",
"passwords",
"regeneratePassword",
"saveLoginToBitwarden",
"toggleBitwardenVaultOverlay", "toggleBitwardenVaultOverlay",
"unlockYourAccountToViewAutofillSuggestions", "totpCodeAria",
"totpSecondsSpanAria",
"unlockAccount", "unlockAccount",
"unlockAccountAria", "unlockAccountAria",
"fillCredentialsFor", "unlockYourAccountToViewAutofillSuggestions",
"uppercaseAriaLabel",
"username", "username",
"view", "view",
"noItemsToShow",
"newItem",
"addNewVaultItem",
"newLogin",
"addNewLoginItemAria",
"newCard",
"addNewCardItemAria",
"newIdentity",
"addNewIdentityItemAria",
"cardNumberEndsWith",
"passkeys",
"passwords",
"logInWithPasskeyAriaLabel",
"authenticating",
"fillGeneratedPassword",
"regeneratePassword",
"passwordRegenerated",
"saveLoginToBitwarden",
"lowercaseAriaLabel",
"uppercaseAriaLabel",
"generatedPassword",
...Object.values(specialCharacterToKeyMap), ...Object.values(specialCharacterToKeyMap),
]; ];
this.inlineMenuPageTranslations = translationKeys.reduce( this.inlineMenuPageTranslations = translationKeys.reduce(

View File

@ -681,10 +681,121 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f
class="cipher-container" class="cipher-container"
> >
<button <button
aria-description="username: username5"
aria-label="fillCredentialsFor website login 5" aria-label="fillCredentialsFor website login 5"
class="fill-cipher-button inline-menu-list-action" class="fill-cipher-button inline-menu-list-action"
tabindex="-1" tabindex="-1"
>
<span
aria-hidden="true"
class="cipher-icon"
>
<div
style="position: relative;"
>
<svg
aria-hidden="true"
viewBox="0 0 29 29"
xmlns="http://www.w3.org/2000/svg"
>
<circle
class="circle-color"
cx="14.5"
cy="14.5"
fill="none"
r="12.5"
stroke-dasharray="78.5"
stroke-dashoffset="78.5"
stroke-width="3"
style="stroke-dashoffset: NaN;"
transform="rotate(-90 14.5 14.5)"
/>
<circle
class="circle-color"
cx="14.5"
cy="14.5"
fill="none"
r="14"
stroke-width="1"
/>
</svg>
<span
aria-label=""
bittypography="helper"
class="totp-sec-span"
>
NaN
</span>
</div>
</span>
<div
class="cipher-details"
>
<span
aria-label=""
class="cipher-name"
/>
<span
aria-label=""
class="cipher-subtitle"
data-testid="totp-code"
>
123 456
</span>
</div>
</button>
<button
aria-label="view website login 5, opensInANewWindow"
class="view-cipher-button"
tabindex="-1"
>
<svg
aria-hidden="true"
fill="none"
height="20"
viewBox="0 0 20 20"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<g
clip-path="url(#a)"
>
<path
d="M16.587 7.932H5.9a.455.455 0 0 1-.31-.12.393.393 0 0 1-.127-.287c0-.108.046-.211.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.076.128.179.128.287a.393.393 0 0 1-.128.288.455.455 0 0 1-.31.119Zm0 2.474H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm0 2.468H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm2.163-8.103v10.457H1.25V4.771h17.5Zm0-1.162H1.25a1.3 1.3 0 0 0-.884.34A1.122 1.122 0 0 0 0 4.772v10.457c0 .308.132.604.366.822a1.3 1.3 0 0 0 .884.34h17.5a1.3 1.3 0 0 0 .884-.34c.234-.218.366-.514.366-.822V4.771c0-.308-.132-.603-.366-.821a1.3 1.3 0 0 0-.884-.34ZM3.213 8.01c.287 0 .52-.217.52-.484s-.234-.483-.52-.483c-.288 0-.52.216-.52.483s.233.483.52.483Zm0 4.903c.287 0 .52-.217.52-.484 0-.266-.234-.483-.52-.483-.287 0-.52.216-.52.483s.233.484.52.484Zm0-2.452c.287 0 .52-.216.52-.483 0-.268-.234-.484-.52-.484-.288 0-.52.216-.52.484 0 .267.233.483.52.483Z"
fill="#175DDC"
/>
</g>
<defs>
<clippath
id="a"
>
<path
d="M0 .113h20v19.773H0z"
fill="#fff"
/>
</clippath>
</defs>
</svg>
</button>
</div>
</li>
<li
class="inline-menu-list-actions-item"
role="listitem"
>
<div
class="cipher-container"
>
<button
aria-description="username: username6"
aria-label="fillCredentialsFor website login 6"
class="fill-cipher-button inline-menu-list-action"
tabindex="-1"
> >
<span <span
aria-hidden="true" aria-hidden="true"
@ -696,18 +807,420 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f
> >
<span <span
class="cipher-name" class="cipher-name"
title="website login 5" title="website login 6"
> >
website login 5 website login 6
</span> </span>
<span <span
class="cipher-subtitle" class="cipher-subtitle"
title="username5" title="username6"
> >
username5 username6
</span> </span>
</span> </span>
</button> </button>
<button
aria-label="view website login 6, opensInANewWindow"
class="view-cipher-button"
tabindex="-1"
>
<svg
aria-hidden="true"
fill="none"
height="20"
viewBox="0 0 20 20"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<g
clip-path="url(#a)"
>
<path
d="M16.587 7.932H5.9a.455.455 0 0 1-.31-.12.393.393 0 0 1-.127-.287c0-.108.046-.211.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.076.128.179.128.287a.393.393 0 0 1-.128.288.455.455 0 0 1-.31.119Zm0 2.474H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm0 2.468H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm2.163-8.103v10.457H1.25V4.771h17.5Zm0-1.162H1.25a1.3 1.3 0 0 0-.884.34A1.122 1.122 0 0 0 0 4.772v10.457c0 .308.132.604.366.822a1.3 1.3 0 0 0 .884.34h17.5a1.3 1.3 0 0 0 .884-.34c.234-.218.366-.514.366-.822V4.771c0-.308-.132-.603-.366-.821a1.3 1.3 0 0 0-.884-.34ZM3.213 8.01c.287 0 .52-.217.52-.484s-.234-.483-.52-.483c-.288 0-.52.216-.52.483s.233.483.52.483Zm0 4.903c.287 0 .52-.217.52-.484 0-.266-.234-.483-.52-.483-.287 0-.52.216-.52.483s.233.484.52.484Zm0-2.452c.287 0 .52-.216.52-.483 0-.268-.234-.484-.52-.484-.288 0-.52.216-.52.484 0 .267.233.483.52.483Z"
fill="#175DDC"
/>
</g>
<defs>
<clippath
id="a"
>
<path
d="M0 .113h20v19.773H0z"
fill="#fff"
/>
</clippath>
</defs>
</svg>
</button>
</div>
</li>
</ul>
</div>
`;
exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers for an authenticated user creates the view for a totp field 1`] = `
<div
class="inline-menu-list-container theme_light"
>
<ul
class="inline-menu-list-actions"
role="list"
>
<li
class="inline-menu-list-actions-item"
role="listitem"
>
<div
class="cipher-container"
>
<button
aria-description="username: username1"
aria-label="fillCredentialsFor website login 1"
class="fill-cipher-button inline-menu-list-action"
tabindex="-1"
>
<span
aria-hidden="true"
class="cipher-icon"
style="background-image: url(https://jest-testing-website.com/image.png);"
/>
<span
class="cipher-details"
>
<span
class="cipher-name"
title="website login 1"
>
website login 1
</span>
<span
class="cipher-subtitle"
title="username1"
>
username1
</span>
</span>
</button>
<button
aria-label="view website login 1, opensInANewWindow"
class="view-cipher-button"
tabindex="-1"
>
<svg
aria-hidden="true"
fill="none"
height="20"
viewBox="0 0 20 20"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<g
clip-path="url(#a)"
>
<path
d="M16.587 7.932H5.9a.455.455 0 0 1-.31-.12.393.393 0 0 1-.127-.287c0-.108.046-.211.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.076.128.179.128.287a.393.393 0 0 1-.128.288.455.455 0 0 1-.31.119Zm0 2.474H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm0 2.468H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm2.163-8.103v10.457H1.25V4.771h17.5Zm0-1.162H1.25a1.3 1.3 0 0 0-.884.34A1.122 1.122 0 0 0 0 4.772v10.457c0 .308.132.604.366.822a1.3 1.3 0 0 0 .884.34h17.5a1.3 1.3 0 0 0 .884-.34c.234-.218.366-.514.366-.822V4.771c0-.308-.132-.603-.366-.821a1.3 1.3 0 0 0-.884-.34ZM3.213 8.01c.287 0 .52-.217.52-.484s-.234-.483-.52-.483c-.288 0-.52.216-.52.483s.233.483.52.483Zm0 4.903c.287 0 .52-.217.52-.484 0-.266-.234-.483-.52-.483-.287 0-.52.216-.52.483s.233.484.52.484Zm0-2.452c.287 0 .52-.216.52-.483 0-.268-.234-.484-.52-.484-.288 0-.52.216-.52.484 0 .267.233.483.52.483Z"
fill="#175DDC"
/>
</g>
<defs>
<clippath
id="a"
>
<path
d="M0 .113h20v19.773H0z"
fill="#fff"
/>
</clippath>
</defs>
</svg>
</button>
</div>
</li>
<li
class="inline-menu-list-actions-item"
role="listitem"
>
<div
class="cipher-container"
>
<button
aria-description="username: username2"
aria-label="fillCredentialsFor website login 2"
class="fill-cipher-button inline-menu-list-action"
tabindex="-1"
>
<span
aria-hidden="true"
class="cipher-icon bwi bw-icon"
/>
<span
class="cipher-details"
>
<span
class="cipher-name"
title="website login 2"
>
website login 2
</span>
<span
class="cipher-subtitle"
title="username2"
>
username2
</span>
</span>
</button>
<button
aria-label="view website login 2, opensInANewWindow"
class="view-cipher-button"
tabindex="-1"
>
<svg
aria-hidden="true"
fill="none"
height="20"
viewBox="0 0 20 20"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<g
clip-path="url(#a)"
>
<path
d="M16.587 7.932H5.9a.455.455 0 0 1-.31-.12.393.393 0 0 1-.127-.287c0-.108.046-.211.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.076.128.179.128.287a.393.393 0 0 1-.128.288.455.455 0 0 1-.31.119Zm0 2.474H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm0 2.468H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm2.163-8.103v10.457H1.25V4.771h17.5Zm0-1.162H1.25a1.3 1.3 0 0 0-.884.34A1.122 1.122 0 0 0 0 4.772v10.457c0 .308.132.604.366.822a1.3 1.3 0 0 0 .884.34h17.5a1.3 1.3 0 0 0 .884-.34c.234-.218.366-.514.366-.822V4.771c0-.308-.132-.603-.366-.821a1.3 1.3 0 0 0-.884-.34ZM3.213 8.01c.287 0 .52-.217.52-.484s-.234-.483-.52-.483c-.288 0-.52.216-.52.483s.233.483.52.483Zm0 4.903c.287 0 .52-.217.52-.484 0-.266-.234-.483-.52-.483-.287 0-.52.216-.52.483s.233.484.52.484Zm0-2.452c.287 0 .52-.216.52-.483 0-.268-.234-.484-.52-.484-.288 0-.52.216-.52.484 0 .267.233.483.52.483Z"
fill="#175DDC"
/>
</g>
<defs>
<clippath
id="a"
>
<path
d="M0 .113h20v19.773H0z"
fill="#fff"
/>
</clippath>
</defs>
</svg>
</button>
</div>
</li>
<li
class="inline-menu-list-actions-item"
role="listitem"
>
<div
class="cipher-container"
>
<button
aria-label="fillCredentialsFor "
class="fill-cipher-button inline-menu-list-action"
tabindex="-1"
>
<span
aria-hidden="true"
class="cipher-icon bwi bw-icon"
/>
<span
class="cipher-details"
/>
</button>
<button
aria-label="view , opensInANewWindow"
class="view-cipher-button"
tabindex="-1"
>
<svg
aria-hidden="true"
fill="none"
height="20"
viewBox="0 0 20 20"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<g
clip-path="url(#a)"
>
<path
d="M16.587 7.932H5.9a.455.455 0 0 1-.31-.12.393.393 0 0 1-.127-.287c0-.108.046-.211.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.076.128.179.128.287a.393.393 0 0 1-.128.288.455.455 0 0 1-.31.119Zm0 2.474H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm0 2.468H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm2.163-8.103v10.457H1.25V4.771h17.5Zm0-1.162H1.25a1.3 1.3 0 0 0-.884.34A1.122 1.122 0 0 0 0 4.772v10.457c0 .308.132.604.366.822a1.3 1.3 0 0 0 .884.34h17.5a1.3 1.3 0 0 0 .884-.34c.234-.218.366-.514.366-.822V4.771c0-.308-.132-.603-.366-.821a1.3 1.3 0 0 0-.884-.34ZM3.213 8.01c.287 0 .52-.217.52-.484s-.234-.483-.52-.483c-.288 0-.52.216-.52.483s.233.483.52.483Zm0 4.903c.287 0 .52-.217.52-.484 0-.266-.234-.483-.52-.483-.287 0-.52.216-.52.483s.233.484.52.484Zm0-2.452c.287 0 .52-.216.52-.483 0-.268-.234-.484-.52-.484-.288 0-.52.216-.52.484 0 .267.233.483.52.483Z"
fill="#175DDC"
/>
</g>
<defs>
<clippath
id="a"
>
<path
d="M0 .113h20v19.773H0z"
fill="#fff"
/>
</clippath>
</defs>
</svg>
</button>
</div>
</li>
<li
class="inline-menu-list-actions-item"
role="listitem"
>
<div
class="cipher-container"
>
<button
aria-description="username: username4"
aria-label="fillCredentialsFor website login 4"
class="fill-cipher-button inline-menu-list-action"
tabindex="-1"
>
<span
aria-hidden="true"
class="cipher-icon"
>
<svg
aria-hidden="true"
fill="none"
height="25"
viewBox="0 0 24 25"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M18.026 17.842c-1.418 1.739-3.517 2.84-5.86 2.84a7.364 7.364 0 0 1-3.431-.848l.062-.15.062-.151.063-.157c.081-.203.17-.426.275-.646.133-.28.275-.522.426-.68.026-.028.101-.075.275-.115.165-.037.376-.059.629-.073.138-.008.288-.014.447-.02.399-.016.847-.034 1.266-.092.314-.044.566-.131.755-.271a.884.884 0 0 0 .352-.555c.037-.2.008-.392-.03-.543-.035-.135-.084-.264-.12-.355l-.01-.03a4.26 4.26 0 0 0-.145-.33c-.126-.264-.237-.497-.288-1.085-.03-.344.09-.73.251-1.138l.089-.22c.05-.123.1-.247.14-.355.064-.171.129-.375.129-.566a1.51 1.51 0 0 0-.134-.569 2.573 2.573 0 0 0-.319-.547c-.246-.323-.635-.669-1.093-.669-.44 0-1.006.169-1.487.368-.246.102-.48.216-.68.33-.192.111-.372.235-.492.359-.93.96-1.48 1.239-1.81 1.258-.277.017-.478-.15-.736-.525a9.738 9.738 0 0 1-.19-.29l-.006-.01a11.568 11.568 0 0 0-.198-.305 2.76 2.76 0 0 0-.521-.6 1.39 1.39 0 0 0-1.088-.314 8.302 8.302 0 0 1 1.987-3.936c.055.342.146.626.272.856.23.42.561.64.926.716.406.086.857-.061 1.26-.216.125-.047.248-.097.372-.147.309-.125.618-.25.947-.341.26-.072.581-.057.959.012.264.049.529.118.8.19l.36.091c.379.094.782.178 1.135.148.374-.032.733-.197.934-.623a.874.874 0 0 0 .024-.752c-.087-.197-.24-.355-.35-.47-.26-.267-.412-.427-.412-.685 0-.125.037-.2.09-.263a.982.982 0 0 1 .303-.211c.059-.03.119-.058.183-.089l.036-.016a3.79 3.79 0 0 0 .236-.118c.047-.026.098-.056.148-.093 1.936.747 3.51 2.287 4.368 4.249a7.739 7.739 0 0 0-.031-.004c-.38-.047-.738-.056-1.063.061-.34.123-.603.368-.817.74-.122.211-.284.43-.463.67l-.095.129c-.207.281-.431.595-.58.92-.15.326-.245.705-.142 1.103.104.397.387.738.837 1.036.099.065.225.112.314.145l.02.008c.108.04.195.074.268.117.07.042.106.08.124.114.017.03.037.087.022.206-.047.376-.069.73-.052 1.034.017.292.071.59.218.809.118.174.12.421.108.786v.01a2.46 2.46 0 0 0 .021.518.809.809 0 0 0 .15.35Zm1.357.059a9.654 9.654 0 0 0 1.62-5.386c0-5.155-3.957-9.334-8.836-9.334-4.88 0-8.836 4.179-8.836 9.334 0 3.495 1.82 6.543 4.513 8.142v.093h.161a8.426 8.426 0 0 0 4.162 1.098c2.953 0 5.568-1.53 7.172-3.882a.569.569 0 0 0 .048-.062l-.004-.003ZM8.152 19.495a43.345 43.345 0 0 1 .098-.238l.057-.142c.082-.205.182-.455.297-.698.143-.301.323-.624.552-.864.163-.172.392-.254.602-.302.219-.05.473-.073.732-.088.162-.01.328-.016.495-.023.386-.015.782-.03 1.168-.084.255-.036.392-.099.461-.15.06-.045.076-.084.083-.12a.534.534 0 0 0-.02-.223 2.552 2.552 0 0 0-.095-.278l-.01-.027a3.128 3.128 0 0 0-.104-.232c-.134-.282-.31-.65-.374-1.381-.046-.533.138-1.063.3-1.472.035-.09.069-.172.1-.249.046-.11.086-.21.123-.31.062-.169.083-.264.083-.312a.812.812 0 0 0-.076-.283 1.867 1.867 0 0 0-.23-.394c-.21-.274-.428-.408-.577-.408-.315 0-.788.13-1.246.32a5.292 5.292 0 0 0-.603.293 1.727 1.727 0 0 0-.347.244c-.936.968-1.641 1.421-2.235 1.457-.646.04-1.036-.413-1.31-.813-.07-.103-.139-.21-.203-.311l-.005-.007c-.064-.101-.125-.197-.188-.29a2.098 2.098 0 0 0-.387-.453.748.748 0 0 0-.436-.18c-.1-.006-.22.005-.365.046a8.707 8.707 0 0 0-.056.992c0 2.957 1.488 5.547 3.716 6.98Zm10.362-2.316.003-.192.002-.046c.01-.305.026-.786-.232-1.169-.036-.054-.082-.189-.096-.444-.014-.243.003-.55.047-.9a1.051 1.051 0 0 0-.105-.649.987.987 0 0 0-.374-.374 2.285 2.285 0 0 0-.367-.166h-.003a1.243 1.243 0 0 1-.205-.088c-.369-.244-.505-.46-.549-.629-.044-.168-.015-.364.099-.61.115-.25.297-.511.507-.796l.089-.12c.178-.239.368-.495.512-.745.152-.263.302-.382.466-.441.18-.065.416-.073.77-.03.142.018.275.04.397.063.274.837.423 1.736.423 2.671a8.45 8.45 0 0 1-1.384 4.665Zm-4.632-12.63a7.362 7.362 0 0 0-1.715-.201c-1.89 0-3.621.716-4.965 1.905.025.54.12.887.24 1.105.13.238.295.34.482.38.2.042.484-.027.905-.188l.328-.13c.32-.13.681-.275 1.048-.377.398-.111.833-.075 1.24 0 .289.053.59.132.871.205l.326.084c.383.094.694.151.932.13.216-.017.326-.092.395-.237.039-.083.027-.114.014-.144-.027-.062-.088-.136-.212-.264l-.043-.044c-.218-.222-.567-.578-.567-1.142 0-.305.101-.547.262-.734.137-.159.308-.267.46-.347Z"
fill="#777"
fill-rule="evenodd"
/>
</svg>
</span>
<span
class="cipher-details"
>
<span
class="cipher-name"
title="website login 4"
>
website login 4
</span>
<span
class="cipher-subtitle"
title="username4"
>
username4
</span>
</span>
</button>
<button
aria-label="view website login 4, opensInANewWindow"
class="view-cipher-button"
tabindex="-1"
>
<svg
aria-hidden="true"
fill="none"
height="20"
viewBox="0 0 20 20"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<g
clip-path="url(#a)"
>
<path
d="M16.587 7.932H5.9a.455.455 0 0 1-.31-.12.393.393 0 0 1-.127-.287c0-.108.046-.211.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.076.128.179.128.287a.393.393 0 0 1-.128.288.455.455 0 0 1-.31.119Zm0 2.474H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm0 2.468H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm2.163-8.103v10.457H1.25V4.771h17.5Zm0-1.162H1.25a1.3 1.3 0 0 0-.884.34A1.122 1.122 0 0 0 0 4.772v10.457c0 .308.132.604.366.822a1.3 1.3 0 0 0 .884.34h17.5a1.3 1.3 0 0 0 .884-.34c.234-.218.366-.514.366-.822V4.771c0-.308-.132-.603-.366-.821a1.3 1.3 0 0 0-.884-.34ZM3.213 8.01c.287 0 .52-.217.52-.484s-.234-.483-.52-.483c-.288 0-.52.216-.52.483s.233.483.52.483Zm0 4.903c.287 0 .52-.217.52-.484 0-.266-.234-.483-.52-.483-.287 0-.52.216-.52.483s.233.484.52.484Zm0-2.452c.287 0 .52-.216.52-.483 0-.268-.234-.484-.52-.484-.288 0-.52.216-.52.484 0 .267.233.483.52.483Z"
fill="#175DDC"
/>
</g>
<defs>
<clippath
id="a"
>
<path
d="M0 .113h20v19.773H0z"
fill="#fff"
/>
</clippath>
</defs>
</svg>
</button>
</div>
</li>
<li
class="inline-menu-list-actions-item"
role="listitem"
>
<div
class="cipher-container"
>
<button
aria-label="fillCredentialsFor website login 5"
class="fill-cipher-button inline-menu-list-action"
tabindex="-1"
>
<span
aria-hidden="true"
class="cipher-icon"
>
<div
style="position: relative;"
>
<svg
aria-hidden="true"
viewBox="0 0 29 29"
xmlns="http://www.w3.org/2000/svg"
>
<circle
class="circle-color"
cx="14.5"
cy="14.5"
fill="none"
r="12.5"
stroke-dasharray="78.5"
stroke-dashoffset="78.5"
stroke-width="3"
style="stroke-dashoffset: NaN;"
transform="rotate(-90 14.5 14.5)"
/>
<circle
class="circle-color"
cx="14.5"
cy="14.5"
fill="none"
r="14"
stroke-width="1"
/>
</svg>
<span
aria-label=""
bittypography="helper"
class="totp-sec-span"
>
NaN
</span>
</div>
</span>
<div
class="cipher-details"
>
<span
aria-label=""
class="cipher-name"
/>
<span
aria-label=""
class="cipher-subtitle"
data-testid="totp-code"
>
123 456
</span>
</div>
</button>
<button <button
aria-label="view website login 5, opensInANewWindow" aria-label="view website login 5, opensInANewWindow"
class="view-cipher-button" class="view-cipher-button"
@ -1112,7 +1625,6 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f
class="cipher-container" class="cipher-container"
> >
<button <button
aria-description="username: username5"
aria-label="fillCredentialsFor website login 5" aria-label="fillCredentialsFor website login 5"
class="fill-cipher-button inline-menu-list-action" class="fill-cipher-button inline-menu-list-action"
tabindex="-1" tabindex="-1"
@ -1120,24 +1632,66 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f
<span <span
aria-hidden="true" aria-hidden="true"
class="cipher-icon" class="cipher-icon"
style="background-image: url(https://jest-testing-website.com/image.png);" >
/> <div
<span style="position: relative;"
>
<svg
aria-hidden="true"
viewBox="0 0 29 29"
xmlns="http://www.w3.org/2000/svg"
>
<circle
class="circle-color"
cx="14.5"
cy="14.5"
fill="none"
r="12.5"
stroke-dasharray="78.5"
stroke-dashoffset="78.5"
stroke-width="3"
style="stroke-dashoffset: NaN;"
transform="rotate(-90 14.5 14.5)"
/>
<circle
class="circle-color"
cx="14.5"
cy="14.5"
fill="none"
r="14"
stroke-width="1"
/>
</svg>
<span
aria-label=""
bittypography="helper"
class="totp-sec-span"
>
NaN
</span>
</div>
</span>
<div
class="cipher-details" class="cipher-details"
> >
<span <span
aria-label=""
class="cipher-name" class="cipher-name"
title="website login 5" />
>
website login 5
</span>
<span <span
aria-label=""
class="cipher-subtitle" class="cipher-subtitle"
title="username5" data-testid="totp-code"
> >
username5 123 456
</span> </span>
</span> </div>
</button> </button>
<button <button
aria-label="view website login 5, opensInANewWindow" aria-label="view website login 5, opensInANewWindow"
@ -1543,7 +2097,6 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f
class="cipher-container" class="cipher-container"
> >
<button <button
aria-description="username: username5"
aria-label="fillCredentialsFor website login 5" aria-label="fillCredentialsFor website login 5"
class="fill-cipher-button inline-menu-list-action" class="fill-cipher-button inline-menu-list-action"
tabindex="-1" tabindex="-1"
@ -1551,24 +2104,66 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f
<span <span
aria-hidden="true" aria-hidden="true"
class="cipher-icon" class="cipher-icon"
style="background-image: url(https://jest-testing-website.com/image.png);" >
/> <div
<span style="position: relative;"
>
<svg
aria-hidden="true"
viewBox="0 0 29 29"
xmlns="http://www.w3.org/2000/svg"
>
<circle
class="circle-color"
cx="14.5"
cy="14.5"
fill="none"
r="12.5"
stroke-dasharray="78.5"
stroke-dashoffset="78.5"
stroke-width="3"
style="stroke-dashoffset: NaN;"
transform="rotate(-90 14.5 14.5)"
/>
<circle
class="circle-color"
cx="14.5"
cy="14.5"
fill="none"
r="14"
stroke-width="1"
/>
</svg>
<span
aria-label=""
bittypography="helper"
class="totp-sec-span"
>
NaN
</span>
</div>
</span>
<div
class="cipher-details" class="cipher-details"
> >
<span <span
aria-label=""
class="cipher-name" class="cipher-name"
title="website login 5" />
>
website login 5
</span>
<span <span
aria-label=""
class="cipher-subtitle" class="cipher-subtitle"
title="username5" data-testid="totp-code"
> >
username5 123 456
</span> </span>
</span> </div>
</button> </button>
<button <button
aria-label="view website login 5, opensInANewWindow" aria-label="view website login 5, opensInANewWindow"

View File

@ -140,6 +140,31 @@ describe("AutofillInlineMenuList", () => {
expect(autofillInlineMenuList["inlineMenuListContainer"]).toMatchSnapshot(); expect(autofillInlineMenuList["inlineMenuListContainer"]).toMatchSnapshot();
}); });
it("creates the view for a totp field", () => {
postWindowMessage(
createInitAutofillInlineMenuListMessageMock({
inlineMenuFillType: CipherType.Login,
ciphers: [
createAutofillOverlayCipherDataMock(5, {
type: CipherType.Login,
login: {
totp: "123456",
totpField: true,
},
}),
],
}),
);
const cipherSubtitleElement = autofillInlineMenuList[
"inlineMenuListContainer"
].querySelector('[data-testid="totp-code"]');
expect(autofillInlineMenuList["inlineMenuListContainer"]).toMatchSnapshot();
expect(cipherSubtitleElement).not.toBeNull();
expect(cipherSubtitleElement.textContent).toBe("123 456");
});
it("creates the views for a list of card ciphers", () => { it("creates the views for a list of card ciphers", () => {
postWindowMessage( postWindowMessage(
createInitAutofillInlineMenuListMessageMock({ createInitAutofillInlineMenuListMessageMock({

View File

@ -1046,6 +1046,64 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
cipherIcon.classList.add("cipher-icon"); cipherIcon.classList.add("cipher-icon");
cipherIcon.setAttribute("aria-hidden", "true"); cipherIcon.setAttribute("aria-hidden", "true");
if (cipher.login?.totpField && cipher.login?.totp) {
const totpContainer = document.createElement("div");
totpContainer.style.position = "relative";
const svgElement = buildSvgDomElement(`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 29 29">
<circle fill="none" cx="14.5" cy="14.5" r="12.5"
stroke-width="3" stroke-dasharray="78.5"
stroke-dashoffset="78.5" transform="rotate(-90 14.5 14.5)"></circle>
<circle fill="none" cx="14.5" cy="14.5" r="14" stroke-width="1"></circle>
</svg>
`);
const [innerCircleElement, outerCircleElement] = svgElement.querySelectorAll("circle");
innerCircleElement.classList.add("circle-color");
outerCircleElement.classList.add("circle-color");
totpContainer.appendChild(svgElement);
const totpSecondsSpan = document.createElement("span");
totpSecondsSpan.classList.add("totp-sec-span");
totpSecondsSpan.setAttribute("bitTypography", "helper");
totpSecondsSpan.setAttribute("aria-label", this.getTranslation("totpSecondsSpanAria"));
totpContainer.appendChild(totpSecondsSpan);
cipherIcon.appendChild(totpContainer);
const intervalSeconds = cipher.login.totpCodeTimeInterval;
const updateCountdown = () => {
const epoch = Math.round(Date.now() / 1000);
const mod = epoch % intervalSeconds;
const totpSeconds = intervalSeconds - mod;
totpSecondsSpan.textContent = `${totpSeconds}`;
/**
* Design specifies a seven-second time span as the period where expiry is approaching.
*/
const totpExpiryApproaching = totpSeconds <= 7;
totpSecondsSpan.classList.toggle("totp-sec-span-danger", totpExpiryApproaching);
innerCircleElement.classList.toggle("circle-danger-color", totpExpiryApproaching);
outerCircleElement.classList.toggle("circle-danger-color", totpExpiryApproaching);
innerCircleElement.style.strokeDashoffset = `${((intervalSeconds - totpSeconds) / intervalSeconds) * (2 * Math.PI * 12.5)}`;
if (mod === 0) {
this.postMessageToParent({ command: "refreshOverlayCiphers" });
}
};
updateCountdown();
setInterval(updateCountdown, 1000);
return cipherIcon;
}
if (cipher.icon?.image) { if (cipher.icon?.image) {
try { try {
const url = new URL(cipher.icon.image); const url = new URL(cipher.icon.image);
@ -1104,6 +1162,9 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
return this.buildPasskeysCipherDetailsElement(cipher, cipherDetailsElement); return this.buildPasskeysCipherDetailsElement(cipher, cipherDetailsElement);
} }
if (cipher.login?.totpField && cipher.login?.totp) {
return this.buildTotpElement(cipher.login?.totp);
}
const subTitleText = this.getSubTitleText(cipher); const subTitleText = this.getSubTitleText(cipher);
const cipherSubtitleElement = this.buildCipherSubtitleElement(subTitleText); const cipherSubtitleElement = this.buildCipherSubtitleElement(subTitleText);
if (cipherSubtitleElement) { if (cipherSubtitleElement) {
@ -1113,6 +1174,38 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
return cipherDetailsElement; return cipherDetailsElement;
} }
/**
* Builds a TOTP element for a given TOTP code.
*
* @param totp - The TOTP code to display.
*/
private buildTotpElement(totpCode: string): HTMLDivElement | null {
if (!totpCode) {
return null;
}
const formattedTotpCode = `${totpCode.substring(0, 3)} ${totpCode.substring(3)}`;
const containerElement = globalThis.document.createElement("div");
containerElement.classList.add("cipher-details");
const totpHeading = document.createElement("span");
totpHeading.classList.add("cipher-name");
totpHeading.textContent = this.getTranslation("fillVerificationCode");
totpHeading.setAttribute("aria-label", this.getTranslation("fillVerificationCodeAria"));
containerElement.appendChild(totpHeading);
const subtitleElement = document.createElement("span");
subtitleElement.classList.add("cipher-subtitle");
subtitleElement.textContent = formattedTotpCode;
subtitleElement.setAttribute("aria-label", this.getTranslation("totpCodeAria"));
subtitleElement.setAttribute("data-testid", "totp-code");
containerElement.appendChild(subtitleElement);
return containerElement;
}
/** /**
* Builds the name element for a given cipher. * Builds the name element for a given cipher.
* *

View File

@ -350,6 +350,32 @@ body * {
text-align: left; text-align: left;
} }
.totp-sec-span {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
@include themify($themes) {
color: themed("textColor");
}
}
.totp-sec-span-danger {
@include themify($themes) {
color: themed("passwordSpecialColor");
}
}
.circle-color {
@include themify($themes) {
stroke: themed("primaryColor");
}
}
.circle-danger-color {
@include themify($themes) {
stroke: themed("passwordSpecialColor");
}
}
.cipher-name, .cipher-name,
.cipher-subtitle { .cipher-subtitle {
display: block; display: block;

View File

@ -45,4 +45,5 @@ export interface InlineMenuFieldQualificationService {
isFieldForIdentityUsername(field: AutofillField): boolean; isFieldForIdentityUsername(field: AutofillField): boolean;
isElementLoginSubmitButton(element: Element): boolean; isElementLoginSubmitButton(element: Element): boolean;
isElementChangePasswordSubmitButton(element: Element): boolean; isElementChangePasswordSubmitButton(element: Element): boolean;
isTotpField(field: AutofillField): boolean;
} }

View File

@ -21,7 +21,23 @@ describe("InlineMenuFieldQualificationService", () => {
}); });
describe("isFieldForLoginForm", () => { describe("isFieldForLoginForm", () => {
it("disqualifies totp fields", () => { it("does not disqualify totp fields with flag set to true", () => {
inlineMenuFieldQualificationService["inlineMenuTotpFeatureFlag"] = true;
const field = mock<AutofillField>({
type: "text",
autoCompleteType: "one-time-code",
htmlName: "totp",
htmlID: "totp",
placeholder: "totp",
});
expect(inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails)).toBe(
true,
);
});
it("disqualify totp fields with flag set to false", () => {
inlineMenuFieldQualificationService["inlineMenuTotpFeatureFlag"] = false;
const field = mock<AutofillField>({ const field = mock<AutofillField>({
type: "text", type: "text",
autoCompleteType: "one-time-code", autoCompleteType: "one-time-code",

View File

@ -150,12 +150,16 @@ export class InlineMenuFieldQualificationService
]); ]);
private totpFieldAutocompleteValue = "one-time-code"; private totpFieldAutocompleteValue = "one-time-code";
private inlineMenuFieldQualificationFlagSet = false; private inlineMenuFieldQualificationFlagSet = false;
private inlineMenuTotpFeatureFlag = false;
constructor() { constructor() {
void sendExtensionMessage("getInlineMenuFieldQualificationFeatureFlag").then( void Promise.all([
(getInlineMenuFieldQualificationFlag) => sendExtensionMessage("getInlineMenuFieldQualificationFeatureFlag"),
(this.inlineMenuFieldQualificationFlagSet = !!getInlineMenuFieldQualificationFlag?.result), sendExtensionMessage("getInlineMenuTotpFeatureFlag"),
); ]).then(([fieldQualificationFlag, totpFeatureFlag]) => {
this.inlineMenuFieldQualificationFlagSet = !!fieldQualificationFlag?.result;
this.inlineMenuTotpFeatureFlag = !!totpFeatureFlag?.result;
});
} }
/** /**
@ -169,8 +173,15 @@ export class InlineMenuFieldQualificationService
return this.isFieldForLoginFormFallback(field); return this.isFieldForLoginFormFallback(field);
} }
if (this.isTotpField(field)) { /**
return false; * Autofill does not fill password type totp input fields
*/
if (this.inlineMenuTotpFeatureFlag) {
const isTotpField = this.isTotpField(field);
const passwordType = field.type === "password";
if (isTotpField && !passwordType) {
return true;
}
} }
const isCurrentPasswordField = this.isCurrentPasswordField(field); const isCurrentPasswordField = this.isCurrentPasswordField(field);
@ -987,7 +998,7 @@ export class InlineMenuFieldQualificationService
* *
* @param field - The field to validate * @param field - The field to validate
*/ */
private isTotpField = (field: AutofillField): boolean => { isTotpField = (field: AutofillField): boolean => {
if (this.fieldContainsAutocompleteValues(field, this.totpFieldAutocompleteValue)) { if (this.fieldContainsAutocompleteValues(field, this.totpFieldAutocompleteValue)) {
return true; return true;
} }

View File

@ -5,20 +5,34 @@ $dark-icon-themes: "theme_dark", "theme_solarizedDark", "theme_nord";
$font-family-sans-serif: "DM Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; $font-family-sans-serif: "DM Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
$font-family-source-code-pro: "Source Code Pro", monospace; $font-family-source-code-pro: "Source Code Pro", monospace;
$font-size-base: 14px; $font-size-base: 14px;
$text-color: #212529;
$muted-text-color: #6c747c; $text-color-light: #212529;
$border-color: #ced4dc; $muted-text-color-light: #6c747c;
$background-color-light: #ffffff;
$background-offset-color-light: #f0f0f0;
$brand-primary-light: #175ddc;
$password-special-color-light: #b80017;
$password-number-color-light: #1452c1;
$success-color-light: #017e45;
$error-color-light: #c83522;
$text-color-dark: #ffffff;
$muted-text-color-dark: #bac0ce;
$background-color-dark: #2f343d;
$background-offset-color-dark: darken(#2f343d, 2.75%);
$border-color-dark: #ddd; $border-color-dark: #ddd;
$brand-primary-dark: #6f9df1;
$password-special-color-dark: #ff8d85;
$password-number-color-dark: #6f9df1;
$success-color-dark: #8db89b;
$error-color-dark: #ee9792;
$muted-blue: #5a6d91;
$muted-grey: #bac0ce;
$border-color: #ced4dc;
$border-radius: 3px; $border-radius: 3px;
$focus-outline-color: #1252a3; $focus-outline-color: #1252a3;
$muted-blue: #5a6d91;
$password-special-color: #b80017;
$password-number-color: #1452c1;
$brand-primary: #175ddc;
$background-color: #ffffff;
$background-offset-color: #f0f0f0;
$solarizedDarkBase0: #839496; $solarizedDarkBase0: #839496;
$solarizedDarkBase03: #002b36; $solarizedDarkBase03: #002b36;
@ -28,48 +42,42 @@ $solarizedDarkBase2: #eee8d5;
$solarizedDarkCyan: #2aa198; $solarizedDarkCyan: #2aa198;
$solarizedDarkGreen: #859900; $solarizedDarkGreen: #859900;
$success-color-light: #017e45;
$success-color-dark: #8db89b;
$error-color-light: #c83522;
$error-color-dark: #ee9792;
$themes: ( $themes: (
light: ( light: (
textColor: $text-color, textColor: $text-color-light,
mutedTextColor: $muted-text-color, mutedTextColor: $muted-text-color-light,
backgroundColor: $background-color, backgroundColor: $background-color-light,
backgroundOffsetColor: $background-offset-color, backgroundOffsetColor: $background-offset-color-light,
primaryColor: $brand-primary, primaryColor: $brand-primary-light,
buttonPrimaryColor: $brand-primary, buttonPrimaryColor: $brand-primary-light,
textContrast: $background-color, textContrast: $background-color-light,
inputBorderColor: darken($border-color-dark, 2.75%), inputBorderColor: darken($border-color-dark, 2.75%),
inputBackgroundColor: #ffffff, inputBackgroundColor: $background-color-light,
borderColor: $border-color, borderColor: $border-color,
focusOutlineColor: $focus-outline-color, focusOutlineColor: $focus-outline-color,
successColor: $success-color-light, successColor: $success-color-light,
errorColor: $error-color-light, errorColor: $error-color-light,
passkeysAuthenticating: $muted-blue, passkeysAuthenticating: $muted-blue,
passwordSpecialColor: $password-special-color, passwordSpecialColor: $password-special-color-light,
passwordNumberColor: $password-number-color, passwordNumberColor: $password-number-color-light,
), ),
dark: ( dark: (
textColor: #ffffff, textColor: $text-color-dark,
mutedTextColor: #bac0ce, mutedTextColor: $muted-text-color-dark,
backgroundColor: #2f343d, backgroundColor: $background-color-dark,
backgroundOffsetColor: darken(#2f343d, 2.75%), backgroundOffsetColor: $background-offset-color-dark,
buttonPrimaryColor: #6f9df1, buttonPrimaryColor: $brand-primary-dark,
primaryColor: #6f9df1, primaryColor: $brand-primary-dark,
textContrast: #2f343d, textContrast: $background-color-dark,
inputBorderColor: #4c525f, inputBorderColor: #4c525f,
inputBackgroundColor: #2f343d, inputBackgroundColor: $background-color-dark,
borderColor: #4c525f, borderColor: #4c525f,
focusOutlineColor: lighten($focus-outline-color, 25%), focusOutlineColor: lighten($focus-outline-color, 25%),
successColor: $success-color-dark, successColor: $success-color-dark,
errorColor: $error-color-dark, errorColor: $error-color-dark,
passkeysAuthenticating: #bac0ce, passkeysAuthenticating: $muted-grey,
passwordSpecialColor: #ff8d85, passwordSpecialColor: $password-special-color-dark,
passwordNumberColor: #6f9df1, passwordNumberColor: $password-number-color-dark,
), ),
nord: ( nord: (
textColor: $nord5, textColor: $nord5,

View File

@ -241,7 +241,7 @@ export function createInitAutofillInlineMenuListMessageMock(
createAutofillOverlayCipherDataMock(4, { createAutofillOverlayCipherDataMock(4, {
icon: { imageEnabled: false, image: "", fallbackImage: "", icon: "" }, icon: { imageEnabled: false, image: "", fallbackImage: "", icon: "" },
}), }),
createAutofillOverlayCipherDataMock(5), createAutofillOverlayCipherDataMock(5, { login: { totp: "123456", totpField: true } }),
createAutofillOverlayCipherDataMock(6), createAutofillOverlayCipherDataMock(6),
createAutofillOverlayCipherDataMock(7), createAutofillOverlayCipherDataMock(7),
createAutofillOverlayCipherDataMock(8), createAutofillOverlayCipherDataMock(8),

View File

@ -73,6 +73,7 @@ export default class RuntimeBackground {
"biometricUnlockAvailable", "biometricUnlockAvailable",
"getUseTreeWalkerApiForPageDetailsCollectionFeatureFlag", "getUseTreeWalkerApiForPageDetailsCollectionFeatureFlag",
"getInlineMenuFieldQualificationFeatureFlag", "getInlineMenuFieldQualificationFeatureFlag",
"getInlineMenuTotpFeatureFlag",
]; ];
if (messagesWithResponse.includes(msg.command)) { if (messagesWithResponse.includes(msg.command)) {
@ -197,6 +198,9 @@ export default class RuntimeBackground {
case "getInlineMenuFieldQualificationFeatureFlag": { case "getInlineMenuFieldQualificationFeatureFlag": {
return await this.configService.getFeatureFlag(FeatureFlag.InlineMenuFieldQualification); return await this.configService.getFeatureFlag(FeatureFlag.InlineMenuFieldQualification);
} }
case "getInlineMenuTotpFeatureFlag": {
return await this.configService.getFeatureFlag(FeatureFlag.InlineMenuTotp);
}
} }
} }

View File

@ -38,6 +38,7 @@ export enum FeatureFlag {
NewDeviceVerificationTemporaryDismiss = "new-device-temporary-dismiss", NewDeviceVerificationTemporaryDismiss = "new-device-temporary-dismiss",
NewDeviceVerificationPermanentDismiss = "new-device-permanent-dismiss", NewDeviceVerificationPermanentDismiss = "new-device-permanent-dismiss",
DisableFreeFamiliesSponsorship = "PM-12274-disable-free-families-sponsorship", DisableFreeFamiliesSponsorship = "PM-12274-disable-free-families-sponsorship",
InlineMenuTotp = "inline-menu-totp",
MacOsNativeCredentialSync = "macos-native-credential-sync", MacOsNativeCredentialSync = "macos-native-credential-sync",
PM11360RemoveProviderExportPermission = "pm-11360-remove-provider-export-permission", PM11360RemoveProviderExportPermission = "pm-11360-remove-provider-export-permission",
PM12443RemovePagingLogic = "pm-12443-remove-paging-logic", PM12443RemovePagingLogic = "pm-12443-remove-paging-logic",
@ -89,6 +90,7 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.NewDeviceVerificationTemporaryDismiss]: FALSE, [FeatureFlag.NewDeviceVerificationTemporaryDismiss]: FALSE,
[FeatureFlag.NewDeviceVerificationPermanentDismiss]: FALSE, [FeatureFlag.NewDeviceVerificationPermanentDismiss]: FALSE,
[FeatureFlag.DisableFreeFamiliesSponsorship]: FALSE, [FeatureFlag.DisableFreeFamiliesSponsorship]: FALSE,
[FeatureFlag.InlineMenuTotp]: FALSE,
[FeatureFlag.MacOsNativeCredentialSync]: FALSE, [FeatureFlag.MacOsNativeCredentialSync]: FALSE,
[FeatureFlag.PM11360RemoveProviderExportPermission]: FALSE, [FeatureFlag.PM11360RemoveProviderExportPermission]: FALSE,
[FeatureFlag.PM12443RemovePagingLogic]: FALSE, [FeatureFlag.PM12443RemovePagingLogic]: FALSE,