From 963e339e4f55fbe407ab419f9a948765bc73491e Mon Sep 17 00:00:00 2001
From: Jason Ng
Date: Fri, 30 Aug 2024 15:38:37 -0400
Subject: [PATCH 01/64] PM-11429 Users with Except PW permissions will not get
Copy Password Option Vault V2 (#10831)
---
.../item-copy-action/item-copy-actions.component.html | 8 +++++++-
libs/vault/src/services/copy-cipher-field.service.spec.ts | 6 ------
libs/vault/src/services/copy-cipher-field.service.ts | 2 +-
3 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.html b/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.html
index 487168539b9..f4444a10aeb 100644
--- a/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.html
+++ b/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.html
@@ -13,7 +13,13 @@
-
Date: Tue, 3 Sep 2024 10:30:46 -0500
Subject: [PATCH 15/64] Use alt background for view dialog. (#10763)
---
apps/web/src/app/vault/individual-vault/view.component.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/apps/web/src/app/vault/individual-vault/view.component.html b/apps/web/src/app/vault/individual-vault/view.component.html
index a70f1be49d7..d1caf76192d 100644
--- a/apps/web/src/app/vault/individual-vault/view.component.html
+++ b/apps/web/src/app/vault/individual-vault/view.component.html
@@ -1,4 +1,4 @@
-
+
{{ cipherTypeString }}
From b27dc44298436a8c6cc64d5681cd5a78b4a8fd95 Mon Sep 17 00:00:00 2001
From: rr-bw <102181210+rr-bw@users.noreply.github.com>
Date: Tue, 3 Sep 2024 09:40:11 -0700
Subject: [PATCH 16/64] [PM-11136] Convert LoginEmailService email property to
state provider (#10624)
* convert email property to state provider
* update tests
* assign loginEmail to variable before passing in
* remove nav logic in ngOnInit
---
apps/browser/src/auth/popup/home.component.ts | 4 +--
.../browser/src/auth/popup/login.component.ts | 13 ++++----
apps/web/src/app/auth/hint.component.ts | 4 +--
...base-login-decryption-options.component.ts | 4 +--
.../src/auth/components/hint.component.ts | 5 ++--
.../login-via-auth-request.component.ts | 10 ++-----
.../src/auth/components/login.component.ts | 4 +--
.../abstractions/login-email.service.ts | 19 ++++++------
.../login-email/login-email.service.spec.ts | 18 +++++------
.../login-email/login-email.service.ts | 30 ++++++++++++-------
.../src/platform/state/state-definitions.ts | 1 +
11 files changed, 58 insertions(+), 54 deletions(-)
diff --git a/apps/browser/src/auth/popup/home.component.ts b/apps/browser/src/auth/popup/home.component.ts
index 505931ad0f1..cd9dfc3702b 100644
--- a/apps/browser/src/auth/popup/home.component.ts
+++ b/apps/browser/src/auth/popup/home.component.ts
@@ -41,7 +41,7 @@ export class HomeComponent implements OnInit, OnDestroy {
) {}
async ngOnInit(): Promise {
- const email = this.loginEmailService.getEmail();
+ const email = await firstValueFrom(this.loginEmailService.loginEmail$);
const rememberEmail = this.loginEmailService.getRememberEmail();
if (email != null) {
@@ -93,7 +93,7 @@ export class HomeComponent implements OnInit, OnDestroy {
async setLoginEmailValues() {
// Note: Browser saves email settings here instead of the login component
this.loginEmailService.setRememberEmail(this.formGroup.value.rememberEmail);
- this.loginEmailService.setEmail(this.formGroup.value.email);
+ await this.loginEmailService.setLoginEmail(this.formGroup.value.email);
await this.loginEmailService.saveEmailSettings();
}
}
diff --git a/apps/browser/src/auth/popup/login.component.ts b/apps/browser/src/auth/popup/login.component.ts
index 6e73199969a..09bfdbbc240 100644
--- a/apps/browser/src/auth/popup/login.component.ts
+++ b/apps/browser/src/auth/popup/login.component.ts
@@ -1,4 +1,4 @@
-import { Component, NgZone } from "@angular/core";
+import { Component, NgZone, OnInit } from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router";
import { firstValueFrom } from "rxjs";
@@ -31,7 +31,7 @@ import { flagEnabled } from "../../platform/flags";
selector: "app-login",
templateUrl: "login.component.html",
})
-export class LoginComponent extends BaseLoginComponent {
+export class LoginComponent extends BaseLoginComponent implements OnInit {
showPasswordless = false;
constructor(
devicesApiService: DevicesApiServiceAbstraction,
@@ -83,13 +83,14 @@ export class LoginComponent extends BaseLoginComponent {
};
super.successRoute = "/tabs/vault";
this.showPasswordless = flagEnabled("showPasswordless");
+ }
+ async ngOnInit(): Promise {
if (this.showPasswordless) {
- this.formGroup.controls.email.setValue(this.loginEmailService.getEmail());
+ const loginEmail = await firstValueFrom(this.loginEmailService.loginEmail$);
+ this.formGroup.controls.email.setValue(loginEmail);
this.formGroup.controls.rememberEmail.setValue(this.loginEmailService.getRememberEmail());
- // 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
- this.validateEmail();
+ await this.validateEmail();
}
}
diff --git a/apps/web/src/app/auth/hint.component.ts b/apps/web/src/app/auth/hint.component.ts
index 42744546234..753bdb342f9 100644
--- a/apps/web/src/app/auth/hint.component.ts
+++ b/apps/web/src/app/auth/hint.component.ts
@@ -44,8 +44,8 @@ export class HintComponent extends BaseHintComponent implements OnInit {
);
}
- ngOnInit(): void {
- super.ngOnInit();
+ async ngOnInit(): Promise {
+ await super.ngOnInit();
this.emailFormControl.setValue(this.email);
}
diff --git a/libs/angular/src/auth/components/base-login-decryption-options.component.ts b/libs/angular/src/auth/components/base-login-decryption-options.component.ts
index 80088bf7f91..6487c0cf847 100644
--- a/libs/angular/src/auth/components/base-login-decryption-options.component.ts
+++ b/libs/angular/src/auth/components/base-login-decryption-options.component.ts
@@ -251,12 +251,12 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy {
return;
}
- this.loginEmailService.setEmail(this.data.userEmail);
+ this.loginEmailService.setLoginEmail(this.data.userEmail);
await this.router.navigate(["/login-with-device"]);
}
async requestAdminApproval() {
- this.loginEmailService.setEmail(this.data.userEmail);
+ this.loginEmailService.setLoginEmail(this.data.userEmail);
await this.router.navigate(["/admin-approval-requested"]);
}
diff --git a/libs/angular/src/auth/components/hint.component.ts b/libs/angular/src/auth/components/hint.component.ts
index 7a152efbb9f..f7ae1e4c182 100644
--- a/libs/angular/src/auth/components/hint.component.ts
+++ b/libs/angular/src/auth/components/hint.component.ts
@@ -1,5 +1,6 @@
import { Directive, OnInit } from "@angular/core";
import { Router } from "@angular/router";
+import { firstValueFrom } from "rxjs";
import { LoginEmailServiceAbstraction } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
@@ -27,8 +28,8 @@ export class HintComponent implements OnInit {
protected toastService: ToastService,
) {}
- ngOnInit(): void {
- this.email = this.loginEmailService.getEmail() ?? "";
+ async ngOnInit(): Promise {
+ this.email = (await firstValueFrom(this.loginEmailService.loginEmail$)) ?? "";
}
async submit() {
diff --git a/libs/angular/src/auth/components/login-via-auth-request.component.ts b/libs/angular/src/auth/components/login-via-auth-request.component.ts
index 452b5ceee1e..a89952e024f 100644
--- a/libs/angular/src/auth/components/login-via-auth-request.component.ts
+++ b/libs/angular/src/auth/components/login-via-auth-request.component.ts
@@ -93,13 +93,6 @@ export class LoginViaAuthRequestComponent
) {
super(environmentService, i18nService, platformUtilsService, toastService);
- // TODO: I don't know why this is necessary.
- // Why would the existence of the email depend on the navigation?
- const navigation = this.router.getCurrentNavigation();
- if (navigation) {
- this.email = this.loginEmailService.getEmail();
- }
-
// Gets signalR push notification
// Only fires on approval to prevent enumeration
this.authRequestService.authRequestPushNotification$
@@ -118,6 +111,7 @@ export class LoginViaAuthRequestComponent
}
async ngOnInit() {
+ this.email = await firstValueFrom(this.loginEmailService.loginEmail$);
this.userAuthNStatus = await this.authService.getAuthStatus();
const matchOptions: IsActiveMatchOptions = {
@@ -165,7 +159,7 @@ export class LoginViaAuthRequestComponent
} else {
// Standard auth request
// TODO: evaluate if we can remove the setting of this.email in the constructor
- this.email = this.loginEmailService.getEmail();
+ this.email = await firstValueFrom(this.loginEmailService.loginEmail$);
if (!this.email) {
this.toastService.showToast({
diff --git a/libs/angular/src/auth/components/login.component.ts b/libs/angular/src/auth/components/login.component.ts
index 501d753a976..3b927a05716 100644
--- a/libs/angular/src/auth/components/login.component.ts
+++ b/libs/angular/src/auth/components/login.component.ts
@@ -304,7 +304,7 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit,
private async loadEmailSettings() {
// Try to load from memory first
- const email = this.loginEmailService.getEmail();
+ const email = await firstValueFrom(this.loginEmailService.loginEmail$);
const rememberEmail = this.loginEmailService.getRememberEmail();
if (email) {
this.formGroup.controls.email.setValue(email);
@@ -321,7 +321,7 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit,
}
protected async saveEmailSettings() {
- this.loginEmailService.setEmail(this.formGroup.value.email);
+ this.loginEmailService.setLoginEmail(this.formGroup.value.email);
this.loginEmailService.setRememberEmail(this.formGroup.value.rememberEmail);
await this.loginEmailService.saveEmailSettings();
}
diff --git a/libs/auth/src/common/abstractions/login-email.service.ts b/libs/auth/src/common/abstractions/login-email.service.ts
index d4fbbaff840..496d890f162 100644
--- a/libs/auth/src/common/abstractions/login-email.service.ts
+++ b/libs/auth/src/common/abstractions/login-email.service.ts
@@ -1,29 +1,28 @@
import { Observable } from "rxjs";
export abstract class LoginEmailServiceAbstraction {
+ /**
+ * An observable that monitors the loginEmail in memory.
+ * The loginEmail is the email that is being used in the current login process.
+ */
+ loginEmail$: Observable;
/**
* An observable that monitors the storedEmail on disk.
* This will return null if an account is being added.
*/
storedEmail$: Observable;
/**
- * Gets the current email being used in the login process from memory.
- * @returns A string of the email.
+ * Sets the loginEmail in memory.
+ * The loginEmail is the email that is being used in the current login process.
*/
- getEmail: () => string;
- /**
- * Sets the current email being used in the login process in memory.
- * @param email The email to be set.
- */
- setEmail: (email: string) => void;
+ setLoginEmail: (email: string) => Promise;
/**
* Gets from memory whether or not the email should be stored on disk when `saveEmailSettings` is called.
* @returns A boolean stating whether or not the email should be stored on disk.
*/
getRememberEmail: () => boolean;
/**
- * Sets in memory whether or not the email should be stored on disk when
- * `saveEmailSettings` is called.
+ * Sets in memory whether or not the email should be stored on disk when `saveEmailSettings` is called.
*/
setRememberEmail: (value: boolean) => void;
/**
diff --git a/libs/auth/src/common/services/login-email/login-email.service.spec.ts b/libs/auth/src/common/services/login-email/login-email.service.spec.ts
index 55e54c82f6e..8bb9b962eaf 100644
--- a/libs/auth/src/common/services/login-email/login-email.service.spec.ts
+++ b/libs/auth/src/common/services/login-email/login-email.service.spec.ts
@@ -43,7 +43,7 @@ describe("LoginEmailService", () => {
describe("storedEmail$", () => {
it("returns the stored email when not adding an account", async () => {
- sut.setEmail("userEmail@bitwarden.com");
+ await sut.setLoginEmail("userEmail@bitwarden.com");
sut.setRememberEmail(true);
await sut.saveEmailSettings();
@@ -53,7 +53,7 @@ describe("LoginEmailService", () => {
});
it("returns the stored email when not adding an account and the user has just logged in", async () => {
- sut.setEmail("userEmail@bitwarden.com");
+ await sut.setLoginEmail("userEmail@bitwarden.com");
sut.setRememberEmail(true);
await sut.saveEmailSettings();
@@ -66,7 +66,7 @@ describe("LoginEmailService", () => {
});
it("returns null when adding an account", async () => {
- sut.setEmail("userEmail@bitwarden.com");
+ await sut.setLoginEmail("userEmail@bitwarden.com");
sut.setRememberEmail(true);
await sut.saveEmailSettings();
@@ -83,7 +83,7 @@ describe("LoginEmailService", () => {
describe("saveEmailSettings", () => {
it("saves the email when not adding an account", async () => {
- sut.setEmail("userEmail@bitwarden.com");
+ await sut.setLoginEmail("userEmail@bitwarden.com");
sut.setRememberEmail(true);
await sut.saveEmailSettings();
@@ -95,7 +95,7 @@ describe("LoginEmailService", () => {
it("clears the email when not adding an account and rememberEmail is false", async () => {
storedEmailState.stateSubject.next("initialEmail@bitwarden.com");
- sut.setEmail("userEmail@bitwarden.com");
+ await sut.setLoginEmail("userEmail@bitwarden.com");
sut.setRememberEmail(false);
await sut.saveEmailSettings();
@@ -110,7 +110,7 @@ describe("LoginEmailService", () => {
["OtherUserId" as UserId]: AuthenticationStatus.Locked,
});
- sut.setEmail("userEmail@bitwarden.com");
+ await sut.setLoginEmail("userEmail@bitwarden.com");
sut.setRememberEmail(true);
await sut.saveEmailSettings();
@@ -127,7 +127,7 @@ describe("LoginEmailService", () => {
["OtherUserId" as UserId]: AuthenticationStatus.Locked,
});
- sut.setEmail("userEmail@bitwarden.com");
+ await sut.setLoginEmail("userEmail@bitwarden.com");
sut.setRememberEmail(false);
await sut.saveEmailSettings();
@@ -140,11 +140,11 @@ describe("LoginEmailService", () => {
it("does not clear the email and rememberEmail after saving", async () => {
// Browser uses these values to maintain the email between login and 2fa components so
// we do not want to clear them too early.
- sut.setEmail("userEmail@bitwarden.com");
+ await sut.setLoginEmail("userEmail@bitwarden.com");
sut.setRememberEmail(true);
await sut.saveEmailSettings();
- const result = sut.getEmail();
+ const result = await firstValueFrom(sut.loginEmail$);
expect(result).toBe("userEmail@bitwarden.com");
});
diff --git a/libs/auth/src/common/services/login-email/login-email.service.ts b/libs/auth/src/common/services/login-email/login-email.service.ts
index 7793d3e7ff6..bb89b412c51 100644
--- a/libs/auth/src/common/services/login-email/login-email.service.ts
+++ b/libs/auth/src/common/services/login-email/login-email.service.ts
@@ -8,21 +8,28 @@ import {
GlobalState,
KeyDefinition,
LOGIN_EMAIL_DISK,
+ LOGIN_EMAIL_MEMORY,
StateProvider,
} from "../../../../../common/src/platform/state";
import { LoginEmailServiceAbstraction } from "../../abstractions/login-email.service";
+export const LOGIN_EMAIL = new KeyDefinition(LOGIN_EMAIL_MEMORY, "loginEmail", {
+ deserializer: (value: string) => value,
+});
+
export const STORED_EMAIL = new KeyDefinition(LOGIN_EMAIL_DISK, "storedEmail", {
deserializer: (value: string) => value,
});
export class LoginEmailService implements LoginEmailServiceAbstraction {
- private email: string | null;
private rememberEmail: boolean;
// True if an account is currently being added through account switching
private readonly addingAccount$: Observable;
+ private readonly loginEmailState: GlobalState;
+ loginEmail$: Observable;
+
private readonly storedEmailState: GlobalState;
storedEmail$: Observable;
@@ -31,6 +38,7 @@ export class LoginEmailService implements LoginEmailServiceAbstraction {
private authService: AuthService,
private stateProvider: StateProvider,
) {
+ this.loginEmailState = this.stateProvider.getGlobal(LOGIN_EMAIL);
this.storedEmailState = this.stateProvider.getGlobal(STORED_EMAIL);
// In order to determine if an account is being added, we check if any account is not logged out
@@ -46,6 +54,8 @@ export class LoginEmailService implements LoginEmailServiceAbstraction {
}),
);
+ this.loginEmail$ = this.loginEmailState.state$;
+
this.storedEmail$ = this.storedEmailState.state$.pipe(
switchMap(async (storedEmail) => {
// When adding an account, we don't show the stored email
@@ -57,12 +67,8 @@ export class LoginEmailService implements LoginEmailServiceAbstraction {
);
}
- getEmail() {
- return this.email;
- }
-
- setEmail(email: string) {
- this.email = email;
+ async setLoginEmail(email: string) {
+ await this.loginEmailState.update((_) => email);
}
getRememberEmail() {
@@ -76,25 +82,27 @@ export class LoginEmailService implements LoginEmailServiceAbstraction {
// Note: only clear values on successful login or you are sure they are not needed.
// Browser uses these values to maintain the email between login and 2fa components so
// we do not want to clear them too early.
- clearValues() {
- this.email = null;
+ async clearValues() {
+ await this.setLoginEmail(null);
this.rememberEmail = false;
}
async saveEmailSettings() {
const addingAccount = await firstValueFrom(this.addingAccount$);
+ const email = await firstValueFrom(this.loginEmail$);
+
await this.storedEmailState.update((storedEmail) => {
// If we're adding an account, only overwrite the stored email when rememberEmail is true
if (addingAccount) {
if (this.rememberEmail) {
- return this.email;
+ return email;
}
return storedEmail;
}
// Saving with rememberEmail set to false will clear the stored email
if (this.rememberEmail) {
- return this.email;
+ return email;
}
return null;
});
diff --git a/libs/common/src/platform/state/state-definitions.ts b/libs/common/src/platform/state/state-definitions.ts
index 32307203b29..47b7199b940 100644
--- a/libs/common/src/platform/state/state-definitions.ts
+++ b/libs/common/src/platform/state/state-definitions.ts
@@ -53,6 +53,7 @@ export const KEY_CONNECTOR_DISK = new StateDefinition("keyConnector", "disk");
export const LOGIN_EMAIL_DISK = new StateDefinition("loginEmail", "disk", {
web: "disk-local",
});
+export const LOGIN_EMAIL_MEMORY = new StateDefinition("loginEmail", "memory");
export const LOGIN_STRATEGY_MEMORY = new StateDefinition("loginStrategy", "memory");
export const MASTER_PASSWORD_DISK = new StateDefinition("masterPassword", "disk");
export const MASTER_PASSWORD_MEMORY = new StateDefinition("masterPassword", "memory");
From 5f2eecd7beba1a17c10db3728d7051d182c40d62 Mon Sep 17 00:00:00 2001
From: Jonathan Prusik
Date: Tue, 3 Sep 2024 15:12:36 -0400
Subject: [PATCH 17/64] [PM-11350] Use shared expiration year normalization
util function (#10735)
* use shared expiration year normalization util function
* use shared exp year normalization in web and desktop client
* handle cases where input has leading zeroes
* add utils tests
* handle cases where input is all zeroes
---
.../src/autofill/services/autofill.service.ts | 5 +-
.../components/vault/add-edit.component.ts | 6 ++
.../vault/components/add-edit.component.ts | 6 ++
.../common/src/vault/models/view/card.view.ts | 12 +--
libs/common/src/vault/utils.spec.ts | 74 +++++++++++++++++++
libs/common/src/vault/utils.ts | 42 +++++++++++
libs/importer/src/importers/base-importer.ts | 4 +-
.../card-details-section.component.ts | 5 +-
8 files changed, 142 insertions(+), 12 deletions(-)
create mode 100644 libs/common/src/vault/utils.spec.ts
create mode 100644 libs/common/src/vault/utils.ts
diff --git a/apps/browser/src/autofill/services/autofill.service.ts b/apps/browser/src/autofill/services/autofill.service.ts
index 6f953e68b93..1002ca99225 100644
--- a/apps/browser/src/autofill/services/autofill.service.ts
+++ b/apps/browser/src/autofill/services/autofill.service.ts
@@ -29,6 +29,7 @@ import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-repromp
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { FieldView } from "@bitwarden/common/vault/models/view/field.view";
import { IdentityView } from "@bitwarden/common/vault/models/view/identity.view";
+import { normalizeExpiryYearFormat } from "@bitwarden/common/vault/utils";
import { BrowserApi } from "../../platform/browser/browser-api";
import { ScriptInjectorService } from "../../platform/services/abstractions/script-injector.service";
@@ -1095,7 +1096,7 @@ export default class AutofillService implements AutofillServiceInterface {
fillFields.expYear.maxLength === 4
) {
if (expYear.length === 2) {
- expYear = "20" + expYear;
+ expYear = normalizeExpiryYearFormat(expYear);
}
} else if (
this.fieldAttrsContain(fillFields.expYear, "yy") ||
@@ -1121,7 +1122,7 @@ export default class AutofillService implements AutofillServiceInterface {
let partYear: string = null;
if (fullYear.length === 2) {
partYear = fullYear;
- fullYear = "20" + fullYear;
+ fullYear = normalizeExpiryYearFormat(fullYear);
} else if (fullYear.length === 4) {
partYear = fullYear.substr(2, 2);
}
diff --git a/apps/browser/src/vault/popup/components/vault/add-edit.component.ts b/apps/browser/src/vault/popup/components/vault/add-edit.component.ts
index 1a944d5599c..02654f37efe 100644
--- a/apps/browser/src/vault/popup/components/vault/add-edit.component.ts
+++ b/apps/browser/src/vault/popup/components/vault/add-edit.component.ts
@@ -23,6 +23,7 @@ import { CollectionService } from "@bitwarden/common/vault/abstractions/collecti
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { CipherType } from "@bitwarden/common/vault/enums";
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
+import { normalizeExpiryYearFormat } from "@bitwarden/common/vault/utils";
import { DialogService } from "@bitwarden/components";
import { PasswordRepromptService } from "@bitwarden/vault";
@@ -182,6 +183,11 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit {
const { isFido2Session, sessionId, userVerification } = fido2SessionData;
const inFido2PopoutWindow = BrowserPopupUtils.inPopout(window) && isFido2Session;
+ // normalize card expiry year on save
+ if (this.cipher.type === this.cipherType.Card) {
+ this.cipher.card.expYear = normalizeExpiryYearFormat(this.cipher.card.expYear);
+ }
+
// TODO: Revert to use fido2 user verification service once user verification for passkeys is approved for production.
// PM-4577 - https://github.com/bitwarden/clients/pull/8746
if (
diff --git a/libs/angular/src/vault/components/add-edit.component.ts b/libs/angular/src/vault/components/add-edit.component.ts
index 909a905e9b0..96589fd2b07 100644
--- a/libs/angular/src/vault/components/add-edit.component.ts
+++ b/libs/angular/src/vault/components/add-edit.component.ts
@@ -37,6 +37,7 @@ import { IdentityView } from "@bitwarden/common/vault/models/view/identity.view"
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
import { SecureNoteView } from "@bitwarden/common/vault/models/view/secure-note.view";
+import { normalizeExpiryYearFormat } from "@bitwarden/common/vault/utils";
import { DialogService } from "@bitwarden/components";
import { PasswordRepromptService } from "@bitwarden/vault";
@@ -330,6 +331,11 @@ export class AddEditComponent implements OnInit, OnDestroy {
return this.restore();
}
+ // normalize card expiry year on save
+ if (this.cipher.type === this.cipherType.Card) {
+ this.cipher.card.expYear = normalizeExpiryYearFormat(this.cipher.card.expYear);
+ }
+
if (this.cipher.name == null || this.cipher.name === "") {
this.platformUtilsService.showToast(
"error",
diff --git a/libs/common/src/vault/models/view/card.view.ts b/libs/common/src/vault/models/view/card.view.ts
index d83b2c6f0a8..f3bf4e1fab2 100644
--- a/libs/common/src/vault/models/view/card.view.ts
+++ b/libs/common/src/vault/models/view/card.view.ts
@@ -2,6 +2,7 @@ import { Jsonify } from "type-fest";
import { CardLinkedId as LinkedId } from "../../enums";
import { linkedFieldOption } from "../../linked-field-option.decorator";
+import { normalizeExpiryYearFormat } from "../../utils";
import { ItemView } from "./item.view";
@@ -65,17 +66,16 @@ export class CardView extends ItemView {
}
get expiration(): string {
- if (!this.expMonth && !this.expYear) {
+ const normalizedYear = normalizeExpiryYearFormat(this.expYear);
+
+ if (!this.expMonth && !normalizedYear) {
return null;
}
let exp = this.expMonth != null ? ("0" + this.expMonth).slice(-2) : "__";
- exp += " / " + (this.expYear != null ? this.formatYear(this.expYear) : "____");
- return exp;
- }
+ exp += " / " + (normalizedYear || "____");
- private formatYear(year: string): string {
- return year.length === 2 ? "20" + year : year;
+ return exp;
}
static fromJSON(obj: Partial>): CardView {
diff --git a/libs/common/src/vault/utils.spec.ts b/libs/common/src/vault/utils.spec.ts
new file mode 100644
index 00000000000..1cb185cffd3
--- /dev/null
+++ b/libs/common/src/vault/utils.spec.ts
@@ -0,0 +1,74 @@
+import { normalizeExpiryYearFormat } from "@bitwarden/common/vault/utils";
+
+function getExpiryYearValueFormats(currentCentury: string) {
+ return [
+ [-12, `${currentCentury}12`],
+ [0, `${currentCentury}00`],
+ [2043, "2043"], // valid year with a length of four should be taken directly
+ [24, `${currentCentury}24`],
+ [3054, "3054"], // valid year with a length of four should be taken directly
+ [31423524543, `${currentCentury}43`],
+ [4, `${currentCentury}04`],
+ [null, null],
+ [undefined, null],
+ ["-12", `${currentCentury}12`],
+ ["", null],
+ ["0", `${currentCentury}00`],
+ ["00", `${currentCentury}00`],
+ ["000", `${currentCentury}00`],
+ ["0000", `${currentCentury}00`],
+ ["00000", `${currentCentury}00`],
+ ["0234234", `${currentCentury}34`],
+ ["04", `${currentCentury}04`],
+ ["2043", "2043"], // valid year with a length of four should be taken directly
+ ["24", `${currentCentury}24`],
+ ["3054", "3054"], // valid year with a length of four should be taken directly
+ ["31423524543", `${currentCentury}43`],
+ ["4", `${currentCentury}04`],
+ ["aaaa", null],
+ ["adgshsfhjsdrtyhsrth", null],
+ ["agdredg42grg35grrr. ea3534@#^145345ag$%^ -_#$rdg ", `${currentCentury}45`],
+ ];
+}
+
+describe("normalizeExpiryYearFormat", () => {
+ const currentCentury = `${new Date().getFullYear()}`.slice(0, 2);
+
+ const expiryYearValueFormats = getExpiryYearValueFormats(currentCentury);
+
+ expiryYearValueFormats.forEach(([inputValue, expectedValue]) => {
+ it(`should return '${expectedValue}' when '${inputValue}' is passed`, () => {
+ const formattedValue = normalizeExpiryYearFormat(inputValue);
+
+ expect(formattedValue).toEqual(expectedValue);
+ });
+ });
+
+ describe("in the year 3107", () => {
+ const theDistantFuture = new Date(Date.UTC(3107, 1, 1));
+ jest.spyOn(Date, "now").mockReturnValue(theDistantFuture.valueOf());
+
+ beforeAll(() => {
+ jest.useFakeTimers({ advanceTimers: true });
+ jest.setSystemTime(theDistantFuture);
+ });
+
+ afterAll(() => {
+ jest.useRealTimers();
+ });
+
+ const currentCentury = `${new Date(Date.now()).getFullYear()}`.slice(0, 2);
+ expect(currentCentury).toBe("31");
+
+ const expiryYearValueFormats = getExpiryYearValueFormats(currentCentury);
+
+ expiryYearValueFormats.forEach(([inputValue, expectedValue]) => {
+ it(`should return '${expectedValue}' when '${inputValue}' is passed`, () => {
+ const formattedValue = normalizeExpiryYearFormat(inputValue);
+
+ expect(formattedValue).toEqual(expectedValue);
+ });
+ });
+ jest.clearAllTimers();
+ });
+});
diff --git a/libs/common/src/vault/utils.ts b/libs/common/src/vault/utils.ts
new file mode 100644
index 00000000000..7fed4abc12e
--- /dev/null
+++ b/libs/common/src/vault/utils.ts
@@ -0,0 +1,42 @@
+type NonZeroIntegers = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
+type Year = `${NonZeroIntegers}${NonZeroIntegers}${0 | NonZeroIntegers}${0 | NonZeroIntegers}`;
+
+/**
+ * Takes a string or number value and returns a string value formatted as a valid 4-digit year
+ *
+ * @export
+ * @param {(string | number)} yearInput
+ * @return {*} {(Year | null)}
+ */
+export function normalizeExpiryYearFormat(yearInput: string | number): Year | null {
+ // The input[type="number"] is returning a number, convert it to a string
+ // An empty field returns null, avoid casting `"null"` to a string
+ const yearInputIsEmpty = yearInput == null || yearInput === "";
+ let expirationYear = yearInputIsEmpty ? null : `${yearInput}`;
+
+ // Exit early if year is already formatted correctly or empty
+ if (yearInputIsEmpty || /^[1-9]{1}\d{3}$/.test(expirationYear)) {
+ return expirationYear as Year;
+ }
+
+ expirationYear = expirationYear
+ // For safety, because even input[type="number"] will allow decimals
+ .replace(/[^\d]/g, "")
+ // remove any leading zero padding (leave the last leading zero if it ends the string)
+ .replace(/^[0]+(?=.)/, "");
+
+ if (expirationYear === "") {
+ expirationYear = null;
+ }
+
+ // given the context of payment card expiry, a year character length of 3, or over 4
+ // is more likely to be a mistake than an intentional value for the far past or far future.
+ if (expirationYear && expirationYear.length !== 4) {
+ const paddedYear = ("00" + expirationYear).slice(-2);
+ const currentCentury = `${new Date().getFullYear()}`.slice(0, 2);
+
+ expirationYear = currentCentury + paddedYear;
+ }
+
+ return expirationYear as Year | null;
+}
diff --git a/libs/importer/src/importers/base-importer.ts b/libs/importer/src/importers/base-importer.ts
index f50044c6336..215210eda14 100644
--- a/libs/importer/src/importers/base-importer.ts
+++ b/libs/importer/src/importers/base-importer.ts
@@ -11,6 +11,7 @@ import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
import { SecureNoteView } from "@bitwarden/common/vault/models/view/secure-note.view";
+import { normalizeExpiryYearFormat } from "@bitwarden/common/vault/utils";
import { ImportResult } from "../models/import-result";
@@ -263,7 +264,8 @@ export abstract class BaseImporter {
cipher.card.expMonth = expiryMatch.groups.month;
const year: string = expiryMatch.groups.year;
- cipher.card.expYear = year.length === 2 ? "20" + year : year;
+ cipher.card.expYear = normalizeExpiryYearFormat(year);
+
return true;
}
diff --git a/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.ts b/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.ts
index a80954a0445..df45bcbcac0 100644
--- a/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.ts
+++ b/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.ts
@@ -7,6 +7,7 @@ import { JslibModule } from "@bitwarden/angular/jslib.module";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { CardView } from "@bitwarden/common/vault/models/view/card.view";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
+import { normalizeExpiryYearFormat } from "@bitwarden/common/vault/utils";
import {
CardComponent,
FormFieldModule,
@@ -101,9 +102,7 @@ export class CardDetailsSectionComponent implements OnInit {
.pipe(takeUntilDestroyed())
.subscribe(({ cardholderName, number, brand, expMonth, expYear, code }) => {
this.cipherFormContainer.patchCipher((cipher) => {
- // The input[type="number"] is returning a number, convert it to a string
- // An empty field returns null, avoid casting `"null"` to a string
- const expirationYear = expYear !== null ? `${expYear}` : null;
+ const expirationYear = normalizeExpiryYearFormat(expYear);
Object.assign(cipher.card, {
cardholderName,
From 77e03aee47071f7a4d41361c2be91012a639c7ea Mon Sep 17 00:00:00 2001
From: Bitwarden DevOps
<106330231+bitwarden-devops-bot@users.noreply.github.com>
Date: Tue, 3 Sep 2024 21:09:02 -0400
Subject: [PATCH 18/64] Bumped client version(s) (#10873)
---
apps/web/package.json | 2 +-
package-lock.json | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/apps/web/package.json b/apps/web/package.json
index 8d4b130f72b..391cd4b5cc6 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -1,6 +1,6 @@
{
"name": "@bitwarden/web-vault",
- "version": "2024.8.1",
+ "version": "2024.8.2",
"scripts": {
"build:oss": "webpack",
"build:bit": "webpack -c ../../bitwarden_license/bit-web/webpack.config.js",
diff --git a/package-lock.json b/package-lock.json
index a9e95fec1f9..54e3a5cc6a7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -246,7 +246,7 @@
},
"apps/web": {
"name": "@bitwarden/web-vault",
- "version": "2024.8.1"
+ "version": "2024.8.2"
},
"libs/admin-console": {
"name": "@bitwarden/admin-console",
From 46835f0a58edf4ad5945e8dc5290ae15bb0f8069 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Wed, 4 Sep 2024 09:44:00 -0400
Subject: [PATCH 19/64] [deps] DevOps: Update gh minor (#10847)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
---
.github/workflows/build-browser.yml | 24 +++++-----
.github/workflows/build-cli.yml | 16 +++----
.github/workflows/build-desktop.yml | 52 +++++++++++-----------
.github/workflows/build-web.yml | 4 +-
.github/workflows/release-desktop-beta.yml | 48 ++++++++++----------
.github/workflows/scan.yml | 2 +-
6 files changed, 73 insertions(+), 73 deletions(-)
diff --git a/.github/workflows/build-browser.yml b/.github/workflows/build-browser.yml
index ae4f2f37ba8..610769859fe 100644
--- a/.github/workflows/build-browser.yml
+++ b/.github/workflows/build-browser.yml
@@ -173,63 +173,63 @@ jobs:
working-directory: browser-source/apps/browser
- name: Upload Opera artifact
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: dist-opera-${{ env._BUILD_NUMBER }}.zip
path: browser-source/apps/browser/dist/dist-opera.zip
if-no-files-found: error
- name: Upload Opera MV3 artifact (DO NOT USE FOR PROD)
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: DO-NOT-USE-FOR-PROD-dist-opera-MV3-${{ env._BUILD_NUMBER }}.zip
path: browser-source/apps/browser/dist/dist-opera-mv3.zip
if-no-files-found: error
- name: Upload Chrome MV3 artifact
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: dist-chrome-MV3-${{ env._BUILD_NUMBER }}.zip
path: browser-source/apps/browser/dist/dist-chrome-mv3.zip
if-no-files-found: error
- name: Upload Chrome MV3 Beta artifact (DO NOT USE FOR PROD)
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: DO-NOT-USE-FOR-PROD-dist-chrome-MV3-beta-${{ env._BUILD_NUMBER }}.zip
path: browser-source/apps/browser/dist/dist-chrome-mv3-beta.zip
if-no-files-found: error
- name: Upload Firefox artifact
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: dist-firefox-${{ env._BUILD_NUMBER }}.zip
path: browser-source/apps/browser/dist/dist-firefox.zip
if-no-files-found: error
- name: Upload Firefox MV3 artifact (DO NOT USE FOR PROD)
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: DO-NOT-USE-FOR-PROD-dist-firefox-MV3-${{ env._BUILD_NUMBER }}.zip
path: browser-source/apps/browser/dist/dist-firefox-mv3.zip
if-no-files-found: error
- name: Upload Edge artifact
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: dist-edge-${{ env._BUILD_NUMBER }}.zip
path: browser-source/apps/browser/dist/dist-edge.zip
if-no-files-found: error
- name: Upload Edge MV3 artifact (DO NOT USE FOR PROD)
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: DO-NOT-USE-FOR-PROD-dist-edge-MV3-${{ env._BUILD_NUMBER }}.zip
path: browser-source/apps/browser/dist/dist-edge-mv3.zip
if-no-files-found: error
- name: Upload browser source
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: browser-source-${{ env._BUILD_NUMBER }}.zip
path: browser-source.zip
@@ -237,7 +237,7 @@ jobs:
- name: Upload coverage artifact
if: false
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: coverage-${{ env._BUILD_NUMBER }}.zip
path: browser-source/apps/browser/coverage/coverage-${{ env._BUILD_NUMBER }}.zip
@@ -352,7 +352,7 @@ jobs:
ls -la
- name: Upload Safari artifact
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: dist-safari-${{ env._BUILD_NUMBER }}.zip
path: apps/browser/dist/dist-safari.zip
@@ -382,7 +382,7 @@ jobs:
secrets: "crowdin-api-token"
- name: Upload Sources
- uses: crowdin/github-action@c953b17499daa6be3e5afbf7a63616fb02d8b18d # v1.19.0
+ uses: crowdin/github-action@30849777a3cba6ee9a09e24e195272b8287a0a5b # v1.20.4
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml
index fd864cf99a5..76d86b45500 100644
--- a/.github/workflows/build-cli.yml
+++ b/.github/workflows/build-cli.yml
@@ -130,14 +130,14 @@ jobs:
matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}-sha256-${{ env._PACKAGE_VERSION }}.txt
- name: Upload unix zip asset
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: bw${{ matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}-${{ env._PACKAGE_VERSION }}.zip
path: apps/cli/dist/bw${{ matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}-${{ env._PACKAGE_VERSION }}.zip
if-no-files-found: error
- name: Upload unix checksum asset
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: bw${{ matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}-sha256-${{ env._PACKAGE_VERSION }}.txt
path: apps/cli/dist/bw${{ matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}-sha256-${{ env._PACKAGE_VERSION }}.txt
@@ -269,14 +269,14 @@ jobs:
-t sha256 | Out-File -Encoding ASCII ./dist/bw${{ matrix.license_type.artifact_prefix }}-windows-sha256-${env:_PACKAGE_VERSION}.txt
- name: Upload windows zip asset
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: bw${{ matrix.license_type.artifact_prefix }}-windows-${{ env._PACKAGE_VERSION }}.zip
path: apps/cli/dist/bw${{ matrix.license_type.artifact_prefix }}-windows-${{ env._PACKAGE_VERSION }}.zip
if-no-files-found: error
- name: Upload windows checksum asset
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: bw${{ matrix.license_type.artifact_prefix }}-windows-sha256-${{ env._PACKAGE_VERSION }}.txt
path: apps/cli/dist/bw${{ matrix.license_type.artifact_prefix }}-windows-sha256-${{ env._PACKAGE_VERSION }}.txt
@@ -284,7 +284,7 @@ jobs:
- name: Upload Chocolatey asset
if: matrix.license_type.build_prefix == 'bit'
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: bitwarden-cli.${{ env._PACKAGE_VERSION }}.nupkg
path: apps/cli/dist/chocolatey/bitwarden-cli.${{ env._PACKAGE_VERSION }}.nupkg
@@ -295,7 +295,7 @@ jobs:
- name: Upload NPM Build Directory asset
if: matrix.license_type.build_prefix == 'bit'
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: bitwarden-cli-${{ env._PACKAGE_VERSION }}-npm-build.zip
path: apps/cli/bitwarden-cli-${{ env._PACKAGE_VERSION }}-npm-build.zip
@@ -364,14 +364,14 @@ jobs:
run: sudo snap remove bw
- name: Upload snap asset
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: bw_${{ env._PACKAGE_VERSION }}_amd64.snap
path: apps/cli/dist/snap/bw_${{ env._PACKAGE_VERSION }}_amd64.snap
if-no-files-found: error
- name: Upload snap checksum asset
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: bw-snap-sha256-${{ env._PACKAGE_VERSION }}.txt
path: apps/cli/dist/snap/bw-snap-sha256-${{ env._PACKAGE_VERSION }}.txt
diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml
index a4dcf698faa..8ac65d257c6 100644
--- a/.github/workflows/build-desktop.yml
+++ b/.github/workflows/build-desktop.yml
@@ -193,42 +193,42 @@ jobs:
run: npm run dist:lin
- name: Upload .deb artifact
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-amd64.deb
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-amd64.deb
if-no-files-found: error
- name: Upload .rpm artifact
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.rpm
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.rpm
if-no-files-found: error
- name: Upload .freebsd artifact
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-x64.freebsd
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x64.freebsd
if-no-files-found: error
- name: Upload .snap artifact
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: bitwarden_${{ env._PACKAGE_VERSION }}_amd64.snap
path: apps/desktop/dist/bitwarden_${{ env._PACKAGE_VERSION }}_amd64.snap
if-no-files-found: error
- name: Upload .AppImage artifact
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.AppImage
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.AppImage
if-no-files-found: error
- name: Upload auto-update artifact
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: ${{ needs.setup.outputs.release_channel }}-linux.yml
path: apps/desktop/dist/${{ needs.setup.outputs.release_channel }}-linux.yml
@@ -351,91 +351,91 @@ jobs:
-NewName bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z
- name: Upload portable exe artifact
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: Bitwarden-Portable-${{ env._PACKAGE_VERSION }}.exe
path: apps/desktop/dist/Bitwarden-Portable-${{ env._PACKAGE_VERSION }}.exe
if-no-files-found: error
- name: Upload installer exe artifact
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: Bitwarden-Installer-${{ env._PACKAGE_VERSION }}.exe
path: apps/desktop/dist/nsis-web/Bitwarden-Installer-${{ env._PACKAGE_VERSION }}.exe
if-no-files-found: error
- name: Upload appx ia32 artifact
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-ia32.appx
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-ia32.appx
if-no-files-found: error
- name: Upload store appx ia32 artifact
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-ia32-store.appx
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-ia32-store.appx
if-no-files-found: error
- name: Upload NSIS ia32 artifact
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: bitwarden-${{ env._PACKAGE_VERSION }}-ia32.nsis.7z
path: apps/desktop/dist/nsis-web/bitwarden-${{ env._PACKAGE_VERSION }}-ia32.nsis.7z
if-no-files-found: error
- name: Upload appx x64 artifact
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-x64.appx
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x64.appx
if-no-files-found: error
- name: Upload store appx x64 artifact
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-x64-store.appx
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x64-store.appx
if-no-files-found: error
- name: Upload NSIS x64 artifact
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: bitwarden-${{ env._PACKAGE_VERSION }}-x64.nsis.7z
path: apps/desktop/dist/nsis-web/bitwarden-${{ env._PACKAGE_VERSION }}-x64.nsis.7z
if-no-files-found: error
- name: Upload appx ARM64 artifact
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-arm64.appx
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-arm64.appx
if-no-files-found: error
- name: Upload store appx ARM64 artifact
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-arm64-store.appx
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-arm64-store.appx
if-no-files-found: error
- name: Upload NSIS ARM64 artifact
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z
path: apps/desktop/dist/nsis-web/bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z
if-no-files-found: error
- name: Upload nupkg artifact
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: bitwarden.${{ env._PACKAGE_VERSION }}.nupkg
path: apps/desktop/dist/chocolatey/bitwarden.${{ env._PACKAGE_VERSION }}.nupkg
if-no-files-found: error
- name: Upload auto-update artifact
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: ${{ needs.setup.outputs.release_channel }}.yml
path: apps/desktop/dist/nsis-web/${{ needs.setup.outputs.release_channel }}.yml
@@ -792,28 +792,28 @@ jobs:
run: npm run pack:mac
- name: Upload .zip artifact
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal-mac.zip
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-universal-mac.zip
if-no-files-found: error
- name: Upload .dmg artifact
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg
if-no-files-found: error
- name: Upload .dmg blockmap artifact
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg.blockmap
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg.blockmap
if-no-files-found: error
- name: Upload auto-update artifact
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: ${{ needs.setup.outputs.release_channel }}-mac.yml
path: apps/desktop/dist/${{ needs.setup.outputs.release_channel }}-mac.yml
@@ -1009,7 +1009,7 @@ jobs:
run: npm run pack:mac:mas
- name: Upload .pkg artifact
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal.pkg
path: apps/desktop/dist/mas-universal/Bitwarden-${{ env._PACKAGE_VERSION }}-universal.pkg
@@ -1215,7 +1215,7 @@ jobs:
zip -r Bitwarden-${{ env._PACKAGE_VERSION }}-masdev-universal.zip Bitwarden.app
- name: Upload masdev artifact
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-masdev-universal.zip
path: apps/desktop/dist/mas-dev-universal/Bitwarden-${{ env._PACKAGE_VERSION }}-masdev-universal.zip
@@ -1248,7 +1248,7 @@ jobs:
secrets: "crowdin-api-token"
- name: Upload Sources
- uses: crowdin/github-action@c953b17499daa6be3e5afbf7a63616fb02d8b18d # v1.19.0
+ uses: crowdin/github-action@30849777a3cba6ee9a09e24e195272b8287a0a5b # v1.20.4
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
diff --git a/.github/workflows/build-web.yml b/.github/workflows/build-web.yml
index fbab45ddb72..d875078757c 100644
--- a/.github/workflows/build-web.yml
+++ b/.github/workflows/build-web.yml
@@ -130,7 +130,7 @@ jobs:
run: zip -r web-${{ env._VERSION }}-${{ matrix.name }}.zip build
- name: Upload ${{ matrix.name }} artifact
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: web-${{ env._VERSION }}-${{ matrix.name }}.zip
path: apps/web/web-${{ env._VERSION }}-${{ matrix.name }}.zip
@@ -270,7 +270,7 @@ jobs:
secrets: "crowdin-api-token"
- name: Upload Sources
- uses: crowdin/github-action@c953b17499daa6be3e5afbf7a63616fb02d8b18d # v1.19.0
+ uses: crowdin/github-action@30849777a3cba6ee9a09e24e195272b8287a0a5b # v1.20.4
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
diff --git a/.github/workflows/release-desktop-beta.yml b/.github/workflows/release-desktop-beta.yml
index 3f8bc45d51d..5a6a3d52361 100644
--- a/.github/workflows/release-desktop-beta.yml
+++ b/.github/workflows/release-desktop-beta.yml
@@ -159,42 +159,42 @@ jobs:
run: npm run dist:lin
- name: Upload .deb artifact
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-amd64.deb
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-amd64.deb
if-no-files-found: error
- name: Upload .rpm artifact
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.rpm
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.rpm
if-no-files-found: error
- name: Upload .freebsd artifact
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-x64.freebsd
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x64.freebsd
if-no-files-found: error
- name: Upload .snap artifact
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: bitwarden_${{ env._PACKAGE_VERSION }}_amd64.snap
path: apps/desktop/dist/bitwarden_${{ env._PACKAGE_VERSION }}_amd64.snap
if-no-files-found: error
- name: Upload .AppImage artifact
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.AppImage
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.AppImage
if-no-files-found: error
- name: Upload auto-update artifact
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: ${{ needs.setup.outputs.release-channel }}-linux.yml
path: apps/desktop/dist/${{ needs.setup.outputs.release-channel }}-linux.yml
@@ -300,91 +300,91 @@ jobs:
-NewName bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z
- name: Upload portable exe artifact
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: Bitwarden-Portable-${{ env._PACKAGE_VERSION }}.exe
path: apps/desktop/dist/Bitwarden-Portable-${{ env._PACKAGE_VERSION }}.exe
if-no-files-found: error
- name: Upload installer exe artifact
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: Bitwarden-Installer-${{ env._PACKAGE_VERSION }}.exe
path: apps/desktop/dist/nsis-web/Bitwarden-Installer-${{ env._PACKAGE_VERSION }}.exe
if-no-files-found: error
- name: Upload appx ia32 artifact
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-ia32.appx
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-ia32.appx
if-no-files-found: error
- name: Upload store appx ia32 artifact
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-ia32-store.appx
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-ia32-store.appx
if-no-files-found: error
- name: Upload NSIS ia32 artifact
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: bitwarden-${{ env._PACKAGE_VERSION }}-ia32.nsis.7z
path: apps/desktop/dist/nsis-web/bitwarden-${{ env._PACKAGE_VERSION }}-ia32.nsis.7z
if-no-files-found: error
- name: Upload appx x64 artifact
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-x64.appx
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x64.appx
if-no-files-found: error
- name: Upload store appx x64 artifact
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-x64-store.appx
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x64-store.appx
if-no-files-found: error
- name: Upload NSIS x64 artifact
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: bitwarden-${{ env._PACKAGE_VERSION }}-x64.nsis.7z
path: apps/desktop/dist/nsis-web/bitwarden-${{ env._PACKAGE_VERSION }}-x64.nsis.7z
if-no-files-found: error
- name: Upload appx ARM64 artifact
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-arm64.appx
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-arm64.appx
if-no-files-found: error
- name: Upload store appx ARM64 artifact
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-arm64-store.appx
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-arm64-store.appx
if-no-files-found: error
- name: Upload NSIS ARM64 artifact
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z
path: apps/desktop/dist/nsis-web/bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z
if-no-files-found: error
- name: Upload nupkg artifact
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: bitwarden.${{ env._PACKAGE_VERSION }}.nupkg
path: apps/desktop/dist/chocolatey/bitwarden.${{ env._PACKAGE_VERSION }}.nupkg
if-no-files-found: error
- name: Upload auto-update artifact
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: ${{ needs.setup.outputs.release-channel }}.yml
path: apps/desktop/dist/nsis-web/${{ needs.setup.outputs.release-channel }}.yml
@@ -708,28 +708,28 @@ jobs:
run: npm run pack:mac
- name: Upload .zip artifact
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal-mac.zip
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-universal-mac.zip
if-no-files-found: error
- name: Upload .dmg artifact
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg
if-no-files-found: error
- name: Upload .dmg blockmap artifact
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg.blockmap
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg.blockmap
if-no-files-found: error
- name: Upload auto-update artifact
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: ${{ needs.setup.outputs.release-channel }}-mac.yml
path: apps/desktop/dist/${{ needs.setup.outputs.release-channel }}-mac.yml
@@ -916,7 +916,7 @@ jobs:
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
- name: Upload .pkg artifact
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal.pkg
path: apps/desktop/dist/mas-universal/Bitwarden-${{ env._PACKAGE_VERSION }}-universal.pkg
diff --git a/.github/workflows/scan.yml b/.github/workflows/scan.yml
index d90e009bf36..dc62d3ed63a 100644
--- a/.github/workflows/scan.yml
+++ b/.github/workflows/scan.yml
@@ -47,7 +47,7 @@ jobs:
--output-path . ${{ env.INCREMENTAL }}
- name: Upload Checkmarx results to GitHub
- uses: github/codeql-action/upload-sarif@2c779ab0d087cd7fe7b826087247c2c81f27bfa6 # v3.26.5
+ uses: github/codeql-action/upload-sarif@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6
with:
sarif_file: cx_result.sarif
From b90563aa504471cca2df16a5bb83cc78e54c0493 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Wed, 4 Sep 2024 09:52:22 -0400
Subject: [PATCH 20/64] [deps] DevOps: Update
sonarsource/sonarcloud-github-action action to v3 (#10851)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
---
.github/workflows/scan.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/scan.yml b/.github/workflows/scan.yml
index dc62d3ed63a..076bfb46e80 100644
--- a/.github/workflows/scan.yml
+++ b/.github/workflows/scan.yml
@@ -67,7 +67,7 @@ jobs:
ref: ${{ github.event.pull_request.head.sha }}
- name: Scan with SonarCloud
- uses: sonarsource/sonarcloud-github-action@e44258b109568baa0df60ed515909fc6c72cba92 # v2.3.0
+ uses: sonarsource/sonarcloud-github-action@eb211723266fe8e83102bac7361f0a05c3ac1d1b # v3.0.0
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
From 86fab07a376f3a5b0496f85d49cd21d982d88671 Mon Sep 17 00:00:00 2001
From: Todd Martin <106564991+trmartin4@users.noreply.github.com>
Date: Wed, 4 Sep 2024 10:22:06 -0400
Subject: [PATCH 21/64] Auth/PM-5099 Ensure consistent casing of email used for
fingerprint generation in Auth Requests (#8571)
* Created method for handilng email-address-based fingerprint.
* Added test for new method.
* Added returns to annotation
---
.../src/auth/login/login-approval.component.ts | 7 ++++---
.../components/login-via-auth-request.component.ts | 14 ++++++++------
.../auth-request.service.abstraction.ts | 8 ++++++++
.../auth-request/auth-request.service.spec.ts | 12 ++++++++++++
.../services/auth-request/auth-request.service.ts | 4 ++++
5 files changed, 36 insertions(+), 9 deletions(-)
diff --git a/apps/desktop/src/auth/login/login-approval.component.ts b/apps/desktop/src/auth/login/login-approval.component.ts
index 39876f2945f..4bffc338b3e 100644
--- a/apps/desktop/src/auth/login/login-approval.component.ts
+++ b/apps/desktop/src/auth/login/login-approval.component.ts
@@ -79,9 +79,10 @@ export class LoginApprovalComponent implements OnInit, OnDestroy {
this.email = await await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.email)),
);
- this.fingerprintPhrase = (
- await this.cryptoService.getFingerprint(this.email, publicKey)
- ).join("-");
+ this.fingerprintPhrase = await this.authRequestService.getFingerprintPhrase(
+ this.email,
+ publicKey,
+ );
this.updateTimeText();
this.interval = setInterval(() => {
diff --git a/libs/angular/src/auth/components/login-via-auth-request.component.ts b/libs/angular/src/auth/components/login-via-auth-request.component.ts
index a89952e024f..ed9ed6ef706 100644
--- a/libs/angular/src/auth/components/login-via-auth-request.component.ts
+++ b/libs/angular/src/auth/components/login-via-auth-request.component.ts
@@ -210,9 +210,10 @@ export class LoginViaAuthRequestComponent
const derivedPublicKeyArrayBuffer = await this.cryptoFunctionService.rsaExtractPublicKey(
adminAuthReqStorable.privateKey,
);
- this.fingerprintPhrase = (
- await this.cryptoService.getFingerprint(this.email, derivedPublicKeyArrayBuffer)
- ).join("-");
+ this.fingerprintPhrase = await this.authRequestService.getFingerprintPhrase(
+ this.email,
+ derivedPublicKeyArrayBuffer,
+ );
// Request denied
if (adminAuthReqResponse.isAnswered && !adminAuthReqResponse.requestApproved) {
@@ -259,9 +260,10 @@ export class LoginViaAuthRequestComponent
length: 25,
});
- this.fingerprintPhrase = (
- await this.cryptoService.getFingerprint(this.email, this.authRequestKeyPair.publicKey)
- ).join("-");
+ this.fingerprintPhrase = await this.authRequestService.getFingerprintPhrase(
+ this.email,
+ this.authRequestKeyPair.publicKey,
+ );
this.authRequest = new CreateAuthRequest(
this.email,
diff --git a/libs/auth/src/common/abstractions/auth-request.service.abstraction.ts b/libs/auth/src/common/abstractions/auth-request.service.abstraction.ts
index aa5f52a8c9c..371c32e42d3 100644
--- a/libs/auth/src/common/abstractions/auth-request.service.abstraction.ts
+++ b/libs/auth/src/common/abstractions/auth-request.service.abstraction.ts
@@ -96,4 +96,12 @@ export abstract class AuthRequestServiceAbstraction {
* @remark We should only be receiving approved push notifications to prevent enumeration.
*/
abstract sendAuthRequestPushNotification: (notification: AuthRequestPushNotification) => void;
+
+ /**
+ * Creates a dash-delimited fingerprint for use in confirming the `AuthRequest` between the requesting and approving device.
+ * @param email The email address of the user.
+ * @param publicKey The public key for the user.
+ * @returns The dash-delimited fingerprint phrase.
+ */
+ abstract getFingerprintPhrase(email: string, publicKey: Uint8Array): Promise;
}
diff --git a/libs/auth/src/common/services/auth-request/auth-request.service.spec.ts b/libs/auth/src/common/services/auth-request/auth-request.service.spec.ts
index 885856517b8..14f807a7708 100644
--- a/libs/auth/src/common/services/auth-request/auth-request.service.spec.ts
+++ b/libs/auth/src/common/services/auth-request/auth-request.service.spec.ts
@@ -27,6 +27,7 @@ describe("AuthRequestService", () => {
const apiService = mock();
let mockPrivateKey: Uint8Array;
+ let mockPublicKey: Uint8Array;
const mockUserId = Utils.newGuid() as UserId;
beforeEach(() => {
@@ -44,6 +45,7 @@ describe("AuthRequestService", () => {
);
mockPrivateKey = new Uint8Array(64);
+ mockPublicKey = new Uint8Array(64);
});
describe("authRequestPushNotification$", () => {
@@ -262,4 +264,14 @@ describe("AuthRequestService", () => {
expect(result.masterKeyHash).toEqual(mockDecryptedMasterKeyHash);
});
});
+
+ describe("getFingerprintPhrase", () => {
+ it("returns the same fingerprint regardless of email casing", () => {
+ const email = "test@email.com";
+ const emailUpperCase = email.toUpperCase();
+ const phrase = sut.getFingerprintPhrase(email, mockPublicKey);
+ const phraseUpperCase = sut.getFingerprintPhrase(emailUpperCase, mockPublicKey);
+ expect(phrase).toEqual(phraseUpperCase);
+ });
+ });
});
diff --git a/libs/auth/src/common/services/auth-request/auth-request.service.ts b/libs/auth/src/common/services/auth-request/auth-request.service.ts
index 68302cae92d..eefee511f82 100644
--- a/libs/auth/src/common/services/auth-request/auth-request.service.ts
+++ b/libs/auth/src/common/services/auth-request/auth-request.service.ts
@@ -198,4 +198,8 @@ export class AuthRequestService implements AuthRequestServiceAbstraction {
this.authRequestPushNotificationSubject.next(notification.id);
}
}
+
+ async getFingerprintPhrase(email: string, publicKey: Uint8Array): Promise {
+ return (await this.cryptoService.getFingerprint(email.toLowerCase(), publicKey)).join("-");
+ }
}
From 192fd885d5a5cf0f939e47f557a2d53d0cc52d3e Mon Sep 17 00:00:00 2001
From: Bernd Schoolmann
Date: Wed, 4 Sep 2024 17:16:57 +0200
Subject: [PATCH 22/64] Return null in derivePublicKey if privateKey is null
(#10882)
---
libs/common/src/platform/services/crypto.service.ts | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/libs/common/src/platform/services/crypto.service.ts b/libs/common/src/platform/services/crypto.service.ts
index e6860dadf3a..8ce2b5e1a0c 100644
--- a/libs/common/src/platform/services/crypto.service.ts
+++ b/libs/common/src/platform/services/crypto.service.ts
@@ -945,6 +945,10 @@ export class CryptoService implements CryptoServiceAbstraction {
}
private async derivePublicKey(privateKey: UserPrivateKey) {
+ if (privateKey == null) {
+ return null;
+ }
+
return (await this.cryptoFunctionService.rsaExtractPublicKey(privateKey)) as UserPublicKey;
}
From 3e9fb2009ebb252182af08b369e4828cb441bf59 Mon Sep 17 00:00:00 2001
From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com>
Date: Wed, 4 Sep 2024 10:50:34 -0500
Subject: [PATCH 23/64] [PM-10934] Remove last form-field bottom border
(#10751)
* match API of new CL FormField component
* remove readonly border for additional options component
* remove readonly border for last autofill option
* remove readonly border for last custom-field form field
* remove readonly border for when collection,org or folder is available
* add `ReadOnlyCipherCardComponent` to handle readonly border
* remove readonly border for the last identity form field
* remove readonly border for the last card form field
* remove readonly border for the last login form field
* remove unneeded true value
---
.../src/form-field/form-field.component.ts | 8 ++++++
.../additional-options.component.html | 2 +-
.../autofill-options-view.component.html | 6 ++++-
.../card-details-view.component.html | 4 +--
.../card-details-view.component.ts | 3 +++
.../custom-fields-v2.component.html | 6 ++---
.../item-details-v2.component.html | 3 +++
.../login-credentials-view.component.html | 4 +--
.../login-credentials-view.component.ts | 2 ++
.../read-only-cipher-card.component.html | 3 +++
.../read-only-cipher-card.component.ts | 26 +++++++++++++++++++
.../view-identity-sections.component.html | 12 ++++-----
.../view-identity-sections.component.ts | 3 +++
13 files changed, 67 insertions(+), 15 deletions(-)
create mode 100644 libs/vault/src/cipher-view/read-only-cipher-card/read-only-cipher-card.component.html
create mode 100644 libs/vault/src/cipher-view/read-only-cipher-card/read-only-cipher-card.component.ts
diff --git a/libs/components/src/form-field/form-field.component.ts b/libs/components/src/form-field/form-field.component.ts
index 6fcb4090ddd..1e364115a6f 100644
--- a/libs/components/src/form-field/form-field.component.ts
+++ b/libs/components/src/form-field/form-field.component.ts
@@ -1,6 +1,7 @@
import { coerceBooleanProperty } from "@angular/cdk/coercion";
import {
AfterContentChecked,
+ booleanAttribute,
Component,
ContentChild,
ContentChildren,
@@ -38,6 +39,13 @@ export class BitFormFieldComponent implements AfterContentChecked {
return this._disableMargin;
}
+ /**
+ * NOTE: Placeholder to match the API of the form-field component in the `ps/extension` branch,
+ * no functionality is implemented as of now.
+ */
+ @Input({ transform: booleanAttribute })
+ disableReadOnlyBorder = false;
+
@HostBinding("class")
get classList() {
return ["tw-block"].concat(this.disableMargin ? [] : ["tw-mb-6"]);
diff --git a/libs/vault/src/cipher-view/additional-options/additional-options.component.html b/libs/vault/src/cipher-view/additional-options/additional-options.component.html
index 6f254b8c729..0913629ad9e 100644
--- a/libs/vault/src/cipher-view/additional-options/additional-options.component.html
+++ b/libs/vault/src/cipher-view/additional-options/additional-options.component.html
@@ -3,7 +3,7 @@
{{ "additionalOptions" | i18n }}
-
+
{{ "note" | i18n }}
-
+
{{ "website" | i18n }}
diff --git a/libs/vault/src/cipher-view/card-details/card-details-view.component.html b/libs/vault/src/cipher-view/card-details/card-details-view.component.html
index c446ba4f319..8503be69059 100644
--- a/libs/vault/src/cipher-view/card-details/card-details-view.component.html
+++ b/libs/vault/src/cipher-view/card-details/card-details-view.component.html
@@ -2,7 +2,7 @@
{{ setSectionTitle }}
-
+
{{ "cardholderName" | i18n }}
-
+
diff --git a/libs/vault/src/cipher-view/card-details/card-details-view.component.ts b/libs/vault/src/cipher-view/card-details/card-details-view.component.ts
index 028417faf16..6ab2795afd9 100644
--- a/libs/vault/src/cipher-view/card-details/card-details-view.component.ts
+++ b/libs/vault/src/cipher-view/card-details/card-details-view.component.ts
@@ -13,6 +13,8 @@ import {
IconButtonModule,
} from "@bitwarden/components";
+import { ReadOnlyCipherCardComponent } from "../read-only-cipher-card/read-only-cipher-card.component";
+
@Component({
selector: "app-card-details-view",
templateUrl: "card-details-view.component.html",
@@ -26,6 +28,7 @@ import {
TypographyModule,
FormFieldModule,
IconButtonModule,
+ ReadOnlyCipherCardComponent,
],
})
export class CardDetailsComponent {
diff --git a/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.html b/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.html
index d4c29cf262b..96cb63fe39b 100644
--- a/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.html
+++ b/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.html
@@ -8,7 +8,7 @@
*ngFor="let field of fields; let last = last"
[ngClass]="{ 'tw-mb-4': !last }"
>
-
+
{{ field.name }}
-
+
{{ field.name }}
@@ -45,7 +45,7 @@
/>
{{ field.name }}
-
+
{{ "linked" | i18n }}: {{ field.name }}
{{ "itemName" | i18n }}
diff --git a/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.html b/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.html
index 6913c34ee5d..17d02658c48 100644
--- a/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.html
+++ b/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.html
@@ -2,7 +2,7 @@
{{ "loginCredentials" | i18n }}
-
+
{{ "username" | i18n }}
@@ -132,5 +132,5 @@
class="disabled:tw-cursor-default"
>
-
+
diff --git a/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.ts b/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.ts
index 3973a666847..6f572f31e87 100644
--- a/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.ts
+++ b/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.ts
@@ -19,6 +19,7 @@ import {
} from "@bitwarden/components";
import { BitTotpCountdownComponent } from "../../components/totp-countdown/totp-countdown.component";
+import { ReadOnlyCipherCardComponent } from "../read-only-cipher-card/read-only-cipher-card.component";
type TotpCodeValues = {
totpCode: string;
@@ -41,6 +42,7 @@ type TotpCodeValues = {
BadgeModule,
ColorPasswordModule,
BitTotpCountdownComponent,
+ ReadOnlyCipherCardComponent,
],
})
export class LoginCredentialsViewComponent {
diff --git a/libs/vault/src/cipher-view/read-only-cipher-card/read-only-cipher-card.component.html b/libs/vault/src/cipher-view/read-only-cipher-card/read-only-cipher-card.component.html
new file mode 100644
index 00000000000..65061e818cb
--- /dev/null
+++ b/libs/vault/src/cipher-view/read-only-cipher-card/read-only-cipher-card.component.html
@@ -0,0 +1,3 @@
+
+
+
diff --git a/libs/vault/src/cipher-view/read-only-cipher-card/read-only-cipher-card.component.ts b/libs/vault/src/cipher-view/read-only-cipher-card/read-only-cipher-card.component.ts
new file mode 100644
index 00000000000..ed16f3a7cc0
--- /dev/null
+++ b/libs/vault/src/cipher-view/read-only-cipher-card/read-only-cipher-card.component.ts
@@ -0,0 +1,26 @@
+import { AfterViewInit, Component, ContentChildren, QueryList } from "@angular/core";
+
+import { CardComponent, BitFormFieldComponent } from "@bitwarden/components";
+
+@Component({
+ selector: "read-only-cipher-card",
+ templateUrl: "./read-only-cipher-card.component.html",
+ standalone: true,
+ imports: [CardComponent],
+})
+/**
+ * A thin wrapper around the `bit-card` component that disables the bottom border for the last form field.
+ */
+export class ReadOnlyCipherCardComponent implements AfterViewInit {
+ @ContentChildren(BitFormFieldComponent) formFields: QueryList;
+
+ ngAfterViewInit(): void {
+ // Disable the bottom border for the last form field
+ if (this.formFields.last) {
+ // Delay model update until next change detection cycle
+ setTimeout(() => {
+ this.formFields.last.disableReadOnlyBorder = true;
+ });
+ }
+ }
+}
diff --git a/libs/vault/src/cipher-view/view-identity-sections/view-identity-sections.component.html b/libs/vault/src/cipher-view/view-identity-sections/view-identity-sections.component.html
index d12a729f99a..29ccd5daa6b 100644
--- a/libs/vault/src/cipher-view/view-identity-sections/view-identity-sections.component.html
+++ b/libs/vault/src/cipher-view/view-identity-sections/view-identity-sections.component.html
@@ -3,7 +3,7 @@
{{ "personalDetails" | i18n }}
-
+
{{ "name" | i18n }}
@@ -43,7 +43,7 @@
[valueLabel]="'company' | i18n"
>
-
+
@@ -51,7 +51,7 @@
{{ "identification" | i18n }}
-
+
{{ "ssn" | i18n }}
@@ -111,7 +111,7 @@
[valueLabel]="'licenseNumber' | i18n"
>
-
+
@@ -119,7 +119,7 @@
{{ "contactInfo" | i18n }}
-
+
{{ "email" | i18n }}
@@ -166,5 +166,5 @@
[valueLabel]="'address' | i18n"
>
-
+
diff --git a/libs/vault/src/cipher-view/view-identity-sections/view-identity-sections.component.ts b/libs/vault/src/cipher-view/view-identity-sections/view-identity-sections.component.ts
index 0fd2c292952..0f3a9f89712 100644
--- a/libs/vault/src/cipher-view/view-identity-sections/view-identity-sections.component.ts
+++ b/libs/vault/src/cipher-view/view-identity-sections/view-identity-sections.component.ts
@@ -12,6 +12,8 @@ import {
TypographyModule,
} from "@bitwarden/components";
+import { ReadOnlyCipherCardComponent } from "../read-only-cipher-card/read-only-cipher-card.component";
+
@Component({
standalone: true,
selector: "app-view-identity-sections",
@@ -25,6 +27,7 @@ import {
TypographyModule,
FormFieldModule,
IconButtonModule,
+ ReadOnlyCipherCardComponent,
],
})
export class ViewIdentitySectionsComponent implements OnInit {
From c5c8a0dd5e76da730cfb633ed3a1b8a02520f561 Mon Sep 17 00:00:00 2001
From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com>
Date: Wed, 4 Sep 2024 10:51:53 -0500
Subject: [PATCH 24/64] swap account font color for muted (#10883)
---
apps/web/src/app/layouts/header/web-header.component.html | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/apps/web/src/app/layouts/header/web-header.component.html b/apps/web/src/app/layouts/header/web-header.component.html
index c8cbd9f8dab..7cba19b29ad 100644
--- a/apps/web/src/app/layouts/header/web-header.component.html
+++ b/apps/web/src/app/layouts/header/web-header.component.html
@@ -36,13 +36,13 @@
{{ "loggedInAs" | i18n }}
-
+
{{ account | userName }}
From c73ee8812639d0183429e43718249bbba411502b Mon Sep 17 00:00:00 2001
From: Merissa Weinstein
Date: Wed, 4 Sep 2024 10:52:22 -0500
Subject: [PATCH 25/64] turn enableCipherKeyEncryption flag off (#10621)
---
apps/browser/config/base.json | 2 +-
apps/browser/config/development.json | 2 +-
apps/browser/config/production.json | 2 +-
apps/cli/config/development.json | 2 +-
apps/cli/config/production.json | 2 +-
apps/desktop/config/base.json | 2 +-
apps/desktop/config/development.json | 2 +-
apps/desktop/config/production.json | 2 +-
apps/web/config/base.json | 2 +-
apps/web/config/cloud.json | 2 +-
apps/web/config/development.json | 2 +-
apps/web/config/euprd.json | 2 +-
apps/web/config/qa.json | 2 +-
apps/web/config/selfhosted.json | 2 +-
14 files changed, 14 insertions(+), 14 deletions(-)
diff --git a/apps/browser/config/base.json b/apps/browser/config/base.json
index b6f24bf9ae3..6c428c43d26 100644
--- a/apps/browser/config/base.json
+++ b/apps/browser/config/base.json
@@ -2,7 +2,7 @@
"devFlags": {},
"flags": {
"showPasswordless": true,
- "enableCipherKeyEncryption": true,
+ "enableCipherKeyEncryption": false,
"accountSwitching": false
}
}
diff --git a/apps/browser/config/development.json b/apps/browser/config/development.json
index 950c5372d8f..e0925ebecc9 100644
--- a/apps/browser/config/development.json
+++ b/apps/browser/config/development.json
@@ -7,7 +7,7 @@
},
"flags": {
"showPasswordless": true,
- "enableCipherKeyEncryption": true,
+ "enableCipherKeyEncryption": false,
"accountSwitching": true
}
}
diff --git a/apps/browser/config/production.json b/apps/browser/config/production.json
index 64c6cb92a3b..027003f6c75 100644
--- a/apps/browser/config/production.json
+++ b/apps/browser/config/production.json
@@ -1,6 +1,6 @@
{
"flags": {
- "enableCipherKeyEncryption": true,
+ "enableCipherKeyEncryption": false,
"accountSwitching": true
}
}
diff --git a/apps/cli/config/development.json b/apps/cli/config/development.json
index bc06f69d657..f57c3d9bc38 100644
--- a/apps/cli/config/development.json
+++ b/apps/cli/config/development.json
@@ -1,5 +1,5 @@
{
"flags": {
- "enableCipherKeyEncryption": true
+ "enableCipherKeyEncryption": false
}
}
diff --git a/apps/cli/config/production.json b/apps/cli/config/production.json
index bc06f69d657..f57c3d9bc38 100644
--- a/apps/cli/config/production.json
+++ b/apps/cli/config/production.json
@@ -1,5 +1,5 @@
{
"flags": {
- "enableCipherKeyEncryption": true
+ "enableCipherKeyEncryption": false
}
}
diff --git a/apps/desktop/config/base.json b/apps/desktop/config/base.json
index 7f18c63878b..7a8659feffe 100644
--- a/apps/desktop/config/base.json
+++ b/apps/desktop/config/base.json
@@ -1,6 +1,6 @@
{
"devFlags": {},
"flags": {
- "enableCipherKeyEncryption": true
+ "enableCipherKeyEncryption": false
}
}
diff --git a/apps/desktop/config/development.json b/apps/desktop/config/development.json
index 7f18c63878b..7a8659feffe 100644
--- a/apps/desktop/config/development.json
+++ b/apps/desktop/config/development.json
@@ -1,6 +1,6 @@
{
"devFlags": {},
"flags": {
- "enableCipherKeyEncryption": true
+ "enableCipherKeyEncryption": false
}
}
diff --git a/apps/desktop/config/production.json b/apps/desktop/config/production.json
index bc06f69d657..f57c3d9bc38 100644
--- a/apps/desktop/config/production.json
+++ b/apps/desktop/config/production.json
@@ -1,5 +1,5 @@
{
"flags": {
- "enableCipherKeyEncryption": true
+ "enableCipherKeyEncryption": false
}
}
diff --git a/apps/web/config/base.json b/apps/web/config/base.json
index b9102a769d7..5dc03a4633d 100644
--- a/apps/web/config/base.json
+++ b/apps/web/config/base.json
@@ -12,6 +12,6 @@
},
"flags": {
"showPasswordless": false,
- "enableCipherKeyEncryption": true
+ "enableCipherKeyEncryption": false
}
}
diff --git a/apps/web/config/cloud.json b/apps/web/config/cloud.json
index c8ba07e755e..3faa2926929 100644
--- a/apps/web/config/cloud.json
+++ b/apps/web/config/cloud.json
@@ -18,6 +18,6 @@
},
"flags": {
"showPasswordless": true,
- "enableCipherKeyEncryption": true
+ "enableCipherKeyEncryption": false
}
}
diff --git a/apps/web/config/development.json b/apps/web/config/development.json
index 3fcd8641b32..44391a7450d 100644
--- a/apps/web/config/development.json
+++ b/apps/web/config/development.json
@@ -21,7 +21,7 @@
],
"flags": {
"showPasswordless": true,
- "enableCipherKeyEncryption": true
+ "enableCipherKeyEncryption": false
},
"devFlags": {}
}
diff --git a/apps/web/config/euprd.json b/apps/web/config/euprd.json
index 2d554e57043..72f0c1857d9 100644
--- a/apps/web/config/euprd.json
+++ b/apps/web/config/euprd.json
@@ -12,6 +12,6 @@
},
"flags": {
"showPasswordless": true,
- "enableCipherKeyEncryption": true
+ "enableCipherKeyEncryption": false
}
}
diff --git a/apps/web/config/qa.json b/apps/web/config/qa.json
index f03d47fe4ee..ac36b107846 100644
--- a/apps/web/config/qa.json
+++ b/apps/web/config/qa.json
@@ -28,6 +28,6 @@
],
"flags": {
"showPasswordless": true,
- "enableCipherKeyEncryption": true
+ "enableCipherKeyEncryption": false
}
}
diff --git a/apps/web/config/selfhosted.json b/apps/web/config/selfhosted.json
index 121f59ba0b3..7e916a11169 100644
--- a/apps/web/config/selfhosted.json
+++ b/apps/web/config/selfhosted.json
@@ -8,6 +8,6 @@
},
"flags": {
"showPasswordless": true,
- "enableCipherKeyEncryption": true
+ "enableCipherKeyEncryption": false
}
}
From fdeac584697f40fd6fb68bf1175dbe8fd83179fc Mon Sep 17 00:00:00 2001
From: Will Martin
Date: Wed, 4 Sep 2024 12:12:47 -0400
Subject: [PATCH 26/64] [CL-312] fix dialog scroll blocking + virtual scroll
(#9606)
---
libs/components/src/dialog/dialog.service.ts | 26 +++++++-
.../dialog-virtual-scroll-block.component.ts | 61 +++++++++++++++++++
.../kitchen-sink/kitchen-sink.stories.ts | 33 +++++++++-
3 files changed, 117 insertions(+), 3 deletions(-)
create mode 100644 libs/components/src/stories/kitchen-sink/components/dialog-virtual-scroll-block.component.ts
diff --git a/libs/components/src/dialog/dialog.service.ts b/libs/components/src/dialog/dialog.service.ts
index 9488da4ac6d..62a56d20af0 100644
--- a/libs/components/src/dialog/dialog.service.ts
+++ b/libs/components/src/dialog/dialog.service.ts
@@ -5,7 +5,7 @@ import {
DialogRef,
DIALOG_SCROLL_STRATEGY,
} from "@angular/cdk/dialog";
-import { ComponentType, Overlay, OverlayContainer } from "@angular/cdk/overlay";
+import { ComponentType, Overlay, OverlayContainer, ScrollStrategy } from "@angular/cdk/overlay";
import {
Inject,
Injectable,
@@ -25,12 +25,35 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
import { SimpleConfigurableDialogComponent } from "./simple-dialog/simple-configurable-dialog/simple-configurable-dialog.component";
import { SimpleDialogOptions, Translation } from "./simple-dialog/types";
+/**
+ * The default `BlockScrollStrategy` does not work well with virtual scrolling.
+ *
+ * https://github.com/angular/components/issues/7390
+ */
+class CustomBlockScrollStrategy implements ScrollStrategy {
+ enable() {
+ document.body.classList.add("tw-overflow-hidden");
+ }
+
+ disable() {
+ document.body.classList.remove("tw-overflow-hidden");
+ }
+
+ /** Noop */
+ attach() {}
+
+ /** Noop */
+ detach() {}
+}
+
@Injectable()
export class DialogService extends Dialog implements OnDestroy {
private _destroy$ = new Subject();
private backDropClasses = ["tw-fixed", "tw-bg-black", "tw-bg-opacity-30", "tw-inset-0"];
+ private defaultScrollStrategy = new CustomBlockScrollStrategy();
+
constructor(
/** Parent class constructor */
_overlay: Overlay,
@@ -73,6 +96,7 @@ export class DialogService extends Dialog implements OnDestroy {
): DialogRef {
config = {
backdropClass: this.backDropClasses,
+ scrollStrategy: this.defaultScrollStrategy,
...config,
};
diff --git a/libs/components/src/stories/kitchen-sink/components/dialog-virtual-scroll-block.component.ts b/libs/components/src/stories/kitchen-sink/components/dialog-virtual-scroll-block.component.ts
new file mode 100644
index 00000000000..a867d9cdf53
--- /dev/null
+++ b/libs/components/src/stories/kitchen-sink/components/dialog-virtual-scroll-block.component.ts
@@ -0,0 +1,61 @@
+import { ScrollingModule } from "@angular/cdk/scrolling";
+import { Component, OnInit } from "@angular/core";
+
+import { DialogModule, DialogService } from "../../../dialog";
+import { IconButtonModule } from "../../../icon-button";
+import { SectionComponent } from "../../../section";
+import { TableDataSource, TableModule } from "../../../table";
+
+@Component({
+ selector: "dialog-virtual-scroll-block",
+ standalone: true,
+ imports: [DialogModule, IconButtonModule, SectionComponent, TableModule, ScrollingModule],
+ template: `
+
+
+
+
+ | Id |
+ Name |
+ Options |
+
+
+
+
+ | {{ r.id }} |
+ {{ r.name }} |
+
+
+ |
+
+
+
+
+ `,
+})
+export class DialogVirtualScrollBlockComponent implements OnInit {
+ constructor(public dialogService: DialogService) {}
+
+ protected dataSource = new TableDataSource<{ id: number; name: string; other: string }>();
+
+ ngOnInit(): void {
+ this.dataSource.data = [...Array(100).keys()].map((i) => ({
+ id: i,
+ name: `name-${i}`,
+ other: `other-${i}`,
+ }));
+ }
+
+ async openDefaultDialog() {
+ await this.dialogService.openSimpleDialog({
+ type: "info",
+ title: "Foo",
+ content: "Bar",
+ });
+ }
+}
diff --git a/libs/components/src/stories/kitchen-sink/kitchen-sink.stories.ts b/libs/components/src/stories/kitchen-sink/kitchen-sink.stories.ts
index fa78f04d236..203c510f814 100644
--- a/libs/components/src/stories/kitchen-sink/kitchen-sink.stories.ts
+++ b/libs/components/src/stories/kitchen-sink/kitchen-sink.stories.ts
@@ -8,7 +8,15 @@ import {
componentWrapperDecorator,
moduleMetadata,
} from "@storybook/angular";
-import { userEvent, getAllByRole, getByRole, getByLabelText, fireEvent } from "@storybook/test";
+import {
+ userEvent,
+ getAllByRole,
+ getByRole,
+ getByLabelText,
+ fireEvent,
+ getByText,
+ getAllByLabelText,
+} from "@storybook/test";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -16,6 +24,7 @@ import { DialogService } from "../../dialog";
import { LayoutComponent } from "../../layout";
import { I18nMockService } from "../../utils/i18n-mock.service";
+import { DialogVirtualScrollBlockComponent } from "./components/dialog-virtual-scroll-block.component";
import { KitchenSinkForm } from "./components/kitchen-sink-form.component";
import { KitchenSinkMainComponent } from "./components/kitchen-sink-main.component";
import { KitchenSinkTable } from "./components/kitchen-sink-table.component";
@@ -64,7 +73,9 @@ export default {
skipToContent: "Skip to content",
submenu: "submenu",
toggleCollapse: "toggle collapse",
- toggleSideNavigation: "toggle side navigation",
+ toggleSideNavigation: "Toggle side navigation",
+ yes: "Yes",
+ no: "No",
});
},
},
@@ -78,6 +89,7 @@ export default {
[
{ path: "", redirectTo: "bitwarden", pathMatch: "full" },
{ path: "bitwarden", component: KitchenSinkMainComponent },
+ { path: "virtual-scroll", component: DialogVirtualScrollBlockComponent },
],
{ useHash: true },
),
@@ -100,6 +112,7 @@ export const Default: Story = {
+
@@ -165,3 +178,19 @@ export const EmptyTab: Story = {
await userEvent.click(emptyTab);
},
};
+
+export const VirtualScrollBlockingDialog: Story = {
+ ...Default,
+ play: async (context) => {
+ const canvas = context.canvasElement;
+ const navItem = getByText(canvas, "Virtual Scroll");
+ await userEvent.click(navItem);
+
+ const htmlEl = canvas.ownerDocument.documentElement;
+ htmlEl.scrollTop = 2000;
+
+ const dialogButton = getAllByLabelText(canvas, "Options")[0];
+
+ await userEvent.click(dialogButton);
+ },
+};
From 6edc3edb9a10d7489e6de91e2cfd0857b1c843f0 Mon Sep 17 00:00:00 2001
From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com>
Date: Wed, 4 Sep 2024 12:30:47 -0400
Subject: [PATCH 27/64] [AC-2960] Create new adjust-storage-dialog component
without payment.component (#10793)
* Add adjust-storage-dialog-v2.component
* (No Logic) Rename old adjust-storage.component to adjust-storage-dialog.component
* (No Logic) Move existing adjust-storage-dialog.component into new adjust-storage-dialog folder
* Use adjust-storage-dialog-v2.component in adjustStorage methods when FF is on
---
.../individual/user-subscription.component.ts | 49 +++++++--
...ganization-subscription-cloud.component.ts | 51 +++++++--
.../adjust-storage-dialog-v2.component.html | 34 ++++++
.../adjust-storage-dialog-v2.component.ts | 104 ++++++++++++++++++
.../adjust-storage-dialog.component.html} | 0
.../adjust-storage-dialog.component.ts} | 8 +-
.../billing/shared/billing-shared.module.ts | 8 +-
7 files changed, 225 insertions(+), 29 deletions(-)
create mode 100644 apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog-v2.component.html
create mode 100644 apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog-v2.component.ts
rename apps/web/src/app/billing/shared/{adjust-storage.component.html => adjust-storage-dialog/adjust-storage-dialog.component.html} (100%)
rename apps/web/src/app/billing/shared/{adjust-storage.component.ts => adjust-storage-dialog/adjust-storage-dialog.component.ts} (95%)
diff --git a/apps/web/src/app/billing/individual/user-subscription.component.ts b/apps/web/src/app/billing/individual/user-subscription.component.ts
index 2d02cbc5bdf..113d2feabe4 100644
--- a/apps/web/src/app/billing/individual/user-subscription.component.ts
+++ b/apps/web/src/app/billing/individual/user-subscription.component.ts
@@ -5,6 +5,8 @@ import { firstValueFrom, lastValueFrom } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { SubscriptionResponse } from "@bitwarden/common/billing/models/response/subscription.response";
+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 { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -12,10 +14,14 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { DialogService, ToastService } from "@bitwarden/components";
+import {
+ AdjustStorageDialogV2Component,
+ AdjustStorageDialogV2ResultType,
+} from "../shared/adjust-storage-dialog/adjust-storage-dialog-v2.component";
import {
AdjustStorageDialogResult,
openAdjustStorageDialog,
-} from "../shared/adjust-storage.component";
+} from "../shared/adjust-storage-dialog/adjust-storage-dialog.component";
import {
OffboardingSurveyDialogResultType,
openOffboardingSurvey,
@@ -38,6 +44,10 @@ export class UserSubscriptionComponent implements OnInit {
cancelPromise: Promise;
reinstatePromise: Promise;
+ protected deprecateStripeSourcesAPI$ = this.configService.getFeatureFlag$(
+ FeatureFlag.AC2476_DeprecateStripeSourcesAPI,
+ );
+
constructor(
private apiService: ApiService,
private platformUtilsService: PlatformUtilsService,
@@ -49,6 +59,7 @@ export class UserSubscriptionComponent implements OnInit {
private environmentService: EnvironmentService,
private billingAccountProfileStateService: BillingAccountProfileStateService,
private toastService: ToastService,
+ private configService: ConfigService,
) {
this.selfHosted = platformUtilsService.isSelfHost();
}
@@ -150,15 +161,33 @@ export class UserSubscriptionComponent implements OnInit {
};
adjustStorage = async (add: boolean) => {
- const dialogRef = openAdjustStorageDialog(this.dialogService, {
- data: {
- storageGbPrice: 4,
- add: add,
- },
- });
- const result = await lastValueFrom(dialogRef.closed);
- if (result === AdjustStorageDialogResult.Adjusted) {
- await this.load();
+ const deprecateStripeSourcesAPI = await firstValueFrom(this.deprecateStripeSourcesAPI$);
+
+ if (deprecateStripeSourcesAPI) {
+ const dialogRef = AdjustStorageDialogV2Component.open(this.dialogService, {
+ data: {
+ price: 4,
+ cadence: "year",
+ type: add ? "Add" : "Remove",
+ },
+ });
+
+ const result = await lastValueFrom(dialogRef.closed);
+
+ if (result === AdjustStorageDialogV2ResultType.Submitted) {
+ await this.load();
+ }
+ } else {
+ const dialogRef = openAdjustStorageDialog(this.dialogService, {
+ data: {
+ storageGbPrice: 4,
+ add: add,
+ },
+ });
+ const result = await lastValueFrom(dialogRef.closed);
+ if (result === AdjustStorageDialogResult.Adjusted) {
+ await this.load();
+ }
}
};
diff --git a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts
index f28933a4ecc..2a565face75 100644
--- a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts
+++ b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts
@@ -18,10 +18,14 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { DialogService, ToastService } from "@bitwarden/components";
+import {
+ AdjustStorageDialogV2Component,
+ AdjustStorageDialogV2ResultType,
+} from "../shared/adjust-storage-dialog/adjust-storage-dialog-v2.component";
import {
AdjustStorageDialogResult,
openAdjustStorageDialog,
-} from "../shared/adjust-storage.component";
+} from "../shared/adjust-storage-dialog/adjust-storage-dialog.component";
import {
OffboardingSurveyDialogResultType,
openOffboardingSurvey,
@@ -71,6 +75,10 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
FeatureFlag.EnableUpgradePasswordManagerSub,
);
+ protected deprecateStripeSourcesAPI$ = this.configService.getFeatureFlag$(
+ FeatureFlag.AC2476_DeprecateStripeSourcesAPI,
+ );
+
constructor(
private apiService: ApiService,
private platformUtilsService: PlatformUtilsService,
@@ -458,17 +466,36 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
adjustStorage = (add: boolean) => {
return async () => {
- const dialogRef = openAdjustStorageDialog(this.dialogService, {
- data: {
- storageGbPrice: this.storageGbPrice,
- add: add,
- organizationId: this.organizationId,
- interval: this.billingInterval,
- },
- });
- const result = await lastValueFrom(dialogRef.closed);
- if (result === AdjustStorageDialogResult.Adjusted) {
- await this.load();
+ const deprecateStripeSourcesAPI = await firstValueFrom(this.deprecateStripeSourcesAPI$);
+
+ if (deprecateStripeSourcesAPI) {
+ const dialogRef = AdjustStorageDialogV2Component.open(this.dialogService, {
+ data: {
+ price: this.storageGbPrice,
+ cadence: this.billingInterval,
+ type: add ? "Add" : "Remove",
+ organizationId: this.organizationId,
+ },
+ });
+
+ const result = await lastValueFrom(dialogRef.closed);
+
+ if (result === AdjustStorageDialogV2ResultType.Submitted) {
+ await this.load();
+ }
+ } else {
+ const dialogRef = openAdjustStorageDialog(this.dialogService, {
+ data: {
+ storageGbPrice: this.storageGbPrice,
+ add: add,
+ organizationId: this.organizationId,
+ interval: this.billingInterval,
+ },
+ });
+ const result = await lastValueFrom(dialogRef.closed);
+ if (result === AdjustStorageDialogResult.Adjusted) {
+ await this.load();
+ }
}
};
};
diff --git a/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog-v2.component.html b/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog-v2.component.html
new file mode 100644
index 00000000000..7b74379acb6
--- /dev/null
+++ b/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog-v2.component.html
@@ -0,0 +1,34 @@
+
diff --git a/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog-v2.component.ts b/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog-v2.component.ts
new file mode 100644
index 00000000000..23d5e46fa1b
--- /dev/null
+++ b/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog-v2.component.ts
@@ -0,0 +1,104 @@
+import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
+import { Component, Inject } from "@angular/core";
+import { FormControl, FormGroup, Validators } from "@angular/forms";
+
+import { ApiService } from "@bitwarden/common/abstractions/api.service";
+import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
+import { StorageRequest } from "@bitwarden/common/models/request/storage.request";
+import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
+import { DialogService, ToastService } from "@bitwarden/components";
+
+export interface AdjustStorageDialogV2Params {
+ price: number;
+ cadence: "month" | "year";
+ type: "Add" | "Remove";
+ organizationId?: string;
+}
+
+export enum AdjustStorageDialogV2ResultType {
+ Submitted = "submitted",
+ Closed = "closed",
+}
+
+@Component({
+ templateUrl: "./adjust-storage-dialog-v2.component.html",
+})
+export class AdjustStorageDialogV2Component {
+ protected formGroup = new FormGroup({
+ storage: new FormControl(0, [
+ Validators.required,
+ Validators.min(0),
+ Validators.max(99),
+ ]),
+ });
+
+ protected organizationId?: string;
+ protected price: number;
+ protected cadence: "month" | "year";
+
+ protected title: string;
+ protected body: string;
+ protected storageFieldLabel: string;
+
+ protected ResultType = AdjustStorageDialogV2ResultType;
+
+ constructor(
+ private apiService: ApiService,
+ @Inject(DIALOG_DATA) protected dialogParams: AdjustStorageDialogV2Params,
+ private dialogRef: DialogRef,
+ private i18nService: I18nService,
+ private organizationApiService: OrganizationApiServiceAbstraction,
+ private toastService: ToastService,
+ ) {
+ this.price = this.dialogParams.price;
+ this.cadence = this.dialogParams.cadence;
+ this.organizationId = this.dialogParams.organizationId;
+ switch (this.dialogParams.type) {
+ case "Add":
+ this.title = this.i18nService.t("addStorage");
+ this.body = this.i18nService.t("storageAddNote");
+ this.storageFieldLabel = this.i18nService.t("gbStorageAdd");
+ break;
+ case "Remove":
+ this.title = this.i18nService.t("removeStorage");
+ this.body = this.i18nService.t("storageRemoveNote");
+ this.storageFieldLabel = this.i18nService.t("gbStorageRemove");
+ break;
+ }
+ }
+
+ submit = async () => {
+ const request = new StorageRequest();
+ switch (this.dialogParams.type) {
+ case "Add":
+ request.storageGbAdjustment = this.formGroup.value.storage;
+ break;
+ case "Remove":
+ request.storageGbAdjustment = this.formGroup.value.storage * -1;
+ break;
+ }
+
+ if (this.organizationId) {
+ await this.organizationApiService.updateStorage(this.organizationId, request);
+ } else {
+ await this.apiService.postAccountStorage(request);
+ }
+
+ this.toastService.showToast({
+ variant: "success",
+ title: null,
+ message: this.i18nService.t("adjustedStorage", request.storageGbAdjustment.toString()),
+ });
+
+ this.dialogRef.close(this.ResultType.Submitted);
+ };
+
+ static open = (
+ dialogService: DialogService,
+ dialogConfig: DialogConfig,
+ ) =>
+ dialogService.open(
+ AdjustStorageDialogV2Component,
+ dialogConfig,
+ );
+}
diff --git a/apps/web/src/app/billing/shared/adjust-storage.component.html b/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog.component.html
similarity index 100%
rename from apps/web/src/app/billing/shared/adjust-storage.component.html
rename to apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog.component.html
diff --git a/apps/web/src/app/billing/shared/adjust-storage.component.ts b/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog.component.ts
similarity index 95%
rename from apps/web/src/app/billing/shared/adjust-storage.component.ts
rename to apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog.component.ts
index 5cf05ea015c..a67c63a9fad 100644
--- a/apps/web/src/app/billing/shared/adjust-storage.component.ts
+++ b/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog.component.ts
@@ -12,7 +12,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { DialogService, ToastService } from "@bitwarden/components";
-import { PaymentComponent } from "./payment/payment.component";
+import { PaymentComponent } from "../payment/payment.component";
export interface AdjustStorageDialogData {
storageGbPrice: number;
@@ -27,9 +27,9 @@ export enum AdjustStorageDialogResult {
}
@Component({
- templateUrl: "adjust-storage.component.html",
+ templateUrl: "adjust-storage-dialog.component.html",
})
-export class AdjustStorageComponent {
+export class AdjustStorageDialogComponent {
storageGbPrice: number;
add: boolean;
organizationId: string;
@@ -126,5 +126,5 @@ export function openAdjustStorageDialog(
dialogService: DialogService,
config: DialogConfig,
) {
- return dialogService.open(AdjustStorageComponent, config);
+ return dialogService.open(AdjustStorageDialogComponent, config);
}
diff --git a/apps/web/src/app/billing/shared/billing-shared.module.ts b/apps/web/src/app/billing/shared/billing-shared.module.ts
index 300817bad55..c9b3f2de855 100644
--- a/apps/web/src/app/billing/shared/billing-shared.module.ts
+++ b/apps/web/src/app/billing/shared/billing-shared.module.ts
@@ -6,7 +6,8 @@ import { SharedModule } from "../../shared";
import { AddCreditDialogComponent } from "./add-credit-dialog.component";
import { AdjustPaymentDialogV2Component } from "./adjust-payment-dialog/adjust-payment-dialog-v2.component";
import { AdjustPaymentDialogComponent } from "./adjust-payment-dialog/adjust-payment-dialog.component";
-import { AdjustStorageComponent } from "./adjust-storage.component";
+import { AdjustStorageDialogV2Component } from "./adjust-storage-dialog/adjust-storage-dialog-v2.component";
+import { AdjustStorageDialogComponent } from "./adjust-storage-dialog/adjust-storage-dialog.component";
import { BillingHistoryComponent } from "./billing-history.component";
import { OffboardingSurveyComponent } from "./offboarding-survey.component";
import { PaymentV2Component } from "./payment/payment-v2.component";
@@ -30,7 +31,7 @@ import { VerifyBankAccountComponent } from "./verify-bank-account/verify-bank-ac
declarations: [
AddCreditDialogComponent,
AdjustPaymentDialogComponent,
- AdjustStorageComponent,
+ AdjustStorageDialogComponent,
BillingHistoryComponent,
PaymentMethodComponent,
SecretsManagerSubscribeComponent,
@@ -38,12 +39,13 @@ import { VerifyBankAccountComponent } from "./verify-bank-account/verify-bank-ac
UpdateLicenseDialogComponent,
OffboardingSurveyComponent,
AdjustPaymentDialogV2Component,
+ AdjustStorageDialogV2Component,
],
exports: [
SharedModule,
PaymentComponent,
TaxInfoComponent,
- AdjustStorageComponent,
+ AdjustStorageDialogComponent,
BillingHistoryComponent,
SecretsManagerSubscribeComponent,
UpdateLicenseComponent,
From 72dab94216160c15799b2b8e537fc8693921d7aa Mon Sep 17 00:00:00 2001
From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com>
Date: Wed, 4 Sep 2024 11:44:52 -0500
Subject: [PATCH 28/64] remove brand from logic that determines if the card
section should show (#10871)
---
libs/vault/src/cipher-view/cipher-view.component.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/libs/vault/src/cipher-view/cipher-view.component.ts b/libs/vault/src/cipher-view/cipher-view.component.ts
index cb8f86b9809..e737e43f355 100644
--- a/libs/vault/src/cipher-view/cipher-view.component.ts
+++ b/libs/vault/src/cipher-view/cipher-view.component.ts
@@ -70,8 +70,8 @@ export class CipherViewComponent implements OnInit, OnDestroy {
}
get hasCard() {
- const { cardholderName, code, expMonth, expYear, brand, number } = this.cipher.card;
- return cardholderName || code || expMonth || expYear || brand || number;
+ const { cardholderName, code, expMonth, expYear, number } = this.cipher.card;
+ return cardholderName || code || expMonth || expYear || number;
}
get hasLogin() {
From 44f1fc156c6c160b8de30885869adbc03c51cadf Mon Sep 17 00:00:00 2001
From: Jonathan Prusik
Date: Wed, 4 Sep 2024 13:39:48 -0400
Subject: [PATCH 29/64] [PM-11458] Bugfix - If two digit year was entered for
card, the expired card message shows if card is not expired (#10801)
* normalize card expiry year before determining if it is expired
* add tests
---
.../individual-vault/add-edit.component.ts | 22 +-------
libs/common/src/vault/utils.spec.ts | 50 ++++++++++++++++++-
libs/common/src/vault/utils.ts | 41 +++++++++++++++
.../src/cipher-view/cipher-view.component.ts | 24 +--------
4 files changed, 94 insertions(+), 43 deletions(-)
diff --git a/apps/web/src/app/vault/individual-vault/add-edit.component.ts b/apps/web/src/app/vault/individual-vault/add-edit.component.ts
index 71ccaab7dd7..d1b51b611f5 100644
--- a/apps/web/src/app/vault/individual-vault/add-edit.component.ts
+++ b/apps/web/src/app/vault/individual-vault/add-edit.component.ts
@@ -24,7 +24,7 @@ import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folde
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { Launchable } from "@bitwarden/common/vault/interfaces/launchable";
-import { CardView } from "@bitwarden/common/vault/models/view/card.view";
+import { isCardExpired } from "@bitwarden/common/vault/utils";
import { DialogService } from "@bitwarden/components";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
import { PasswordRepromptService } from "@bitwarden/vault";
@@ -123,7 +123,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On
this.configService.getFeatureFlag$(FeatureFlag.ExtensionRefresh),
);
- this.cardIsExpired = extensionRefreshEnabled && this.isCardExpiryInThePast();
+ this.cardIsExpired = extensionRefreshEnabled && isCardExpired(this.cipher.card);
}
ngOnDestroy() {
@@ -235,24 +235,6 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On
this.viewingPasswordHistory = !this.viewingPasswordHistory;
}
- isCardExpiryInThePast() {
- if (this.cipher.card) {
- const { expMonth, expYear }: CardView = this.cipher.card;
-
- if (expYear && expMonth) {
- // `Date` months are zero-indexed
- const parsedMonth = parseInt(expMonth) - 1;
- const parsedYear = parseInt(expYear);
-
- // First day of the next month minus one, to get last day of the card month
- const cardExpiry = new Date(parsedYear, parsedMonth + 1, 0);
- const now = new Date();
-
- return cardExpiry < now;
- }
- }
- }
-
protected cleanUp() {
if (this.totpInterval) {
window.clearInterval(this.totpInterval);
diff --git a/libs/common/src/vault/utils.spec.ts b/libs/common/src/vault/utils.spec.ts
index 1cb185cffd3..54ec66984e2 100644
--- a/libs/common/src/vault/utils.spec.ts
+++ b/libs/common/src/vault/utils.spec.ts
@@ -1,4 +1,5 @@
-import { normalizeExpiryYearFormat } from "@bitwarden/common/vault/utils";
+import { CardView } from "@bitwarden/common/vault/models/view/card.view";
+import { normalizeExpiryYearFormat, isCardExpired } from "@bitwarden/common/vault/utils";
function getExpiryYearValueFormats(currentCentury: string) {
return [
@@ -72,3 +73,50 @@ describe("normalizeExpiryYearFormat", () => {
jest.clearAllTimers();
});
});
+
+function getCardExpiryDateValues() {
+ const currentDate = new Date();
+
+ const currentYear = currentDate.getFullYear();
+
+ // `Date` months are zero-indexed, our expiry date month inputs are one-indexed
+ const currentMonth = currentDate.getMonth() + 1;
+
+ return [
+ [null, null, false], // no month, no year
+ [undefined, undefined, false], // no month, no year, invalid values
+ ["", "", false], // no month, no year, invalid values
+ ["12", "agdredg42grg35grrr. ea3534@#^145345ag$%^ -_#$rdg ", false], // invalid values
+ ["0", `${currentYear - 1}`, true], // invalid 0 month
+ ["00", `${currentYear + 1}`, false], // invalid 0 month
+ [`${currentMonth}`, "0000", true], // current month, in the year 2000
+ [null, `${currentYear}`.slice(-2), false], // no month, this year
+ [null, `${currentYear - 1}`.slice(-2), true], // no month, last year
+ ["1", null, false], // no year, January
+ ["1", `${currentYear - 1}`, true], // January last year
+ ["13", `${currentYear}`, false], // 12 + 1 is Feb. in the next year (Date is zero-indexed)
+ [`${currentMonth + 36}`, `${currentYear - 1}`, true], // even though the month value would put the date 3 years into the future when calculated with `Date`, an explicit year in the past indicates the card is expired
+ [`${currentMonth}`, `${currentYear}`, false], // this year, this month (not expired until the month is over)
+ [`${currentMonth}`, `${currentYear}`.slice(-2), false], // This month, this year (not expired until the month is over)
+ [`${currentMonth - 1}`, `${currentYear}`, true], // last month
+ [`${currentMonth - 1}`, `${currentYear + 1}`, false], // 11 months from now
+ ];
+}
+
+describe("isCardExpired", () => {
+ const expiryYearValueFormats = getCardExpiryDateValues();
+
+ expiryYearValueFormats.forEach(
+ ([inputMonth, inputYear, expectedValue]: [string | null, string | null, boolean]) => {
+ it(`should return ${expectedValue} when the card expiry month is ${inputMonth} and the card expiry year is ${inputYear}`, () => {
+ const testCardView = new CardView();
+ testCardView.expMonth = inputMonth;
+ testCardView.expYear = inputYear;
+
+ const cardIsExpired = isCardExpired(testCardView);
+
+ expect(cardIsExpired).toBe(expectedValue);
+ });
+ },
+ );
+});
diff --git a/libs/common/src/vault/utils.ts b/libs/common/src/vault/utils.ts
index 7fed4abc12e..7d8784eda78 100644
--- a/libs/common/src/vault/utils.ts
+++ b/libs/common/src/vault/utils.ts
@@ -1,3 +1,5 @@
+import { CardView } from "@bitwarden/common/vault/models/view/card.view";
+
type NonZeroIntegers = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
type Year = `${NonZeroIntegers}${NonZeroIntegers}${0 | NonZeroIntegers}${0 | NonZeroIntegers}`;
@@ -40,3 +42,42 @@ export function normalizeExpiryYearFormat(yearInput: string | number): Year | nu
return expirationYear as Year | null;
}
+
+/**
+ * Takes a cipher card view and returns "true" if the month and year affirmativey indicate
+ * the card is expired.
+ *
+ * @export
+ * @param {CardView} cipherCard
+ * @return {*} {boolean}
+ */
+export function isCardExpired(cipherCard: CardView): boolean {
+ if (cipherCard) {
+ const { expMonth = null, expYear = null } = cipherCard;
+
+ const now = new Date();
+ const normalizedYear = normalizeExpiryYearFormat(expYear);
+
+ // If the card year is before the current year, don't bother checking the month
+ if (normalizedYear && parseInt(normalizedYear) < now.getFullYear()) {
+ return true;
+ }
+
+ if (normalizedYear && expMonth) {
+ // `Date` months are zero-indexed
+ const parsedMonth =
+ parseInt(expMonth) - 1 ||
+ // Add a month floor of 0 to protect against an invalid low month value of "0"
+ 0;
+
+ const parsedYear = parseInt(normalizedYear);
+
+ // First day of the next month minus one, to get last day of the card month
+ const cardExpiry = new Date(parsedYear, parsedMonth + 1, 0);
+
+ return cardExpiry < now;
+ }
+ }
+
+ return false;
+}
diff --git a/libs/vault/src/cipher-view/cipher-view.component.ts b/libs/vault/src/cipher-view/cipher-view.component.ts
index e737e43f355..10701083b79 100644
--- a/libs/vault/src/cipher-view/cipher-view.component.ts
+++ b/libs/vault/src/cipher-view/cipher-view.component.ts
@@ -8,10 +8,10 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
import { CollectionId } from "@bitwarden/common/types/guid";
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
-import { CardView } from "@bitwarden/common/vault/models/view/card.view";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
+import { isCardExpired } from "@bitwarden/common/vault/utils";
import { SearchModule, CalloutModule } from "@bitwarden/components";
import { AdditionalOptionsComponent } from "./additional-options/additional-options.component";
@@ -61,7 +61,7 @@ export class CipherViewComponent implements OnInit, OnDestroy {
async ngOnInit() {
await this.loadCipherData();
- this.cardIsExpired = this.isCardExpiryInThePast();
+ this.cardIsExpired = isCardExpired(this.cipher.card);
}
ngOnDestroy(): void {
@@ -102,24 +102,4 @@ export class CipherViewComponent implements OnInit, OnDestroy {
.pipe(takeUntil(this.destroyed$));
}
}
-
- isCardExpiryInThePast() {
- if (this.cipher.card) {
- const { expMonth, expYear }: CardView = this.cipher.card;
-
- if (expYear && expMonth) {
- // `Date` months are zero-indexed
- const parsedMonth = parseInt(expMonth) - 1;
- const parsedYear = parseInt(expYear);
-
- // First day of the next month minus one, to get last day of the card month
- const cardExpiry = new Date(parsedYear, parsedMonth + 1, 0);
- const now = new Date();
-
- return cardExpiry < now;
- }
- }
-
- return false;
- }
}
From 095ce7ec30061e49dafc9fbff8581f3f9571e57d Mon Sep 17 00:00:00 2001
From: Jonathan Prusik
Date: Wed, 4 Sep 2024 13:50:48 -0400
Subject: [PATCH 30/64] [PM-7956] Update passkey pop-out to new UI (#10796)
* rename existing fido2 components to use v1 designation
* use fido2 message type value constants in components
* add v2 fido2 components
* add search to login UX of fido2 v2 component
* add new item button in top nav of fido2 v2 component
* get and pass activeUserId to cipher key decription methods
* cleanup / PR suggestions
---
apps/browser/src/_locales/en/messages.json | 10 +-
.../src/auth/popup/lock.component.html | 2 +-
.../fido2/content/messaging/message.ts | 4 +-
.../fido2/fido2-cipher-row-v1.component.html | 36 ++
.../fido2/fido2-cipher-row-v1.component.ts | 39 ++
.../fido2/fido2-cipher-row.component.html | 57 +--
.../popup/fido2/fido2-cipher-row.component.ts | 25 +-
.../fido2-use-browser-link-v1.component.html | 52 ++
.../fido2-use-browser-link-v1.component.ts | 113 +++++
.../fido2/fido2-use-browser-link.component.ts | 11 +-
.../popup/fido2/fido2-v1.component.html | 142 ++++++
.../popup/fido2/fido2-v1.component.ts | 443 ++++++++++++++++++
.../autofill/popup/fido2/fido2.component.html | 254 +++++-----
.../autofill/popup/fido2/fido2.component.ts | 233 +++++----
apps/browser/src/popup/app-routing.module.ts | 8 +-
apps/browser/src/popup/app.module.ts | 12 +-
apps/browser/src/popup/scss/pages.scss | 2 +-
17 files changed, 1159 insertions(+), 284 deletions(-)
create mode 100644 apps/browser/src/autofill/popup/fido2/fido2-cipher-row-v1.component.html
create mode 100644 apps/browser/src/autofill/popup/fido2/fido2-cipher-row-v1.component.ts
create mode 100644 apps/browser/src/autofill/popup/fido2/fido2-use-browser-link-v1.component.html
create mode 100644 apps/browser/src/autofill/popup/fido2/fido2-use-browser-link-v1.component.ts
create mode 100644 apps/browser/src/autofill/popup/fido2/fido2-v1.component.html
create mode 100644 apps/browser/src/autofill/popup/fido2/fido2-v1.component.ts
diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json
index 89b605ab632..3aa1ac097ce 100644
--- a/apps/browser/src/_locales/en/messages.json
+++ b/apps/browser/src/_locales/en/messages.json
@@ -3477,7 +3477,7 @@
"passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": {
"message": "Verification required by the initiating site. This feature is not yet implemented for accounts without master password."
},
- "logInWithPasskey": {
+ "logInWithPasskeyQuestion": {
"message": "Log in with passkey?"
},
"passkeyAlreadyExists": {
@@ -3489,6 +3489,9 @@
"noMatchingPasskeyLogin": {
"message": "You do not have a matching login for this site."
},
+ "noMatchingLoginsForSite": {
+ "message": "No matching logins for this site"
+ },
"confirm": {
"message": "Confirm"
},
@@ -3498,9 +3501,12 @@
"savePasskeyNewLogin": {
"message": "Save passkey as new login"
},
- "choosePasskey": {
+ "chooseCipherForPasskeySave": {
"message": "Choose a login to save this passkey to"
},
+ "chooseCipherForPasskeyAuth": {
+ "message": "Choose a passkey to log in with"
+ },
"passkeyItem": {
"message": "Passkey Item"
},
diff --git a/apps/browser/src/auth/popup/lock.component.html b/apps/browser/src/auth/popup/lock.component.html
index ccc743d86d4..fb1b09de49c 100644
--- a/apps/browser/src/auth/popup/lock.component.html
+++ b/apps/browser/src/auth/popup/lock.component.html
@@ -94,7 +94,7 @@
{{ "awaitDesktop" | i18n }}
-
+
diff --git a/apps/browser/src/autofill/fido2/content/messaging/message.ts b/apps/browser/src/autofill/fido2/content/messaging/message.ts
index d42c10a5d88..5815be9eb60 100644
--- a/apps/browser/src/autofill/fido2/content/messaging/message.ts
+++ b/apps/browser/src/autofill/fido2/content/messaging/message.ts
@@ -18,7 +18,7 @@ export enum MessageType {
}
/**
- * The params provided by the page-script are created in an insecure environemnt and
+ * The params provided by the page-script are created in an insecure environment and
* should not be trusted. This type is used to ensure that the content-script does not
* trust the `origin` or `sameOriginWithAncestors` params.
*/
@@ -38,7 +38,7 @@ export type CredentialCreationResponse = {
};
/**
- * The params provided by the page-script are created in an insecure environemnt and
+ * The params provided by the page-script are created in an insecure environment and
* should not be trusted. This type is used to ensure that the content-script does not
* trust the `origin` or `sameOriginWithAncestors` params.
*/
diff --git a/apps/browser/src/autofill/popup/fido2/fido2-cipher-row-v1.component.html b/apps/browser/src/autofill/popup/fido2/fido2-cipher-row-v1.component.html
new file mode 100644
index 00000000000..852fd4a0e81
--- /dev/null
+++ b/apps/browser/src/autofill/popup/fido2/fido2-cipher-row-v1.component.html
@@ -0,0 +1,36 @@
+
diff --git a/apps/browser/src/autofill/popup/fido2/fido2-cipher-row-v1.component.ts b/apps/browser/src/autofill/popup/fido2/fido2-cipher-row-v1.component.ts
new file mode 100644
index 00000000000..d9d492bdcc1
--- /dev/null
+++ b/apps/browser/src/autofill/popup/fido2/fido2-cipher-row-v1.component.ts
@@ -0,0 +1,39 @@
+import { Component, EventEmitter, Input, Output, ChangeDetectionStrategy } from "@angular/core";
+
+import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
+
+@Component({
+ selector: "app-fido2-cipher-row-v1",
+ templateUrl: "fido2-cipher-row-v1.component.html",
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class Fido2CipherRowV1Component {
+ @Output() onSelected = new EventEmitter();
+ @Input() cipher: CipherView;
+ @Input() last: boolean;
+ @Input() title: string;
+ @Input() isSearching: boolean;
+ @Input() isSelected: boolean;
+
+ protected selectCipher(c: CipherView) {
+ this.onSelected.emit(c);
+ }
+
+ /**
+ * Returns a subname for the cipher.
+ * If this has a FIDO2 credential, and the cipher.name is different from the FIDO2 credential's rpId, return the rpId.
+ * @param c Cipher
+ * @returns
+ */
+ protected getSubName(c: CipherView): string | null {
+ const fido2Credentials = c.login?.fido2Credentials;
+
+ if (!fido2Credentials || fido2Credentials.length === 0) {
+ return null;
+ }
+
+ const [fido2Credential] = fido2Credentials;
+
+ return c.name !== fido2Credential.rpId ? fido2Credential.rpId : null;
+ }
+}
diff --git a/apps/browser/src/autofill/popup/fido2/fido2-cipher-row.component.html b/apps/browser/src/autofill/popup/fido2/fido2-cipher-row.component.html
index 852fd4a0e81..0328a91bff5 100644
--- a/apps/browser/src/autofill/popup/fido2/fido2-cipher-row.component.html
+++ b/apps/browser/src/autofill/popup/fido2/fido2-cipher-row.component.html
@@ -1,36 +1,21 @@
-
+
+
+
+
+ {{ cipher.name }}
+
+
+ {{ getSubName(cipher) }}
+ {{ cipher.subTitle }}
+
+
diff --git a/apps/browser/src/autofill/popup/fido2/fido2-cipher-row.component.ts b/apps/browser/src/autofill/popup/fido2/fido2-cipher-row.component.ts
index 25d623b1692..91bcd6494e6 100644
--- a/apps/browser/src/autofill/popup/fido2/fido2-cipher-row.component.ts
+++ b/apps/browser/src/autofill/popup/fido2/fido2-cipher-row.component.ts
@@ -1,19 +1,40 @@
+import { CommonModule } from "@angular/common";
import { Component, EventEmitter, Input, Output, ChangeDetectionStrategy } from "@angular/core";
+import { JslibModule } from "@bitwarden/angular/jslib.module";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
+import {
+ BadgeModule,
+ ButtonModule,
+ IconButtonModule,
+ ItemModule,
+ SectionComponent,
+ SectionHeaderComponent,
+ TypographyModule,
+} from "@bitwarden/components";
@Component({
selector: "app-fido2-cipher-row",
templateUrl: "fido2-cipher-row.component.html",
changeDetection: ChangeDetectionStrategy.OnPush,
+ standalone: true,
+ imports: [
+ BadgeModule,
+ ButtonModule,
+ CommonModule,
+ IconButtonModule,
+ ItemModule,
+ JslibModule,
+ SectionComponent,
+ SectionHeaderComponent,
+ TypographyModule,
+ ],
})
export class Fido2CipherRowComponent {
@Output() onSelected = new EventEmitter();
@Input() cipher: CipherView;
@Input() last: boolean;
@Input() title: string;
- @Input() isSearching: boolean;
- @Input() isSelected: boolean;
protected selectCipher(c: CipherView) {
this.onSelected.emit(c);
diff --git a/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link-v1.component.html b/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link-v1.component.html
new file mode 100644
index 00000000000..9f6c0aca50d
--- /dev/null
+++ b/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link-v1.component.html
@@ -0,0 +1,52 @@
+
+
+
+
+ {{ "useDeviceOrHardwareKey" | i18n }}
+
+
+
+
+
+
+
+
+
+ {{ "justOnce" | i18n }}
+
+
+
+ {{ "alwaysForThisSite" | i18n }}
+
+
+
+
+
+
+
diff --git a/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link-v1.component.ts b/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link-v1.component.ts
new file mode 100644
index 00000000000..cf79dfc6520
--- /dev/null
+++ b/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link-v1.component.ts
@@ -0,0 +1,113 @@
+import { animate, state, style, transition, trigger } from "@angular/animations";
+import { ConnectedPosition } from "@angular/cdk/overlay";
+import { Component } from "@angular/core";
+import { firstValueFrom } from "rxjs";
+
+import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
+import { NeverDomains } from "@bitwarden/common/models/domain/domain-service";
+import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
+import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
+import { Utils } from "@bitwarden/common/platform/misc/utils";
+
+import { fido2PopoutSessionData$ } from "../../../vault/popup/utils/fido2-popout-session-data";
+import { BrowserFido2UserInterfaceSession } from "../../fido2/services/browser-fido2-user-interface.service";
+
+@Component({
+ selector: "app-fido2-use-browser-link-v1",
+ templateUrl: "fido2-use-browser-link-v1.component.html",
+ animations: [
+ trigger("transformPanel", [
+ state(
+ "void",
+ style({
+ opacity: 0,
+ }),
+ ),
+ transition(
+ "void => open",
+ animate(
+ "100ms linear",
+ style({
+ opacity: 1,
+ }),
+ ),
+ ),
+ transition("* => void", animate("100ms linear", style({ opacity: 0 }))),
+ ]),
+ ],
+})
+export class Fido2UseBrowserLinkV1Component {
+ showOverlay = false;
+ isOpen = false;
+ overlayPosition: ConnectedPosition[] = [
+ {
+ originX: "start",
+ originY: "bottom",
+ overlayX: "start",
+ overlayY: "top",
+ offsetY: 5,
+ },
+ ];
+
+ protected fido2PopoutSessionData$ = fido2PopoutSessionData$();
+
+ constructor(
+ private domainSettingsService: DomainSettingsService,
+ private platformUtilsService: PlatformUtilsService,
+ private i18nService: I18nService,
+ ) {}
+
+ toggle() {
+ this.isOpen = !this.isOpen;
+ }
+
+ close() {
+ this.isOpen = false;
+ }
+
+ /**
+ * Aborts the current FIDO2 session and fallsback to the browser.
+ * @param excludeDomain - Identifies if the domain should be excluded from future FIDO2 prompts.
+ */
+ protected async abort(excludeDomain = true) {
+ this.close();
+ const sessionData = await firstValueFrom(this.fido2PopoutSessionData$);
+
+ if (!excludeDomain) {
+ this.abortSession(sessionData.sessionId);
+ return;
+ }
+ // Show overlay to prevent the user from interacting with the page.
+ this.showOverlay = true;
+ await this.handleDomainExclusion(sessionData.senderUrl);
+ // Give the user a chance to see the toast before closing the popout.
+ await Utils.delay(2000);
+ this.abortSession(sessionData.sessionId);
+ }
+
+ /**
+ * Excludes the domain from future FIDO2 prompts.
+ * @param uri - The domain uri to exclude from future FIDO2 prompts.
+ */
+ private async handleDomainExclusion(uri: string) {
+ const existingDomains = await firstValueFrom(this.domainSettingsService.neverDomains$);
+
+ const validDomain = Utils.getHostname(uri);
+ const savedDomains: NeverDomains = {
+ ...existingDomains,
+ };
+ savedDomains[validDomain] = null;
+
+ await this.domainSettingsService.setNeverDomains(savedDomains);
+
+ this.platformUtilsService.showToast(
+ "success",
+ null,
+ this.i18nService.t("domainAddedToExcludedDomains", validDomain),
+ );
+ }
+
+ private abortSession(sessionId: string) {
+ BrowserFido2UserInterfaceSession.abortPopout(sessionId, true);
+ }
+}
diff --git a/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link.component.ts b/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link.component.ts
index d9a7c7c9cbc..86f13d29c7a 100644
--- a/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link.component.ts
+++ b/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link.component.ts
@@ -1,8 +1,11 @@
import { animate, state, style, transition, trigger } from "@angular/animations";
-import { ConnectedPosition } from "@angular/cdk/overlay";
+import { A11yModule } from "@angular/cdk/a11y";
+import { ConnectedPosition, CdkOverlayOrigin, CdkConnectedOverlay } from "@angular/cdk/overlay";
+import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { firstValueFrom } from "rxjs";
+import { JslibModule } from "@bitwarden/angular/jslib.module";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { NeverDomains } from "@bitwarden/common/models/domain/domain-service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -15,6 +18,8 @@ import { BrowserFido2UserInterfaceSession } from "../../fido2/services/browser-f
@Component({
selector: "app-fido2-use-browser-link",
templateUrl: "fido2-use-browser-link.component.html",
+ standalone: true,
+ imports: [A11yModule, CdkConnectedOverlay, CdkOverlayOrigin, CommonModule, JslibModule],
animations: [
trigger("transformPanel", [
state(
@@ -90,11 +95,11 @@ export class Fido2UseBrowserLinkComponent {
* @param uri - The domain uri to exclude from future FIDO2 prompts.
*/
private async handleDomainExclusion(uri: string) {
- const exisitingDomains = await firstValueFrom(this.domainSettingsService.neverDomains$);
+ const existingDomains = await firstValueFrom(this.domainSettingsService.neverDomains$);
const validDomain = Utils.getHostname(uri);
const savedDomains: NeverDomains = {
- ...exisitingDomains,
+ ...existingDomains,
};
savedDomains[validDomain] = null;
diff --git a/apps/browser/src/autofill/popup/fido2/fido2-v1.component.html b/apps/browser/src/autofill/popup/fido2/fido2-v1.component.html
new file mode 100644
index 00000000000..8a052fbc5b7
--- /dev/null
+++ b/apps/browser/src/autofill/popup/fido2/fido2-v1.component.html
@@ -0,0 +1,142 @@
+
+
+
+
+
+
+
+
+ {{ subtitleText | i18n }}
+
+
+
0">
+
+
+
+
+
+ {{ credentialText | i18n }}
+
+
+
+
+
+
+
+
+
+
+ {{ "savePasskeyNewLogin" | i18n }}
+
+
+
+
+
+
+
+
+
+
{{ "passkeyAlreadyExists" | i18n }}
+
+
+ {{ "viewItem" | i18n }}
+
+
+
+
+
+
+
{{ "noPasskeysFoundForThisApplication" | i18n }}
+
+
+ {{ "close" | i18n }}
+
+
+
+
+
+
+
+
diff --git a/apps/browser/src/autofill/popup/fido2/fido2-v1.component.ts b/apps/browser/src/autofill/popup/fido2/fido2-v1.component.ts
new file mode 100644
index 00000000000..d6026a8c7a0
--- /dev/null
+++ b/apps/browser/src/autofill/popup/fido2/fido2-v1.component.ts
@@ -0,0 +1,443 @@
+import { Component, OnDestroy, OnInit } from "@angular/core";
+import { ActivatedRoute, Router } from "@angular/router";
+import {
+ BehaviorSubject,
+ combineLatest,
+ concatMap,
+ filter,
+ firstValueFrom,
+ map,
+ Observable,
+ Subject,
+ take,
+ takeUntil,
+} from "rxjs";
+
+import { SearchService } from "@bitwarden/common/abstractions/search.service";
+import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
+import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
+import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
+import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
+import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
+import { CipherType, SecureNoteType } from "@bitwarden/common/vault/enums";
+import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
+import { CardView } from "@bitwarden/common/vault/models/view/card.view";
+import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
+import { IdentityView } from "@bitwarden/common/vault/models/view/identity.view";
+import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
+import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
+import { SecureNoteView } from "@bitwarden/common/vault/models/view/secure-note.view";
+import { DialogService } from "@bitwarden/components";
+import { PasswordRepromptService } from "@bitwarden/vault";
+
+import { ZonedMessageListenerService } from "../../../platform/browser/zoned-message-listener.service";
+import { VaultPopoutType } from "../../../vault/popup/utils/vault-popout-window";
+import { Fido2UserVerificationService } from "../../../vault/services/fido2-user-verification.service";
+import {
+ BrowserFido2Message,
+ BrowserFido2UserInterfaceSession,
+ BrowserFido2MessageTypes,
+} from "../../fido2/services/browser-fido2-user-interface.service";
+
+interface ViewData {
+ message: BrowserFido2Message;
+ fallbackSupported: boolean;
+}
+
+@Component({
+ selector: "app-fido2-v1",
+ templateUrl: "fido2-v1.component.html",
+ styleUrls: [],
+})
+export class Fido2V1Component implements OnInit, OnDestroy {
+ private destroy$ = new Subject();
+ private hasSearched = false;
+
+ protected cipher: CipherView;
+ protected searchTypeSearch = false;
+ protected searchPending = false;
+ protected searchText: string;
+ protected url: string;
+ protected hostname: string;
+ protected data$: Observable;
+ protected sessionId?: string;
+ protected senderTabId?: string;
+ protected ciphers?: CipherView[] = [];
+ protected displayedCiphers?: CipherView[] = [];
+ protected loading = false;
+ protected subtitleText: string;
+ protected credentialText: string;
+ protected BrowserFido2MessageTypes = BrowserFido2MessageTypes;
+
+ private message$ = new BehaviorSubject(null);
+
+ constructor(
+ private router: Router,
+ private activatedRoute: ActivatedRoute,
+ private cipherService: CipherService,
+ private platformUtilsService: PlatformUtilsService,
+ private domainSettingsService: DomainSettingsService,
+ private searchService: SearchService,
+ private logService: LogService,
+ private dialogService: DialogService,
+ private browserMessagingApi: ZonedMessageListenerService,
+ private passwordRepromptService: PasswordRepromptService,
+ private fido2UserVerificationService: Fido2UserVerificationService,
+ private accountService: AccountService,
+ ) {}
+
+ ngOnInit() {
+ this.searchTypeSearch = !this.platformUtilsService.isSafari();
+
+ const queryParams$ = this.activatedRoute.queryParamMap.pipe(
+ take(1),
+ map((queryParamMap) => ({
+ sessionId: queryParamMap.get("sessionId"),
+ senderTabId: queryParamMap.get("senderTabId"),
+ senderUrl: queryParamMap.get("senderUrl"),
+ })),
+ );
+
+ combineLatest([
+ queryParams$,
+ this.browserMessagingApi.messageListener$() as Observable,
+ ])
+ .pipe(
+ concatMap(async ([queryParams, message]) => {
+ this.sessionId = queryParams.sessionId;
+ this.senderTabId = queryParams.senderTabId;
+ this.url = queryParams.senderUrl;
+ // For a 'NewSessionCreatedRequest', abort if it doesn't belong to the current session.
+ if (
+ message.type === BrowserFido2MessageTypes.NewSessionCreatedRequest &&
+ message.sessionId !== queryParams.sessionId
+ ) {
+ this.abort(false);
+ return;
+ }
+
+ // Ignore messages that don't belong to the current session.
+ if (message.sessionId !== queryParams.sessionId) {
+ return;
+ }
+
+ if (message.type === BrowserFido2MessageTypes.AbortRequest) {
+ this.abort(false);
+ return;
+ }
+
+ return message;
+ }),
+ filter((message) => !!message),
+ takeUntil(this.destroy$),
+ )
+ .subscribe((message) => {
+ this.message$.next(message);
+ });
+
+ this.data$ = this.message$.pipe(
+ filter((message) => message != undefined),
+ concatMap(async (message) => {
+ switch (message.type) {
+ case BrowserFido2MessageTypes.ConfirmNewCredentialRequest: {
+ const equivalentDomains = await firstValueFrom(
+ this.domainSettingsService.getUrlEquivalentDomains(this.url),
+ );
+
+ this.ciphers = (await this.cipherService.getAllDecrypted()).filter(
+ (cipher) => cipher.type === CipherType.Login && !cipher.isDeleted,
+ );
+ this.displayedCiphers = this.ciphers.filter(
+ (cipher) =>
+ cipher.login.matchesUri(this.url, equivalentDomains) &&
+ this.hasNoOtherPasskeys(cipher, message.userHandle),
+ );
+
+ if (this.displayedCiphers.length > 0) {
+ this.selectedPasskey(this.displayedCiphers[0]);
+ }
+ break;
+ }
+
+ case BrowserFido2MessageTypes.PickCredentialRequest: {
+ const activeUserId = await firstValueFrom(
+ this.accountService.activeAccount$.pipe(map((a) => a?.id)),
+ );
+
+ this.ciphers = await Promise.all(
+ message.cipherIds.map(async (cipherId) => {
+ const cipher = await this.cipherService.get(cipherId);
+ return cipher.decrypt(
+ await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
+ );
+ }),
+ );
+ this.displayedCiphers = [...this.ciphers];
+ if (this.displayedCiphers.length > 0) {
+ this.selectedPasskey(this.displayedCiphers[0]);
+ }
+ break;
+ }
+
+ case BrowserFido2MessageTypes.InformExcludedCredentialRequest: {
+ const activeUserId = await firstValueFrom(
+ this.accountService.activeAccount$.pipe(map((a) => a?.id)),
+ );
+
+ this.ciphers = await Promise.all(
+ message.existingCipherIds.map(async (cipherId) => {
+ const cipher = await this.cipherService.get(cipherId);
+ return cipher.decrypt(
+ await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
+ );
+ }),
+ );
+ this.displayedCiphers = [...this.ciphers];
+
+ if (this.displayedCiphers.length > 0) {
+ this.selectedPasskey(this.displayedCiphers[0]);
+ }
+ break;
+ }
+ }
+
+ this.subtitleText =
+ this.displayedCiphers.length > 0
+ ? this.getCredentialSubTitleText(message.type)
+ : "noMatchingPasskeyLogin";
+
+ this.credentialText = this.getCredentialButtonText(message.type);
+ return {
+ message,
+ fallbackSupported: "fallbackSupported" in message && message.fallbackSupported,
+ };
+ }),
+ takeUntil(this.destroy$),
+ );
+
+ queryParams$.pipe(takeUntil(this.destroy$)).subscribe((queryParams) => {
+ this.send({
+ sessionId: queryParams.sessionId,
+ type: BrowserFido2MessageTypes.ConnectResponse,
+ });
+ });
+ }
+
+ protected async submit() {
+ const data = this.message$.value;
+ if (data?.type === BrowserFido2MessageTypes.PickCredentialRequest) {
+ // TODO: Revert to use fido2 user verification service once user verification for passkeys is approved for production.
+ // PM-4577 - https://github.com/bitwarden/clients/pull/8746
+ const userVerified = await this.handleUserVerification(data.userVerification, this.cipher);
+
+ this.send({
+ sessionId: this.sessionId,
+ cipherId: this.cipher.id,
+ type: BrowserFido2MessageTypes.PickCredentialResponse,
+ userVerified,
+ });
+ } else if (data?.type === BrowserFido2MessageTypes.ConfirmNewCredentialRequest) {
+ if (this.cipher.login.hasFido2Credentials) {
+ const confirmed = await this.dialogService.openSimpleDialog({
+ title: { key: "overwritePasskey" },
+ content: { key: "overwritePasskeyAlert" },
+ type: "info",
+ });
+
+ if (!confirmed) {
+ return false;
+ }
+ }
+
+ // TODO: Revert to use fido2 user verification service once user verification for passkeys is approved for production.
+ // PM-4577 - https://github.com/bitwarden/clients/pull/8746
+ const userVerified = await this.handleUserVerification(data.userVerification, this.cipher);
+
+ this.send({
+ sessionId: this.sessionId,
+ cipherId: this.cipher.id,
+ type: BrowserFido2MessageTypes.ConfirmNewCredentialResponse,
+ userVerified,
+ });
+ }
+
+ this.loading = true;
+ }
+
+ protected async saveNewLogin() {
+ const data = this.message$.value;
+ if (data?.type === BrowserFido2MessageTypes.ConfirmNewCredentialRequest) {
+ const name = data.credentialName || data.rpId;
+ // TODO: Revert to check for user verification once user verification for passkeys is approved for production.
+ // PM-4577 - https://github.com/bitwarden/clients/pull/8746
+ await this.createNewCipher(name, data.userName);
+
+ // We are bypassing user verification pending approval.
+ this.send({
+ sessionId: this.sessionId,
+ cipherId: this.cipher?.id,
+ type: BrowserFido2MessageTypes.ConfirmNewCredentialResponse,
+ userVerified: data.userVerification,
+ });
+ }
+
+ this.loading = true;
+ }
+
+ getCredentialSubTitleText(messageType: string): string {
+ return messageType == BrowserFido2MessageTypes.ConfirmNewCredentialRequest
+ ? "chooseCipherForPasskeySave"
+ : "logInWithPasskeyQuestion";
+ }
+
+ getCredentialButtonText(messageType: string): string {
+ return messageType == BrowserFido2MessageTypes.ConfirmNewCredentialRequest
+ ? "savePasskey"
+ : "confirm";
+ }
+
+ selectedPasskey(item: CipherView) {
+ this.cipher = item;
+ }
+
+ viewPasskey() {
+ // 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
+ this.router.navigate(["/view-cipher"], {
+ queryParams: {
+ cipherId: this.cipher.id,
+ uilocation: "popout",
+ senderTabId: this.senderTabId,
+ sessionId: this.sessionId,
+ singleActionPopout: `${VaultPopoutType.fido2Popout}_${this.sessionId}`,
+ },
+ });
+ }
+
+ addCipher() {
+ const data = this.message$.value;
+
+ if (data?.type !== BrowserFido2MessageTypes.ConfirmNewCredentialRequest) {
+ return;
+ }
+
+ // 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
+ this.router.navigate(["/add-cipher"], {
+ queryParams: {
+ name: data.credentialName || data.rpId,
+ uri: this.url,
+ type: CipherType.Login.toString(),
+ uilocation: "popout",
+ username: data.userName,
+ senderTabId: this.senderTabId,
+ sessionId: this.sessionId,
+ userVerification: data.userVerification,
+ singleActionPopout: `${VaultPopoutType.fido2Popout}_${this.sessionId}`,
+ },
+ });
+ }
+
+ protected async search() {
+ this.hasSearched = await this.searchService.isSearchable(this.searchText);
+ this.searchPending = true;
+ if (this.hasSearched) {
+ this.displayedCiphers = await this.searchService.searchCiphers(
+ this.searchText,
+ null,
+ this.ciphers,
+ );
+ } else {
+ const equivalentDomains = await firstValueFrom(
+ this.domainSettingsService.getUrlEquivalentDomains(this.url),
+ );
+ this.displayedCiphers = this.ciphers.filter((cipher) =>
+ cipher.login.matchesUri(this.url, equivalentDomains),
+ );
+ }
+ this.searchPending = false;
+ this.selectedPasskey(this.displayedCiphers[0]);
+ }
+
+ abort(fallback: boolean) {
+ this.unload(fallback);
+ window.close();
+ }
+
+ unload(fallback = false) {
+ this.send({
+ sessionId: this.sessionId,
+ type: BrowserFido2MessageTypes.AbortResponse,
+ fallbackRequested: fallback,
+ });
+ }
+
+ ngOnDestroy(): void {
+ this.destroy$.next();
+ this.destroy$.complete();
+ }
+
+ private buildCipher(name: string, username: string) {
+ this.cipher = new CipherView();
+ this.cipher.name = name;
+
+ this.cipher.type = CipherType.Login;
+ this.cipher.login = new LoginView();
+ this.cipher.login.username = username;
+ this.cipher.login.uris = [new LoginUriView()];
+ this.cipher.login.uris[0].uri = this.url;
+ this.cipher.card = new CardView();
+ this.cipher.identity = new IdentityView();
+ this.cipher.secureNote = new SecureNoteView();
+ this.cipher.secureNote.type = SecureNoteType.Generic;
+ this.cipher.reprompt = CipherRepromptType.None;
+ }
+
+ private async createNewCipher(name: string, username: string) {
+ const activeUserId = await firstValueFrom(
+ this.accountService.activeAccount$.pipe(map((a) => a?.id)),
+ );
+
+ this.buildCipher(name, username);
+ const cipher = await this.cipherService.encrypt(this.cipher, activeUserId);
+ try {
+ await this.cipherService.createWithServer(cipher);
+ this.cipher.id = cipher.id;
+ } catch (e) {
+ this.logService.error(e);
+ }
+ }
+
+ // TODO: Remove and use fido2 user verification service once user verification for passkeys is approved for production.
+ private async handleUserVerification(
+ userVerificationRequested: boolean,
+ cipher: CipherView,
+ ): Promise {
+ const masterPasswordRepromptRequired = cipher && cipher.reprompt !== 0;
+
+ if (masterPasswordRepromptRequired) {
+ return await this.passwordRepromptService.showPasswordPrompt();
+ }
+
+ return userVerificationRequested;
+ }
+
+ private send(msg: BrowserFido2Message) {
+ BrowserFido2UserInterfaceSession.sendMessage({
+ sessionId: this.sessionId,
+ ...msg,
+ });
+ }
+
+ /**
+ * This methods returns true if a cipher either has no passkeys, or has a passkey matching with userHandle
+ * @param userHandle
+ */
+ private hasNoOtherPasskeys(cipher: CipherView, userHandle: string): boolean {
+ if (cipher.login.fido2Credentials == null || cipher.login.fido2Credentials.length === 0) {
+ return true;
+ }
+
+ return cipher.login.fido2Credentials.some((passkey) => passkey.userHandle === userHandle);
+ }
+}
diff --git a/apps/browser/src/autofill/popup/fido2/fido2.component.html b/apps/browser/src/autofill/popup/fido2/fido2.component.html
index 9036d6d991c..00cd55d31b5 100644
--- a/apps/browser/src/autofill/popup/fido2/fido2.component.html
+++ b/apps/browser/src/autofill/popup/fido2/fido2.component.html
@@ -1,136 +1,134 @@
-
-
-