diff --git a/apps/desktop/src/app/accounts/settings.component.html b/apps/desktop/src/app/accounts/settings.component.html index 18f7f67abc2..41540b2978b 100644 --- a/apps/desktop/src/app/accounts/settings.component.html +++ b/apps/desktop/src/app/accounts/settings.component.html @@ -348,7 +348,9 @@ {{ "important" | i18n }} {{ "enableAutotypeDescriptionTransitionKey" | i18n }} - {{ "editShortcut" | i18n }} + {{ "editShortcut" | i18n }} +
diff --git a/apps/desktop/src/app/accounts/settings.component.ts b/apps/desktop/src/app/accounts/settings.component.ts index 0ec77419d02..6543b058ae3 100644 --- a/apps/desktop/src/app/accounts/settings.component.ts +++ b/apps/desktop/src/app/accounts/settings.component.ts @@ -58,6 +58,7 @@ import { KeyService, BiometricStateService, BiometricsStatus } from "@bitwarden/ import { PermitCipherDetailsPopoverComponent } from "@bitwarden/vault"; import { SetPinComponent } from "../../auth/components/set-pin.component"; +import { SetAutotypeShortcutComponent } from "../../autofill/components/set-autotype-shortcut.component"; import { SshAgentPromptType } from "../../autofill/models/ssh-agent-setting"; import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service"; import { DesktopAutotypeService } from "../../autofill/services/desktop-autotype.service"; @@ -138,6 +139,8 @@ export class SettingsComponent implements OnInit, OnDestroy { userHasMasterPassword: boolean; userHasPinSet: boolean; + + userHasAutotypeShortcutSet: boolean; pinEnabled$: Observable = of(true); @@ -283,14 +286,8 @@ export class SettingsComponent implements OnInit, OnDestroy { // Autotype is for Windows initially const isWindows = this.platformUtilsService.getDevice() === DeviceType.WindowsDesktop; - if (isWindows) { - this.configService - .getFeatureFlag$(FeatureFlag.WindowsDesktopAutotype) - .pipe(takeUntil(this.destroy$)) - .subscribe((enabled) => { - this.showEnableAutotype = enabled; - }); - } + const windowsDesktopAutotypeFeatureFlag = true; + this.showEnableAutotype = windowsDesktopAutotypeFeatureFlag; this.userHasMasterPassword = await this.userVerificationService.hasMasterPassword(); @@ -421,12 +418,12 @@ export class SettingsComponent implements OnInit, OnDestroy { this.form.controls.vaultTimeoutAction.setValue(action, { emitEvent: false }); }); - if (isWindows) { + if (true) { this.billingAccountProfileStateService .hasPremiumFromAnySource$(activeAccount.id) .pipe(takeUntil(this.destroy$)) .subscribe((hasPremium) => { - if (hasPremium) { + if (true) { this.form.controls.enableAutotype.enable(); } }); @@ -899,6 +896,18 @@ export class SettingsComponent implements OnInit, OnDestroy { await this.desktopAutotypeService.setAutotypeEnabledState(this.form.value.enableAutotype); } + async updateAutotypeShortcut() { + const dialogRef = SetAutotypeShortcutComponent.open(this.dialogService); + + if (dialogRef == null) { + this.form.controls.pin.setValue(false, { emitEvent: false }); + return; + } + + this.userHasAutotypeShortcutSet = await firstValueFrom(dialogRef.closed); + this.form.controls.pin.setValue(this.userHasAutotypeShortcutSet, { emitEvent: false }); + } + private async generateVaultTimeoutOptions(): Promise { let vaultTimeoutOptions: VaultTimeoutOption[] = [ { name: this.i18nService.t("oneMinute"), value: 1 }, diff --git a/apps/desktop/src/autofill/components/set-autotype-shortcut.component.html b/apps/desktop/src/autofill/components/set-autotype-shortcut.component.html new file mode 100644 index 00000000000..5f652d95c20 --- /dev/null +++ b/apps/desktop/src/autofill/components/set-autotype-shortcut.component.html @@ -0,0 +1,33 @@ +
+ +
+ {{ "editAutotypeShortcut" | i18n }} +
+
+

+ {{ "editAutotypeShortcutDescription" | i18n }} +

+ + {{ "typeShortcut" | i18n }} + + +
+ + + + +
+
diff --git a/apps/desktop/src/autofill/components/set-autotype-shortcut.component.ts b/apps/desktop/src/autofill/components/set-autotype-shortcut.component.ts new file mode 100644 index 00000000000..372d4ca7cf3 --- /dev/null +++ b/apps/desktop/src/autofill/components/set-autotype-shortcut.component.ts @@ -0,0 +1,135 @@ +import { CommonModule } from "@angular/common"; +import { Component, OnInit } from "@angular/core"; +import { FormBuilder, ReactiveFormsModule, Validators, ValidatorFn, AbstractControl, ValidationErrors } from "@angular/forms"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { + AsyncActionsModule, + ButtonModule, + DialogModule, + DialogRef, + DialogService, + FormFieldModule, + IconButtonModule, +} from "@bitwarden/components"; +import { firstValueFrom } from "rxjs"; + +@Component({ + templateUrl: "set-autotype-shortcut.component.html", + imports: [ + DialogModule, + CommonModule, + JslibModule, + ButtonModule, + IconButtonModule, + ReactiveFormsModule, + AsyncActionsModule, + FormFieldModule, + ], +}) +export class SetAutotypeShortcutComponent implements OnInit { + + constructor( + private accountService: AccountService, + private dialogRef: DialogRef, + private formBuilder: FormBuilder, + // Autotype service? + ) { } + + ngOnInit(): void { + // set form value from state + } + + setShortcutForm = this.formBuilder.group({ + shortcut: [ + "", + [Validators.required, this.shortcutCombinationValidator()], + ], + requireMasterPasswordOnClientRestart: true, + }); + + submit = async () => { + const shortcutFormControl = this.setShortcutForm.controls.shortcut; + + if (Utils.isNullOrWhitespace(shortcutFormControl.value) || shortcutFormControl.invalid) { + return; + } + + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + + // Save shortcut via autotype service + console.log(shortcutFormControl.value); + + this.dialogRef.close(true); + }; + + static open(dialogService: DialogService) { + return dialogService.open(SetAutotypeShortcutComponent); + } + + onShortcutKeydown(event: KeyboardEvent): void { + event.preventDefault(); + + const shortcut = this.buildShortcutFromEvent(event); + + if (shortcut != null) { + this.setShortcutForm.controls.shortcut.setValue(shortcut); + this.setShortcutForm.controls.shortcut.markAsDirty(); + this.setShortcutForm.controls.shortcut.updateValueAndValidity(); + } + } + + private buildShortcutFromEvent(event: KeyboardEvent): string | null { + const hasCtrl = event.ctrlKey; + const hasAlt = event.altKey; + const hasShift = event.shiftKey; + + // Require at least one modifier (Ctrl, Alt, or Shift) + if (!hasCtrl && !hasAlt && !hasShift) { + return null; + } + + const key = event.key; + + // Ignore pure modifier keys themselves + if (key === "Control" || key === "Alt" || key === "Shift" || key === "Meta") { + return null; + } + + // Accept a single alphanumeric letter or number as the base key + const isAlphaNumeric = typeof key === "string" && /^[a-zA-Z0-9]$/.test(key); + if (!isAlphaNumeric) { + return null; + } + + const parts: string[] = []; + if (hasCtrl) { + parts.push("Ctrl"); + } + if (hasAlt) { + parts.push("Alt"); + } + if (hasShift) { + parts.push("Shift"); + } + parts.push(key.toUpperCase()); + + return parts.join("+"); + } + + private shortcutCombinationValidator(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const value = (control.value ?? "").toString(); + if (value.length === 0) { + return null; // handled by required + } + + // Must include at least one modifier and end with a single alphanumeric + // Valid examples: Ctrl+A, Alt+5, Shift+Z, Ctrl+Alt+7, Ctrl+Shift+X, Alt+Shift+Q + const pattern = /^(?=.*\b(Ctrl|Alt|Shift)\b)(?:Ctrl\+)?(?:Alt\+)?(?:Shift\+)?[A-Z0-9]$/i; + return pattern.test(value) ? null : { invalidShortcut: true }; + }; + } +} diff --git a/apps/desktop/src/autofill/main/main-desktop-autotype.service.ts b/apps/desktop/src/autofill/main/main-desktop-autotype.service.ts index 265263fadf8..fee607aa278 100644 --- a/apps/desktop/src/autofill/main/main-desktop-autotype.service.ts +++ b/apps/desktop/src/autofill/main/main-desktop-autotype.service.ts @@ -5,6 +5,7 @@ import { LogService } from "@bitwarden/logging"; import { WindowMain } from "../../main/window.main"; import { stringIsNotUndefinedNullAndEmpty } from "../../utils"; +import { AutotypeKeyboardShortcut } from "../models/main-autotype-keyboard-shortcut"; export class MainDesktopAutotypeService { autotypeKeyboardShortcut: AutotypeKeyboardShortcut; diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index aa597a6bf97..389dd111432 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -4086,6 +4086,12 @@ "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." }, + "typeShortcut": { + "message": "Type shortcut" + }, + "editAutotypeShortcutDescription": { + "message": "Include one of the following modifiers: Ctrl, Alt, or Shift, and a letter or number." + }, "moreBreadcrumbs": { "message": "More breadcrumbs", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." diff --git a/apps/desktop/src/scss/misc.scss b/apps/desktop/src/scss/misc.scss index 3c3d4ff508c..a0d9f85908d 100644 --- a/apps/desktop/src/scss/misc.scss +++ b/apps/desktop/src/scss/misc.scss @@ -360,6 +360,11 @@ form, } } +.settings-link { + color: #175ddc; + font-weight: bold; +} + app-root > #loading, .loading { display: flex;