Skip to content

Commit

Permalink
[PM-10079] Add keyboard shortcut to autofill identity and credit cards (
Browse files Browse the repository at this point in the history
#10254)

* [BEEEP] Autofill Identity and Card Ciphers From Keyboard Shortcut

* [PM-10079] Add keyboard shortcut to autofill identity and credit card ciphers

* [PM-10079] Fixing jest tests

* [PM-10079] Added an enum for the autofill commands, and adjusted how we filter out cipher types before sorting them by last used when calling for ID and card ciphers

* [PM-10079] Updating copywriting for the autofill settings revolving around keyboard shortcuts

* [PM-10079] Setting a method within CipherService as private
  • Loading branch information
cagonzalezcs authored Jul 31, 2024
1 parent 85c8ff0 commit 86acca3
Show file tree
Hide file tree
Showing 17 changed files with 187 additions and 61 deletions.
21 changes: 15 additions & 6 deletions apps/browser/src/_locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -1343,8 +1343,14 @@
"commandOpenSidebar": {
"message": "Open vault in sidebar"
},
"commandAutofillDesc": {
"message": "Auto-fill the last used login for the current website"
"commandAutofillLoginDesc": {
"message": "Autofill the last used login for the current website"
},
"commandAutofillCardDesc": {
"message": "Autofill the last used card for the current website"
},
"commandAutofillIdentityDesc": {
"message": "Autofill the last used identity for the current website"
},
"commandGeneratePasswordDesc": {
"message": "Generate and copy a new random password to the clipboard"
Expand Down Expand Up @@ -2774,14 +2780,17 @@
"autofillKeyboardShortcutUpdateLabel": {
"message": "Change shortcut"
},
"autofillKeyboardManagerShortcutsLabel": {
"message": "Manage shortcuts"
},
"autofillShortcut": {
"message": "Autofill keyboard shortcut"
},
"autofillShortcutNotSet": {
"message": "The autofill shortcut is not set. Change this in the browser's settings."
"autofillLoginShortcutNotSet": {
"message": "The autofill login shortcut is not set. Change this in the browser's settings."
},
"autofillShortcutText": {
"message": "The autofill shortcut is: $COMMAND$. Change this in the browser's settings.",
"autofillLoginShortcutText": {
"message": "The autofill login shortcut is $COMMAND$. Manage all shortcuts in the browser's settings.",
"placeholders": {
"command": {
"content": "$1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { BehaviorSubject, firstValueFrom } from "rxjs";
import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
import { ExtensionCommand } from "@bitwarden/common/autofill/constants";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { UserNotificationSettingsService } from "@bitwarden/common/autofill/services/user-notification-settings.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
Expand Down Expand Up @@ -151,7 +152,7 @@ describe("NotificationBackground", () => {
const message: NotificationBackgroundExtensionMessage = {
command: "unlockCompleted",
data: {
commandToRetry: { message: { command: "autofill_login" } },
commandToRetry: { message: { command: ExtensionCommand.AutofillLogin } },
} as LockedVaultPendingNotificationsData,
};
jest.spyOn(BrowserApi, "tabSendMessageData").mockImplementation();
Expand Down
15 changes: 12 additions & 3 deletions apps/browser/src/autofill/background/notification.background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { NOTIFICATION_BAR_LIFESPAN_MS } from "@bitwarden/common/autofill/constants";
import {
ExtensionCommand,
ExtensionCommandType,
NOTIFICATION_BAR_LIFESPAN_MS,
} from "@bitwarden/common/autofill/constants";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { UserNotificationSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/user-notification-settings.service";
import { NeverDomains } from "@bitwarden/common/models/domain/domain-service";
Expand Down Expand Up @@ -45,6 +49,11 @@ export default class NotificationBackground {
private openUnlockPopout = openUnlockPopout;
private openAddEditVaultItemPopout = openAddEditVaultItemPopout;
private notificationQueue: NotificationQueueMessageItem[] = [];
private allowedRetryCommands: Set<ExtensionCommandType> = new Set([
ExtensionCommand.AutofillLogin,
ExtensionCommand.AutofillCard,
ExtensionCommand.AutofillIdentity,
]);
private readonly extensionMessageHandlers: NotificationBackgroundExtensionMessageHandlers = {
unlockCompleted: ({ message, sender }) => this.handleUnlockCompleted(message, sender),
bgGetFolderData: () => this.getFolderData(),
Expand Down Expand Up @@ -689,8 +698,8 @@ export default class NotificationBackground {
sender: chrome.runtime.MessageSender,
): Promise<void> {
const messageData = message.data as LockedVaultPendingNotificationsData;
const retryCommand = messageData.commandToRetry.message.command;
if (retryCommand === "autofill_login") {
const retryCommand = messageData.commandToRetry.message.command as ExtensionCommandType;
if (this.allowedRetryCommands.has(retryCommand)) {
await BrowserApi.tabSendMessageData(sender.tab, "closeNotificationBar");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
CREATE_CARD_ID,
CREATE_IDENTITY_ID,
CREATE_LOGIN_ID,
ExtensionCommand,
GENERATE_PASSWORD_ID,
NOOP_COMMAND_SUFFIX,
} from "@bitwarden/common/autofill/constants";
Expand Down Expand Up @@ -79,7 +80,7 @@ export class ContextMenuClickedHandler {
if ((await this.authService.getAuthStatus()) < AuthenticationStatus.Unlocked) {
const retryMessage: LockedVaultPendingNotificationsData = {
commandToRetry: {
message: { command: NOOP_COMMAND_SUFFIX, contextMenuOnClickData: info },
message: { command: ExtensionCommand.NoopCommand, contextMenuOnClickData: info },
sender: { tab: tab },
},
target: "contextmenus.background",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,9 +159,9 @@ export class AutofillV1Component implements OnInit {

private async setAutofillKeyboardHelperText(command: string) {
if (command) {
this.autofillKeyboardHelperText = this.i18nService.t("autofillShortcutText", command);
this.autofillKeyboardHelperText = this.i18nService.t("autofillLoginShortcutText", command);
} else {
this.autofillKeyboardHelperText = this.i18nService.t("autofillShortcutNotSet");
this.autofillKeyboardHelperText = this.i18nService.t("autofillLoginShortcutNotSet");
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ <h2 bitTypography="h6">{{ "autofillKeyboardShortcutSectionTitle" | i18n }}</h2>
</bit-section-header>
<bit-item>
<button bit-item-content type="button" (click)="openURI($event, browserShortcutsURI)">
<h3 bitTypography="h5">{{ "autofillKeyboardShortcutUpdateLabel" | i18n }}</h3>
<h3 bitTypography="h5">{{ "autofillKeyboardManagerShortcutsLabel" | i18n }}</h3>
<bit-hint slot="secondary" class="tw-text-sm tw-whitespace-normal">
{{ autofillKeyboardHelperText }}
</bit-hint>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,9 +215,9 @@ export class AutofillComponent implements OnInit {

private async setAutofillKeyboardHelperText(command: string) {
if (command) {
this.autofillKeyboardHelperText = this.i18nService.t("autofillShortcutText", command);
this.autofillKeyboardHelperText = this.i18nService.t("autofillLoginShortcutText", command);
} else {
this.autofillKeyboardHelperText = this.i18nService.t("autofillShortcutNotSet");
this.autofillKeyboardHelperText = this.i18nService.t("autofillLoginShortcutNotSet");
}
}

Expand Down
38 changes: 16 additions & 22 deletions apps/browser/src/autofill/services/autofill.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1232,22 +1232,21 @@ describe("AutofillService", () => {
jest.spyOn(autofillService as any, "getActiveTab").mockResolvedValueOnce(tab);
jest.spyOn(autofillService, "doAutoFill").mockImplementation();
jest
.spyOn(autofillService["cipherService"], "getAllDecryptedForUrl")
.mockResolvedValueOnce([cardCipher]);
.spyOn(autofillService["cipherService"], "getNextCardCipher")
.mockResolvedValueOnce(cardCipher);

await autofillService.doAutoFillActiveTab(cardFormPageDetails, false, CipherType.Card);
await autofillService.doAutoFillActiveTab(cardFormPageDetails, true, CipherType.Card);

expect(autofillService["cipherService"].getAllDecryptedForUrl).toHaveBeenCalled();
expect(autofillService.doAutoFill).toHaveBeenCalledWith({
tab: tab,
cipher: cardCipher,
pageDetails: cardFormPageDetails,
skipLastUsed: true,
skipUsernameOnlyFill: true,
onlyEmptyFields: true,
onlyVisibleFields: true,
skipLastUsed: false,
skipUsernameOnlyFill: false,
onlyEmptyFields: false,
onlyVisibleFields: false,
fillNewPassword: false,
allowUntrustedIframe: false,
allowUntrustedIframe: true,
allowTotpAutofill: false,
});
});
Expand Down Expand Up @@ -1280,26 +1279,21 @@ describe("AutofillService", () => {
jest.spyOn(autofillService as any, "getActiveTab").mockResolvedValueOnce(tab);
jest.spyOn(autofillService, "doAutoFill").mockImplementation();
jest
.spyOn(autofillService["cipherService"], "getAllDecryptedForUrl")
.mockResolvedValueOnce([identityCipher]);
.spyOn(autofillService["cipherService"], "getNextIdentityCipher")
.mockResolvedValueOnce(identityCipher);

await autofillService.doAutoFillActiveTab(
identityFormPageDetails,
false,
CipherType.Identity,
);
await autofillService.doAutoFillActiveTab(identityFormPageDetails, true, CipherType.Identity);

expect(autofillService["cipherService"].getAllDecryptedForUrl).toHaveBeenCalled();
expect(autofillService.doAutoFill).toHaveBeenCalledWith({
tab: tab,
cipher: identityCipher,
pageDetails: identityFormPageDetails,
skipLastUsed: true,
skipUsernameOnlyFill: true,
onlyEmptyFields: true,
onlyVisibleFields: true,
skipLastUsed: false,
skipUsernameOnlyFill: false,
onlyEmptyFields: false,
onlyVisibleFields: false,
fillNewPassword: false,
allowUntrustedIframe: false,
allowUntrustedIframe: true,
allowTotpAutofill: false,
});
});
Expand Down
32 changes: 26 additions & 6 deletions apps/browser/src/autofill/services/autofill.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -520,16 +520,30 @@ export default class AutofillService implements AutofillServiceInterface {
return await this.doAutoFillOnTab(pageDetails, tab, fromCommand);
}

// Cipher is a non-login type
const cipher: CipherView = (
(await this.cipherService.getAllDecryptedForUrl(tab.url, [cipherType])) || []
).find(({ type }) => type === cipherType);
let cipher: CipherView;
let cacheKey = "";

if (!cipher || cipher.reprompt !== CipherRepromptType.None) {
if (cipherType === CipherType.Card) {
cacheKey = "cardCiphers";
cipher = await this.cipherService.getNextCardCipher();
} else {
cacheKey = "identityCiphers";
cipher = await this.cipherService.getNextIdentityCipher();
}

if (!cipher || !cacheKey || (cipher.reprompt === CipherRepromptType.Password && !fromCommand)) {
return null;
}

return await this.doAutoFill({
if (await this.isPasswordRepromptRequired(cipher, tab)) {
if (fromCommand) {
this.cipherService.updateLastUsedIndexForUrl(cacheKey);
}

return null;
}

const totpCode = await this.doAutoFill({
tab: tab,
cipher: cipher,
pageDetails: pageDetails,
Expand All @@ -541,6 +555,12 @@ export default class AutofillService implements AutofillServiceInterface {
allowUntrustedIframe: fromCommand,
allowTotpAutofill: false,
});

if (fromCommand) {
this.cipherService.updateLastUsedIndexForUrl(cacheKey);
}

return totpCode;
}

/**
Expand Down
36 changes: 30 additions & 6 deletions apps/browser/src/background/commands.background.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { ExtensionCommand, ExtensionCommandType } from "@bitwarden/common/autofill/constants";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";

Expand Down Expand Up @@ -47,8 +48,23 @@ export default class CommandsBackground {
case "generate_password":
await this.generatePasswordToClipboard();
break;
case "autofill_login":
await this.autoFillLogin(sender ? sender.tab : null);
case ExtensionCommand.AutofillLogin:
await this.triggerAutofillCommand(
sender ? sender.tab : null,
ExtensionCommand.AutofillCommand,
);
break;
case ExtensionCommand.AutofillCard:
await this.triggerAutofillCommand(
sender ? sender.tab : null,
ExtensionCommand.AutofillCard,
);
break;
case ExtensionCommand.AutofillIdentity:
await this.triggerAutofillCommand(
sender ? sender.tab : null,
ExtensionCommand.AutofillIdentity,
);
break;
case "open_popup":
await this.openPopup();
Expand All @@ -68,19 +84,27 @@ export default class CommandsBackground {
await this.passwordGenerationService.addHistory(password);
}

private async autoFillLogin(tab?: chrome.tabs.Tab) {
private async triggerAutofillCommand(
tab?: chrome.tabs.Tab,
commandSender?: ExtensionCommandType,
) {
if (!tab) {
tab = await BrowserApi.getTabFromCurrentWindowId();
}

if (tab == null) {
if (tab == null || !commandSender) {
return;
}

if ((await this.authService.getAuthStatus()) < AuthenticationStatus.Unlocked) {
const retryMessage: LockedVaultPendingNotificationsData = {
commandToRetry: {
message: { command: "autofill_login" },
message: {
command:
commandSender === ExtensionCommand.AutofillCommand
? ExtensionCommand.AutofillLogin
: commandSender,
},
sender: { tab: tab },
},
target: "commands.background",
Expand All @@ -95,7 +119,7 @@ export default class CommandsBackground {
return;
}

await this.main.collectPageDetailsForContentScript(tab, "autofill_cmd");
await this.main.collectPageDetailsForContentScript(tab, commandSender);
}

private async openPopup() {
Expand Down
14 changes: 7 additions & 7 deletions apps/browser/src/background/runtime.background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { firstValueFrom, map, mergeMap } from "rxjs";

import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants";
import { AutofillOverlayVisibility, ExtensionCommand } from "@bitwarden/common/autofill/constants";
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
Expand Down Expand Up @@ -117,7 +117,7 @@ export default class RuntimeBackground {
case "collectPageDetailsResponse":
switch (msg.sender) {
case "autofiller":
case "autofill_cmd": {
case ExtensionCommand.AutofillCommand: {
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
Expand All @@ -130,14 +130,14 @@ export default class RuntimeBackground {
details: msg.details,
},
],
msg.sender === "autofill_cmd",
msg.sender === ExtensionCommand.AutofillCommand,
);
if (totpCode != null) {
this.platformUtilsService.copyToClipboard(totpCode);
}
break;
}
case "autofill_card": {
case ExtensionCommand.AutofillCard: {
await this.autofillService.doAutoFillActiveTab(
[
{
Expand All @@ -146,12 +146,12 @@ export default class RuntimeBackground {
details: msg.details,
},
],
false,
msg.sender === ExtensionCommand.AutofillCard,
CipherType.Card,
);
break;
}
case "autofill_identity": {
case ExtensionCommand.AutofillIdentity: {
await this.autofillService.doAutoFillActiveTab(
[
{
Expand All @@ -160,7 +160,7 @@ export default class RuntimeBackground {
details: msg.details,
},
],
false,
msg.sender === ExtensionCommand.AutofillIdentity,
CipherType.Identity,
);
break;
Expand Down
Loading

0 comments on commit 86acca3

Please sign in to comment.