diff --git a/apps/browser/src/auth/popup/home.component.ts b/apps/browser/src/auth/popup/home.component.ts
index db83736be8a..e647dfd05b9 100644
--- a/apps/browser/src/auth/popup/home.component.ts
+++ b/apps/browser/src/auth/popup/home.component.ts
@@ -5,6 +5,8 @@ import { Subject, firstValueFrom, takeUntil } from "rxjs";
import { EnvironmentSelectorComponent } from "@bitwarden/angular/auth/components/environment-selector.component";
import { LoginEmailServiceAbstraction } from "@bitwarden/auth/common";
+import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
+import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@@ -26,6 +28,9 @@ export class HomeComponent implements OnInit, OnDestroy {
rememberEmail: [false],
});
+ // TODO: remove when email verification flag is removed
+ registerRoute = "/register";
+
constructor(
protected platformUtilsService: PlatformUtilsService,
private formBuilder: FormBuilder,
@@ -34,9 +39,19 @@ export class HomeComponent implements OnInit, OnDestroy {
private environmentService: EnvironmentService,
private loginEmailService: LoginEmailServiceAbstraction,
private accountSwitcherService: AccountSwitcherService,
+ private configService: ConfigService,
) {}
async ngOnInit(): Promise {
+ // TODO: remove when email verification flag is removed
+ const emailVerification = await this.configService.getFeatureFlag(
+ FeatureFlag.EmailVerification,
+ );
+
+ if (emailVerification) {
+ this.registerRoute = "/signup";
+ }
+
const email = this.loginEmailService.getEmail();
const rememberEmail = this.loginEmailService.getRememberEmail();
diff --git a/apps/browser/src/auth/popup/login-decryption-options/login-decryption-options.component.html b/apps/browser/src/auth/popup/login-decryption-options/login-decryption-options.component.html
index 32e3ea0c598..e996f9a6ff4 100644
--- a/apps/browser/src/auth/popup/login-decryption-options/login-decryption-options.component.html
+++ b/apps/browser/src/auth/popup/login-decryption-options/login-decryption-options.component.html
@@ -1,10 +1,13 @@
-
-
+
+
+
+
+
{{ "loginInitiated" | i18n }}
-
-
+
+
diff --git a/apps/browser/src/auth/popup/login.component.ts b/apps/browser/src/auth/popup/login.component.ts
index ff0ee8a392d..56a9aca68c7 100644
--- a/apps/browser/src/auth/popup/login.component.ts
+++ b/apps/browser/src/auth/popup/login.component.ts
@@ -13,6 +13,7 @@ import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstraction
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
+import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -51,6 +52,7 @@ export class LoginComponent extends BaseLoginComponent {
loginEmailService: LoginEmailServiceAbstraction,
ssoLoginService: SsoLoginServiceAbstraction,
webAuthnLoginService: WebAuthnLoginServiceAbstraction,
+ configService: ConfigService,
) {
super(
devicesApiService,
@@ -71,6 +73,7 @@ export class LoginComponent extends BaseLoginComponent {
loginEmailService,
ssoLoginService,
webAuthnLoginService,
+ configService,
);
super.onSuccessfulLogin = async () => {
await syncService.fullSync(true);
diff --git a/apps/browser/src/autofill/content/bootstrap-autofill-overlay.ts b/apps/browser/src/autofill/content/bootstrap-autofill-overlay.ts
index 3dfa2506056..22430227660 100644
--- a/apps/browser/src/autofill/content/bootstrap-autofill-overlay.ts
+++ b/apps/browser/src/autofill/content/bootstrap-autofill-overlay.ts
@@ -1,12 +1,16 @@
import { AutofillInlineMenuContentService } from "../overlay/inline-menu/content/autofill-inline-menu-content.service";
import { AutofillOverlayContentService } from "../services/autofill-overlay-content.service";
+import { InlineMenuFieldQualificationService } from "../services/inline-menu-field-qualification.service";
import { setupAutofillInitDisconnectAction } from "../utils";
import AutofillInit from "./autofill-init";
(function (windowContext) {
if (!windowContext.bitwardenAutofillInit) {
- const autofillOverlayContentService = new AutofillOverlayContentService();
+ const inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService();
+ const autofillOverlayContentService = new AutofillOverlayContentService(
+ inlineMenuFieldQualificationService,
+ );
let inlineMenuElements: AutofillInlineMenuContentService;
if (globalThis.self === globalThis.top) {
inlineMenuElements = new AutofillInlineMenuContentService();
diff --git a/apps/browser/src/autofill/services/abstractions/autofill-overlay-content.service.ts b/apps/browser/src/autofill/services/abstractions/autofill-overlay-content.service.ts
index 1cb06a788a7..f259d6a63f8 100644
--- a/apps/browser/src/autofill/services/abstractions/autofill-overlay-content.service.ts
+++ b/apps/browser/src/autofill/services/abstractions/autofill-overlay-content.service.ts
@@ -3,6 +3,7 @@ import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authenticatio
import { SubFrameOffsetData } from "../../background/abstractions/overlay.background";
import { AutofillExtensionMessageParam } from "../../content/abstractions/autofill-init";
import AutofillField from "../../models/autofill-field";
+import AutofillPageDetails from "../../models/autofill-page-details";
import { ElementWithOpId, FormFieldElement } from "../../types";
export type OpenAutofillInlineMenuOptions = {
@@ -36,9 +37,10 @@ export interface AutofillOverlayContentService {
pageDetailsUpdateRequired: boolean;
messageHandlers: AutofillOverlayContentExtensionMessageHandlers;
init(): void;
- setupInlineMenuListenerOnField(
+ setupInlineMenu(
autofillFieldElement: ElementWithOpId,
autofillFieldData: AutofillField,
+ pageDetails: AutofillPageDetails,
): Promise;
blurMostRecentlyFocusedField(isClosingInlineMenu?: boolean): void;
destroy(): void;
diff --git a/apps/browser/src/autofill/services/abstractions/inline-menu-field-qualifications.service.ts b/apps/browser/src/autofill/services/abstractions/inline-menu-field-qualifications.service.ts
new file mode 100644
index 00000000000..0be30b2f1ea
--- /dev/null
+++ b/apps/browser/src/autofill/services/abstractions/inline-menu-field-qualifications.service.ts
@@ -0,0 +1,6 @@
+import AutofillField from "../../models/autofill-field";
+import AutofillPageDetails from "../../models/autofill-page-details";
+
+export interface InlineMenuFieldQualificationService {
+ isFieldForLoginForm(field: AutofillField, pageDetails: AutofillPageDetails): boolean;
+}
diff --git a/apps/browser/src/autofill/services/autofill-overlay-content.service.spec.ts b/apps/browser/src/autofill/services/autofill-overlay-content.service.spec.ts
index 2c0cfcc7228..1efd1356cf8 100644
--- a/apps/browser/src/autofill/services/autofill-overlay-content.service.spec.ts
+++ b/apps/browser/src/autofill/services/autofill-overlay-content.service.spec.ts
@@ -10,17 +10,21 @@ import {
RedirectFocusDirection,
} from "../enums/autofill-overlay.enum";
import AutofillField from "../models/autofill-field";
+import AutofillForm from "../models/autofill-form";
+import AutofillPageDetails from "../models/autofill-page-details";
import { createAutofillFieldMock } from "../spec/autofill-mocks";
import { flushPromises, postWindowMessage, sendMockExtensionMessage } from "../spec/testing-utils";
import { ElementWithOpId, FillableFormFieldElement, FormFieldElement } from "../types";
import { AutoFillConstants } from "./autofill-constants";
import { AutofillOverlayContentService } from "./autofill-overlay-content.service";
+import { InlineMenuFieldQualificationService } from "./inline-menu-field-qualification.service";
const defaultWindowReadyState = document.readyState;
const defaultDocumentVisibilityState = document.visibilityState;
describe("AutofillOverlayContentService", () => {
let autofillInit: AutofillInit;
+ let inlineMenuFieldQualificationService: InlineMenuFieldQualificationService;
let autofillOverlayContentService: AutofillOverlayContentService;
let sendExtensionMessageSpy: jest.SpyInstance;
const sendResponseSpy = jest.fn();
@@ -35,7 +39,10 @@ describe("AutofillOverlayContentService", () => {
});
beforeEach(() => {
- autofillOverlayContentService = new AutofillOverlayContentService();
+ inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService();
+ autofillOverlayContentService = new AutofillOverlayContentService(
+ inlineMenuFieldQualificationService,
+ );
autofillInit = new AutofillInit(autofillOverlayContentService);
autofillInit.init();
sendExtensionMessageSpy = jest
@@ -120,9 +127,10 @@ describe("AutofillOverlayContentService", () => {
});
});
- describe("setupInlineMenuListenerOnField", () => {
+ describe("setupInlineMenu", () => {
let autofillFieldElement: ElementWithOpId;
let autofillFieldData: AutofillField;
+ let pageDetailsMock: AutofillPageDetails;
beforeEach(() => {
document.body.innerHTML = `
@@ -144,87 +152,52 @@ describe("AutofillOverlayContentService", () => {
placeholder: "username",
elementNumber: 1,
});
+ const passwordFieldData = createAutofillFieldMock({
+ opid: "password-field",
+ form: "validFormId",
+ elementNumber: 2,
+ autocompleteType: "current-password",
+ type: "password",
+ });
+ pageDetailsMock = mock({
+ forms: { validFormId: mock() },
+ fields: [autofillFieldData, passwordFieldData],
+ });
});
describe("skips setup for ignored form fields", () => {
beforeEach(() => {
- autofillFieldData = mock();
- });
-
- it("ignores fields that are readonly", async () => {
- autofillFieldData.readonly = true;
-
- await autofillOverlayContentService.setupInlineMenuListenerOnField(
- autofillFieldElement,
- autofillFieldData,
- );
-
- expect(autofillFieldElement.addEventListener).not.toHaveBeenCalled();
- });
-
- it("ignores fields that contain a disabled attribute", async () => {
- autofillFieldData.disabled = true;
-
- await autofillOverlayContentService.setupInlineMenuListenerOnField(
- autofillFieldElement,
- autofillFieldData,
- );
-
- expect(autofillFieldElement.addEventListener).not.toHaveBeenCalled();
- });
-
- it("ignores fields that are not viewable", async () => {
- autofillFieldData.viewable = false;
-
- await autofillOverlayContentService.setupInlineMenuListenerOnField(
- autofillFieldElement,
- autofillFieldData,
- );
-
- expect(autofillFieldElement.addEventListener).not.toHaveBeenCalled();
+ autofillFieldData = mock({
+ type: "text",
+ htmlName: "username",
+ htmlID: "username",
+ placeholder: "username",
+ });
});
it("ignores fields that are part of the ExcludedInlineMenuTypes", () => {
AutoFillConstants.ExcludedInlineMenuTypes.forEach(async (excludedType) => {
autofillFieldData.type = excludedType;
- await autofillOverlayContentService.setupInlineMenuListenerOnField(
+ await autofillOverlayContentService.setupInlineMenu(
autofillFieldElement,
autofillFieldData,
+ pageDetailsMock,
);
expect(autofillFieldElement.addEventListener).not.toHaveBeenCalled();
});
});
- it("ignores fields that contain the keyword `search`", async () => {
- autofillFieldData.placeholder = "search";
-
- await autofillOverlayContentService.setupInlineMenuListenerOnField(
- autofillFieldElement,
- autofillFieldData,
- );
-
- expect(autofillFieldElement.addEventListener).not.toHaveBeenCalled();
- });
-
- it("ignores fields that contain the keyword `captcha` ", async () => {
- autofillFieldData.placeholder = "captcha";
-
- await autofillOverlayContentService.setupInlineMenuListenerOnField(
- autofillFieldElement,
- autofillFieldData,
- );
-
- expect(autofillFieldElement.addEventListener).not.toHaveBeenCalled();
- });
-
it("ignores fields that do not appear as a login field", async () => {
+ autofillFieldData.htmlName = "another-type-of-field";
+ autofillFieldData.htmlID = "another-type-of-field";
autofillFieldData.placeholder = "another-type-of-field";
- await autofillOverlayContentService.setupInlineMenuListenerOnField(
+ await autofillOverlayContentService.setupInlineMenu(
autofillFieldElement,
autofillFieldData,
+ pageDetailsMock,
);
expect(autofillFieldElement.addEventListener).not.toHaveBeenCalled();
@@ -234,9 +207,10 @@ describe("AutofillOverlayContentService", () => {
it("skips setup on fields that have been previously set up", async () => {
autofillOverlayContentService["formFieldElements"].add(autofillFieldElement);
- await autofillOverlayContentService.setupInlineMenuListenerOnField(
+ await autofillOverlayContentService.setupInlineMenu(
autofillFieldElement,
autofillFieldData,
+ pageDetailsMock,
);
expect(autofillFieldElement.addEventListener).not.toHaveBeenCalled();
@@ -247,9 +221,10 @@ describe("AutofillOverlayContentService", () => {
sendExtensionMessageSpy.mockResolvedValueOnce(undefined);
autofillOverlayContentService["inlineMenuVisibility"] = undefined;
- await autofillOverlayContentService.setupInlineMenuListenerOnField(
+ await autofillOverlayContentService.setupInlineMenu(
autofillFieldElement,
autofillFieldData,
+ pageDetailsMock,
);
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("getAutofillInlineMenuVisibility");
@@ -262,9 +237,10 @@ describe("AutofillOverlayContentService", () => {
sendExtensionMessageSpy.mockResolvedValueOnce(AutofillOverlayVisibility.OnFieldFocus);
autofillOverlayContentService["inlineMenuVisibility"] = undefined;
- await autofillOverlayContentService.setupInlineMenuListenerOnField(
+ await autofillOverlayContentService.setupInlineMenu(
autofillFieldElement,
autofillFieldData,
+ pageDetailsMock,
);
expect(autofillOverlayContentService["inlineMenuVisibility"]).toEqual(
@@ -285,9 +261,10 @@ describe("AutofillOverlayContentService", () => {
"op-1-username-field-focus-handler": focusHandler,
};
- await autofillOverlayContentService.setupInlineMenuListenerOnField(
+ await autofillOverlayContentService.setupInlineMenu(
autofillFieldElement,
autofillFieldData,
+ pageDetailsMock,
);
expect(autofillFieldElement.removeEventListener).toHaveBeenNthCalledWith(
@@ -314,9 +291,10 @@ describe("AutofillOverlayContentService", () => {
describe("form field blur event listener", () => {
beforeEach(async () => {
- await autofillOverlayContentService.setupInlineMenuListenerOnField(
+ await autofillOverlayContentService.setupInlineMenu(
autofillFieldElement,
autofillFieldData,
+ pageDetailsMock,
);
});
@@ -337,9 +315,10 @@ describe("AutofillOverlayContentService", () => {
describe("form field keyup event listener", () => {
beforeEach(async () => {
- await autofillOverlayContentService.setupInlineMenuListenerOnField(
+ await autofillOverlayContentService.setupInlineMenu(
autofillFieldElement,
autofillFieldData,
+ pageDetailsMock,
);
jest.spyOn(globalThis.customElements, "define").mockImplementation();
});
@@ -433,9 +412,10 @@ describe("AutofillOverlayContentService", () => {
) as ElementWithOpId;
jest.spyOn(autofillOverlayContentService as any, "storeModifiedFormElement");
- await autofillOverlayContentService.setupInlineMenuListenerOnField(
+ await autofillOverlayContentService.setupInlineMenu(
spanAutofillFieldElement,
autofillFieldData,
+ pageDetailsMock,
);
spanAutofillFieldElement.dispatchEvent(new Event("input"));
@@ -447,9 +427,10 @@ describe("AutofillOverlayContentService", () => {
autofillOverlayContentService["mostRecentlyFocusedField"] =
mock>();
- await autofillOverlayContentService.setupInlineMenuListenerOnField(
+ await autofillOverlayContentService.setupInlineMenu(
autofillFieldElement,
autofillFieldData,
+ pageDetailsMock,
);
autofillFieldElement.dispatchEvent(new Event("input"));
@@ -461,9 +442,10 @@ describe("AutofillOverlayContentService", () => {
});
it("stores the field as a user filled field if the form field data indicates that it is for a username", async () => {
- await autofillOverlayContentService.setupInlineMenuListenerOnField(
+ await autofillOverlayContentService.setupInlineMenu(
autofillFieldElement,
autofillFieldData,
+ pageDetailsMock,
);
autofillFieldElement.dispatchEvent(new Event("input"));
@@ -477,9 +459,10 @@ describe("AutofillOverlayContentService", () => {
"password-field",
) as ElementWithOpId;
- await autofillOverlayContentService.setupInlineMenuListenerOnField(
+ await autofillOverlayContentService.setupInlineMenu(
passwordFieldElement,
autofillFieldData,
+ pageDetailsMock,
);
passwordFieldElement.dispatchEvent(new Event("input"));
@@ -492,9 +475,10 @@ describe("AutofillOverlayContentService", () => {
jest.spyOn(autofillOverlayContentService as any, "isUserAuthed").mockReturnValue(false);
(autofillFieldElement as HTMLInputElement).value = "test";
- await autofillOverlayContentService.setupInlineMenuListenerOnField(
+ await autofillOverlayContentService.setupInlineMenu(
autofillFieldElement,
autofillFieldData,
+ pageDetailsMock,
);
autofillFieldElement.dispatchEvent(new Event("input"));
await flushPromises();
@@ -513,9 +497,10 @@ describe("AutofillOverlayContentService", () => {
(autofillFieldElement as HTMLInputElement).value = "test";
- await autofillOverlayContentService.setupInlineMenuListenerOnField(
+ await autofillOverlayContentService.setupInlineMenu(
autofillFieldElement,
autofillFieldData,
+ pageDetailsMock,
);
autofillFieldElement.dispatchEvent(new Event("input"));
await flushPromises();
@@ -530,9 +515,10 @@ describe("AutofillOverlayContentService", () => {
jest.spyOn(autofillOverlayContentService as any, "openInlineMenu");
(autofillFieldElement as HTMLInputElement).value = "";
- await autofillOverlayContentService.setupInlineMenuListenerOnField(
+ await autofillOverlayContentService.setupInlineMenu(
autofillFieldElement,
autofillFieldData,
+ pageDetailsMock,
);
autofillFieldElement.dispatchEvent(new Event("input"));
await flushPromises();
@@ -545,9 +531,10 @@ describe("AutofillOverlayContentService", () => {
jest.spyOn(autofillOverlayContentService as any, "openInlineMenu");
(autofillFieldElement as HTMLInputElement).value = "";
- await autofillOverlayContentService.setupInlineMenuListenerOnField(
+ await autofillOverlayContentService.setupInlineMenu(
autofillFieldElement,
autofillFieldData,
+ pageDetailsMock,
);
autofillFieldElement.dispatchEvent(new Event("input"));
await flushPromises();
@@ -563,9 +550,10 @@ describe("AutofillOverlayContentService", () => {
jest.spyOn(autofillOverlayContentService as any, "openInlineMenu");
(autofillFieldElement as HTMLInputElement).value = "";
- await autofillOverlayContentService.setupInlineMenuListenerOnField(
+ await autofillOverlayContentService.setupInlineMenu(
autofillFieldElement,
autofillFieldData,
+ pageDetailsMock,
);
autofillFieldElement.dispatchEvent(new Event("input"));
await flushPromises();
@@ -579,9 +567,10 @@ describe("AutofillOverlayContentService", () => {
jest
.spyOn(autofillOverlayContentService as any, "triggerFormFieldFocusedAction")
.mockImplementation();
- await autofillOverlayContentService.setupInlineMenuListenerOnField(
+ await autofillOverlayContentService.setupInlineMenu(
autofillFieldElement,
autofillFieldData,
+ pageDetailsMock,
);
});
@@ -635,9 +624,10 @@ describe("AutofillOverlayContentService", () => {
autofillOverlayContentService["mostRecentlyFocusedField"] = autofillFieldElement;
autofillOverlayContentService["inlineMenuVisibility"] =
AutofillOverlayVisibility.OnFieldFocus;
- await autofillOverlayContentService.setupInlineMenuListenerOnField(
+ await autofillOverlayContentService.setupInlineMenu(
autofillFieldElement,
autofillFieldData,
+ pageDetailsMock,
);
autofillFieldElement.dispatchEvent(new Event("focus"));
@@ -647,9 +637,10 @@ describe("AutofillOverlayContentService", () => {
});
it("updates the most recently focused field", async () => {
- await autofillOverlayContentService.setupInlineMenuListenerOnField(
+ await autofillOverlayContentService.setupInlineMenu(
autofillFieldElement,
autofillFieldData,
+ pageDetailsMock,
);
autofillFieldElement.dispatchEvent(new Event("focus"));
@@ -664,9 +655,10 @@ describe("AutofillOverlayContentService", () => {
it("removes the overlay list if the autofill visibility is set to onClick", async () => {
autofillOverlayContentService["inlineMenuVisibility"] =
AutofillOverlayVisibility.OnButtonClick;
- await autofillOverlayContentService.setupInlineMenuListenerOnField(
+ await autofillOverlayContentService.setupInlineMenu(
autofillFieldElement,
autofillFieldData,
+ pageDetailsMock,
);
autofillFieldElement.dispatchEvent(new Event("focus"));
@@ -683,9 +675,10 @@ describe("AutofillOverlayContentService", () => {
"input",
) as ElementWithOpId;
(autofillFieldElement as HTMLInputElement).value = "test";
- await autofillOverlayContentService.setupInlineMenuListenerOnField(
+ await autofillOverlayContentService.setupInlineMenu(
autofillFieldElement,
autofillFieldData,
+ pageDetailsMock,
);
autofillFieldElement.dispatchEvent(new Event("focus"));
@@ -701,9 +694,10 @@ describe("AutofillOverlayContentService", () => {
(autofillFieldElement as HTMLInputElement).value = "";
autofillOverlayContentService["inlineMenuVisibility"] =
AutofillOverlayVisibility.OnFieldFocus;
- await autofillOverlayContentService.setupInlineMenuListenerOnField(
+ await autofillOverlayContentService.setupInlineMenu(
autofillFieldElement,
autofillFieldData,
+ pageDetailsMock,
);
autofillFieldElement.dispatchEvent(new Event("focus"));
@@ -717,9 +711,10 @@ describe("AutofillOverlayContentService", () => {
autofillOverlayContentService["inlineMenuVisibility"] =
AutofillOverlayVisibility.OnFieldFocus;
jest.spyOn(autofillOverlayContentService as any, "isUserAuthed").mockReturnValue(true);
- await autofillOverlayContentService.setupInlineMenuListenerOnField(
+ await autofillOverlayContentService.setupInlineMenu(
autofillFieldElement,
autofillFieldData,
+ pageDetailsMock,
);
autofillFieldElement.dispatchEvent(new Event("focus"));
@@ -735,9 +730,10 @@ describe("AutofillOverlayContentService", () => {
jest
.spyOn(autofillOverlayContentService as any, "isInlineMenuCiphersPopulated")
.mockReturnValue(true);
- await autofillOverlayContentService.setupInlineMenuListenerOnField(
+ await autofillOverlayContentService.setupInlineMenu(
autofillFieldElement,
autofillFieldData,
+ pageDetailsMock,
);
autofillFieldElement.dispatchEvent(new Event("focus"));
@@ -752,9 +748,10 @@ describe("AutofillOverlayContentService", () => {
describe("hidden form field focus event", () => {
it("sets up the inline menu listeners if the autofill field data is in the cache", async () => {
autofillFieldData.viewable = false;
- await autofillOverlayContentService.setupInlineMenuListenerOnField(
+ await autofillOverlayContentService.setupInlineMenu(
autofillFieldElement,
autofillFieldData,
+ pageDetailsMock,
);
autofillFieldElement.dispatchEvent(new Event("focus"));
@@ -785,9 +782,10 @@ describe("AutofillOverlayContentService", () => {
it("skips setting up the inline menu listeners if the autofill field data is not in the cache", async () => {
autofillFieldData.viewable = false;
- await autofillOverlayContentService.setupInlineMenuListenerOnField(
+ await autofillOverlayContentService.setupInlineMenu(
autofillFieldElement,
autofillFieldData,
+ pageDetailsMock,
);
autofillOverlayContentService["formFieldElements"].delete(autofillFieldElement);
@@ -821,9 +819,10 @@ describe("AutofillOverlayContentService", () => {
writable: true,
});
- await autofillOverlayContentService.setupInlineMenuListenerOnField(
+ await autofillOverlayContentService.setupInlineMenu(
autofillFieldElement,
autofillFieldData,
+ pageDetailsMock,
);
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("openAutofillInlineMenu");
@@ -835,9 +834,10 @@ describe("AutofillOverlayContentService", () => {
it("sets the most recently focused field to the passed form field element if the value is not set", async () => {
autofillOverlayContentService["mostRecentlyFocusedField"] = undefined;
- await autofillOverlayContentService.setupInlineMenuListenerOnField(
+ await autofillOverlayContentService.setupInlineMenu(
autofillFieldElement,
autofillFieldData,
+ pageDetailsMock,
);
expect(autofillOverlayContentService["mostRecentlyFocusedField"]).toEqual(
@@ -1608,8 +1608,8 @@ describe("AutofillOverlayContentService", () => {
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("updateSubFrameData", {
subFrameData: {
frameId: 10,
- left: 168,
- top: 168,
+ left: expect.any(Number),
+ top: expect.any(Number),
url: "https://example.com/",
parentFrameIds: [1, 2, 3, 4],
subFrameDepth: expect.any(Number),
@@ -1652,6 +1652,7 @@ describe("AutofillOverlayContentService", () => {
describe("destroy", () => {
let autofillFieldElement: ElementWithOpId;
let autofillFieldData: AutofillField;
+ let pageDetailsMock: AutofillPageDetails;
beforeEach(() => {
document.body.innerHTML = `
@@ -1671,11 +1672,21 @@ describe("AutofillOverlayContentService", () => {
placeholder: "username",
elementNumber: 1,
});
- // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
- autofillOverlayContentService.setupInlineMenuListenerOnField(
+ const passwordFieldData = createAutofillFieldMock({
+ opid: "password-field",
+ form: "validFormId",
+ elementNumber: 2,
+ autocompleteType: "current-password",
+ type: "password",
+ });
+ pageDetailsMock = mock({
+ forms: { validFormId: mock() },
+ fields: [autofillFieldData, passwordFieldData],
+ });
+ void autofillOverlayContentService.setupInlineMenu(
autofillFieldElement,
autofillFieldData,
+ pageDetailsMock,
);
autofillOverlayContentService["mostRecentlyFocusedField"] = autofillFieldElement;
});
diff --git a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts
index 195d9c33213..766d13aab0c 100644
--- a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts
+++ b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts
@@ -16,6 +16,7 @@ import {
RedirectFocusDirection,
} from "../enums/autofill-overlay.enum";
import AutofillField from "../models/autofill-field";
+import AutofillPageDetails from "../models/autofill-page-details";
import { ElementWithOpId, FillableFormFieldElement, FormFieldElement } from "../types";
import {
elementIsFillableFormField,
@@ -30,6 +31,7 @@ import {
OpenAutofillInlineMenuOptions,
SubFrameDataFromWindowMessage,
} from "./abstractions/autofill-overlay-content.service";
+import { InlineMenuFieldQualificationService } from "./abstractions/inline-menu-field-qualifications.service";
import { AutoFillConstants } from "./autofill-constants";
export class AutofillOverlayContentService implements AutofillOverlayContentServiceInterface {
@@ -50,7 +52,6 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
private recalculateSubFrameOffsetsTimeout: number | NodeJS.Timeout;
private reflowPerformanceObserver: PerformanceObserver;
private reflowMutationObserver: MutationObserver;
- private autofillFieldKeywordsMap: WeakMap = new WeakMap();
private eventHandlersMemo: { [key: string]: EventListener } = {};
private readonly extensionMessageHandlers: AutofillOverlayContentExtensionMessageHandlers = {
openAutofillInlineMenu: ({ message }) => this.openInlineMenu(message),
@@ -70,6 +71,8 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
destroyAutofillInlineMenuListeners: () => this.destroy(),
};
+ constructor(private inlineMenuFieldQualificationService: InlineMenuFieldQualificationService) {}
+
/**
* Initializes the autofill overlay content service by setting up the mutation observers.
* The observers will be instantiated on DOMContentLoaded if the page is current loading.
@@ -97,12 +100,17 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
*
* @param formFieldElement - Form field elements identified during the page details collection process.
* @param autofillFieldData - Autofill field data captured from the form field element.
+ * @param pageDetails - The collected page details from the tab.
*/
- async setupInlineMenuListenerOnField(
+ async setupInlineMenu(
formFieldElement: ElementWithOpId,
autofillFieldData: AutofillField,
+ pageDetails: AutofillPageDetails,
) {
- if (this.isIgnoredField(autofillFieldData) || this.formFieldElements.has(formFieldElement)) {
+ if (
+ this.formFieldElements.has(formFieldElement) ||
+ this.isIgnoredField(autofillFieldData, pageDetails)
+ ) {
return;
}
@@ -110,6 +118,12 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
return;
}
+ await this.setupInlineMenuOnQualifiedField(formFieldElement);
+ }
+
+ private async setupInlineMenuOnQualifiedField(
+ formFieldElement: ElementWithOpId,
+ ) {
this.formFieldElements.add(formFieldElement);
if (!this.mostRecentlyFocusedField) {
@@ -517,51 +531,6 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
return this.authStatus === AuthenticationStatus.Unlocked;
}
- /**
- * Identifies if the autofill field's data contains any of
- * the keyboards matching the passed list of keywords.
- *
- * @param autofillFieldData - Autofill field data captured from the form field element.
- * @param keywords - Keywords to search for in the autofill field data.
- */
- private keywordsFoundInFieldData(autofillFieldData: AutofillField, keywords: string[]) {
- const searchedString = this.getAutofillFieldDataKeywords(autofillFieldData);
- return keywords.some((keyword) => searchedString.includes(keyword));
- }
-
- /**
- * Aggregates the autofill field's data into a single string
- * that can be used to search for keywords.
- *
- * @param autofillFieldData - Autofill field data captured from the form field element.
- */
- private getAutofillFieldDataKeywords(autofillFieldData: AutofillField) {
- if (this.autofillFieldKeywordsMap.has(autofillFieldData)) {
- return this.autofillFieldKeywordsMap.get(autofillFieldData);
- }
-
- const keywordValues = [
- autofillFieldData.htmlID,
- autofillFieldData.htmlName,
- autofillFieldData.htmlClass,
- autofillFieldData.type,
- autofillFieldData.title,
- autofillFieldData.placeholder,
- autofillFieldData.autoCompleteType,
- autofillFieldData["label-data"],
- autofillFieldData["label-aria"],
- autofillFieldData["label-left"],
- autofillFieldData["label-right"],
- autofillFieldData["label-tag"],
- autofillFieldData["label-top"],
- ]
- .join(",")
- .toLowerCase();
- this.autofillFieldKeywordsMap.set(autofillFieldData, keywordValues);
-
- return keywordValues;
- }
-
/**
* Validates that the most recently focused field is currently
* focused within the root node relative to the field.
@@ -698,20 +667,20 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
* updated in the future to support other types of forms.
*
* @param autofillFieldData - Autofill field data captured from the form field element.
+ * @param pageDetails - The collected page details from the tab.
*/
- private isIgnoredField(autofillFieldData: AutofillField): boolean {
- if (
- this.ignoredFieldTypes.has(autofillFieldData.type) ||
- this.keywordsFoundInFieldData(autofillFieldData, ["search", "captcha"])
- ) {
+ private isIgnoredField(
+ autofillFieldData: AutofillField,
+ pageDetails: AutofillPageDetails,
+ ): boolean {
+ if (this.ignoredFieldTypes.has(autofillFieldData.type)) {
return true;
}
- const isLoginCipherField =
- autofillFieldData.type === "password" ||
- this.keywordsFoundInFieldData(autofillFieldData, AutoFillConstants.UsernameFieldNames);
-
- return !isLoginCipherField;
+ return !this.inlineMenuFieldQualificationService.isFieldForLoginForm(
+ autofillFieldData,
+ pageDetails,
+ );
}
/**
@@ -732,7 +701,6 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
}
this.setupHiddenFieldFallbackListener(formFieldElement, autofillFieldData);
-
return true;
}
@@ -775,7 +743,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
autofillFieldData.readonly = getAttributeBoolean(formFieldElement, "disabled");
autofillFieldData.disabled = getAttributeBoolean(formFieldElement, "disabled");
autofillFieldData.viewable = true;
- void this.setupInlineMenuListenerOnField(formFieldElement, autofillFieldData);
+ void this.setupInlineMenuOnQualifiedField(formFieldElement);
}
this.removeHiddenFieldFallbackListener(formFieldElement);
diff --git a/apps/browser/src/autofill/services/collect-autofill-content.service.spec.ts b/apps/browser/src/autofill/services/collect-autofill-content.service.spec.ts
index ae2ab480a60..f67c0e88aa0 100644
--- a/apps/browser/src/autofill/services/collect-autofill-content.service.spec.ts
+++ b/apps/browser/src/autofill/services/collect-autofill-content.service.spec.ts
@@ -11,6 +11,7 @@ import {
FormElementWithAttribute,
} from "../types";
+import { InlineMenuFieldQualificationService } from "./abstractions/inline-menu-field-qualifications.service";
import { AutofillOverlayContentService } from "./autofill-overlay-content.service";
import CollectAutofillContentService from "./collect-autofill-content.service";
import DomElementVisibilityService from "./dom-element-visibility.service";
@@ -28,13 +29,17 @@ const waitForIdleCallback = () => new Promise((resolve) => globalThis.requestIdl
describe("CollectAutofillContentService", () => {
const domElementVisibilityService = new DomElementVisibilityService();
- const autofillOverlayContentService = new AutofillOverlayContentService();
+ const inlineMenuFieldQualificationService = mock();
+ const autofillOverlayContentService = new AutofillOverlayContentService(
+ inlineMenuFieldQualificationService,
+ );
let collectAutofillContentService: CollectAutofillContentService;
const mockIntersectionObserver = mock();
const mockQuerySelectorAll = mockQuerySelectorAllDefinedCall();
beforeEach(() => {
globalThis.requestIdleCallback = jest.fn((cb, options) => setTimeout(cb, 100));
+ globalThis.cancelIdleCallback = jest.fn((id) => clearTimeout(id));
document.body.innerHTML = mockLoginForm;
collectAutofillContentService = new CollectAutofillContentService(
domElementVisibilityService,
@@ -247,11 +252,16 @@ describe("CollectAutofillContentService", () => {
const isFormFieldViewableSpy = jest
.spyOn(collectAutofillContentService["domElementVisibilityService"], "isFormFieldViewable")
.mockResolvedValue(true);
+ const setupAutofillOverlayListenerOnFieldSpy = jest.spyOn(
+ collectAutofillContentService["autofillOverlayContentService"],
+ "setupInlineMenu",
+ );
await collectAutofillContentService.getPageDetails();
expect(autofillField.viewable).toBe(true);
expect(isFormFieldViewableSpy).toHaveBeenCalledWith(fieldElement);
+ expect(setupAutofillOverlayListenerOnFieldSpy).toHaveBeenCalled();
});
it("returns an object containing information about the current page as well as autofill data for the forms and fields of the page", async () => {
@@ -1191,7 +1201,7 @@ describe("CollectAutofillContentService", () => {
"aria-disabled": false,
"aria-haspopup": false,
"aria-hidden": false,
- autoCompleteType: null,
+ autoCompleteType: "off",
checked: false,
"data-stripe": hiddenField.dataStripe,
disabled: false,
@@ -2558,7 +2568,7 @@ describe("CollectAutofillContentService", () => {
);
setupAutofillOverlayListenerOnFieldSpy = jest.spyOn(
collectAutofillContentService["autofillOverlayContentService"],
- "setupInlineMenuListenerOnField",
+ "setupInlineMenu",
);
});
@@ -2622,22 +2632,20 @@ describe("CollectAutofillContentService", () => {
expect(setupAutofillOverlayListenerOnFieldSpy).toHaveBeenCalledWith(
formFieldElement,
autofillField,
+ expect.anything(),
);
});
});
describe("destroy", () => {
- it("clears the updateAutofillElementsAfterMutationTimeout", () => {
+ it("clears the updateAfterMutationIdleCallback", () => {
jest.spyOn(window, "clearTimeout");
- collectAutofillContentService["updateAutofillElementsAfterMutationTimeout"] = setTimeout(
- jest.fn,
- 100,
- );
+ collectAutofillContentService["updateAfterMutationIdleCallback"] = setTimeout(jest.fn, 100);
collectAutofillContentService.destroy();
expect(clearTimeout).toHaveBeenCalledWith(
- collectAutofillContentService["updateAutofillElementsAfterMutationTimeout"],
+ collectAutofillContentService["updateAfterMutationIdleCallback"],
);
});
});
diff --git a/apps/browser/src/autofill/services/collect-autofill-content.service.ts b/apps/browser/src/autofill/services/collect-autofill-content.service.ts
index ea1f74eeaff..b5541ba5eb6 100644
--- a/apps/browser/src/autofill/services/collect-autofill-content.service.ts
+++ b/apps/browser/src/autofill/services/collect-autofill-content.service.ts
@@ -7,26 +7,27 @@ import {
elementIsDescriptionTermElement,
elementIsFillableFormField,
elementIsFormElement,
+ elementIsInputElement,
elementIsLabelElement,
elementIsSelectElement,
elementIsSpanElement,
nodeIsElement,
- elementIsInputElement,
elementIsTextAreaElement,
nodeIsFormElement,
nodeIsInputElement,
- sendExtensionMessage,
+ // sendExtensionMessage,
getAttributeBoolean,
getPropertyOrAttribute,
requestIdleCallbackPolyfill,
+ cancelIdleCallbackPolyfill,
} from "../utils";
import { AutofillOverlayContentService } from "./abstractions/autofill-overlay-content.service";
import {
- UpdateAutofillDataAttributeParams,
AutofillFieldElements,
AutofillFormElements,
CollectAutofillContentService as CollectAutofillContentServiceInterface,
+ UpdateAutofillDataAttributeParams,
} from "./abstractions/collect-autofill-content.service";
import { DomElementVisibilityService } from "./abstractions/dom-element-visibility.service";
@@ -43,9 +44,9 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
private intersectionObserver: IntersectionObserver;
private elementInitializingIntersectionObserver: Set = new Set();
private mutationObserver: MutationObserver;
- private updateAutofillElementsAfterMutationTimeout: number | NodeJS.Timeout;
private mutationsQueue: MutationRecord[][] = [];
- private readonly updateAfterMutationTimeoutDelay = 1000;
+ private updateAfterMutationIdleCallback: NodeJS.Timeout | number;
+ private readonly updateAfterMutationTimeout = 1000;
private readonly formFieldQueryString;
private readonly nonInputFormFieldTags = new Set(["textarea", "select"]);
private readonly ignoredInputTypes = new Set([
@@ -56,7 +57,7 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
"image",
"file",
]);
- private useTreeWalkerStrategyFlagSet = false;
+ private useTreeWalkerStrategyFlagSet = true;
constructor(
domElementVisibilityService: DomElementVisibilityService,
@@ -71,10 +72,10 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
}
this.formFieldQueryString = `${inputQuery}, textarea:not([data-bwignore]), select:not([data-bwignore]), span[data-bwautofill]`;
- void sendExtensionMessage("getUseTreeWalkerApiForPageDetailsCollectionFeatureFlag").then(
- (useTreeWalkerStrategyFlag) =>
- (this.useTreeWalkerStrategyFlagSet = !!useTreeWalkerStrategyFlag?.result),
- );
+ // void sendExtensionMessage("getUseTreeWalkerApiForPageDetailsCollectionFeatureFlag").then(
+ // (useTreeWalkerStrategyFlag) =>
+ // (this.useTreeWalkerStrategyFlagSet = !!useTreeWalkerStrategyFlag?.result),
+ // );
}
/**
@@ -119,7 +120,10 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
}
this.domRecentlyMutated = false;
- return this.getFormattedPageDetails(autofillFormsData, autofillFieldsData);
+ const pageDetails = this.getFormattedPageDetails(autofillFormsData, autofillFieldsData);
+ this.setupInlineMenuListeners(pageDetails);
+
+ return pageDetails;
}
/**
@@ -277,14 +281,11 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
*/
private updateCachedAutofillFieldVisibility() {
this.autofillFieldElements.forEach(async (autofillField, element) => {
- const currentViewableState = autofillField.viewable;
+ const previouslyViewable = autofillField.viewable;
autofillField.viewable = await this.domElementVisibilityService.isFormFieldViewable(element);
- if (!currentViewableState && autofillField.viewable) {
- await this.autofillOverlayContentService?.setupInlineMenuListenerOnField(
- element,
- autofillField,
- );
+ if (!previouslyViewable && autofillField.viewable) {
+ this.setupInlineMenu(element, autofillField);
}
});
}
@@ -458,10 +459,6 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
if (elementIsSpanElement(element)) {
this.cacheAutofillFieldElement(index, element, autofillFieldBase);
- void this.autofillOverlayContentService?.setupInlineMenuListenerOnField(
- element,
- autofillFieldBase,
- );
return autofillFieldBase;
}
@@ -501,7 +498,6 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
};
this.cacheAutofillFieldElement(index, element, autofillField);
- void this.autofillOverlayContentService?.setupInlineMenuListenerOnField(element, autofillField);
return autofillField;
};
@@ -533,11 +529,11 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
* @private
*/
private getAutoCompleteAttribute(element: ElementWithOpId): string {
- const autoCompleteType =
+ return (
this.getPropertyOrAttribute(element, "x-autocompletetype") ||
this.getPropertyOrAttribute(element, "autocompletetype") ||
- this.getPropertyOrAttribute(element, "autocomplete");
- return autoCompleteType !== "off" ? autoCompleteType : null;
+ this.getPropertyOrAttribute(element, "autocomplete")
+ );
}
/**
@@ -1196,13 +1192,13 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
* @private
*/
private updateAutofillElementsAfterMutation() {
- if (this.updateAutofillElementsAfterMutationTimeout) {
- clearTimeout(this.updateAutofillElementsAfterMutationTimeout);
+ if (this.updateAfterMutationIdleCallback) {
+ cancelIdleCallbackPolyfill(this.updateAfterMutationIdleCallback);
}
- this.updateAutofillElementsAfterMutationTimeout = setTimeout(
+ this.updateAfterMutationIdleCallback = requestIdleCallbackPolyfill(
this.getPageDetails.bind(this),
- this.updateAfterMutationTimeoutDelay,
+ { timeout: this.updateAfterMutationTimeout },
);
}
@@ -1392,22 +1388,64 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
}
cachedAutofillFieldElement.viewable = true;
- void this.autofillOverlayContentService?.setupInlineMenuListenerOnField(
- formFieldElement,
- cachedAutofillFieldElement,
- );
+ this.setupInlineMenu(formFieldElement, cachedAutofillFieldElement);
this.intersectionObserver?.unobserve(entry.target);
}
};
+ /**
+ * Iterates over all cached field elements and sets up the inline menu listeners on each field.
+ *
+ * @param pageDetails - The page details to use for the inline menu listeners
+ */
+ private setupInlineMenuListeners(pageDetails: AutofillPageDetails) {
+ if (!this.autofillOverlayContentService) {
+ return;
+ }
+
+ this.autofillFieldElements.forEach((autofillField, formFieldElement) => {
+ this.setupInlineMenu(formFieldElement, autofillField, pageDetails);
+ });
+ }
+
+ /**
+ * Sets up the inline menu listener on the passed field element.
+ *
+ * @param formFieldElement - The form field element to set up the inline menu listener on
+ * @param autofillField - The metadata for the form field
+ * @param pageDetails - The page details to use for the inline menu listeners
+ */
+ private setupInlineMenu(
+ formFieldElement: ElementWithOpId,
+ autofillField: AutofillField,
+ pageDetails?: AutofillPageDetails,
+ ) {
+ if (!this.autofillOverlayContentService) {
+ return;
+ }
+
+ const autofillPageDetails =
+ pageDetails ||
+ this.getFormattedPageDetails(
+ this.getFormattedAutofillFormsData(),
+ this.getFormattedAutofillFieldsData(),
+ );
+
+ void this.autofillOverlayContentService.setupInlineMenu(
+ formFieldElement,
+ autofillField,
+ autofillPageDetails,
+ );
+ }
+
/**
* Destroys the CollectAutofillContentService. Clears all
* timeouts and disconnects the mutation observer.
*/
destroy() {
- if (this.updateAutofillElementsAfterMutationTimeout) {
- clearTimeout(this.updateAutofillElementsAfterMutationTimeout);
+ if (this.updateAfterMutationIdleCallback) {
+ cancelIdleCallbackPolyfill(this.updateAfterMutationIdleCallback);
}
this.mutationObserver?.disconnect();
this.intersectionObserver?.disconnect();
diff --git a/apps/browser/src/autofill/services/inline-menu-field-qualification.service.spec.ts b/apps/browser/src/autofill/services/inline-menu-field-qualification.service.spec.ts
new file mode 100644
index 00000000000..2942ba545ea
--- /dev/null
+++ b/apps/browser/src/autofill/services/inline-menu-field-qualification.service.spec.ts
@@ -0,0 +1,662 @@
+import { mock, MockProxy } from "jest-mock-extended";
+
+import AutofillField from "../models/autofill-field";
+import AutofillForm from "../models/autofill-form";
+import AutofillPageDetails from "../models/autofill-page-details";
+
+import { AutoFillConstants } from "./autofill-constants";
+import { InlineMenuFieldQualificationService } from "./inline-menu-field-qualification.service";
+
+describe("InlineMenuFieldQualificationService", () => {
+ let pageDetails: MockProxy;
+ let inlineMenuFieldQualificationService: InlineMenuFieldQualificationService;
+
+ beforeEach(() => {
+ pageDetails = mock({
+ forms: {},
+ fields: [],
+ });
+ inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService();
+ inlineMenuFieldQualificationService["inlineMenuFieldQualificationFlagSet"] = true;
+ });
+
+ describe("isFieldForLoginForm", () => {
+ describe("qualifying a password field for a login form", () => {
+ describe("an invalid password field", () => {
+ it("has a `new-password` autoCompleteType", () => {
+ const field = mock({
+ type: "password",
+ autoCompleteType: "new-password",
+ });
+
+ expect(inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails)).toBe(
+ false,
+ );
+ });
+
+ it("has a type that is an excluded type", () => {
+ AutoFillConstants.ExcludedAutofillLoginTypes.forEach((excludedType) => {
+ const field = mock({
+ type: excludedType,
+ });
+
+ expect(
+ inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails),
+ ).toBe(false);
+ });
+ });
+
+ it("has an attribute present on the FieldIgnoreList, indicating that the field is a captcha", () => {
+ AutoFillConstants.FieldIgnoreList.forEach((attribute, index) => {
+ const field = mock({
+ type: "password",
+ htmlID: index === 0 ? attribute : "",
+ htmlName: index === 1 ? attribute : "",
+ placeholder: index > 1 ? attribute : "",
+ });
+
+ expect(
+ inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails),
+ ).toBe(false);
+ });
+ });
+
+ it("has a type other than `password` or `text`", () => {
+ const field = mock({
+ type: "number",
+ htmlID: "not-password",
+ htmlName: "not-password",
+ placeholder: "not-password",
+ });
+
+ expect(inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails)).toBe(
+ false,
+ );
+ });
+
+ it("has a type of `text` without an attribute that indicates the field is a password field", () => {
+ const field = mock({
+ type: "text",
+ htmlID: "something-else",
+ htmlName: "something-else",
+ placeholder: "something-else",
+ });
+
+ expect(inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails)).toBe(
+ false,
+ );
+ });
+
+ it("has a type of `text` and contains attributes that indicates the field is a search field", () => {
+ const field = mock({
+ type: "text",
+ htmlID: "search",
+ htmlName: "something-else",
+ placeholder: "something-else",
+ });
+
+ expect(inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails)).toBe(
+ false,
+ );
+ });
+
+ describe("does not have a parent form element", () => {
+ beforeEach(() => {
+ pageDetails.forms = {};
+ });
+
+ it("on a page that has more than one password field", () => {
+ const field = mock({
+ type: "password",
+ htmlID: "user-password",
+ htmlName: "user-password",
+ placeholder: "user-password",
+ form: "",
+ });
+ const secondField = mock({
+ type: "password",
+ htmlID: "some-other-password",
+ htmlName: "some-other-password",
+ placeholder: "some-other-password",
+ });
+ pageDetails.fields = [field, secondField];
+
+ expect(
+ inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails),
+ ).toBe(false);
+ });
+
+ it("on a page that has more than one visible username field", () => {
+ const field = mock({
+ type: "password",
+ htmlID: "user-password",
+ htmlName: "user-password",
+ placeholder: "user-password",
+ form: "",
+ });
+ const usernameField = mock({
+ type: "text",
+ htmlID: "user-username",
+ htmlName: "user-username",
+ placeholder: "user-username",
+ });
+ const secondUsernameField = mock({
+ type: "text",
+ htmlID: "some-other-user-username",
+ htmlName: "some-other-user-username",
+ placeholder: "some-other-user-username",
+ });
+ pageDetails.fields = [field, usernameField, secondUsernameField];
+
+ expect(
+ inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails),
+ ).toBe(false);
+ });
+
+ it("has a disabled `autocompleteType` value", () => {
+ const field = mock({
+ type: "password",
+ htmlID: "user-password",
+ htmlName: "user-password",
+ placeholder: "user-password",
+ form: "",
+ autoCompleteType: "off",
+ });
+
+ expect(
+ inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails),
+ ).toBe(false);
+ });
+ });
+
+ describe("has a parent form element", () => {
+ let form: MockProxy;
+
+ beforeEach(() => {
+ form = mock({ opid: "validFormId" });
+ pageDetails.forms = {
+ validFormId: form,
+ };
+ });
+
+ it("is structured with other password fields in the same form", () => {
+ const field = mock({
+ type: "password",
+ htmlID: "user-password",
+ htmlName: "user-password",
+ placeholder: "user-password",
+ form: "validFormId",
+ });
+ const secondField = mock({
+ type: "password",
+ htmlID: "some-other-password",
+ htmlName: "some-other-password",
+ placeholder: "some-other-password",
+ form: "validFormId",
+ });
+ pageDetails.fields = [field, secondField];
+
+ expect(
+ inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails),
+ ).toBe(false);
+ });
+ });
+ });
+
+ describe("a valid password field", () => {
+ it("has an autoCompleteType of `current-password`", () => {
+ const field = mock({
+ type: "password",
+ autoCompleteType: "current-password",
+ htmlID: "user-password",
+ htmlName: "user-password",
+ placeholder: "user-password",
+ });
+
+ expect(inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails)).toBe(
+ true,
+ );
+ });
+
+ it("has a type of `text` with an attribute that indicates the field is a password field", () => {
+ const field = mock({
+ type: "text",
+ htmlID: null,
+ htmlName: "user-password",
+ placeholder: "user-password",
+ });
+
+ expect(inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails)).toBe(
+ true,
+ );
+ });
+
+ describe("does not have a parent form element", () => {
+ it("is the only password field on the page, has one username field on the page, and has a non-disabled `autocompleteType` value", () => {
+ pageDetails.forms = {};
+ const field = mock({
+ type: "password",
+ htmlID: "user-password",
+ htmlName: "user-password",
+ placeholder: "user-password",
+ form: "",
+ autoCompleteType: "current-password",
+ });
+ const usernameField = mock({
+ type: "text",
+ htmlID: "user-username",
+ htmlName: "user-username",
+ placeholder: "user-username",
+ });
+ pageDetails.fields = [field, usernameField];
+
+ expect(
+ inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails),
+ ).toBe(true);
+ });
+ });
+
+ describe("has a parent form element", () => {
+ let form: MockProxy;
+
+ beforeEach(() => {
+ form = mock({ opid: "validFormId" });
+ pageDetails.forms = {
+ validFormId: form,
+ };
+ });
+
+ it("is the only password field within the form and has a visible username field", () => {
+ const field = mock({
+ type: "password",
+ htmlID: "user-password",
+ htmlName: "user-password",
+ placeholder: "user-password",
+ form: "validFormId",
+ });
+ const secondPasswordField = mock({
+ type: "password",
+ htmlID: "some-other-password",
+ htmlName: "some-other-password",
+ placeholder: "some-other-password",
+ form: "anotherFormId",
+ });
+ const usernameField = mock({
+ type: "text",
+ htmlID: "user-username",
+ htmlName: "user-username",
+ placeholder: "user-username",
+ form: "validFormId",
+ });
+ pageDetails.fields = [field, secondPasswordField, usernameField];
+
+ expect(
+ inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails),
+ ).toBe(true);
+ });
+
+ it("is the only password field within the form and has a non-disabled `autocompleteType` value", () => {
+ const field = mock({
+ type: "password",
+ htmlID: "user-password",
+ htmlName: "user-password",
+ placeholder: "user-password",
+ form: "validFormId",
+ autoCompleteType: "",
+ });
+ const secondPasswordField = mock({
+ type: "password",
+ htmlID: "some-other-password",
+ htmlName: "some-other-password",
+ placeholder: "some-other-password",
+ form: "anotherFormId",
+ });
+ pageDetails.fields = [field, secondPasswordField];
+
+ expect(
+ inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails),
+ ).toBe(true);
+ });
+ });
+ });
+ });
+
+ describe("qualifying a username field for a login form", () => {
+ describe("an invalid username field", () => {
+ ["username", "email"].forEach((autoCompleteType) => {
+ it(`has a ${autoCompleteType} 'autoCompleteType' value when structured on a page with new password fields`, () => {
+ const field = mock({
+ type: "text",
+ autoCompleteType,
+ htmlID: "user-username",
+ htmlName: "user-username",
+ placeholder: "user-username",
+ });
+ const passwordField = mock({
+ type: "password",
+ autoCompleteType: "new-password",
+ htmlID: "user-password",
+ htmlName: "user-password",
+ placeholder: "user-password",
+ });
+ pageDetails.fields = [field, passwordField];
+
+ expect(
+ inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails),
+ ).toBe(false);
+ });
+ });
+
+ ["new", "change", "neue", "ändern"].forEach((keyword) => {
+ it(`has a keyword of ${keyword} that indicates a 'new or changed' username is being filled`, () => {
+ const field = mock({
+ type: "text",
+ autoCompleteType: "",
+ htmlID: "user-username",
+ htmlName: "user-username",
+ placeholder: `${keyword} username`,
+ });
+
+ expect(
+ inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails),
+ ).toBe(false);
+ });
+ });
+
+ describe("does not have a parent form element", () => {
+ beforeEach(() => {
+ pageDetails.forms = {};
+ });
+
+ it("is structured on a page with multiple password fields", () => {
+ const field = mock({
+ type: "text",
+ autoCompleteType: "",
+ htmlID: "user-username",
+ htmlName: "user-username",
+ placeholder: "user-username",
+ });
+ const passwordField = mock({
+ type: "password",
+ autoCompleteType: "current-password",
+ htmlID: "user-password",
+ htmlName: "user-password",
+ placeholder: "user-password",
+ });
+ const secondPasswordField = mock({
+ type: "password",
+ autoCompleteType: "current-password",
+ htmlID: "some-other-password",
+ htmlName: "some-other-password",
+ placeholder: "some-other-password",
+ });
+ pageDetails.fields = [field, passwordField, secondPasswordField];
+
+ expect(
+ inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails),
+ ).toBe(false);
+ });
+ });
+
+ describe("has a parent form element", () => {
+ let form: MockProxy;
+
+ beforeEach(() => {
+ form = mock({ opid: "validFormId" });
+ pageDetails.forms = {
+ validFormId: form,
+ };
+ });
+
+ it("is structured on a page with no password fields and has a disabled `autoCompleteType` value", () => {
+ const field = mock({
+ type: "text",
+ autoCompleteType: "off",
+ htmlID: "user-username",
+ htmlName: "user-username",
+ placeholder: "user-username",
+ form: "validFormId",
+ });
+ pageDetails.fields = [field];
+
+ expect(
+ inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails),
+ ).toBe(false);
+ });
+
+ it("is structured on a page with no password fields but has other types of fields in the form", () => {
+ const field = mock({
+ type: "text",
+ autoCompleteType: "",
+ htmlID: "user-username",
+ htmlName: "user-username",
+ placeholder: "user-username",
+ form: "validFormId",
+ });
+ const otherField = mock({
+ type: "number",
+ autoCompleteType: "",
+ htmlID: "some-other-field",
+ htmlName: "some-other-field",
+ placeholder: "some-other-field",
+ form: "validFormId",
+ });
+ pageDetails.fields = [field, otherField];
+
+ expect(
+ inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails),
+ ).toBe(false);
+ });
+
+ it("is structured on a page with multiple viewable password field", () => {
+ const field = mock({
+ type: "text",
+ autoCompleteType: "",
+ htmlID: "user-username",
+ htmlName: "user-username",
+ placeholder: "user-username",
+ form: "validFormId",
+ });
+ const passwordField = mock({
+ type: "password",
+ autoCompleteType: "current-password",
+ htmlID: "user-password",
+ htmlName: "user-password",
+ placeholder: "user-password",
+ form: "validFormId",
+ });
+ const secondPasswordField = mock({
+ type: "password",
+ autoCompleteType: "current-password",
+ htmlID: "some-other-password",
+ htmlName: "some-other-password",
+ placeholder: "some-other-password",
+ form: "validFormId",
+ });
+ pageDetails.fields = [field, passwordField, secondPasswordField];
+
+ expect(
+ inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails),
+ ).toBe(false);
+ });
+
+ it("is structured on a page with a with no visible password fields and but contains a disabled autocomplete type", () => {
+ const field = mock({
+ type: "text",
+ autoCompleteType: "off",
+ htmlID: "user-username",
+ htmlName: "user-username",
+ placeholder: "user-username",
+ form: "validFormId",
+ });
+ const passwordField = mock({
+ type: "password",
+ autoCompleteType: "current-password",
+ htmlID: "user-password",
+ htmlName: "user-password",
+ placeholder: "user-password",
+ form: "validFormId",
+ viewable: false,
+ });
+ pageDetails.fields = [field, passwordField];
+
+ expect(
+ inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails),
+ ).toBe(false);
+ });
+ });
+ });
+
+ describe("a valid username field", () => {
+ ["username", "email"].forEach((autoCompleteType) => {
+ it(`has a ${autoCompleteType} 'autoCompleteType' value`, () => {
+ const field = mock({
+ type: "text",
+ autoCompleteType,
+ htmlID: "user-username",
+ htmlName: "user-username",
+ placeholder: "user-username",
+ });
+ const passwordField = mock({
+ type: "password",
+ autoCompleteType: "current-password",
+ htmlID: "user-password",
+ htmlName: "user-password",
+ placeholder: "user-password",
+ });
+ pageDetails.fields = [field, passwordField];
+
+ expect(
+ inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails),
+ ).toBe(true);
+ });
+ });
+
+ describe("does not have a parent form element", () => {
+ beforeEach(() => {
+ pageDetails.forms = {};
+ });
+
+ it("is structured on a page with a single visible password field", () => {
+ const field = mock({
+ type: "text",
+ autoCompleteType: "off",
+ htmlID: "user-username",
+ htmlName: "user-username",
+ placeholder: "user-username",
+ });
+ const passwordField = mock({
+ type: "password",
+ autoCompleteType: "current-password",
+ htmlID: "user-password",
+ htmlName: "user-password",
+ placeholder: "user-password",
+ });
+ pageDetails.fields = [field, passwordField];
+
+ expect(
+ inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails),
+ ).toBe(true);
+ });
+
+ it("is structured on a page with a single non-visible password field", () => {
+ const field = mock({
+ type: "text",
+ autoCompleteType: "off",
+ htmlID: "user-username",
+ htmlName: "user-username",
+ placeholder: "user-username",
+ });
+ const passwordField = mock({
+ type: "password",
+ autoCompleteType: "current-password",
+ htmlID: "user-password",
+ htmlName: "user-password",
+ placeholder: "user-password",
+ viewable: false,
+ });
+ pageDetails.fields = [field, passwordField];
+
+ expect(
+ inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails),
+ ).toBe(true);
+ });
+
+ it("has a non-disabled autoCompleteType and is structured on a page with no other password fields", () => {
+ const field = mock({
+ type: "text",
+ autoCompleteType: "",
+ htmlID: "user-username",
+ htmlName: "user-username",
+ placeholder: "user-username",
+ });
+
+ expect(
+ inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails),
+ ).toBe(true);
+ });
+ });
+
+ describe("has a parent form element", () => {
+ let form: MockProxy;
+
+ beforeEach(() => {
+ form = mock({ opid: "validFormId" });
+ pageDetails.forms = {
+ validFormId: form,
+ };
+ });
+
+ it("is structured on a page with a single password field", () => {
+ const field = mock({
+ type: "text",
+ autoCompleteType: "",
+ htmlID: "user-username",
+ htmlName: "user-username",
+ placeholder: "user-username",
+ form: "validFormId",
+ });
+ const passwordField = mock({
+ type: "password",
+ autoCompleteType: "current-password",
+ htmlID: "user-password",
+ htmlName: "user-password",
+ placeholder: "user-password",
+ form: "validFormId",
+ });
+ pageDetails.fields = [field, passwordField];
+
+ expect(
+ inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails),
+ ).toBe(true);
+ });
+
+ it("is structured on a page with a with no visible password fields and a non-disabled autocomplete type", () => {
+ const field = mock({
+ type: "text",
+ autoCompleteType: "",
+ htmlID: "user-username",
+ htmlName: "user-username",
+ placeholder: "user-username",
+ form: "validFormId",
+ });
+ const passwordField = mock({
+ type: "password",
+ autoCompleteType: "current-password",
+ htmlID: "user-password",
+ htmlName: "user-password",
+ placeholder: "user-password",
+ form: "validFormId",
+ viewable: false,
+ });
+ pageDetails.fields = [field, passwordField];
+
+ expect(
+ inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails),
+ ).toBe(true);
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts b/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts
new file mode 100644
index 00000000000..7a96a0da95c
--- /dev/null
+++ b/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts
@@ -0,0 +1,438 @@
+import AutofillField from "../models/autofill-field";
+import AutofillPageDetails from "../models/autofill-page-details";
+import { sendExtensionMessage } from "../utils";
+
+import { InlineMenuFieldQualificationService as InlineMenuFieldQualificationsServiceInterface } from "./abstractions/inline-menu-field-qualifications.service";
+import { AutoFillConstants } from "./autofill-constants";
+
+export class InlineMenuFieldQualificationService
+ implements InlineMenuFieldQualificationsServiceInterface
+{
+ private searchFieldNamesSet = new Set(AutoFillConstants.SearchFieldNames);
+ private excludedAutofillLoginTypesSet = new Set(AutoFillConstants.ExcludedAutofillLoginTypes);
+ private usernameFieldTypes = new Set(["text", "email", "number", "tel"]);
+ private usernameAutocompleteValues = new Set(["username", "email"]);
+ private fieldIgnoreListString = AutoFillConstants.FieldIgnoreList.join(",");
+ private passwordFieldExcludeListString = AutoFillConstants.PasswordFieldExcludeList.join(",");
+ private autofillFieldKeywordsMap: WeakMap = new WeakMap();
+ private autocompleteDisabledValues = new Set(["off", "false"]);
+ private newFieldKeywords = new Set(["new", "change", "neue", "ändern"]);
+ private inlineMenuFieldQualificationFlagSet = false;
+
+ constructor() {
+ void sendExtensionMessage("getInlineMenuFieldQualificationFeatureFlag").then(
+ (getInlineMenuFieldQualificationFlag) =>
+ (this.inlineMenuFieldQualificationFlagSet = !!getInlineMenuFieldQualificationFlag?.result),
+ );
+ }
+
+ /**
+ * Validates the provided field as a field for a login form.
+ *
+ * @param field - The field to validate, should be a username or password field.
+ * @param pageDetails - The details of the page that the field is on.
+ */
+ isFieldForLoginForm(field: AutofillField, pageDetails: AutofillPageDetails): boolean {
+ if (!this.inlineMenuFieldQualificationFlagSet) {
+ return this.isFieldForLoginFormFallback(field);
+ }
+
+ const isCurrentPasswordField = this.isCurrentPasswordField(field);
+ if (isCurrentPasswordField) {
+ return this.isPasswordFieldForLoginForm(field, pageDetails);
+ }
+
+ const isUsernameField = this.isUsernameField(field);
+ if (!isUsernameField) {
+ return false;
+ }
+
+ return this.isUsernameFieldForLoginForm(field, pageDetails);
+ }
+
+ /**
+ * Validates the provided field as a password field for a login form.
+ *
+ * @param field - The field to validate
+ * @param pageDetails - The details of the page that the field is on.
+ */
+ private isPasswordFieldForLoginForm(
+ field: AutofillField,
+ pageDetails: AutofillPageDetails,
+ ): boolean {
+ // If the provided field is set with an autocomplete value of "current-password", we should assume that
+ // the page developer intends for this field to be interpreted as a password field for a login form.
+ if (field.autoCompleteType === "current-password") {
+ return true;
+ }
+
+ const usernameFieldsInPageDetails = pageDetails.fields.filter(this.isUsernameField);
+ const passwordFieldsInPageDetails = pageDetails.fields.filter(this.isCurrentPasswordField);
+
+ // If a single username and a single password field exists on the page, we
+ // should assume that this field is part of a login form.
+ if (usernameFieldsInPageDetails.length === 1 && passwordFieldsInPageDetails.length === 1) {
+ return true;
+ }
+
+ // If the field is not structured within a form, we need to identify if the field is present on
+ // a page with multiple password fields. If that isn't the case, we can assume this is a login form field.
+ const parentForm = pageDetails.forms[field.form];
+ if (!parentForm) {
+ // If no parent form is found, and multiple password fields are present, we should assume that
+ // the passed field belongs to a user account creation form.
+ if (passwordFieldsInPageDetails.length > 1) {
+ return false;
+ }
+
+ // If multiple username fields exist on the page, we should assume that
+ // the provided field is part of an account creation form.
+ const visibleUsernameFields = usernameFieldsInPageDetails.filter((f) => f.viewable);
+ if (visibleUsernameFields.length > 1) {
+ return false;
+ }
+
+ // If a single username field or less is present on the page, then we can assume that the
+ // provided field is for a login form. This will only be the case if the field does not
+ // explicitly have its autocomplete attribute set to "off" or "false".
+ return !this.autocompleteDisabledValues.has(field.autoCompleteType);
+ }
+
+ // If the field has a form parent and there are multiple visible password fields
+ // in the form, this is not a login form field
+ const visiblePasswordFieldsInPageDetails = passwordFieldsInPageDetails.filter(
+ (f) => f.form === field.form && f.viewable,
+ );
+ if (visiblePasswordFieldsInPageDetails.length > 1) {
+ return false;
+ }
+
+ // If the form has any visible username fields, we should treat the field as part of a login form
+ const visibleUsernameFields = usernameFieldsInPageDetails.filter(
+ (f) => f.form === field.form && f.viewable,
+ );
+ if (visibleUsernameFields.length > 0) {
+ return true;
+ }
+
+ // If the field has a form parent and no username field exists and the field has an
+ // autocomplete attribute set to "off" or "false", this is not a password field
+ return !this.autocompleteDisabledValues.has(field.autoCompleteType);
+ }
+
+ /**
+ * Validates the provided field as a username field for a login form.
+ *
+ * @param field - The field to validate
+ * @param pageDetails - The details of the page that the field is on.
+ */
+ private isUsernameFieldForLoginForm(
+ field: AutofillField,
+ pageDetails: AutofillPageDetails,
+ ): boolean {
+ // If the provided field is set with an autocomplete of "username", we should assume that
+ // the page developer intends for this field to be interpreted as a username field.
+ if (this.usernameAutocompleteValues.has(field.autoCompleteType)) {
+ const newPasswordFieldsInPageDetails = pageDetails.fields.filter(this.isNewPasswordField);
+ return newPasswordFieldsInPageDetails.length === 0;
+ }
+
+ // If any keywords in the field's data indicates that this is a field for a "new" or "changed"
+ // username, we should assume that this field is not for a login form.
+ if (this.keywordsFoundInFieldData(field, [...this.newFieldKeywords])) {
+ return false;
+ }
+
+ // If the field is not explicitly set as a username field, we need to qualify
+ // the field based on the other fields that are present on the page.
+ const parentForm = pageDetails.forms[field.form];
+ const passwordFieldsInPageDetails = pageDetails.fields.filter(this.isCurrentPasswordField);
+
+ // If the field is not structured within a form, we need to identify if the field is used in conjunction
+ // with a password field. If that's the case, then we should assume that it is a form field element.
+ if (!parentForm) {
+ // If a formless field is present in a webpage with a single password field, we
+ // should assume that it is part of a login workflow.
+ const visiblePasswordFieldsInPageDetails = passwordFieldsInPageDetails.filter(
+ (passwordField) => passwordField.viewable,
+ );
+ if (visiblePasswordFieldsInPageDetails.length === 1) {
+ return true;
+ }
+
+ // If more than a single password field exists on the page, we should assume that the field
+ // is part of an account creation form.
+ if (visiblePasswordFieldsInPageDetails.length > 1) {
+ return false;
+ }
+
+ // If no visible fields are found on the page, but we have a single password
+ // field we should assume that the field is part of a login form.
+ if (passwordFieldsInPageDetails.length === 1) {
+ return true;
+ }
+
+ // If the page does not contain any password fields, it might be part of a multistep login form.
+ // That will only be the case if the field does not explicitly have its autocomplete attribute
+ // set to "off" or "false".
+ return !this.autocompleteDisabledValues.has(field.autoCompleteType);
+ }
+
+ // If the field is structured within a form, but no password fields are present in the form,
+ // we need to consider whether the field is part of a multistep login form.
+ if (passwordFieldsInPageDetails.length === 0) {
+ // If the field's autocomplete is set to a disabled value, we should assume that the field is
+ // not part of a login form.
+ if (this.autocompleteDisabledValues.has(field.autoCompleteType)) {
+ return false;
+ }
+
+ // If the form that contains the field has more than one visible field, we should assume
+ // that the field is part of an account creation form.
+ const fieldsWithinForm = pageDetails.fields.filter(
+ (pageDetailsField) => pageDetailsField.form === field.form && pageDetailsField.viewable,
+ );
+ return fieldsWithinForm.length === 1;
+ }
+
+ // If a single password field exists within the page details, and that password field is part of
+ // the same form as the provided field, we should assume that the field is part of a login form.
+ const visiblePasswordFieldsInPageDetails = passwordFieldsInPageDetails.filter(
+ (passwordField) => passwordField.form === field.form && passwordField.viewable,
+ );
+ if (visiblePasswordFieldsInPageDetails.length === 1) {
+ return true;
+ }
+
+ // If multiple visible password fields exist within the page details, we need to assume that the
+ // provided field is part of an account creation form.
+ if (visiblePasswordFieldsInPageDetails.length > 1) {
+ return false;
+ }
+
+ // If no visible password fields are found, this field might be part of a multipart form.
+ // Check for an invalid autocompleteType to determine if the field is part of a login form.
+ return !this.autocompleteDisabledValues.has(field.autoCompleteType);
+ }
+
+ /**
+ * Validates the provided field as a username field.
+ *
+ * @param field - The field to validate
+ */
+ private isUsernameField = (field: AutofillField): boolean => {
+ if (
+ !this.usernameFieldTypes.has(field.type) ||
+ this.isExcludedFieldType(field, this.excludedAutofillLoginTypesSet)
+ ) {
+ return false;
+ }
+
+ return this.keywordsFoundInFieldData(field, AutoFillConstants.UsernameFieldNames);
+ };
+
+ /**
+ * Validates the provided field as a current password field.
+ *
+ * @param field - The field to validate
+ */
+ private isCurrentPasswordField = (field: AutofillField): boolean => {
+ if (field.autoCompleteType === "new-password") {
+ return false;
+ }
+
+ return this.isPasswordField(field);
+ };
+
+ /**
+ * Validates the provided field as a new password field.
+ *
+ * @param field - The field to validate
+ */
+ private isNewPasswordField = (field: AutofillField): boolean => {
+ if (field.autoCompleteType === "current-password") {
+ return false;
+ }
+
+ return this.isPasswordField(field);
+ };
+
+ /**
+ * Validates the provided field as a password field.
+ *
+ * @param field - The field to validate
+ */
+ private isPasswordField = (field: AutofillField): boolean => {
+ const isInputPasswordType = field.type === "password";
+ if (
+ (!isInputPasswordType &&
+ this.isExcludedFieldType(field, this.excludedAutofillLoginTypesSet)) ||
+ this.fieldHasDisqualifyingAttributeValue(field)
+ ) {
+ return false;
+ }
+
+ return isInputPasswordType || this.isLikePasswordField(field);
+ };
+
+ /**
+ * Validates the provided field as a field to indicate if the
+ * field potentially acts as a password field.
+ *
+ * @param field - The field to validate
+ */
+ private isLikePasswordField(field: AutofillField): boolean {
+ if (field.type !== "text") {
+ return false;
+ }
+
+ const testedValues = [field.htmlID, field.htmlName, field.placeholder];
+ for (let i = 0; i < testedValues.length; i++) {
+ if (this.valueIsLikePassword(testedValues[i])) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Validates the provided value to indicate if the value is like a password.
+ *
+ * @param value - The value to validate
+ */
+ private valueIsLikePassword(value: string): boolean {
+ if (value == null) {
+ return false;
+ }
+ // Removes all whitespace, _ and - characters
+ const cleanedValue = value.toLowerCase().replace(/[\s_-]/g, "");
+
+ if (cleanedValue.indexOf("password") < 0) {
+ return false;
+ }
+
+ return !(this.passwordFieldExcludeListString.indexOf(cleanedValue) > -1);
+ }
+
+ /**
+ * Validates the provided field to indicate if the field has a
+ * disqualifying attribute that would impede autofill entirely.
+ *
+ * @param field - The field to validate
+ */
+ private fieldHasDisqualifyingAttributeValue(field: AutofillField): boolean {
+ const checkedAttributeValues = [field.htmlID, field.htmlName, field.placeholder];
+
+ for (let i = 0; i < checkedAttributeValues.length; i++) {
+ const checkedAttributeValue = checkedAttributeValues[i];
+ const cleanedValue = checkedAttributeValue?.toLowerCase().replace(/[\s_-]/g, "");
+
+ if (cleanedValue && this.fieldIgnoreListString.indexOf(cleanedValue) > -1) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Validates the provided field to indicate if the field is excluded from autofill.
+ *
+ * @param field - The field to validate
+ * @param excludedTypes - The set of excluded types
+ */
+ private isExcludedFieldType(field: AutofillField, excludedTypes: Set): boolean {
+ if (excludedTypes.has(field.type)) {
+ return true;
+ }
+
+ return this.isSearchField(field);
+ }
+
+ /**
+ * Validates the provided field to indicate if the field is a search field.
+ *
+ * @param field - The field to validate
+ */
+ private isSearchField(field: AutofillField): boolean {
+ const matchFieldAttributeValues = [field.type, field.htmlName, field.htmlID, field.placeholder];
+ for (let attrIndex = 0; attrIndex < matchFieldAttributeValues.length; attrIndex++) {
+ if (!matchFieldAttributeValues[attrIndex]) {
+ continue;
+ }
+
+ // Separate camel case words and case them to lower case values
+ const camelCaseSeparatedFieldAttribute = matchFieldAttributeValues[attrIndex]
+ .replace(/([a-z])([A-Z])/g, "$1 $2")
+ .toLowerCase();
+ // Split the attribute by non-alphabetical characters to get the keywords
+ const attributeKeywords = camelCaseSeparatedFieldAttribute.split(/[^a-z]/gi);
+
+ for (let keywordIndex = 0; keywordIndex < attributeKeywords.length; keywordIndex++) {
+ if (this.searchFieldNamesSet.has(attributeKeywords[keywordIndex])) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Validates the provided field to indicate if the field has any of the provided keywords.
+ *
+ * @param autofillFieldData - The field data to search for keywords
+ * @param keywords - The keywords to search for
+ */
+ private keywordsFoundInFieldData(autofillFieldData: AutofillField, keywords: string[]) {
+ const searchedString = this.getAutofillFieldDataKeywords(autofillFieldData);
+ return keywords.some((keyword) => searchedString.includes(keyword));
+ }
+
+ /**
+ * Retrieves the keywords from the provided autofill field data.
+ *
+ * @param autofillFieldData - The field data to search for keywords
+ */
+ private getAutofillFieldDataKeywords(autofillFieldData: AutofillField) {
+ if (this.autofillFieldKeywordsMap.has(autofillFieldData)) {
+ return this.autofillFieldKeywordsMap.get(autofillFieldData);
+ }
+
+ const keywordValues = [
+ autofillFieldData.htmlID,
+ autofillFieldData.htmlName,
+ autofillFieldData.htmlClass,
+ autofillFieldData.type,
+ autofillFieldData.title,
+ autofillFieldData.placeholder,
+ autofillFieldData.autoCompleteType,
+ autofillFieldData["label-data"],
+ autofillFieldData["label-aria"],
+ autofillFieldData["label-left"],
+ autofillFieldData["label-right"],
+ autofillFieldData["label-tag"],
+ autofillFieldData["label-top"],
+ ]
+ .join(",")
+ .toLowerCase();
+ this.autofillFieldKeywordsMap.set(autofillFieldData, keywordValues);
+
+ return keywordValues;
+ }
+
+ /**
+ * This method represents the previous rudimentary approach to qualifying fields for login forms.
+ *
+ * @param field - The field to validate
+ * @deprecated - This method will only be used when the fallback flag is set to true.
+ */
+ private isFieldForLoginFormFallback(field: AutofillField): boolean {
+ if (field.type === "password") {
+ return true;
+ }
+
+ return this.isUsernameField(field);
+ }
+}
diff --git a/apps/browser/src/autofill/services/insert-autofill-content.service.spec.ts b/apps/browser/src/autofill/services/insert-autofill-content.service.spec.ts
index 73ee31e00d5..ff0e82d664d 100644
--- a/apps/browser/src/autofill/services/insert-autofill-content.service.spec.ts
+++ b/apps/browser/src/autofill/services/insert-autofill-content.service.spec.ts
@@ -1,9 +1,12 @@
+import { mock } from "jest-mock-extended";
+
import { EVENTS } from "@bitwarden/common/autofill/constants";
import AutofillScript, { FillScript, FillScriptActions } from "../models/autofill-script";
import { mockQuerySelectorAllDefinedCall } from "../spec/testing-utils";
import { FillableFormFieldElement, FormElementWithAttribute, FormFieldElement } from "../types";
+import { InlineMenuFieldQualificationService } from "./abstractions/inline-menu-field-qualifications.service";
import { AutofillOverlayContentService } from "./autofill-overlay-content.service";
import CollectAutofillContentService from "./collect-autofill-content.service";
import DomElementVisibilityService from "./dom-element-visibility.service";
@@ -64,8 +67,11 @@ function setMockWindowLocation({
}
describe("InsertAutofillContentService", () => {
+ const inlineMenuFieldQualificationService = mock();
const domElementVisibilityService = new DomElementVisibilityService();
- const autofillOverlayContentService = new AutofillOverlayContentService();
+ const autofillOverlayContentService = new AutofillOverlayContentService(
+ inlineMenuFieldQualificationService,
+ );
const collectAutofillContentService = new CollectAutofillContentService(
domElementVisibilityService,
autofillOverlayContentService,
diff --git a/apps/browser/src/autofill/utils/index.ts b/apps/browser/src/autofill/utils/index.ts
index 3785e2a3c12..829cb8a4ee1 100644
--- a/apps/browser/src/autofill/utils/index.ts
+++ b/apps/browser/src/autofill/utils/index.ts
@@ -21,7 +21,10 @@ export function generateRandomChars(length: number): string {
* @param callback - The callback function to run when the browser is idle.
* @param options - The options to pass to the requestIdleCallback function.
*/
-export function requestIdleCallbackPolyfill(callback: () => void, options?: Record) {
+export function requestIdleCallbackPolyfill(
+ callback: () => void,
+ options?: Record,
+): number | NodeJS.Timeout {
if ("requestIdleCallback" in globalThis) {
return globalThis.requestIdleCallback(() => callback(), options);
}
@@ -29,6 +32,19 @@ export function requestIdleCallbackPolyfill(callback: () => void, options?: Reco
return globalThis.setTimeout(() => callback(), 1);
}
+/**
+ * Polyfills the cancelIdleCallback API with a clearTimeout fallback.
+ *
+ * @param id - The ID of the idle callback to cancel.
+ */
+export function cancelIdleCallbackPolyfill(id: NodeJS.Timeout | number) {
+ if ("cancelIdleCallback" in globalThis) {
+ return globalThis.cancelIdleCallback(id as number);
+ }
+
+ return globalThis.clearTimeout(id);
+}
+
/**
* Generates a random string of characters that formatted as a custom element name.
*/
diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts
index 93eda92cf5d..fb5d11d81ce 100644
--- a/apps/browser/src/background/main.background.ts
+++ b/apps/browser/src/background/main.background.ts
@@ -745,7 +745,6 @@ export default class MainBackground {
this.folderApiService = new FolderApiService(this.folderService, this.apiService);
this.userVerificationService = new UserVerificationService(
- this.stateService,
this.cryptoService,
this.accountService,
this.masterPasswordService,
diff --git a/apps/browser/src/background/runtime.background.ts b/apps/browser/src/background/runtime.background.ts
index a1a5de54d21..94e96e2dc89 100644
--- a/apps/browser/src/background/runtime.background.ts
+++ b/apps/browser/src/background/runtime.background.ts
@@ -69,6 +69,7 @@ export default class RuntimeBackground {
const messagesWithResponse = [
"biometricUnlock",
"getUseTreeWalkerApiForPageDetailsCollectionFeatureFlag",
+ "getInlineMenuFieldQualificationFeatureFlag",
];
if (messagesWithResponse.includes(msg.command)) {
@@ -186,6 +187,9 @@ export default class RuntimeBackground {
FeatureFlag.UseTreeWalkerApiForPageDetailsCollection,
);
}
+ case "getInlineMenuFieldQualificationFeatureFlag": {
+ return await this.configService.getFeatureFlag(FeatureFlag.InlineMenuFieldQualification);
+ }
}
}
diff --git a/apps/browser/src/manifest.json b/apps/browser/src/manifest.json
index 4bb7e82fba6..8e97cb6d625 100644
--- a/apps/browser/src/manifest.json
+++ b/apps/browser/src/manifest.json
@@ -2,7 +2,7 @@
"manifest_version": 2,
"name": "__MSG_extName__",
"short_name": "__MSG_appName__",
- "version": "2024.6.0",
+ "version": "2024.6.1",
"description": "__MSG_extDesc__",
"default_locale": "en",
"author": "Bitwarden Inc.",
diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json
index 3f3e2747cc3..3881b1b4499 100644
--- a/apps/browser/src/manifest.v3.json
+++ b/apps/browser/src/manifest.v3.json
@@ -3,7 +3,7 @@
"minimum_chrome_version": "102.0",
"name": "__MSG_extName__",
"short_name": "__MSG_appName__",
- "version": "2024.6.0",
+ "version": "2024.6.1",
"description": "__MSG_extDesc__",
"default_locale": "en",
"author": "Bitwarden Inc.",
diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts
index 1109ab73adf..51152ba0f71 100644
--- a/apps/browser/src/popup/app-routing.module.ts
+++ b/apps/browser/src/popup/app-routing.module.ts
@@ -8,6 +8,16 @@ import {
tdeDecryptionRequiredGuard,
unauthGuardFn,
} from "@bitwarden/angular/auth/guards";
+import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard";
+import {
+ AnonLayoutWrapperComponent,
+ AnonLayoutWrapperData,
+ RegistrationFinishComponent,
+ RegistrationStartComponent,
+ RegistrationStartSecondaryComponent,
+ RegistrationStartSecondaryComponentData,
+} from "@bitwarden/auth/angular";
+import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { fido2AuthGuard } from "../auth/guards/fido2-auth.guard";
import { AccountSwitcherComponent } from "../auth/popup/account-switching/account-switcher.component";
@@ -343,6 +353,45 @@ const routes: Routes = [
canActivate: [AuthGuard],
data: { state: "update-temp-password" },
},
+ {
+ path: "",
+ component: AnonLayoutWrapperComponent,
+ children: [
+ {
+ path: "signup",
+ canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()],
+ data: { pageTitle: "createAccount" } satisfies AnonLayoutWrapperData,
+ children: [
+ {
+ path: "",
+ component: RegistrationStartComponent,
+ },
+ {
+ path: "",
+ component: RegistrationStartSecondaryComponent,
+ outlet: "secondary",
+ data: {
+ loginRoute: "/home",
+ } satisfies RegistrationStartSecondaryComponentData,
+ },
+ ],
+ },
+ {
+ path: "finish-signup",
+ canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()],
+ data: {
+ pageTitle: "setAStrongPassword",
+ pageSubtitle: "finishCreatingYourAccountBySettingAPassword",
+ } satisfies AnonLayoutWrapperData,
+ children: [
+ {
+ path: "",
+ component: RegistrationFinishComponent,
+ },
+ ],
+ },
+ ],
+ },
...extensionRefreshSwap(AboutPageComponent, AboutPageV2Component, {
path: "about",
canActivate: [AuthGuard],
diff --git a/apps/browser/src/vault/popup/components/vault/vault-select.component.html b/apps/browser/src/vault/popup/components/vault/vault-select.component.html
index 84c5f48a4e8..4f6ce3a11e6 100644
--- a/apps/browser/src/vault/popup/components/vault/vault-select.component.html
+++ b/apps/browser/src/vault/popup/components/vault/vault-select.component.html
@@ -57,12 +57,12 @@
>
diff --git a/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts b/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts
index f08f4e836e1..0b40b136ab9 100644
--- a/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts
+++ b/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts
@@ -5,7 +5,7 @@ import { BehaviorSubject } from "rxjs";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
-import { ProductType } from "@bitwarden/common/enums";
+import { ProductTierType } from "@bitwarden/common/billing/enums";
import { ObservableTracker } from "@bitwarden/common/spec";
import { CipherId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
@@ -78,7 +78,7 @@ describe("VaultPopupItemsService", () => {
mockOrg = {
id: "org1",
name: "Organization 1",
- planProductType: ProductType.Enterprise,
+ productTierType: ProductTierType.Enterprise,
} as Organization;
mockCollections = [
diff --git a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts
index b89de79a209..f6573de1c80 100644
--- a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts
+++ b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts
@@ -6,7 +6,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
-import { ProductType } from "@bitwarden/common/enums";
+import { ProductTierType } from "@bitwarden/common/billing/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
@@ -206,7 +206,7 @@ describe("VaultPopupListFiltersService", () => {
name: "family org",
id: "1234-3323-23223",
enabled: true,
- planProductType: ProductType.Families,
+ productTierType: ProductTierType.Families,
},
] as Organization[];
@@ -224,7 +224,7 @@ describe("VaultPopupListFiltersService", () => {
name: "free org",
id: "1234-3323-23223",
enabled: true,
- planProductType: ProductType.Free,
+ productTierType: ProductTierType.Free,
},
] as Organization[];
@@ -242,7 +242,7 @@ describe("VaultPopupListFiltersService", () => {
name: "free org",
id: "1234-3323-23223",
enabled: false,
- planProductType: ProductType.Free,
+ productTierType: ProductTierType.Free,
},
] as Organization[];
diff --git a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts
index 8242637d5de..67213163a64 100644
--- a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts
+++ b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts
@@ -16,7 +16,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
-import { ProductType } from "@bitwarden/common/enums";
+import { ProductTierType } from "@bitwarden/common/billing/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
@@ -216,8 +216,8 @@ export class VaultPopupListFiltersService {
// Show a warning icon if the organization is deactivated
icon = "bwi-exclamation-triangle tw-text-danger";
} else if (
- org.planProductType === ProductType.Families ||
- org.planProductType === ProductType.Free
+ org.productTierType === ProductTierType.Families ||
+ org.productTierType === ProductTierType.Free
) {
// Show a family icon if the organization is a family or free org
icon = "bwi-family";
diff --git a/apps/browser/src/vault/popup/views/popup-cipher.view.ts b/apps/browser/src/vault/popup/views/popup-cipher.view.ts
index 4707eb9eb0f..5bb1905c59a 100644
--- a/apps/browser/src/vault/popup/views/popup-cipher.view.ts
+++ b/apps/browser/src/vault/popup/views/popup-cipher.view.ts
@@ -1,5 +1,5 @@
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
-import { ProductType } from "@bitwarden/common/enums";
+import { ProductTierType } from "@bitwarden/common/billing/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
@@ -26,13 +26,13 @@ export class PopupCipherView extends CipherView {
* Get the bwi icon for the cipher according to the organization type.
*/
get orgIcon(): "bwi-family" | "bwi-business" | null {
- switch (this.organization?.planProductType) {
- case ProductType.Free:
- case ProductType.Families:
+ switch (this.organization?.productTierType) {
+ case ProductTierType.Free:
+ case ProductTierType.Families:
return "bwi-family";
- case ProductType.Teams:
- case ProductType.Enterprise:
- case ProductType.TeamsStarter:
+ case ProductTierType.Teams:
+ case ProductTierType.Enterprise:
+ case ProductTierType.TeamsStarter:
return "bwi-business";
default:
return null;
diff --git a/apps/browser/store/locales/ko/copy.resx b/apps/browser/store/locales/ko/copy.resx
index 595663b1cae..a2fc4e19858 100644
--- a/apps/browser/store/locales/ko/copy.resx
+++ b/apps/browser/store/locales/ko/copy.resx
@@ -118,7 +118,7 @@
System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
- Bitwarden Password Manager
+ Bitwarden 비밀번호 관리자집에서도, 직장에서도, 이동 중에도 Bitwarden은 비밀번호, 패스키, 민감 정보를 쉽게 보호합니다.
diff --git a/apps/browser/store/locales/uk/copy.resx b/apps/browser/store/locales/uk/copy.resx
index e74811a7751..6f994cd0bf1 100644
--- a/apps/browser/store/locales/uk/copy.resx
+++ b/apps/browser/store/locales/uk/copy.resx
@@ -124,48 +124,48 @@
Вдома, на роботі чи в дорозі, Bitwarden захищає ваші паролі, ключі доступу та конфіденційну інформацію.
- Визнаний найкращим менеджером паролів за версією PCMag, WIRED, The Verge, CNET, G2 та інших!
+ Визнаний найкращим менеджером паролів виданнями PCMag, WIRED, The Verge, CNET, G2 та іншими!
ЗАХИСТІТЬ СВОЄ ЦИФРОВЕ ЖИТТЯ
-Убезпечте своє цифрове життя та захистіться від витоку даних, створивши та зберігши унікальні, надійні паролі для кожного облікового запису. Зберігайте всі дані в наскрізному зашифрованому сховищі паролів, доступ до якого маєте лише ви.
+Убезпечте своє цифрове життя та захистіться від витоків даних, генеруючи та зберігаючи унікальні надійні паролі для кожного облікового запису. Зберігайте все в наскрізно зашифрованому сховищі паролів, доступ до якого маєте тільки ви.
-ОТРИМУВАТИ ДОСТУП ДО СВОЇХ ДАНИХ БУДЬ-ДЕ, БУДЬ-КОЛИ, БУДЬ НА ЯКОМУ ПРИСТРОЇ
-Легко керуйте, зберігайте, захищайте та діліться необмеженою кількістю паролів на необмеженій кількості пристроїв без обмежень.
+ДОСТУП ДО ДАНИХ БУДЬ-ДЕ, БУДЬ-КОЛИ, НА БУДЬ-ЯКОМУ ПРИСТРОЇ
+Легко керуйте, зберігайте, захищайте та діліться необмеженою кількістю паролів на необмеженій кількості пристроїв.
-КОЖЕН ПОВИНЕН МАТИ ІНСТРУМЕНТИ, ЩОБ ЗАЛИШАТИСЯ В БЕЗПЕЦІ В ІНТЕРНЕТІ
-Користуйтеся Bitwarden безкоштовно, без реклами і без продажу даних. Bitwarden вважає, що кожен повинен мати можливість залишатися в безпеці в Інтернеті. Преміум-плани пропонують доступ до розширених функцій.
+КОЖЕН ПОВИНЕН МАТИ ІНСТРУМЕНТИ ДЛЯ БЕЗПЕКИ В ІНТЕРНЕТІ
+Використовуйте Bitwarden безплатно без реклами або продажу даних. Bitwarden вважає, що кожен повинен мати можливість залишатися в безпеці в Інтернеті. Завдяки тарифним планам Преміум можна отримати доступ до розширених можливостей.
-РОЗШИРЮЙТЕ МОЖЛИВОСТІ СВОЇХ КОМАНД ЗА ДОПОМОГОЮ BITWARDEN
-Плани для Команд та Підприємства містять професійні бізнес-функції. Деякі приклади включають інтеграцію SSO, самостійний хостинг, інтеграцію каталогів і забезпечення SCIM, глобальні політики, доступ до API, журнали подій і багато іншого.
+РОЗШИРТЕ МОЖЛИВОСТІ СВОЇХ КОМАНД ЗА ДОПОМОГОЮ BITWARDEN
+Плани для команд і компаній мають професійні бізнес-функції. Вони передбачають інтеграцію єдиного входу (SSO), власне розміщення, інтеграцію каталогів та забезпечення SCIM, глобальні політики, доступ до API, журнали подій тощо.
-Використовуйте Bitwarden, щоб захистити своїх співробітників і ділитися конфіденційною інформацією з колегами.
+Використовуйте Bitwarden для захисту персоналу та обміну конфіденційною інформацією з колегами.
-Більше причин вибрати Bitwarden:
+Інші причини для вибору Bitwarden:
Шифрування світового класу
-Паролі захищені вдосконаленим наскрізним шифруванням (біт AES-256, солоний хештег і PBKDF2 SHA-256), тому ваші дані залишаються надійно захищеними та конфіденційними.
+Паролі захищаються розширеним наскрізним шифруванням (AES-256, сіллю хешування і PBKDF2 SHA-256), тому ваші дані завжди зберігаються приватно і в безпеці.
-Аудит третьої сторони
-Bitwarden регулярно проводить комплексні сторонні аудити безпеки з відомими компаніями, що займаються безпекою. Ці щорічні аудити включають оцінку вихідного коду та тестування на проникнення на всіх IP-адресах, серверах і веб-додатках Bitwarden.
+Сторонні аудити
+Bitwarden регулярно проводить комплексні аудити безпеки із залученням третіх сторін – відомих компаній у сфері безпеки. Під час цих щорічних аудитів проводиться оцінка програмного коду і тестування на проникнення через IP-адреси Bitwarden, сервери та вебпрограми.
-Розширений 2FA
-Захистіть свій вхід за допомогою стороннього автентифікатора, кодів, надісланих електронною поштою, або облікових даних FIDO2 WebAuthn, наприклад, апаратного ключа безпеки або пароля.
+Розширена 2FA
+Захистіть свої дані входу за допомогою стороннього автентифікатора, кодів, що надсилаються електронною поштою, або облікових даних FIDO2 WebAuthn, як-от апаратний ключ безпеки або ключ доступу.
-Bitwarden Send
-Передавайте дані безпосередньо іншим, зберігаючи при цьому наскрізну зашифровану безпеку та обмежуючи вразливість.
+Відправлення Bitwarden
+Передавайте дані безпосередньо іншим користувачам, зберігаючи наскрізне шифрування та обмежуючи їх викриття.
Вбудований генератор
-Створюйте довгі, складні та відмінні паролі та унікальні імена користувачів для кожного сайту, який ви відвідуєте. Інтеграція з провайдерами псевдонімів електронної пошти для додаткової конфіденційності.
+Створюйте довгі, складні та чіткі паролі, а також унікальні імена користувачів для кожного сайту, який ви відвідуєте. Користуйтеся інтеграцією з провайдерами псевдонімів електронної пошти для забезпечення додаткової приватності.
-Глобальні переклади
-Переклади Bitwarden існують для більш ніж 60 мов, перекладені світовою спільнотою за допомогою Crowdin.
+Переклад різними мовами
+Bitwarden перекладено понад 60 мовами завдяки зусиллям нашої світової спільноти на Crowdin.
-Крос-платформні додатки
-Захищайте конфіденційні дані у своєму сховищі Bitwarden Vault та діліться ними з будь-якого браузера, мобільного пристрою, настільної операційної системи тощо.
+Програми для різних платформ
+Захищайте та діліться конфіденційними даними в межах свого сховища Bitwarden з будь-якого браузера, мобільного пристрою або комп'ютерної ОС, а також інших можливостей.
-Bitwarden захищає більше, ніж просто паролі
-Наскрізні рішення для управління зашифрованими обліковими даними від Bitwarden дозволяють організаціям захистити все, включаючи секрети розробників і досвід роботи з ключами. Відвідайте Bitwarden.com, щоб дізнатися більше про Bitwarden Secrets Manager і Bitwarden Passwordless.dev!
+Bitwarden захищає не лише паролі
+Комплексні рішення для керування наскрізно зашифрованими обліковими даними від Bitwarden дають змогу організаціям захищати все, включно з секретами розробників та ключами доступу. Відвідайте Bitwarden.com, щоб дізнатися більше про Менеджер секретів Bitwarden і Bitwarden Passwordless.dev!
diff --git a/apps/browser/tailwind.config.js b/apps/browser/tailwind.config.js
index be5c9ce4d96..db1dd55694e 100644
--- a/apps/browser/tailwind.config.js
+++ b/apps/browser/tailwind.config.js
@@ -4,6 +4,7 @@ const config = require("../../libs/components/tailwind.config.base");
config.content = [
"./src/**/*.{html,ts}",
"../../libs/components/src/**/*.{html,ts}",
+ "../../libs/auth/src/**/*.{html,ts}",
"../../libs/angular/src/**/*.{html,ts}",
];
diff --git a/apps/cli/src/auth/commands/unlock.command.ts b/apps/cli/src/auth/commands/unlock.command.ts
index e3bb9257fac..d767ee80b37 100644
--- a/apps/cli/src/auth/commands/unlock.command.ts
+++ b/apps/cli/src/auth/commands/unlock.command.ts
@@ -1,19 +1,18 @@
import { firstValueFrom, map } from "rxjs";
-import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
-import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
-import { SecretVerificationRequest } from "@bitwarden/common/auth/models/request/secret-verification.request";
+import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
+import { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
+import { MasterPasswordVerification } from "@bitwarden/common/auth/types/verification";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
-import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
-import { HashPurpose } from "@bitwarden/common/platform/enums";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service";
+import { MasterKey } from "@bitwarden/common/types/key";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { ConvertToKeyConnectorCommand } from "../../commands/convert-to-key-connector.command";
@@ -26,16 +25,14 @@ export class UnlockCommand {
private accountService: AccountService,
private masterPasswordService: InternalMasterPasswordServiceAbstraction,
private cryptoService: CryptoService,
- private stateService: StateService,
+ private userVerificationService: UserVerificationService,
private cryptoFunctionService: CryptoFunctionService,
- private apiService: ApiService,
private logService: ConsoleLogService,
private keyConnectorService: KeyConnectorService,
private environmentService: EnvironmentService,
private syncService: SyncService,
private organizationApiService: OrganizationApiServiceAbstraction,
private logout: () => Promise,
- private kdfConfigService: KdfConfigService,
) {}
async run(password: string, cmdOptions: Record) {
@@ -52,62 +49,43 @@ export class UnlockCommand {
const [userId, email] = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => [a?.id, a?.email])),
);
- const kdfConfig = await this.kdfConfigService.getKdfConfig();
- const masterKey = await this.cryptoService.makeMasterKey(password, email, kdfConfig);
- const storedMasterKeyHash = await firstValueFrom(
- this.masterPasswordService.masterKeyHash$(userId),
- );
- let passwordValid = false;
- if (masterKey != null) {
- if (storedMasterKeyHash != null) {
- passwordValid = await this.cryptoService.compareAndUpdateKeyHash(password, masterKey);
- } else {
- const serverKeyHash = await this.cryptoService.hashMasterKey(
- password,
- masterKey,
- HashPurpose.ServerAuthorization,
- );
- const request = new SecretVerificationRequest();
- request.masterPasswordHash = serverKeyHash;
- try {
- await this.apiService.postAccountVerifyPassword(request);
- passwordValid = true;
- const localKeyHash = await this.cryptoService.hashMasterKey(
- password,
- masterKey,
- HashPurpose.LocalAuthorization,
- );
- await this.masterPasswordService.setMasterKeyHash(localKeyHash, userId);
- } catch {
- // Ignore
- }
+ const verification = {
+ type: VerificationType.MasterPassword,
+ secret: password,
+ } as MasterPasswordVerification;
+
+ let masterKey: MasterKey;
+ try {
+ const response = await this.userVerificationService.verifyUserByMasterPassword(
+ verification,
+ userId,
+ email,
+ );
+ masterKey = response.masterKey;
+ } catch (e) {
+ // verification failure throws
+ return Response.error(e.message);
+ }
+
+ const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey);
+ await this.cryptoService.setUserKey(userKey);
+
+ if (await this.keyConnectorService.getConvertAccountRequired()) {
+ const convertToKeyConnectorCommand = new ConvertToKeyConnectorCommand(
+ this.keyConnectorService,
+ this.environmentService,
+ this.syncService,
+ this.organizationApiService,
+ this.logout,
+ );
+ const convertResponse = await convertToKeyConnectorCommand.run();
+ if (!convertResponse.success) {
+ return convertResponse;
}
}
- if (passwordValid) {
- await this.masterPasswordService.setMasterKey(masterKey, userId);
- const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey);
- await this.cryptoService.setUserKey(userKey);
-
- if (await this.keyConnectorService.getConvertAccountRequired()) {
- const convertToKeyConnectorCommand = new ConvertToKeyConnectorCommand(
- this.keyConnectorService,
- this.environmentService,
- this.syncService,
- this.organizationApiService,
- this.logout,
- );
- const convertResponse = await convertToKeyConnectorCommand.run();
- if (!convertResponse.success) {
- return convertResponse;
- }
- }
-
- return this.successResponse();
- } else {
- return Response.error("Invalid master password.");
- }
+ return this.successResponse();
}
private async setNewSessionKey() {
diff --git a/apps/cli/src/base-program.ts b/apps/cli/src/base-program.ts
index 46aadc323c3..563b205fa74 100644
--- a/apps/cli/src/base-program.ts
+++ b/apps/cli/src/base-program.ts
@@ -140,16 +140,14 @@ export abstract class BaseProgram {
this.serviceContainer.accountService,
this.serviceContainer.masterPasswordService,
this.serviceContainer.cryptoService,
- this.serviceContainer.stateService,
+ this.serviceContainer.userVerificationService,
this.serviceContainer.cryptoFunctionService,
- this.serviceContainer.apiService,
this.serviceContainer.logService,
this.serviceContainer.keyConnectorService,
this.serviceContainer.environmentService,
this.serviceContainer.syncService,
this.serviceContainer.organizationApiService,
this.serviceContainer.logout,
- this.serviceContainer.kdfConfigService,
);
const response = await command.run(null, null);
if (!response.success) {
diff --git a/apps/cli/src/commands/get.command.ts b/apps/cli/src/commands/get.command.ts
index 142f0576b50..a91df2a1caa 100644
--- a/apps/cli/src/commands/get.command.ts
+++ b/apps/cli/src/commands/get.command.ts
@@ -468,7 +468,7 @@ export class GetCommand extends DownloadCommand {
if (Utils.isGuid(id)) {
org = await this.organizationService.getFromState(id);
} else if (id.trim() !== "") {
- let orgs = await this.organizationService.getAll();
+ let orgs = await firstValueFrom(this.organizationService.organizations$);
orgs = CliUtils.searchOrganizations(orgs, id);
if (orgs.length > 1) {
return Response.multipleResults(orgs.map((c) => c.id));
diff --git a/apps/cli/src/commands/list.command.ts b/apps/cli/src/commands/list.command.ts
index 63ec13a8c97..536c9e3b8c2 100644
--- a/apps/cli/src/commands/list.command.ts
+++ b/apps/cli/src/commands/list.command.ts
@@ -1,3 +1,5 @@
+import { firstValueFrom } from "rxjs";
+
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
@@ -239,7 +241,7 @@ export class ListCommand {
}
private async listOrganizations(options: Options) {
- let organizations = await this.organizationService.getAll();
+ let organizations = await firstValueFrom(this.organizationService.memberOrganizations$);
if (options.search != null && options.search.trim() !== "") {
organizations = CliUtils.searchOrganizations(organizations, options.search);
diff --git a/apps/cli/src/oss-serve-configurator.ts b/apps/cli/src/oss-serve-configurator.ts
index 970be7a4bb9..13f50da78b7 100644
--- a/apps/cli/src/oss-serve-configurator.ts
+++ b/apps/cli/src/oss-serve-configurator.ts
@@ -120,16 +120,14 @@ export class OssServeConfigurator {
this.serviceContainer.accountService,
this.serviceContainer.masterPasswordService,
this.serviceContainer.cryptoService,
- this.serviceContainer.stateService,
+ this.serviceContainer.userVerificationService,
this.serviceContainer.cryptoFunctionService,
- this.serviceContainer.apiService,
this.serviceContainer.logService,
this.serviceContainer.keyConnectorService,
this.serviceContainer.environmentService,
this.serviceContainer.syncService,
this.serviceContainer.organizationApiService,
async () => await this.serviceContainer.logout(),
- this.serviceContainer.kdfConfigService,
);
this.sendCreateCommand = new SendCreateCommand(
diff --git a/apps/cli/src/program.ts b/apps/cli/src/program.ts
index b8ddca11de3..37c838b6646 100644
--- a/apps/cli/src/program.ts
+++ b/apps/cli/src/program.ts
@@ -270,16 +270,14 @@ export class Program extends BaseProgram {
this.serviceContainer.accountService,
this.serviceContainer.masterPasswordService,
this.serviceContainer.cryptoService,
- this.serviceContainer.stateService,
+ this.serviceContainer.userVerificationService,
this.serviceContainer.cryptoFunctionService,
- this.serviceContainer.apiService,
this.serviceContainer.logService,
this.serviceContainer.keyConnectorService,
this.serviceContainer.environmentService,
this.serviceContainer.syncService,
this.serviceContainer.organizationApiService,
async () => await this.serviceContainer.logout(),
- this.serviceContainer.kdfConfigService,
);
const response = await command.run(password, cmd);
this.processResponse(response);
diff --git a/apps/cli/src/service-container.ts b/apps/cli/src/service-container.ts
index 8749eeb982f..2d5f83787f1 100644
--- a/apps/cli/src/service-container.ts
+++ b/apps/cli/src/service-container.ts
@@ -613,7 +613,6 @@ export class ServiceContainer {
await this.cryptoService.clearStoredUserKey(KeySuffixOptions.Auto);
this.userVerificationService = new UserVerificationService(
- this.stateService,
this.cryptoService,
this.accountService,
this.masterPasswordService,
diff --git a/apps/desktop/electron-builder.json b/apps/desktop/electron-builder.json
index 39c62998a67..fd92d0007f3 100644
--- a/apps/desktop/electron-builder.json
+++ b/apps/desktop/electron-builder.json
@@ -24,7 +24,7 @@
"**/node_modules/argon2/package.json",
"**/node_modules/argon2/lib/binding/napi-v3/argon2.node"
],
- "electronVersion": "29.4.2",
+ "electronVersion": "30.1.0",
"generateUpdatesFilesForAllChannels": true,
"publish": {
"provider": "generic",
diff --git a/apps/desktop/package.json b/apps/desktop/package.json
index 4bb0a94961e..c78b72cf268 100644
--- a/apps/desktop/package.json
+++ b/apps/desktop/package.json
@@ -1,7 +1,7 @@
{
"name": "@bitwarden/desktop",
"description": "A secure and free password manager for all of your devices.",
- "version": "2024.6.1",
+ "version": "2024.6.3",
"keywords": [
"bitwarden",
"password",
diff --git a/apps/desktop/src/app/app-routing.module.ts b/apps/desktop/src/app/app-routing.module.ts
index bb8deb2339a..c7a66f510ca 100644
--- a/apps/desktop/src/app/app-routing.module.ts
+++ b/apps/desktop/src/app/app-routing.module.ts
@@ -6,7 +6,18 @@ import {
lockGuard,
redirectGuard,
tdeDecryptionRequiredGuard,
+ unauthGuardFn,
} from "@bitwarden/angular/auth/guards";
+import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard";
+import {
+ AnonLayoutWrapperComponent,
+ AnonLayoutWrapperData,
+ RegistrationFinishComponent,
+ RegistrationStartComponent,
+ RegistrationStartSecondaryComponent,
+ RegistrationStartSecondaryComponentData,
+} from "@bitwarden/auth/angular";
+import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { AccessibilityCookieComponent } from "../auth/accessibility-cookie.component";
import { maxAccountsGuardFn } from "../auth/guards/max-accounts.guard";
@@ -82,6 +93,45 @@ const routes: Routes = [
canActivate: [AuthGuard],
data: { titleId: "removeMasterPassword" },
},
+ {
+ path: "",
+ component: AnonLayoutWrapperComponent,
+ children: [
+ {
+ path: "signup",
+ canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()],
+ data: { pageTitle: "createAccount" } satisfies AnonLayoutWrapperData,
+ children: [
+ {
+ path: "",
+ component: RegistrationStartComponent,
+ },
+ {
+ path: "",
+ component: RegistrationStartSecondaryComponent,
+ outlet: "secondary",
+ data: {
+ loginRoute: "/login",
+ } satisfies RegistrationStartSecondaryComponentData,
+ },
+ ],
+ },
+ {
+ path: "finish-signup",
+ canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()],
+ data: {
+ pageTitle: "setAStrongPassword",
+ pageSubtitle: "finishCreatingYourAccountBySettingAPassword",
+ } satisfies AnonLayoutWrapperData,
+ children: [
+ {
+ path: "",
+ component: RegistrationFinishComponent,
+ },
+ ],
+ },
+ ],
+ },
];
@NgModule({
diff --git a/apps/desktop/src/auth/login/login.component.html b/apps/desktop/src/auth/login/login.component.html
index eef0580d4e1..d9983220745 100644
--- a/apps/desktop/src/auth/login/login.component.html
+++ b/apps/desktop/src/auth/login/login.component.html
@@ -48,7 +48,7 @@
{{ "newAroundHere" | i18n }}
-
diff --git a/apps/desktop/src/auth/login/login.component.ts b/apps/desktop/src/auth/login/login.component.ts
index a810a29a26c..fd57e9015b1 100644
--- a/apps/desktop/src/auth/login/login.component.ts
+++ b/apps/desktop/src/auth/login/login.component.ts
@@ -15,6 +15,7 @@ import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/
import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
+import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -71,6 +72,7 @@ export class LoginComponent extends BaseLoginComponent implements OnDestroy {
loginEmailService: LoginEmailServiceAbstraction,
ssoLoginService: SsoLoginServiceAbstraction,
webAuthnLoginService: WebAuthnLoginServiceAbstraction,
+ configService: ConfigService,
) {
super(
devicesApiService,
@@ -91,6 +93,7 @@ export class LoginComponent extends BaseLoginComponent implements OnDestroy {
loginEmailService,
ssoLoginService,
webAuthnLoginService,
+ configService,
);
super.onSuccessfulLogin = () => {
return syncService.fullSync(true);
diff --git a/apps/desktop/src/locales/af/messages.json b/apps/desktop/src/locales/af/messages.json
index 1c835869d63..42970d49be2 100644
--- a/apps/desktop/src/locales/af/messages.json
+++ b/apps/desktop/src/locales/af/messages.json
@@ -2401,9 +2401,6 @@
"denyLogIn": {
"message": "Weier aantekening"
},
- "approveLoginRequests": {
- "message": "Bevestig aantekenings versoeke"
- },
"logInConfirmedForEmailOnDevice": {
"message": "Aantekening gebevestig vir $EMAIL$ op $DEVICE$",
"placeholders": {
@@ -2438,9 +2435,6 @@
"thisRequestIsNoLongerValid": {
"message": "Hierdie versoek is nie meer geldig nie."
},
- "approveLoginRequestDesc": {
- "message": "Gebruik hierdie toestel om aantekenings versoeke, van ander toetstelle, goed te keer."
- },
"confirmLoginAtemptForMail": {
"message": "Bevestig aantekenings poging vir $EMAIL$",
"placeholders": {
diff --git a/apps/desktop/src/locales/ar/messages.json b/apps/desktop/src/locales/ar/messages.json
index 4f71977603a..32b4b48f512 100644
--- a/apps/desktop/src/locales/ar/messages.json
+++ b/apps/desktop/src/locales/ar/messages.json
@@ -2401,9 +2401,6 @@
"denyLogIn": {
"message": "رفض تسجيل الدخول"
},
- "approveLoginRequests": {
- "message": "الموافقة على طلبات تسجيل الدخول"
- },
"logInConfirmedForEmailOnDevice": {
"message": "تم تأكيد تسجيل الدخول لـ $EMAIL$ في $DEVICE$",
"placeholders": {
@@ -2438,9 +2435,6 @@
"thisRequestIsNoLongerValid": {
"message": "هذا الطلب لم يعد صالحًا."
},
- "approveLoginRequestDesc": {
- "message": "استخدم هذا الجهاز للموافقة على طلبات تسجيل الدخول من الأجهزة الأخرى."
- },
"confirmLoginAtemptForMail": {
"message": "تأكيد محاولة تسجيل الدخول لـ $EMAIL$",
"placeholders": {
diff --git a/apps/desktop/src/locales/az/messages.json b/apps/desktop/src/locales/az/messages.json
index dc2c5b28f8a..052b3ac3a9b 100644
--- a/apps/desktop/src/locales/az/messages.json
+++ b/apps/desktop/src/locales/az/messages.json
@@ -995,7 +995,7 @@
"message": "Menyu çubuğuna kiçildiləndə belə Bitwarden ikonunu Yuvada göstər."
},
"confirmTrayTitle": {
- "message": "Bildiriş sahəsi nişanını ləğv et"
+ "message": "Gizlətmə sahəsini təsdiqlə"
},
"confirmTrayDesc": {
"message": "Bu ayarı söndürsəniz, bütün əlaqəli ayarlar da söndürüləcək."
@@ -2401,9 +2401,6 @@
"denyLogIn": {
"message": "Girişi rədd et"
},
- "approveLoginRequests": {
- "message": "Giriş tələblərini təsdiqlə"
- },
"logInConfirmedForEmailOnDevice": {
"message": "$DEVICE$ cihazında $EMAIL$ üçün giriş təsdiqləndi",
"placeholders": {
@@ -2438,9 +2435,6 @@
"thisRequestIsNoLongerValid": {
"message": "Bu tələb artıq yararsızdır."
},
- "approveLoginRequestDesc": {
- "message": "Digər cihazlardan edilən giriş tələblərini təsdiqləmək üçün bu cihazı istifadə edin."
- },
"confirmLoginAtemptForMail": {
"message": "$EMAIL$ üçün giriş cəhdini təsdiqlə",
"placeholders": {
diff --git a/apps/desktop/src/locales/be/messages.json b/apps/desktop/src/locales/be/messages.json
index 73bd046a82d..075318ad609 100644
--- a/apps/desktop/src/locales/be/messages.json
+++ b/apps/desktop/src/locales/be/messages.json
@@ -2401,9 +2401,6 @@
"denyLogIn": {
"message": "Адхіліць уваход"
},
- "approveLoginRequests": {
- "message": "Ухваліць запыт уваходу"
- },
"logInConfirmedForEmailOnDevice": {
"message": "Уваход пацверджаны для $EMAIL$ на $DEVICE$",
"placeholders": {
@@ -2438,9 +2435,6 @@
"thisRequestIsNoLongerValid": {
"message": "Гэты запыт больш не дзейнічае."
},
- "approveLoginRequestDesc": {
- "message": "Выкарыстоўваць гэту прыладу для ўхвалення запытаў на ўваход, якія зроблены з іншых прылад."
- },
"confirmLoginAtemptForMail": {
"message": "Пацвердзіць спробу ўваходу для $EMAIL$",
"placeholders": {
diff --git a/apps/desktop/src/locales/bg/messages.json b/apps/desktop/src/locales/bg/messages.json
index 425380fdcea..68e217d3d21 100644
--- a/apps/desktop/src/locales/bg/messages.json
+++ b/apps/desktop/src/locales/bg/messages.json
@@ -2401,9 +2401,6 @@
"denyLogIn": {
"message": "Отказване на вписването"
},
- "approveLoginRequests": {
- "message": "Одобряване на заявки за вписване"
- },
"logInConfirmedForEmailOnDevice": {
"message": "Вписването за $EMAIL$ на $DEVICE$ е одобрено",
"placeholders": {
@@ -2438,9 +2435,6 @@
"thisRequestIsNoLongerValid": {
"message": "Тази заявка вече не е активна."
},
- "approveLoginRequestDesc": {
- "message": "Използвайте това устройство за одобряване на заявки за вписване направени от други устройства."
- },
"confirmLoginAtemptForMail": {
"message": "Потвърждаване на заявката за вписване за $EMAIL$",
"placeholders": {
diff --git a/apps/desktop/src/locales/bn/messages.json b/apps/desktop/src/locales/bn/messages.json
index 36a4dc85f20..fa0aa398ef1 100644
--- a/apps/desktop/src/locales/bn/messages.json
+++ b/apps/desktop/src/locales/bn/messages.json
@@ -2401,9 +2401,6 @@
"denyLogIn": {
"message": "Deny login"
},
- "approveLoginRequests": {
- "message": "Approve login requests"
- },
"logInConfirmedForEmailOnDevice": {
"message": "Login confirmed for $EMAIL$ on $DEVICE$",
"placeholders": {
@@ -2438,9 +2435,6 @@
"thisRequestIsNoLongerValid": {
"message": "This request is no longer valid."
},
- "approveLoginRequestDesc": {
- "message": "Use this device to approve login requests made from other devices."
- },
"confirmLoginAtemptForMail": {
"message": "Confirm login attempt for $EMAIL$",
"placeholders": {
diff --git a/apps/desktop/src/locales/bs/messages.json b/apps/desktop/src/locales/bs/messages.json
index 491c9413f99..2d9e2ebba00 100644
--- a/apps/desktop/src/locales/bs/messages.json
+++ b/apps/desktop/src/locales/bs/messages.json
@@ -2401,9 +2401,6 @@
"denyLogIn": {
"message": "Deny login"
},
- "approveLoginRequests": {
- "message": "Approve login requests"
- },
"logInConfirmedForEmailOnDevice": {
"message": "Login confirmed for $EMAIL$ on $DEVICE$",
"placeholders": {
@@ -2438,9 +2435,6 @@
"thisRequestIsNoLongerValid": {
"message": "This request is no longer valid."
},
- "approveLoginRequestDesc": {
- "message": "Use this device to approve login requests made from other devices."
- },
"confirmLoginAtemptForMail": {
"message": "Confirm login attempt for $EMAIL$",
"placeholders": {
diff --git a/apps/desktop/src/locales/ca/messages.json b/apps/desktop/src/locales/ca/messages.json
index 2fcc36136bb..207c1e553f1 100644
--- a/apps/desktop/src/locales/ca/messages.json
+++ b/apps/desktop/src/locales/ca/messages.json
@@ -2401,9 +2401,6 @@
"denyLogIn": {
"message": "Denega l'inici de sessió"
},
- "approveLoginRequests": {
- "message": "Aprova les sol·licituds d'inici de sessió"
- },
"logInConfirmedForEmailOnDevice": {
"message": "S'ha confirmat l'inici de sessió per a $EMAIL$ a $DEVICE$",
"placeholders": {
@@ -2438,9 +2435,6 @@
"thisRequestIsNoLongerValid": {
"message": "Aquesta sol·licitud ja no és vàlida."
},
- "approveLoginRequestDesc": {
- "message": "Utilitzeu aquest dispositiu per aprovar les sol·licituds d'inici de sessió fetes des d'altres dispositius."
- },
"confirmLoginAtemptForMail": {
"message": "Confirmeu l'intent d'inici de sessió per a $EMAIL$",
"placeholders": {
diff --git a/apps/desktop/src/locales/cs/messages.json b/apps/desktop/src/locales/cs/messages.json
index 7dc48c3bf11..f0990d9f9f8 100644
--- a/apps/desktop/src/locales/cs/messages.json
+++ b/apps/desktop/src/locales/cs/messages.json
@@ -2401,9 +2401,6 @@
"denyLogIn": {
"message": "Zamítnout přihlášení"
},
- "approveLoginRequests": {
- "message": "Schválit žádosti o přihlášení"
- },
"logInConfirmedForEmailOnDevice": {
"message": "Přihlášení bylo potvrzeno z $EMAIL$ pro $DEVICE$",
"placeholders": {
@@ -2438,9 +2435,6 @@
"thisRequestIsNoLongerValid": {
"message": "Tento požadavek již není platný."
},
- "approveLoginRequestDesc": {
- "message": "Použije toto zařízení pro schvalování žádostí o přihlášení z jiných zařízení."
- },
"confirmLoginAtemptForMail": {
"message": "Potvrďte pokus o přihlášení z $EMAIL$",
"placeholders": {
diff --git a/apps/desktop/src/locales/cy/messages.json b/apps/desktop/src/locales/cy/messages.json
index bec1804ae2a..e92ffae7151 100644
--- a/apps/desktop/src/locales/cy/messages.json
+++ b/apps/desktop/src/locales/cy/messages.json
@@ -2401,9 +2401,6 @@
"denyLogIn": {
"message": "Deny login"
},
- "approveLoginRequests": {
- "message": "Approve login requests"
- },
"logInConfirmedForEmailOnDevice": {
"message": "Login confirmed for $EMAIL$ on $DEVICE$",
"placeholders": {
@@ -2438,9 +2435,6 @@
"thisRequestIsNoLongerValid": {
"message": "This request is no longer valid."
},
- "approveLoginRequestDesc": {
- "message": "Use this device to approve login requests made from other devices."
- },
"confirmLoginAtemptForMail": {
"message": "Confirm login attempt for $EMAIL$",
"placeholders": {
diff --git a/apps/desktop/src/locales/da/messages.json b/apps/desktop/src/locales/da/messages.json
index 333bfcd1800..96ae76f858b 100644
--- a/apps/desktop/src/locales/da/messages.json
+++ b/apps/desktop/src/locales/da/messages.json
@@ -2401,9 +2401,6 @@
"denyLogIn": {
"message": "Afvis login"
},
- "approveLoginRequests": {
- "message": "Godkend loginanmodninger"
- },
"logInConfirmedForEmailOnDevice": {
"message": "Login bekræftet for $EMAIL$ på $DEVICE$",
"placeholders": {
@@ -2438,9 +2435,6 @@
"thisRequestIsNoLongerValid": {
"message": "Anmodningen er ikke længere gyldig."
},
- "approveLoginRequestDesc": {
- "message": "Brug denne enhed til at godkende loginanmodninger fra andre enheder."
- },
"confirmLoginAtemptForMail": {
"message": "Bekræft loginforsøg for $EMAIL$",
"placeholders": {
diff --git a/apps/desktop/src/locales/de/messages.json b/apps/desktop/src/locales/de/messages.json
index 769be2a4295..7b366036e94 100644
--- a/apps/desktop/src/locales/de/messages.json
+++ b/apps/desktop/src/locales/de/messages.json
@@ -2401,9 +2401,6 @@
"denyLogIn": {
"message": "Anmeldung ablehnen"
},
- "approveLoginRequests": {
- "message": "Anmeldeanfragen genehmigen"
- },
"logInConfirmedForEmailOnDevice": {
"message": "Anmeldung von $EMAIL$ auf $DEVICE$ bestätigt",
"placeholders": {
@@ -2438,9 +2435,6 @@
"thisRequestIsNoLongerValid": {
"message": "Diese Anfrage ist nicht mehr gültig."
},
- "approveLoginRequestDesc": {
- "message": "Benutze dieses Gerät, um Anmeldeanfragen von anderen Geräten zu genehmigen."
- },
"confirmLoginAtemptForMail": {
"message": "Anmeldeversuch von $EMAIL$ genehmigen",
"placeholders": {
diff --git a/apps/desktop/src/locales/el/messages.json b/apps/desktop/src/locales/el/messages.json
index fd261a8d032..21ddb65a9ef 100644
--- a/apps/desktop/src/locales/el/messages.json
+++ b/apps/desktop/src/locales/el/messages.json
@@ -696,10 +696,10 @@
"message": "Καθορίστε τη βασική διεύθυνση URL, της εγκατάστασης του Bitwarden που φιλοξενείται στο χώρο σας."
},
"selfHostedBaseUrlHint": {
- "message": "Specify the base URL of your on-premises hosted Bitwarden installation. Example: https://bitwarden.company.com"
+ "message": "Καθορίστε τη βασική διεύθυνση URL της εγκατάστασης Bitwarden στον τομέα σας. Παράδειγμα: https://bitwarden.company.com"
},
"selfHostedCustomEnvHeader": {
- "message": "For advanced configuration, you can specify the base URL of each service independently."
+ "message": "Για προχωρημένη ρύθμιση, μπορείτε να ορίσετε ανεξάρτητα τη βασική διεύθυνση URL κάθε υπηρεσίας."
},
"selfHostedEnvFormInvalid": {
"message": "You must add either the base Server URL or at least one custom environment."
@@ -2401,9 +2401,6 @@
"denyLogIn": {
"message": "Άρνηση σύνδεσης"
},
- "approveLoginRequests": {
- "message": "Έγκριση αιτημάτων σύνδεσης"
- },
"logInConfirmedForEmailOnDevice": {
"message": "Επιβεβαιώθηκε η σύνδεση του $EMAIL$ στη συσκευή $DEVICE$",
"placeholders": {
@@ -2438,9 +2435,6 @@
"thisRequestIsNoLongerValid": {
"message": "Αυτό το αίτημα δεν είναι πλέον έγκυρο."
},
- "approveLoginRequestDesc": {
- "message": "Χρησιμοποιήστε αυτήν τη συσκευή για να εγκρίνετε αιτήματα σύνδεσης από άλλες συσκευές."
- },
"confirmLoginAtemptForMail": {
"message": "Επιβεβαίωση προσπάθειας σύνδεσης για $EMAIL$",
"placeholders": {
diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json
index 7846457294e..0c5f7244f01 100644
--- a/apps/desktop/src/locales/en/messages.json
+++ b/apps/desktop/src/locales/en/messages.json
@@ -499,6 +499,12 @@
"createAccount": {
"message": "Create account"
},
+ "setAStrongPassword": {
+ "message": "Set a strong password"
+ },
+ "finishCreatingYourAccountBySettingAPassword": {
+ "message": "Finish creating your account by setting a password"
+ },
"logIn": {
"message": "Log in"
},
@@ -1659,6 +1665,21 @@
"masterPasswordPolicyRequirementsNotMet": {
"message": "Your new master password does not meet the policy requirements."
},
+ "receiveMarketingEmails": {
+ "message": "Get emails from Bitwarden for announcements, advice, and research opportunities."
+ },
+ "unsubscribe": {
+ "message": "Unsubscribe"
+ },
+ "atAnyTime": {
+ "message": "at any time."
+ },
+ "byContinuingYouAgreeToThe": {
+ "message": "By continuing, you agree to the"
+ },
+ "and": {
+ "message": "and"
+ },
"acceptPolicies": {
"message": "By checking this box you agree to the following:"
},
diff --git a/apps/desktop/src/locales/en_GB/messages.json b/apps/desktop/src/locales/en_GB/messages.json
index 7f28fee4b2c..f15b661034f 100644
--- a/apps/desktop/src/locales/en_GB/messages.json
+++ b/apps/desktop/src/locales/en_GB/messages.json
@@ -2401,9 +2401,6 @@
"denyLogIn": {
"message": "Deny login"
},
- "approveLoginRequests": {
- "message": "Approve login requests"
- },
"logInConfirmedForEmailOnDevice": {
"message": "Login confirmed for $EMAIL$ on $DEVICE$",
"placeholders": {
@@ -2438,9 +2435,6 @@
"thisRequestIsNoLongerValid": {
"message": "This request is no longer valid."
},
- "approveLoginRequestDesc": {
- "message": "Use this device to approve login requests made from other devices."
- },
"confirmLoginAtemptForMail": {
"message": "Confirm login attempt for $EMAIL$",
"placeholders": {
diff --git a/apps/desktop/src/locales/en_IN/messages.json b/apps/desktop/src/locales/en_IN/messages.json
index b3e9a40bb38..1d4809c927b 100644
--- a/apps/desktop/src/locales/en_IN/messages.json
+++ b/apps/desktop/src/locales/en_IN/messages.json
@@ -2401,9 +2401,6 @@
"denyLogIn": {
"message": "Deny login"
},
- "approveLoginRequests": {
- "message": "Approve login requests"
- },
"logInConfirmedForEmailOnDevice": {
"message": "Login confirmed for $EMAIL$ on $DEVICE$",
"placeholders": {
@@ -2438,9 +2435,6 @@
"thisRequestIsNoLongerValid": {
"message": "This request is no longer valid."
},
- "approveLoginRequestDesc": {
- "message": "Use this device to approve login requests made from other devices."
- },
"confirmLoginAtemptForMail": {
"message": "Confirm login attempt for $EMAIL$",
"placeholders": {
diff --git a/apps/desktop/src/locales/eo/messages.json b/apps/desktop/src/locales/eo/messages.json
index 4a429118976..146a1231072 100644
--- a/apps/desktop/src/locales/eo/messages.json
+++ b/apps/desktop/src/locales/eo/messages.json
@@ -2401,9 +2401,6 @@
"denyLogIn": {
"message": "Deny login"
},
- "approveLoginRequests": {
- "message": "Approve login requests"
- },
"logInConfirmedForEmailOnDevice": {
"message": "Login confirmed for $EMAIL$ on $DEVICE$",
"placeholders": {
@@ -2438,9 +2435,6 @@
"thisRequestIsNoLongerValid": {
"message": "This request is no longer valid."
},
- "approveLoginRequestDesc": {
- "message": "Use this device to approve login requests made from other devices."
- },
"confirmLoginAtemptForMail": {
"message": "Confirm login attempt for $EMAIL$",
"placeholders": {
diff --git a/apps/desktop/src/locales/es/messages.json b/apps/desktop/src/locales/es/messages.json
index a3a2caf2e5a..a5b352e1456 100644
--- a/apps/desktop/src/locales/es/messages.json
+++ b/apps/desktop/src/locales/es/messages.json
@@ -2401,9 +2401,6 @@
"denyLogIn": {
"message": "Denegar inicio de sesión"
},
- "approveLoginRequests": {
- "message": "Aprobar solicitudes de inicio de sesión"
- },
"logInConfirmedForEmailOnDevice": {
"message": "Inicio de sesión confirmado para $EMAIL$ en $DEVICE$",
"placeholders": {
@@ -2438,9 +2435,6 @@
"thisRequestIsNoLongerValid": {
"message": "Esta solicitud ya no es válida."
},
- "approveLoginRequestDesc": {
- "message": "Utilice este dispositivo para aprobar las peticiones de inicio de sesión realizadas desde otros dispositivos."
- },
"confirmLoginAtemptForMail": {
"message": "Confirmar intento de inicio de sesión para $EMAIL$",
"placeholders": {
diff --git a/apps/desktop/src/locales/et/messages.json b/apps/desktop/src/locales/et/messages.json
index a38790c47ab..c7d48fd3991 100644
--- a/apps/desktop/src/locales/et/messages.json
+++ b/apps/desktop/src/locales/et/messages.json
@@ -2401,9 +2401,6 @@
"denyLogIn": {
"message": "Tühista sisselogimine"
},
- "approveLoginRequests": {
- "message": "Sisselogimise päringute kinnitamine"
- },
"logInConfirmedForEmailOnDevice": {
"message": "$EMAIL$ sisselogimine seadmes $DEVICE$ on kinnitatud",
"placeholders": {
@@ -2438,9 +2435,6 @@
"thisRequestIsNoLongerValid": {
"message": "See päring ei ole enam kehtiv."
},
- "approveLoginRequestDesc": {
- "message": "Kasuta seda seadet, et kinnitada teistes seadmetes tehtavad sisselogimised."
- },
"confirmLoginAtemptForMail": {
"message": "Kinnita sisselogimise katse $EMAIL$-le",
"placeholders": {
diff --git a/apps/desktop/src/locales/eu/messages.json b/apps/desktop/src/locales/eu/messages.json
index a3b0f0f87b2..89a63fbe913 100644
--- a/apps/desktop/src/locales/eu/messages.json
+++ b/apps/desktop/src/locales/eu/messages.json
@@ -2401,9 +2401,6 @@
"denyLogIn": {
"message": "Deny login"
},
- "approveLoginRequests": {
- "message": "Approve login requests"
- },
"logInConfirmedForEmailOnDevice": {
"message": "Login confirmed for $EMAIL$ on $DEVICE$",
"placeholders": {
@@ -2438,9 +2435,6 @@
"thisRequestIsNoLongerValid": {
"message": "This request is no longer valid."
},
- "approveLoginRequestDesc": {
- "message": "Use this device to approve login requests made from other devices."
- },
"confirmLoginAtemptForMail": {
"message": "Confirm login attempt for $EMAIL$",
"placeholders": {
diff --git a/apps/desktop/src/locales/fa/messages.json b/apps/desktop/src/locales/fa/messages.json
index 7fe2758ec40..6852f1e1ecb 100644
--- a/apps/desktop/src/locales/fa/messages.json
+++ b/apps/desktop/src/locales/fa/messages.json
@@ -2401,9 +2401,6 @@
"denyLogIn": {
"message": "رد ورود"
},
- "approveLoginRequests": {
- "message": "درخواست های ورود را تأیید کنید"
- },
"logInConfirmedForEmailOnDevice": {
"message": "ورود به سیستم برای $EMAIL$ در $DEVICE$ تأیید شد",
"placeholders": {
@@ -2438,9 +2435,6 @@
"thisRequestIsNoLongerValid": {
"message": "این درخواست دیگر معتبر نیست."
},
- "approveLoginRequestDesc": {
- "message": "از این دستگاه برای تأیید درخواستهای ورود به سیستم از دستگاههای دیگر استفاده کنید."
- },
"confirmLoginAtemptForMail": {
"message": "تأیید تلاش برای ورود به سیستم برای $EMAIL$",
"placeholders": {
diff --git a/apps/desktop/src/locales/fi/messages.json b/apps/desktop/src/locales/fi/messages.json
index 4586511cab4..d83f6c7d4ef 100644
--- a/apps/desktop/src/locales/fi/messages.json
+++ b/apps/desktop/src/locales/fi/messages.json
@@ -696,13 +696,13 @@
"message": "Määritä omassa palvelinympäristössäsi suoritettavan Bitwarden-asennuksen pääverkkotunnus."
},
"selfHostedBaseUrlHint": {
- "message": "Specify the base URL of your on-premises hosted Bitwarden installation. Example: https://bitwarden.company.com"
+ "message": "Määritä itse ylläpitämäsi Bitwarden-asennuksen perusosoite. Esimerkki: https://bitwarden.yritys.fi."
},
"selfHostedCustomEnvHeader": {
- "message": "For advanced configuration, you can specify the base URL of each service independently."
+ "message": "Edistynyttä määritystä varten voit syöttää jokaisen palvelun perusosoitteen erikseen."
},
"selfHostedEnvFormInvalid": {
- "message": "You must add either the base Server URL or at least one custom environment."
+ "message": "Sinun on lisättävä joko palvelimen perusosoite tai ainakin yksi mukautettu palvelinympäristö."
},
"customEnvironment": {
"message": "Mukautettu palvelinympäristö"
@@ -753,7 +753,7 @@
"message": "Kirjauduttu ulos"
},
"loggedOutDesc": {
- "message": "You have been logged out of your account."
+ "message": "Sinut on kirjattu ulos tililtäsi."
},
"loginExpired": {
"message": "Kirjautumisistuntosi on erääntynyt."
@@ -827,7 +827,7 @@
"description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing."
},
"goToWebVault": {
- "message": "Siirry verkkoholviin"
+ "message": "Avaa verkkoholvi"
},
"getMobileApp": {
"message": "Hanki mobiilisovellus"
@@ -1225,10 +1225,10 @@
}
},
"errorRefreshingAccessToken": {
- "message": "Access Token Refresh Error"
+ "message": "Käyttötunnisteen päivitysvirhe"
},
"errorRefreshingAccessTokenDesc": {
- "message": "No refresh token or API keys found. Please try logging out and logging back in."
+ "message": "Päivitystunnistetta tai API-avaimia ei löytynyt. Kokeile kirjautua ulos ja takaisin sisään."
},
"help": {
"message": "Ohje"
@@ -2224,7 +2224,7 @@
}
},
"forwaderInvalidToken": {
- "message": "Virheellinen $SERVICENAME$ -rajapinnan tunniste",
+ "message": "Virheellinen $SERVICENAME$ API -tunniste",
"description": "Displayed when the user's API token is empty or rejected by the forwarding service.",
"placeholders": {
"servicename": {
@@ -2234,7 +2234,7 @@
}
},
"forwaderInvalidTokenWithMessage": {
- "message": "Virheellinen $SERVICENAME$ -rajapinnan tunniste: $ERRORMESSAGE$",
+ "message": "Virheellinen $SERVICENAME$ API -tunniste: $ERRORMESSAGE$",
"description": "Displayed when the user's API token is rejected by the forwarding service with an error message.",
"placeholders": {
"servicename": {
@@ -2248,7 +2248,7 @@
}
},
"forwarderNoAccountId": {
- "message": "$SERVICENAME$ -palvelun peittämän sähköpostitilin tunnistetta ei saatu.",
+ "message": "$SERVICENAME$ -palvelun peittämän sähköpostitilin tunnusta ei saatu.",
"description": "Displayed when the forwarding service fails to return an account ID.",
"placeholders": {
"servicename": {
@@ -2401,9 +2401,6 @@
"denyLogIn": {
"message": "Hylkää kirjautuminen"
},
- "approveLoginRequests": {
- "message": "Hyväksy kirjautumispyyntöjä"
- },
"logInConfirmedForEmailOnDevice": {
"message": "Kirjautuminen vahvistettu tunnuksella $EMAIL$ alustalla $DEVICE$",
"placeholders": {
@@ -2438,9 +2435,6 @@
"thisRequestIsNoLongerValid": {
"message": "Pyyntö ei ole enää voimassa."
},
- "approveLoginRequestDesc": {
- "message": "Hyväksy muiden laitteiden kirjautumispyynnöt tältä laitteelta."
- },
"confirmLoginAtemptForMail": {
"message": "Vahvista kirjautuminen tunnuksella $EMAIL$",
"placeholders": {
@@ -2493,10 +2487,10 @@
"message": "Tärkeää:"
},
"accessTokenUnableToBeDecrypted": {
- "message": "You have been logged out because your access token could not be decrypted. Please log in again to resolve this issue."
+ "message": "Sinut on kirjattu ulos, koska käyttötunnisteesi salausta ei voitu purkaa. Ratkaise ongelma kirjautumalla sisään uudelleen."
},
"refreshTokenSecureStorageRetrievalFailure": {
- "message": "You have been logged out because your refresh token could not be retrieved. Please log in again to resolve this issue."
+ "message": "Sinut on kirjattu ulos, koska päivitystunnistettasi ei voitu noutaa. Ratkaise ongelma kirjautumalla sisään uudelleen."
},
"masterPasswordHint": {
"message": "Pääsalasanasi palauttaminen ei ole mahdollista, jos unohdat sen!"
@@ -2584,7 +2578,7 @@
"message": "pakollinen"
},
"search": {
- "message": "Etsi"
+ "message": "Haku"
},
"inputMinLength": {
"message": "Syötteen tulee sisältää ainakin $COUNT$ merkkiä.",
diff --git a/apps/desktop/src/locales/fil/messages.json b/apps/desktop/src/locales/fil/messages.json
index d4b76de533d..fe1ef417712 100644
--- a/apps/desktop/src/locales/fil/messages.json
+++ b/apps/desktop/src/locales/fil/messages.json
@@ -2401,9 +2401,6 @@
"denyLogIn": {
"message": "Tanggihan ang pag login"
},
- "approveLoginRequests": {
- "message": "Aprubahan ang mga kahilingan sa pag login"
- },
"logInConfirmedForEmailOnDevice": {
"message": "Nakumpirma ang pag-login para sa $EMAIL$ sa $DEVICE$",
"placeholders": {
@@ -2438,9 +2435,6 @@
"thisRequestIsNoLongerValid": {
"message": "Hindi na valid ang request na ito."
},
- "approveLoginRequestDesc": {
- "message": "Gamitin ang device na ito para aprubahan ang mga kahilingan sa pag-login na ginawa mula sa iba pang mga device."
- },
"confirmLoginAtemptForMail": {
"message": "Kumpirmahin ang pagtatangka sa pag-login para sa $EMAIL$",
"placeholders": {
diff --git a/apps/desktop/src/locales/fr/messages.json b/apps/desktop/src/locales/fr/messages.json
index a4288526277..e971256036b 100644
--- a/apps/desktop/src/locales/fr/messages.json
+++ b/apps/desktop/src/locales/fr/messages.json
@@ -2401,9 +2401,6 @@
"denyLogIn": {
"message": "Refuser la connexion"
},
- "approveLoginRequests": {
- "message": "Approuver les demandes de connexion"
- },
"logInConfirmedForEmailOnDevice": {
"message": "Connexion confirmée pour $EMAIL$ sur $DEVICE$",
"placeholders": {
@@ -2438,9 +2435,6 @@
"thisRequestIsNoLongerValid": {
"message": "Cette demande n'est plus valide."
},
- "approveLoginRequestDesc": {
- "message": "Utiliser cet appareil pour approuver les demandes de connexion faites à partir d'autres appareils."
- },
"confirmLoginAtemptForMail": {
"message": "Confirmer la tentative de connexion pour $EMAIL$",
"placeholders": {
diff --git a/apps/desktop/src/locales/gl/messages.json b/apps/desktop/src/locales/gl/messages.json
index 4d0d96038f6..d9b67aee6f3 100644
--- a/apps/desktop/src/locales/gl/messages.json
+++ b/apps/desktop/src/locales/gl/messages.json
@@ -2401,9 +2401,6 @@
"denyLogIn": {
"message": "Deny login"
},
- "approveLoginRequests": {
- "message": "Approve login requests"
- },
"logInConfirmedForEmailOnDevice": {
"message": "Login confirmed for $EMAIL$ on $DEVICE$",
"placeholders": {
@@ -2438,9 +2435,6 @@
"thisRequestIsNoLongerValid": {
"message": "This request is no longer valid."
},
- "approveLoginRequestDesc": {
- "message": "Use this device to approve login requests made from other devices."
- },
"confirmLoginAtemptForMail": {
"message": "Confirm login attempt for $EMAIL$",
"placeholders": {
diff --git a/apps/desktop/src/locales/he/messages.json b/apps/desktop/src/locales/he/messages.json
index b559652ce3d..3f3ef43c467 100644
--- a/apps/desktop/src/locales/he/messages.json
+++ b/apps/desktop/src/locales/he/messages.json
@@ -2401,9 +2401,6 @@
"denyLogIn": {
"message": "ביטול התחברות"
},
- "approveLoginRequests": {
- "message": "אישור בקשות התחברות"
- },
"logInConfirmedForEmailOnDevice": {
"message": "התחברות אושרה עבור $EMAIL$ במכשיר $DEVICE$",
"placeholders": {
@@ -2438,9 +2435,6 @@
"thisRequestIsNoLongerValid": {
"message": "This request is no longer valid."
},
- "approveLoginRequestDesc": {
- "message": "Use this device to approve login requests made from other devices."
- },
"confirmLoginAtemptForMail": {
"message": "Confirm login attempt for $EMAIL$",
"placeholders": {
diff --git a/apps/desktop/src/locales/hi/messages.json b/apps/desktop/src/locales/hi/messages.json
index bc4bf462f6b..7bd57bf6919 100644
--- a/apps/desktop/src/locales/hi/messages.json
+++ b/apps/desktop/src/locales/hi/messages.json
@@ -2401,9 +2401,6 @@
"denyLogIn": {
"message": "Deny login"
},
- "approveLoginRequests": {
- "message": "Approve login requests"
- },
"logInConfirmedForEmailOnDevice": {
"message": "Login confirmed for $EMAIL$ on $DEVICE$",
"placeholders": {
@@ -2438,9 +2435,6 @@
"thisRequestIsNoLongerValid": {
"message": "This request is no longer valid."
},
- "approveLoginRequestDesc": {
- "message": "Use this device to approve login requests made from other devices."
- },
"confirmLoginAtemptForMail": {
"message": "Confirm login attempt for $EMAIL$",
"placeholders": {
diff --git a/apps/desktop/src/locales/hr/messages.json b/apps/desktop/src/locales/hr/messages.json
index 2adc28e3ced..190e32d91ee 100644
--- a/apps/desktop/src/locales/hr/messages.json
+++ b/apps/desktop/src/locales/hr/messages.json
@@ -2401,9 +2401,6 @@
"denyLogIn": {
"message": "Odbij prijavu"
},
- "approveLoginRequests": {
- "message": "Odobri pokušaje prijave"
- },
"logInConfirmedForEmailOnDevice": {
"message": "Prijava za $EMAIL$ potvrđena na uređaju $DEVICE$",
"placeholders": {
@@ -2438,9 +2435,6 @@
"thisRequestIsNoLongerValid": {
"message": "Ovaj zahtjev više nije valjan."
},
- "approveLoginRequestDesc": {
- "message": "Koristi ovaj uređaj za odobrenje zahtjeva za prijavu na drugim uređajima."
- },
"confirmLoginAtemptForMail": {
"message": "Potvrdi pokušaj prijave za $EMAIL$",
"placeholders": {
diff --git a/apps/desktop/src/locales/hu/messages.json b/apps/desktop/src/locales/hu/messages.json
index 568dc954e92..78c7797ff11 100644
--- a/apps/desktop/src/locales/hu/messages.json
+++ b/apps/desktop/src/locales/hu/messages.json
@@ -2401,9 +2401,6 @@
"denyLogIn": {
"message": "Bejelentkezés megtagadása"
},
- "approveLoginRequests": {
- "message": "Bejelentkezési kérelmek jóváhagyása"
- },
"logInConfirmedForEmailOnDevice": {
"message": "A bejelelentketés $EMAIL$ email címmel megerősítésre került $DEVICE$ eszközön.",
"placeholders": {
@@ -2438,9 +2435,6 @@
"thisRequestIsNoLongerValid": {
"message": "A kérés a továbbiakban már nem érvényes."
},
- "approveLoginRequestDesc": {
- "message": "Ezen eszköz használata a más eszközökről történő bejelentkezési kérések jóváhagyására."
- },
"confirmLoginAtemptForMail": {
"message": "Bejelentkezési kísérlet megerősítése $EMAIL$ email címmel",
"placeholders": {
diff --git a/apps/desktop/src/locales/id/messages.json b/apps/desktop/src/locales/id/messages.json
index 545901170fd..74f3fa79f4a 100644
--- a/apps/desktop/src/locales/id/messages.json
+++ b/apps/desktop/src/locales/id/messages.json
@@ -2401,9 +2401,6 @@
"denyLogIn": {
"message": "Deny login"
},
- "approveLoginRequests": {
- "message": "Approve login requests"
- },
"logInConfirmedForEmailOnDevice": {
"message": "Login confirmed for $EMAIL$ on $DEVICE$",
"placeholders": {
@@ -2438,9 +2435,6 @@
"thisRequestIsNoLongerValid": {
"message": "This request is no longer valid."
},
- "approveLoginRequestDesc": {
- "message": "Use this device to approve login requests made from other devices."
- },
"confirmLoginAtemptForMail": {
"message": "Confirm login attempt for $EMAIL$",
"placeholders": {
diff --git a/apps/desktop/src/locales/it/messages.json b/apps/desktop/src/locales/it/messages.json
index 72f2115460f..dfc1f92eaa4 100644
--- a/apps/desktop/src/locales/it/messages.json
+++ b/apps/desktop/src/locales/it/messages.json
@@ -2401,9 +2401,6 @@
"denyLogIn": {
"message": "Nega accesso"
},
- "approveLoginRequests": {
- "message": "Approva richieste di accesso"
- },
"logInConfirmedForEmailOnDevice": {
"message": "Login per $EMAIL$ da $DEVICE$ confermato",
"placeholders": {
@@ -2438,9 +2435,6 @@
"thisRequestIsNoLongerValid": {
"message": "La richiesta non è più valida."
},
- "approveLoginRequestDesc": {
- "message": "Usa questo dispositivo per approvare le richieste di accesso fatte da altri dispositivi."
- },
"confirmLoginAtemptForMail": {
"message": "Conferma il tentativo di accesso di $EMAIL$",
"placeholders": {
diff --git a/apps/desktop/src/locales/ja/messages.json b/apps/desktop/src/locales/ja/messages.json
index 94e96a98b37..9d9b0b28f15 100644
--- a/apps/desktop/src/locales/ja/messages.json
+++ b/apps/desktop/src/locales/ja/messages.json
@@ -2401,9 +2401,6 @@
"denyLogIn": {
"message": "ログインを拒否"
},
- "approveLoginRequests": {
- "message": "ログインリクエストを承認する"
- },
"logInConfirmedForEmailOnDevice": {
"message": "$EMAIL$ に $DEVICE$ でのログインを承認しました",
"placeholders": {
@@ -2438,9 +2435,6 @@
"thisRequestIsNoLongerValid": {
"message": "このリクエストは無効になりました。"
},
- "approveLoginRequestDesc": {
- "message": "このデバイスを使用して、他のデバイスからのログインリクエストを承認します。"
- },
"confirmLoginAtemptForMail": {
"message": "$EMAIL$ のログイン試行を確認",
"placeholders": {
diff --git a/apps/desktop/src/locales/ka/messages.json b/apps/desktop/src/locales/ka/messages.json
index 4d0d96038f6..d9b67aee6f3 100644
--- a/apps/desktop/src/locales/ka/messages.json
+++ b/apps/desktop/src/locales/ka/messages.json
@@ -2401,9 +2401,6 @@
"denyLogIn": {
"message": "Deny login"
},
- "approveLoginRequests": {
- "message": "Approve login requests"
- },
"logInConfirmedForEmailOnDevice": {
"message": "Login confirmed for $EMAIL$ on $DEVICE$",
"placeholders": {
@@ -2438,9 +2435,6 @@
"thisRequestIsNoLongerValid": {
"message": "This request is no longer valid."
},
- "approveLoginRequestDesc": {
- "message": "Use this device to approve login requests made from other devices."
- },
"confirmLoginAtemptForMail": {
"message": "Confirm login attempt for $EMAIL$",
"placeholders": {
diff --git a/apps/desktop/src/locales/km/messages.json b/apps/desktop/src/locales/km/messages.json
index 4d0d96038f6..d9b67aee6f3 100644
--- a/apps/desktop/src/locales/km/messages.json
+++ b/apps/desktop/src/locales/km/messages.json
@@ -2401,9 +2401,6 @@
"denyLogIn": {
"message": "Deny login"
},
- "approveLoginRequests": {
- "message": "Approve login requests"
- },
"logInConfirmedForEmailOnDevice": {
"message": "Login confirmed for $EMAIL$ on $DEVICE$",
"placeholders": {
@@ -2438,9 +2435,6 @@
"thisRequestIsNoLongerValid": {
"message": "This request is no longer valid."
},
- "approveLoginRequestDesc": {
- "message": "Use this device to approve login requests made from other devices."
- },
"confirmLoginAtemptForMail": {
"message": "Confirm login attempt for $EMAIL$",
"placeholders": {
diff --git a/apps/desktop/src/locales/kn/messages.json b/apps/desktop/src/locales/kn/messages.json
index 88e371b67fc..5b53742fb97 100644
--- a/apps/desktop/src/locales/kn/messages.json
+++ b/apps/desktop/src/locales/kn/messages.json
@@ -2401,9 +2401,6 @@
"denyLogIn": {
"message": "Deny login"
},
- "approveLoginRequests": {
- "message": "Approve login requests"
- },
"logInConfirmedForEmailOnDevice": {
"message": "Login confirmed for $EMAIL$ on $DEVICE$",
"placeholders": {
@@ -2438,9 +2435,6 @@
"thisRequestIsNoLongerValid": {
"message": "This request is no longer valid."
},
- "approveLoginRequestDesc": {
- "message": "Use this device to approve login requests made from other devices."
- },
"confirmLoginAtemptForMail": {
"message": "Confirm login attempt for $EMAIL$",
"placeholders": {
diff --git a/apps/desktop/src/locales/ko/messages.json b/apps/desktop/src/locales/ko/messages.json
index 672e775af4e..d8686af9786 100644
--- a/apps/desktop/src/locales/ko/messages.json
+++ b/apps/desktop/src/locales/ko/messages.json
@@ -2401,9 +2401,6 @@
"denyLogIn": {
"message": "로그인 거부"
},
- "approveLoginRequests": {
- "message": "로그인 요청 승인"
- },
"logInConfirmedForEmailOnDevice": {
"message": "$DEVICE$에서 $EMAIL$(으)로의 로그인이 확인되었습니다",
"placeholders": {
@@ -2438,9 +2435,6 @@
"thisRequestIsNoLongerValid": {
"message": "더 이상 유효하지 않은 요청입니다."
},
- "approveLoginRequestDesc": {
- "message": "다른 기기에서 발생하는 로그인 요청을 이 기기에서 승인할 수 있습니다."
- },
"confirmLoginAtemptForMail": {
"message": "$EMAIL$(으)로의 로그인 시도 확인",
"placeholders": {
diff --git a/apps/desktop/src/locales/lt/messages.json b/apps/desktop/src/locales/lt/messages.json
index 68698617d47..726439fe48a 100644
--- a/apps/desktop/src/locales/lt/messages.json
+++ b/apps/desktop/src/locales/lt/messages.json
@@ -2401,9 +2401,6 @@
"denyLogIn": {
"message": "Neleisti prisijungti"
},
- "approveLoginRequests": {
- "message": "Patvirtinti prisijungimo užklausas"
- },
"logInConfirmedForEmailOnDevice": {
"message": "$EMAIL$ prisijungimas patvirtinas $DEVICE$ įrenginyje",
"placeholders": {
@@ -2438,9 +2435,6 @@
"thisRequestIsNoLongerValid": {
"message": "Ši užklausa nebegalioja."
},
- "approveLoginRequestDesc": {
- "message": "Naudokite šį įrenginį, kad patvirtintumėte prisijungimo užklausas, pateiktas iš kitų įrenginių."
- },
"confirmLoginAtemptForMail": {
"message": "Patvirtinti bandymą prisijungti iš $EMAIL$",
"placeholders": {
diff --git a/apps/desktop/src/locales/lv/messages.json b/apps/desktop/src/locales/lv/messages.json
index 4579c68742e..fa8d1a50bc8 100644
--- a/apps/desktop/src/locales/lv/messages.json
+++ b/apps/desktop/src/locales/lv/messages.json
@@ -696,13 +696,13 @@
"message": "Norādīt pašuzstādīta Bitwarden pamata URL."
},
"selfHostedBaseUrlHint": {
- "message": "Specify the base URL of your on-premises hosted Bitwarden installation. Example: https://bitwarden.company.com"
+ "message": "Jānorāda sava pašizvietotā Bitward servera pamata URL. Piemērs: https://bitwarden.uznemums.lv"
},
"selfHostedCustomEnvHeader": {
- "message": "For advanced configuration, you can specify the base URL of each service independently."
+ "message": "Papildu konfigurācijā ir iespējams norādīt URL katram pakalpojumam atsevišķi."
},
"selfHostedEnvFormInvalid": {
- "message": "You must add either the base Server URL or at least one custom environment."
+ "message": "Jāpievieno vai no servera pamata URL vai vismaz viena pielāgota vide."
},
"customEnvironment": {
"message": "Pielāgota vide"
@@ -753,7 +753,7 @@
"message": "Atteicies"
},
"loggedOutDesc": {
- "message": "You have been logged out of your account."
+ "message": "Notika izrakstīšanās no Tava konta."
},
"loginExpired": {
"message": "Pieteikšanās sesija ir beigusies."
@@ -1225,10 +1225,10 @@
}
},
"errorRefreshingAccessToken": {
- "message": "Access Token Refresh Error"
+ "message": "Piekļuves pilnvaras atsvaizināšanas kļūda"
},
"errorRefreshingAccessTokenDesc": {
- "message": "No refresh token or API keys found. Please try logging out and logging back in."
+ "message": "Netika atrastas atsvaidzināšanas pilnvaras vai API atslēgas. Lūgums mēģināt izrakstīties un atkal pieteikties."
},
"help": {
"message": "Palīdzība"
@@ -2401,9 +2401,6 @@
"denyLogIn": {
"message": "Atteikt pieteikšanos"
},
- "approveLoginRequests": {
- "message": "Apstiprināt pieteikšanās pieprasījumus"
- },
"logInConfirmedForEmailOnDevice": {
"message": "$EMAIL$ pieteikšanās apstiprināta ierīcē $DEVICE$",
"placeholders": {
@@ -2438,9 +2435,6 @@
"thisRequestIsNoLongerValid": {
"message": "Šis pieprasījums vairs nav derīgs."
},
- "approveLoginRequestDesc": {
- "message": "Izmantot šo ierīci, lai apstiprinātu pieteikšanās pieprasījumus no citām ierīcēm."
- },
"confirmLoginAtemptForMail": {
"message": "Apstiprināt $EMAIL$ pieteikšanās mēģinājumu",
"placeholders": {
@@ -2493,10 +2487,10 @@
"message": "Svarīgi:"
},
"accessTokenUnableToBeDecrypted": {
- "message": "You have been logged out because your access token could not be decrypted. Please log in again to resolve this issue."
+ "message": "Notika izrakstīšanās, jo Tavu piekļuves pilnvaru nevarēja atšifrēt. Lūgums pieteikties atkārtoti, lai novērstu šo sarežģījumu."
},
"refreshTokenSecureStorageRetrievalFailure": {
- "message": "You have been logged out because your refresh token could not be retrieved. Please log in again to resolve this issue."
+ "message": "Notika izrakstīšanās, jo Tavu atsvaidzināšanas pilnvaru nevarēja iegūt. Lūgums pieteikties atkārtoti, lai novērstu šo sarežģījumu."
},
"masterPasswordHint": {
"message": "Galvenā parole nevar tikt atgūta, ja tā ir aizmirsta!"
diff --git a/apps/desktop/src/locales/me/messages.json b/apps/desktop/src/locales/me/messages.json
index dc8a3ebdeeb..42238a63eb4 100644
--- a/apps/desktop/src/locales/me/messages.json
+++ b/apps/desktop/src/locales/me/messages.json
@@ -2401,9 +2401,6 @@
"denyLogIn": {
"message": "Deny login"
},
- "approveLoginRequests": {
- "message": "Approve login requests"
- },
"logInConfirmedForEmailOnDevice": {
"message": "Login confirmed for $EMAIL$ on $DEVICE$",
"placeholders": {
@@ -2438,9 +2435,6 @@
"thisRequestIsNoLongerValid": {
"message": "This request is no longer valid."
},
- "approveLoginRequestDesc": {
- "message": "Use this device to approve login requests made from other devices."
- },
"confirmLoginAtemptForMail": {
"message": "Confirm login attempt for $EMAIL$",
"placeholders": {
diff --git a/apps/desktop/src/locales/ml/messages.json b/apps/desktop/src/locales/ml/messages.json
index 1316189317b..597c61a9bb3 100644
--- a/apps/desktop/src/locales/ml/messages.json
+++ b/apps/desktop/src/locales/ml/messages.json
@@ -2401,9 +2401,6 @@
"denyLogIn": {
"message": "Deny login"
},
- "approveLoginRequests": {
- "message": "Approve login requests"
- },
"logInConfirmedForEmailOnDevice": {
"message": "Login confirmed for $EMAIL$ on $DEVICE$",
"placeholders": {
@@ -2438,9 +2435,6 @@
"thisRequestIsNoLongerValid": {
"message": "This request is no longer valid."
},
- "approveLoginRequestDesc": {
- "message": "Use this device to approve login requests made from other devices."
- },
"confirmLoginAtemptForMail": {
"message": "Confirm login attempt for $EMAIL$",
"placeholders": {
diff --git a/apps/desktop/src/locales/mr/messages.json b/apps/desktop/src/locales/mr/messages.json
index 4d0d96038f6..d9b67aee6f3 100644
--- a/apps/desktop/src/locales/mr/messages.json
+++ b/apps/desktop/src/locales/mr/messages.json
@@ -2401,9 +2401,6 @@
"denyLogIn": {
"message": "Deny login"
},
- "approveLoginRequests": {
- "message": "Approve login requests"
- },
"logInConfirmedForEmailOnDevice": {
"message": "Login confirmed for $EMAIL$ on $DEVICE$",
"placeholders": {
@@ -2438,9 +2435,6 @@
"thisRequestIsNoLongerValid": {
"message": "This request is no longer valid."
},
- "approveLoginRequestDesc": {
- "message": "Use this device to approve login requests made from other devices."
- },
"confirmLoginAtemptForMail": {
"message": "Confirm login attempt for $EMAIL$",
"placeholders": {
diff --git a/apps/desktop/src/locales/my/messages.json b/apps/desktop/src/locales/my/messages.json
index 1ec3bb1630d..b3e3915c43f 100644
--- a/apps/desktop/src/locales/my/messages.json
+++ b/apps/desktop/src/locales/my/messages.json
@@ -2401,9 +2401,6 @@
"denyLogIn": {
"message": "Deny login"
},
- "approveLoginRequests": {
- "message": "Approve login requests"
- },
"logInConfirmedForEmailOnDevice": {
"message": "Login confirmed for $EMAIL$ on $DEVICE$",
"placeholders": {
@@ -2438,9 +2435,6 @@
"thisRequestIsNoLongerValid": {
"message": "This request is no longer valid."
},
- "approveLoginRequestDesc": {
- "message": "Use this device to approve login requests made from other devices."
- },
"confirmLoginAtemptForMail": {
"message": "Confirm login attempt for $EMAIL$",
"placeholders": {
diff --git a/apps/desktop/src/locales/nb/messages.json b/apps/desktop/src/locales/nb/messages.json
index 149d0f75773..d3880df4dbd 100644
--- a/apps/desktop/src/locales/nb/messages.json
+++ b/apps/desktop/src/locales/nb/messages.json
@@ -2401,9 +2401,6 @@
"denyLogIn": {
"message": "Avslå innlogging"
},
- "approveLoginRequests": {
- "message": "Godkjenn innloggingsforespørsler"
- },
"logInConfirmedForEmailOnDevice": {
"message": "Login confirmed for $EMAIL$ on $DEVICE$",
"placeholders": {
@@ -2438,9 +2435,6 @@
"thisRequestIsNoLongerValid": {
"message": "Denne forespørselen er ikke lenger gyldig."
},
- "approveLoginRequestDesc": {
- "message": "Use this device to approve login requests made from other devices."
- },
"confirmLoginAtemptForMail": {
"message": "Bekreft påloggingsforsøket til $EMAIL$",
"placeholders": {
diff --git a/apps/desktop/src/locales/ne/messages.json b/apps/desktop/src/locales/ne/messages.json
index 3ad79ad8bf6..bf64ebd43c0 100644
--- a/apps/desktop/src/locales/ne/messages.json
+++ b/apps/desktop/src/locales/ne/messages.json
@@ -2401,9 +2401,6 @@
"denyLogIn": {
"message": "Deny login"
},
- "approveLoginRequests": {
- "message": "Approve login requests"
- },
"logInConfirmedForEmailOnDevice": {
"message": "Login confirmed for $EMAIL$ on $DEVICE$",
"placeholders": {
@@ -2438,9 +2435,6 @@
"thisRequestIsNoLongerValid": {
"message": "This request is no longer valid."
},
- "approveLoginRequestDesc": {
- "message": "Use this device to approve login requests made from other devices."
- },
"confirmLoginAtemptForMail": {
"message": "Confirm login attempt for $EMAIL$",
"placeholders": {
diff --git a/apps/desktop/src/locales/nl/messages.json b/apps/desktop/src/locales/nl/messages.json
index aafc348d908..25734ff4bf1 100644
--- a/apps/desktop/src/locales/nl/messages.json
+++ b/apps/desktop/src/locales/nl/messages.json
@@ -2401,9 +2401,6 @@
"denyLogIn": {
"message": "Inloggen afwijzen"
},
- "approveLoginRequests": {
- "message": "Inlogverzoeken goedkeuren"
- },
"logInConfirmedForEmailOnDevice": {
"message": "Inloggen voor $EMAIL$ bevestigd op $DEVICE$",
"placeholders": {
@@ -2438,9 +2435,6 @@
"thisRequestIsNoLongerValid": {
"message": "Dit verzoek is niet langer geldig."
},
- "approveLoginRequestDesc": {
- "message": "Gebruik dit apparaat voor het goedkeuren van inlogverzoeken van andere apparaten."
- },
"confirmLoginAtemptForMail": {
"message": "Inlogpoging voor $EMAIL$ bevestigen",
"placeholders": {
diff --git a/apps/desktop/src/locales/nn/messages.json b/apps/desktop/src/locales/nn/messages.json
index e383a118310..23e3b5d69f5 100644
--- a/apps/desktop/src/locales/nn/messages.json
+++ b/apps/desktop/src/locales/nn/messages.json
@@ -2401,9 +2401,6 @@
"denyLogIn": {
"message": "Deny login"
},
- "approveLoginRequests": {
- "message": "Godkjenn innloggingsførespurnadar"
- },
"logInConfirmedForEmailOnDevice": {
"message": "Login confirmed for $EMAIL$ on $DEVICE$",
"placeholders": {
@@ -2438,9 +2435,6 @@
"thisRequestIsNoLongerValid": {
"message": "This request is no longer valid."
},
- "approveLoginRequestDesc": {
- "message": "Use this device to approve login requests made from other devices."
- },
"confirmLoginAtemptForMail": {
"message": "Confirm login attempt for $EMAIL$",
"placeholders": {
diff --git a/apps/desktop/src/locales/or/messages.json b/apps/desktop/src/locales/or/messages.json
index 8f2f11bc30f..f3d78c23588 100644
--- a/apps/desktop/src/locales/or/messages.json
+++ b/apps/desktop/src/locales/or/messages.json
@@ -2401,9 +2401,6 @@
"denyLogIn": {
"message": "Deny login"
},
- "approveLoginRequests": {
- "message": "Approve login requests"
- },
"logInConfirmedForEmailOnDevice": {
"message": "Login confirmed for $EMAIL$ on $DEVICE$",
"placeholders": {
@@ -2438,9 +2435,6 @@
"thisRequestIsNoLongerValid": {
"message": "This request is no longer valid."
},
- "approveLoginRequestDesc": {
- "message": "Use this device to approve login requests made from other devices."
- },
"confirmLoginAtemptForMail": {
"message": "Confirm login attempt for $EMAIL$",
"placeholders": {
diff --git a/apps/desktop/src/locales/pl/messages.json b/apps/desktop/src/locales/pl/messages.json
index 6acefb31b80..703e50c35f6 100644
--- a/apps/desktop/src/locales/pl/messages.json
+++ b/apps/desktop/src/locales/pl/messages.json
@@ -2401,9 +2401,6 @@
"denyLogIn": {
"message": "Odrzuć logowanie"
},
- "approveLoginRequests": {
- "message": "Zatwierdź prośby logowania"
- },
"logInConfirmedForEmailOnDevice": {
"message": "Logowanie potwierdzone dla $EMAIL$ dnia $DEVICE$",
"placeholders": {
@@ -2438,9 +2435,6 @@
"thisRequestIsNoLongerValid": {
"message": "Ta prośba nie jest już ważna."
},
- "approveLoginRequestDesc": {
- "message": "Użyj tego urządzenia, aby zatwierdzić prośby logowania z innych urządzeń."
- },
"confirmLoginAtemptForMail": {
"message": "Potwierdź próbę logowania dla $EMAIL$",
"placeholders": {
diff --git a/apps/desktop/src/locales/pt_BR/messages.json b/apps/desktop/src/locales/pt_BR/messages.json
index b9e051de18e..86789174228 100644
--- a/apps/desktop/src/locales/pt_BR/messages.json
+++ b/apps/desktop/src/locales/pt_BR/messages.json
@@ -2401,9 +2401,6 @@
"denyLogIn": {
"message": "Negar acesso"
},
- "approveLoginRequests": {
- "message": "Aprovar solicitações de acesso"
- },
"logInConfirmedForEmailOnDevice": {
"message": "Acesso confirmado para $EMAIL$ em $DEVICE$",
"placeholders": {
@@ -2438,9 +2435,6 @@
"thisRequestIsNoLongerValid": {
"message": "Este pedido não é mais válido."
},
- "approveLoginRequestDesc": {
- "message": "Use este dispositivo para aprovar solicitações de acesso feitas de outros dispositivos."
- },
"confirmLoginAtemptForMail": {
"message": "Confirmar tentativa de acesso de $EMAIL$",
"placeholders": {
diff --git a/apps/desktop/src/locales/pt_PT/messages.json b/apps/desktop/src/locales/pt_PT/messages.json
index a1fc775bdef..0360ad50432 100644
--- a/apps/desktop/src/locales/pt_PT/messages.json
+++ b/apps/desktop/src/locales/pt_PT/messages.json
@@ -2401,9 +2401,6 @@
"denyLogIn": {
"message": "Recusar início de sessão"
},
- "approveLoginRequests": {
- "message": "Aprovar pedidos de início de sessão"
- },
"logInConfirmedForEmailOnDevice": {
"message": "Início de sessão confirmado para $EMAIL$ em $DEVICE$",
"placeholders": {
@@ -2438,9 +2435,6 @@
"thisRequestIsNoLongerValid": {
"message": "Este pedido já não é válido."
},
- "approveLoginRequestDesc": {
- "message": "Utilize este dispositivo para aprovar pedidos de início de sessão efetuados a partir de outros dispositivos."
- },
"confirmLoginAtemptForMail": {
"message": "Confirmar tentativa de início de sessão de $EMAIL$",
"placeholders": {
diff --git a/apps/desktop/src/locales/ro/messages.json b/apps/desktop/src/locales/ro/messages.json
index 65800596c86..66182bfcbaf 100644
--- a/apps/desktop/src/locales/ro/messages.json
+++ b/apps/desktop/src/locales/ro/messages.json
@@ -2401,9 +2401,6 @@
"denyLogIn": {
"message": "Deny login"
},
- "approveLoginRequests": {
- "message": "Approve login requests"
- },
"logInConfirmedForEmailOnDevice": {
"message": "Login confirmed for $EMAIL$ on $DEVICE$",
"placeholders": {
@@ -2438,9 +2435,6 @@
"thisRequestIsNoLongerValid": {
"message": "This request is no longer valid."
},
- "approveLoginRequestDesc": {
- "message": "Use this device to approve login requests made from other devices."
- },
"confirmLoginAtemptForMail": {
"message": "Confirm login attempt for $EMAIL$",
"placeholders": {
diff --git a/apps/desktop/src/locales/ru/messages.json b/apps/desktop/src/locales/ru/messages.json
index 2392d902289..6aa413d9f11 100644
--- a/apps/desktop/src/locales/ru/messages.json
+++ b/apps/desktop/src/locales/ru/messages.json
@@ -2401,9 +2401,6 @@
"denyLogIn": {
"message": "Запретить вход"
},
- "approveLoginRequests": {
- "message": "Одобрение запросов на вход"
- },
"logInConfirmedForEmailOnDevice": {
"message": "Вход подтвержден для $EMAIL$ на $DEVICE$",
"placeholders": {
@@ -2438,9 +2435,6 @@
"thisRequestIsNoLongerValid": {
"message": "Этот запрос больше не действителен."
},
- "approveLoginRequestDesc": {
- "message": "Использовать это устройство для подтверждения запросов на вход, сделанных с других устройств."
- },
"confirmLoginAtemptForMail": {
"message": "Подтвердите попытку входа для $EMAIL$",
"placeholders": {
diff --git a/apps/desktop/src/locales/si/messages.json b/apps/desktop/src/locales/si/messages.json
index 1d518c4374d..14415f055f0 100644
--- a/apps/desktop/src/locales/si/messages.json
+++ b/apps/desktop/src/locales/si/messages.json
@@ -2401,9 +2401,6 @@
"denyLogIn": {
"message": "Deny login"
},
- "approveLoginRequests": {
- "message": "Approve login requests"
- },
"logInConfirmedForEmailOnDevice": {
"message": "Login confirmed for $EMAIL$ on $DEVICE$",
"placeholders": {
@@ -2438,9 +2435,6 @@
"thisRequestIsNoLongerValid": {
"message": "This request is no longer valid."
},
- "approveLoginRequestDesc": {
- "message": "Use this device to approve login requests made from other devices."
- },
"confirmLoginAtemptForMail": {
"message": "Confirm login attempt for $EMAIL$",
"placeholders": {
diff --git a/apps/desktop/src/locales/sk/messages.json b/apps/desktop/src/locales/sk/messages.json
index dc6aa65d84f..cbb20735337 100644
--- a/apps/desktop/src/locales/sk/messages.json
+++ b/apps/desktop/src/locales/sk/messages.json
@@ -2401,9 +2401,6 @@
"denyLogIn": {
"message": "Odmietnuť prihlásenie"
},
- "approveLoginRequests": {
- "message": "Schvaľovanie žiadostí o prihlásenie"
- },
"logInConfirmedForEmailOnDevice": {
"message": "Potvrdené prihlásenie pomocou $EMAIL$ na $DEVICE$",
"placeholders": {
@@ -2438,9 +2435,6 @@
"thisRequestIsNoLongerValid": {
"message": "Táto žiadosť už nie je platná."
},
- "approveLoginRequestDesc": {
- "message": "Použiť toto zariadenie na schvaľovanie požiadaviek na prihlásenie z iných zariadení."
- },
"confirmLoginAtemptForMail": {
"message": "Potvrdiť pokus o prihlásenie pomocou $EMAIL$",
"placeholders": {
diff --git a/apps/desktop/src/locales/sl/messages.json b/apps/desktop/src/locales/sl/messages.json
index 86cb28289a9..361d484242e 100644
--- a/apps/desktop/src/locales/sl/messages.json
+++ b/apps/desktop/src/locales/sl/messages.json
@@ -2401,9 +2401,6 @@
"denyLogIn": {
"message": "Deny login"
},
- "approveLoginRequests": {
- "message": "Odobri zahtevke za prijavo"
- },
"logInConfirmedForEmailOnDevice": {
"message": "Login confirmed for $EMAIL$ on $DEVICE$",
"placeholders": {
@@ -2438,9 +2435,6 @@
"thisRequestIsNoLongerValid": {
"message": "This request is no longer valid."
},
- "approveLoginRequestDesc": {
- "message": "S to napravo odobri zahtevke za prijavo, ki pridejo z drugih naprav."
- },
"confirmLoginAtemptForMail": {
"message": "Confirm login attempt for $EMAIL$",
"placeholders": {
diff --git a/apps/desktop/src/locales/sr/messages.json b/apps/desktop/src/locales/sr/messages.json
index 90b6e7292f0..76a8f63a321 100644
--- a/apps/desktop/src/locales/sr/messages.json
+++ b/apps/desktop/src/locales/sr/messages.json
@@ -2401,9 +2401,6 @@
"denyLogIn": {
"message": "Одбиј пријављивање"
},
- "approveLoginRequests": {
- "message": "Одобравање захтева за пријављивање"
- },
"logInConfirmedForEmailOnDevice": {
"message": "Пријава потврђена за $EMAIL$ на $DEVICE$",
"placeholders": {
@@ -2438,9 +2435,6 @@
"thisRequestIsNoLongerValid": {
"message": "Овај захтев више не важи."
},
- "approveLoginRequestDesc": {
- "message": "Користите овај уређај за одобравање захтева за пријављивање са других уређаја."
- },
"confirmLoginAtemptForMail": {
"message": "Потврдити пријаву за $EMAIL$",
"placeholders": {
diff --git a/apps/desktop/src/locales/sv/messages.json b/apps/desktop/src/locales/sv/messages.json
index 6d078a34e87..a821e5aedbf 100644
--- a/apps/desktop/src/locales/sv/messages.json
+++ b/apps/desktop/src/locales/sv/messages.json
@@ -2401,9 +2401,6 @@
"denyLogIn": {
"message": "Neka inloggning"
},
- "approveLoginRequests": {
- "message": "Godkänn inloggningsförfrågningar"
- },
"logInConfirmedForEmailOnDevice": {
"message": "Inloggning bekräftad för $EMAIL$ på $DEVICE$",
"placeholders": {
@@ -2438,9 +2435,6 @@
"thisRequestIsNoLongerValid": {
"message": "Denna begäran är inte längre giltig."
},
- "approveLoginRequestDesc": {
- "message": "Använd denna enhet för att godkänna inloggningsförfrågningar från andra enheter."
- },
"confirmLoginAtemptForMail": {
"message": "Bekräfta inloggningsförsök för $EMAIL$",
"placeholders": {
diff --git a/apps/desktop/src/locales/te/messages.json b/apps/desktop/src/locales/te/messages.json
index 4d0d96038f6..d9b67aee6f3 100644
--- a/apps/desktop/src/locales/te/messages.json
+++ b/apps/desktop/src/locales/te/messages.json
@@ -2401,9 +2401,6 @@
"denyLogIn": {
"message": "Deny login"
},
- "approveLoginRequests": {
- "message": "Approve login requests"
- },
"logInConfirmedForEmailOnDevice": {
"message": "Login confirmed for $EMAIL$ on $DEVICE$",
"placeholders": {
@@ -2438,9 +2435,6 @@
"thisRequestIsNoLongerValid": {
"message": "This request is no longer valid."
},
- "approveLoginRequestDesc": {
- "message": "Use this device to approve login requests made from other devices."
- },
"confirmLoginAtemptForMail": {
"message": "Confirm login attempt for $EMAIL$",
"placeholders": {
diff --git a/apps/desktop/src/locales/th/messages.json b/apps/desktop/src/locales/th/messages.json
index 827bbc8b867..b8541361afd 100644
--- a/apps/desktop/src/locales/th/messages.json
+++ b/apps/desktop/src/locales/th/messages.json
@@ -2401,9 +2401,6 @@
"denyLogIn": {
"message": "Deny login"
},
- "approveLoginRequests": {
- "message": "Approve login requests"
- },
"logInConfirmedForEmailOnDevice": {
"message": "Login confirmed for $EMAIL$ on $DEVICE$",
"placeholders": {
@@ -2438,9 +2435,6 @@
"thisRequestIsNoLongerValid": {
"message": "This request is no longer valid."
},
- "approveLoginRequestDesc": {
- "message": "Use this device to approve login requests made from other devices."
- },
"confirmLoginAtemptForMail": {
"message": "Confirm login attempt for $EMAIL$",
"placeholders": {
diff --git a/apps/desktop/src/locales/tr/messages.json b/apps/desktop/src/locales/tr/messages.json
index 8d3d061a58e..8cd34296133 100644
--- a/apps/desktop/src/locales/tr/messages.json
+++ b/apps/desktop/src/locales/tr/messages.json
@@ -2401,9 +2401,6 @@
"denyLogIn": {
"message": "Girişi reddet"
},
- "approveLoginRequests": {
- "message": "Giriş isteklerini onayla"
- },
"logInConfirmedForEmailOnDevice": {
"message": "$DEVICE$ cihazında $EMAIL$ girişi onaylandı",
"placeholders": {
@@ -2438,9 +2435,6 @@
"thisRequestIsNoLongerValid": {
"message": "Bu istek artık geçerli değil."
},
- "approveLoginRequestDesc": {
- "message": "Diğer cihazlardan yapılan giriş isteklerini onaylamak için bu cihazı kullan."
- },
"confirmLoginAtemptForMail": {
"message": "$EMAIL$ giriş isteğini onayla",
"placeholders": {
diff --git a/apps/desktop/src/locales/uk/messages.json b/apps/desktop/src/locales/uk/messages.json
index 0ac31300741..caffd391d09 100644
--- a/apps/desktop/src/locales/uk/messages.json
+++ b/apps/desktop/src/locales/uk/messages.json
@@ -2401,9 +2401,6 @@
"denyLogIn": {
"message": "Заборонити вхід"
},
- "approveLoginRequests": {
- "message": "Схвалювати запити на вхід"
- },
"logInConfirmedForEmailOnDevice": {
"message": "Підтверджено вхід для $EMAIL$ на $DEVICE$",
"placeholders": {
@@ -2438,9 +2435,6 @@
"thisRequestIsNoLongerValid": {
"message": "Цей запит більше недійсний."
},
- "approveLoginRequestDesc": {
- "message": "Схвалювати запити на вхід, виконані з інших пристроїв."
- },
"confirmLoginAtemptForMail": {
"message": "Підтвердити спробу входу для $EMAIL$",
"placeholders": {
diff --git a/apps/desktop/src/locales/vi/messages.json b/apps/desktop/src/locales/vi/messages.json
index 70a920b8587..3b8fba2a400 100644
--- a/apps/desktop/src/locales/vi/messages.json
+++ b/apps/desktop/src/locales/vi/messages.json
@@ -2401,9 +2401,6 @@
"denyLogIn": {
"message": "Deny login"
},
- "approveLoginRequests": {
- "message": "Approve login requests"
- },
"logInConfirmedForEmailOnDevice": {
"message": "Login confirmed for $EMAIL$ on $DEVICE$",
"placeholders": {
@@ -2438,9 +2435,6 @@
"thisRequestIsNoLongerValid": {
"message": "This request is no longer valid."
},
- "approveLoginRequestDesc": {
- "message": "Use this device to approve login requests made from other devices."
- },
"confirmLoginAtemptForMail": {
"message": "Confirm login attempt for $EMAIL$",
"placeholders": {
diff --git a/apps/desktop/src/locales/zh_CN/messages.json b/apps/desktop/src/locales/zh_CN/messages.json
index 8a80875bbf3..47ce6b92262 100644
--- a/apps/desktop/src/locales/zh_CN/messages.json
+++ b/apps/desktop/src/locales/zh_CN/messages.json
@@ -696,13 +696,13 @@
"message": "指定您本地托管的 Bitwarden 安装的基础 URL。"
},
"selfHostedBaseUrlHint": {
- "message": "Specify the base URL of your on-premises hosted Bitwarden installation. Example: https://bitwarden.company.com"
+ "message": "指定您的本地托管 Bitwarden 安装的基础 URL。例如:https://bitwarden.company.com"
},
"selfHostedCustomEnvHeader": {
- "message": "For advanced configuration, you can specify the base URL of each service independently."
+ "message": "对于高级配置,您可以单独指定每个服务的基础 URL。"
},
"selfHostedEnvFormInvalid": {
- "message": "You must add either the base Server URL or at least one custom environment."
+ "message": "您必须添加基础服务器 URL 或至少添加一个自定义环境。"
},
"customEnvironment": {
"message": "自定义环境"
@@ -753,7 +753,7 @@
"message": "已注销"
},
"loggedOutDesc": {
- "message": "You have been logged out of your account."
+ "message": "您已注销您的账户。"
},
"loginExpired": {
"message": "您的登录会话已过期。"
@@ -1225,10 +1225,10 @@
}
},
"errorRefreshingAccessToken": {
- "message": "Access Token Refresh Error"
+ "message": "访问令牌刷新错误"
},
"errorRefreshingAccessTokenDesc": {
- "message": "No refresh token or API keys found. Please try logging out and logging back in."
+ "message": "未找到刷新令牌或 API 密钥。请尝试注销然后重新登录。"
},
"help": {
"message": "帮助"
@@ -2248,7 +2248,7 @@
}
},
"forwarderNoAccountId": {
- "message": "Unable to obtain $SERVICENAME$ masked email account ID.",
+ "message": "无法获取 $SERVICENAME$ 电子邮件账户 ID。",
"description": "Displayed when the forwarding service fails to return an account ID.",
"placeholders": {
"servicename": {
@@ -2401,9 +2401,6 @@
"denyLogIn": {
"message": "拒绝登录"
},
- "approveLoginRequests": {
- "message": "批准登录请求"
- },
"logInConfirmedForEmailOnDevice": {
"message": "已确认 $EMAIL$ 在 $DEVICE$ 上的登录",
"placeholders": {
@@ -2438,9 +2435,6 @@
"thisRequestIsNoLongerValid": {
"message": "此请求已失效。"
},
- "approveLoginRequestDesc": {
- "message": "使用此设备批准其他设备的登录请求。"
- },
"confirmLoginAtemptForMail": {
"message": "确认 $EMAIL$ 的登录尝试",
"placeholders": {
@@ -2493,10 +2487,10 @@
"message": "重要事项:"
},
"accessTokenUnableToBeDecrypted": {
- "message": "You have been logged out because your access token could not be decrypted. Please log in again to resolve this issue."
+ "message": "由于无法解密您的访问令牌,您已被注销。请重新登录以解决此问题。"
},
"refreshTokenSecureStorageRetrievalFailure": {
- "message": "You have been logged out because your refresh token could not be retrieved. Please log in again to resolve this issue."
+ "message": "由于无法获取您的访问令牌,您已被注销。请重新登录以解决此问题。"
},
"masterPasswordHint": {
"message": "主密码忘记后,将无法恢复!"
@@ -2566,7 +2560,7 @@
"message": "批准后,您将收到通知。"
},
"troubleLoggingIn": {
- "message": "登录遇到问题?"
+ "message": "登录遇到问题吗?"
},
"loginApproved": {
"message": "登录已批准"
diff --git a/apps/desktop/src/locales/zh_TW/messages.json b/apps/desktop/src/locales/zh_TW/messages.json
index 47bcaae70e8..7c478d835f4 100644
--- a/apps/desktop/src/locales/zh_TW/messages.json
+++ b/apps/desktop/src/locales/zh_TW/messages.json
@@ -2401,9 +2401,6 @@
"denyLogIn": {
"message": "拒絕登入"
},
- "approveLoginRequests": {
- "message": "批准登入要求"
- },
"logInConfirmedForEmailOnDevice": {
"message": "已確認 $EMAIL$ 在 $DEVICE$ 上的登入",
"placeholders": {
@@ -2438,9 +2435,6 @@
"thisRequestIsNoLongerValid": {
"message": "此請求已失效"
},
- "approveLoginRequestDesc": {
- "message": "使用此裝置準予來自其他裝置的登入要求。"
- },
"confirmLoginAtemptForMail": {
"message": "確認 $EMAIL$ 的登入嘗試",
"placeholders": {
diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts
index 738f053de73..80dbf40cb87 100644
--- a/apps/desktop/src/main.ts
+++ b/apps/desktop/src/main.ts
@@ -35,6 +35,7 @@ import { WindowMain } from "./main/window.main";
import { BiometricsService, BiometricsServiceAbstraction } from "./platform/main/biometric/index";
import { ClipboardMain } from "./platform/main/clipboard.main";
import { DesktopCredentialStorageListener } from "./platform/main/desktop-credential-storage-listener";
+import { MainCryptoFunctionService } from "./platform/main/main-crypto-function.service";
import { DesktopSettingsService } from "./platform/services/desktop-settings.service";
import { ElectronLogMainService } from "./platform/services/electron-log.main.service";
import { ElectronStorageService } from "./platform/services/electron-storage.service";
@@ -52,6 +53,7 @@ export class Main {
environmentService: DefaultEnvironmentService;
desktopCredentialStorageListener: DesktopCredentialStorageListener;
desktopSettingsService: DesktopSettingsService;
+ mainCryptoFunctionService: MainCryptoFunctionService;
migrationRunner: MigrationRunner;
windowMain: WindowMain;
@@ -111,6 +113,9 @@ export class Main {
this.i18nService = new I18nMainService("en", "./locales/", globalStateProvider);
+ this.mainCryptoFunctionService = new MainCryptoFunctionService();
+ this.mainCryptoFunctionService.init();
+
const accountService = new AccountServiceImplementation(
MessageSender.EMPTY,
this.logService,
diff --git a/apps/desktop/src/package-lock.json b/apps/desktop/src/package-lock.json
index c29ec0a9eea..560e812de8b 100644
--- a/apps/desktop/src/package-lock.json
+++ b/apps/desktop/src/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@bitwarden/desktop",
- "version": "2024.6.1",
+ "version": "2024.6.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@bitwarden/desktop",
- "version": "2024.6.1",
+ "version": "2024.6.3",
"license": "GPL-3.0",
"dependencies": {
"@bitwarden/desktop-native": "file:../desktop_native",
diff --git a/apps/desktop/src/package.json b/apps/desktop/src/package.json
index 04406eea1d4..d836b894986 100644
--- a/apps/desktop/src/package.json
+++ b/apps/desktop/src/package.json
@@ -2,7 +2,7 @@
"name": "@bitwarden/desktop",
"productName": "Bitwarden",
"description": "A secure and free password manager for all of your devices.",
- "version": "2024.6.1",
+ "version": "2024.6.3",
"author": "Bitwarden Inc. (https://bitwarden.com)",
"homepage": "https://bitwarden.com",
"license": "GPL-3.0",
diff --git a/apps/desktop/tailwind.config.js b/apps/desktop/tailwind.config.js
index be5c9ce4d96..db1dd55694e 100644
--- a/apps/desktop/tailwind.config.js
+++ b/apps/desktop/tailwind.config.js
@@ -4,6 +4,7 @@ const config = require("../../libs/components/tailwind.config.base");
config.content = [
"./src/**/*.{html,ts}",
"../../libs/components/src/**/*.{html,ts}",
+ "../../libs/auth/src/**/*.{html,ts}",
"../../libs/angular/src/**/*.{html,ts}",
];
diff --git a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.html b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.html
index 9c8d224e6e4..11c1ab2d2e3 100644
--- a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.html
+++ b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.html
@@ -28,7 +28,8 @@
{{
"inviteMultipleEmailDesc"
- | i18n: (organization.planProductType === ProductType.TeamsStarter ? "10" : "20")
+ | i18n
+ : (organization.productTierType === ProductTierType.TeamsStarter ? "10" : "20")
}}
diff --git a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts
index d16435e4d53..81830d12138 100644
--- a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts
+++ b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts
@@ -22,7 +22,7 @@ import {
import { PermissionsApi } from "@bitwarden/common/admin-console/models/api/permissions.api";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
-import { ProductType } from "@bitwarden/common/enums";
+import { ProductTierType } from "@bitwarden/common/billing/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -463,7 +463,8 @@ export class MemberDialogComponent implements OnDestroy {
await this.userService.save(userView);
} else {
userView.id = this.params.organizationUserId;
- const maxEmailsCount = organization.planProductType === ProductType.TeamsStarter ? 10 : 20;
+ const maxEmailsCount =
+ organization.productTierType === ProductTierType.TeamsStarter ? 10 : 20;
const emails = [...new Set(this.formGroup.value.emails.trim().split(/\s*,\s*/))];
if (emails.length > maxEmailsCount) {
this.formGroup.controls.emails.setErrors({
@@ -614,7 +615,7 @@ export class MemberDialogComponent implements OnDestroy {
});
}
- protected readonly ProductType = ProductType;
+ protected readonly ProductTierType = ProductTierType;
}
function mapCollectionToAccessItemView(
diff --git a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/validators/org-seat-limit-reached.validator.spec.ts b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/validators/org-seat-limit-reached.validator.spec.ts
index c26b51e2499..6c693ee8f84 100644
--- a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/validators/org-seat-limit-reached.validator.spec.ts
+++ b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/validators/org-seat-limit-reached.validator.spec.ts
@@ -2,7 +2,7 @@ import { AbstractControl, FormControl, ValidationErrors } from "@angular/forms";
import { OrganizationUserType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
-import { ProductType } from "@bitwarden/common/enums";
+import { ProductTierType } from "@bitwarden/common/billing/enums";
import { orgSeatLimitReachedValidator } from "./org-seat-limit-reached.validator";
@@ -54,7 +54,7 @@ describe("orgSeatLimitReachedValidator", () => {
it("should return null when max seats are not exceeded on free plan", () => {
organization = orgFactory({
- planProductType: ProductType.Free,
+ productTierType: ProductTierType.Free,
seats: 2,
});
validatorFn = orgSeatLimitReachedValidator(
@@ -71,7 +71,7 @@ describe("orgSeatLimitReachedValidator", () => {
it("should return null when max seats are not exceeded on teams starter plan", () => {
organization = orgFactory({
- planProductType: ProductType.TeamsStarter,
+ productTierType: ProductTierType.TeamsStarter,
seats: 10,
});
validatorFn = orgSeatLimitReachedValidator(
@@ -98,7 +98,7 @@ describe("orgSeatLimitReachedValidator", () => {
it("should return validation error when max seats are exceeded on free plan", () => {
organization = orgFactory({
- planProductType: ProductType.Free,
+ productTierType: ProductTierType.Free,
seats: 2,
});
const errorMessage = "You cannot invite more than 2 members without upgrading your plan.";
@@ -117,7 +117,7 @@ describe("orgSeatLimitReachedValidator", () => {
it("should return null when not on free plan", () => {
const control = new FormControl("user2@example.com,user3@example.com");
organization = orgFactory({
- planProductType: ProductType.Enterprise,
+ productTierType: ProductTierType.Enterprise,
seats: 100,
});
validatorFn = orgSeatLimitReachedValidator(
diff --git a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/validators/org-seat-limit-reached.validator.ts b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/validators/org-seat-limit-reached.validator.ts
index 8b521e2c17b..bcd84743918 100644
--- a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/validators/org-seat-limit-reached.validator.ts
+++ b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/validators/org-seat-limit-reached.validator.ts
@@ -1,7 +1,7 @@
import { AbstractControl, ValidationErrors, ValidatorFn } from "@angular/forms";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
-import { ProductType } from "@bitwarden/common/enums";
+import { ProductTierType } from "@bitwarden/common/billing/enums";
/**
* If the organization doesn't allow additional seat options, this checks if the seat limit has been reached when adding
@@ -37,9 +37,9 @@ export function orgSeatLimitReachedValidator(
);
const productHasAdditionalSeatsOption =
- organization.planProductType !== ProductType.Free &&
- organization.planProductType !== ProductType.Families &&
- organization.planProductType !== ProductType.TeamsStarter;
+ organization.productTierType !== ProductTierType.Free &&
+ organization.productTierType !== ProductTierType.Families &&
+ organization.productTierType !== ProductTierType.TeamsStarter;
return !productHasAdditionalSeatsOption &&
allOrganizationUserEmails.length + newEmailsToAdd.length > organization.seats
diff --git a/apps/web/src/app/admin-console/organizations/members/people.component.ts b/apps/web/src/app/admin-console/organizations/members/people.component.ts
index a47e0acd0cd..2349d989955 100644
--- a/apps/web/src/app/admin-console/organizations/members/people.component.ts
+++ b/apps/web/src/app/admin-console/organizations/members/people.component.ts
@@ -33,7 +33,7 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/request/organization-keys.request";
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billilng-api.service.abstraction";
-import { ProductType } from "@bitwarden/common/enums";
+import { ProductTierType } from "@bitwarden/common/billing/enums";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@@ -335,13 +335,13 @@ export class PeopleComponent extends NewBasePeopleComponent orgs.filter((o) => o.planProductType === ProductType.Families)),
+ map((orgs) => orgs.filter((o) => o.productTierType === ProductTierType.Families)),
);
this.existingFamilyOrganizations$.pipe(takeUntil(this._destroy)).subscribe((orgs) => {
diff --git a/apps/web/src/app/admin-console/settings/create-organization.component.ts b/apps/web/src/app/admin-console/settings/create-organization.component.ts
index 2a060d6d565..d2bcb9e69c8 100644
--- a/apps/web/src/app/admin-console/settings/create-organization.component.ts
+++ b/apps/web/src/app/admin-console/settings/create-organization.component.ts
@@ -2,8 +2,7 @@ import { Component, OnInit, ViewChild } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { first } from "rxjs/operators";
-import { PlanType } from "@bitwarden/common/billing/enums";
-import { ProductType } from "@bitwarden/common/enums";
+import { PlanType, ProductTierType } from "@bitwarden/common/billing/enums";
import { OrganizationPlansComponent } from "../../billing";
import { HeaderModule } from "../../layouts/header/header.module";
@@ -26,16 +25,16 @@ export class CreateOrganizationComponent implements OnInit {
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
if (qParams.plan === "families") {
this.orgPlansComponent.plan = PlanType.FamiliesAnnually;
- this.orgPlansComponent.product = ProductType.Families;
+ this.orgPlansComponent.productTier = ProductTierType.Families;
} else if (qParams.plan === "teams") {
this.orgPlansComponent.plan = PlanType.TeamsAnnually;
- this.orgPlansComponent.product = ProductType.Teams;
+ this.orgPlansComponent.productTier = ProductTierType.Teams;
} else if (qParams.plan === "teamsStarter") {
this.orgPlansComponent.plan = PlanType.TeamsStarter;
- this.orgPlansComponent.product = ProductType.TeamsStarter;
+ this.orgPlansComponent.productTier = ProductTierType.TeamsStarter;
} else if (qParams.plan === "enterprise") {
this.orgPlansComponent.plan = PlanType.EnterpriseAnnually;
- this.orgPlansComponent.product = ProductType.Enterprise;
+ this.orgPlansComponent.productTier = ProductTierType.Enterprise;
}
});
}
diff --git a/apps/web/src/app/auth/emergency-access/accept/accept-emergency.component.html b/apps/web/src/app/auth/emergency-access/accept/accept-emergency.component.html
index 3e1db406316..11491bd5560 100644
--- a/apps/web/src/app/auth/emergency-access/accept/accept-emergency.component.html
+++ b/apps/web/src/app/auth/emergency-access/accept/accept-emergency.component.html
@@ -29,7 +29,7 @@
diff --git a/apps/web/src/app/auth/emergency-access/accept/accept-emergency.component.ts b/apps/web/src/app/auth/emergency-access/accept/accept-emergency.component.ts
index 5a92815c91f..378726b8407 100644
--- a/apps/web/src/app/auth/emergency-access/accept/accept-emergency.component.ts
+++ b/apps/web/src/app/auth/emergency-access/accept/accept-emergency.component.ts
@@ -2,6 +2,7 @@ import { Component } from "@angular/core";
import { ActivatedRoute, Params, Router } from "@angular/router";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
+import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@@ -28,9 +29,10 @@ export class AcceptEmergencyComponent extends BaseAcceptComponent {
i18nService: I18nService,
route: ActivatedRoute,
authService: AuthService,
+ configService: ConfigService,
private emergencyAccessService: EmergencyAccessService,
) {
- super(router, platformUtilsService, i18nService, route, authService);
+ super(router, platformUtilsService, i18nService, route, authService, configService);
}
async authedHandler(qParams: Params): Promise {
diff --git a/apps/web/src/app/auth/login/login.component.html b/apps/web/src/app/auth/login/login.component.html
index eb2a9a88aa1..dae5918e40c 100644
--- a/apps/web/src/app/auth/login/login.component.html
+++ b/apps/web/src/app/auth/login/login.component.html
@@ -2,7 +2,6 @@
[bitSubmit]="submitForm.bind(null, false)"
[appApiAction]="formPromise"
[formGroup]="formGroup"
- class="tw-w-96"
>
diff --git a/libs/auth/src/angular/index.ts b/libs/auth/src/angular/index.ts
index eb8fd0416a9..9c660413cd3 100644
--- a/libs/auth/src/angular/index.ts
+++ b/libs/auth/src/angular/index.ts
@@ -17,5 +17,6 @@ export * from "./user-verification/user-verification-form-input.component";
// registration
export * from "./registration/registration-start/registration-start.component";
+export * from "./registration/registration-finish/registration-finish.component";
export * from "./registration/registration-start/registration-start-secondary.component";
export * from "./registration/registration-env-selector/registration-env-selector.component";
diff --git a/libs/auth/src/angular/registration/registration-env-selector/registration-env-selector.component.ts b/libs/auth/src/angular/registration/registration-env-selector/registration-env-selector.component.ts
index fe41f0a3ac7..268fb1cc996 100644
--- a/libs/auth/src/angular/registration/registration-env-selector/registration-env-selector.component.ts
+++ b/libs/auth/src/angular/registration/registration-env-selector/registration-env-selector.component.ts
@@ -93,6 +93,9 @@ export class RegistrationEnvSelectorComponent implements OnInit, OnDestroy {
// Save this off so we can reset the value to the previously selected region
// if the self hosted settings are closed without saving.
this.selectedRegionFromEnv = selectedRegionFromEnv;
+
+ // Emit the initial value
+ this.selectedRegionChange.emit(selectedRegionFromEnv);
}),
takeUntil(this.destroy$),
)
diff --git a/libs/auth/src/angular/registration/registration-finish/registration-finish.component.html b/libs/auth/src/angular/registration/registration-finish/registration-finish.component.html
new file mode 100644
index 00000000000..2bf6b6fc59d
--- /dev/null
+++ b/libs/auth/src/angular/registration/registration-finish/registration-finish.component.html
@@ -0,0 +1 @@
+
This component will be built in the next phase of email verification work.