1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-09 13:10:17 +00:00

Merge branch 'main' into km/rename-encrypt-to-bytes

This commit is contained in:
Bernd Schoolmann
2025-04-24 12:31:56 +02:00
committed by GitHub
111 changed files with 1761 additions and 1378 deletions

View File

@@ -192,7 +192,7 @@
"message": "Copy",
"description": "Copy to clipboard"
},
"fill":{
"fill": {
"message": "Fill",
"description": "This string is used on the vault page to indicate autofilling. Horizontal space is limited in the interface here so try and keep translations as concise as possible."
},
@@ -886,6 +886,9 @@
"followTheStepsBelowToFinishLoggingIn": {
"message": "Follow the steps below to finish logging in."
},
"followTheStepsBelowToFinishLoggingInWithSecurityKey": {
"message": "Follow the steps below to finish logging in with your security key."
},
"restartRegistration": {
"message": "Restart registration"
},
@@ -1059,6 +1062,15 @@
"notificationAddSave": {
"message": "Save"
},
"notificationViewAria": {
"message": "View $ITEMNAME$, opens in new window",
"placeholders": {
"itemName": {
"content": "$1"
}
},
"description": "Aria label for the view button in notification bar confirmation message"
},
"newNotification": {
"message": "New notification"
},
@@ -1072,23 +1084,23 @@
}
}
},
"loginSaveSuccessDetails": {
"message": "$USERNAME$ saved to Bitwarden.",
"placeholders": {
"username": {
"content": "$1"
}
},
"description": "Shown to user after login is saved."
"loginSaveConfirmation": {
"message": "$ITEMNAME$ saved to Bitwarden.",
"placeholders": {
"itemName": {
"content": "$1"
}
},
"description": "Shown to user after item is saved."
},
"loginUpdatedSuccessDetails": {
"message": "$USERNAME$ updated in Bitwarden.",
"placeholders": {
"username": {
"content": "$1"
}
},
"description": "Shown to user after login is updated."
"loginUpdatedConfirmation": {
"message": "$ITEMNAME$ updated in Bitwarden.",
"placeholders": {
"itemName": {
"content": "$1"
}
},
"description": "Shown to user after item is updated."
},
"saveAsNewLoginAction": {
"message": "Save as new login",

View File

@@ -101,6 +101,10 @@ type NotificationBackgroundExtensionMessageHandlers = {
bgChangedPassword: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise<void>;
bgRemoveTabFromNotificationQueue: ({ sender }: BackgroundSenderParam) => void;
bgSaveCipher: ({ message, sender }: BackgroundOnMessageHandlerParams) => void;
bgOpenViewVaultItemPopout: ({
message,
sender,
}: BackgroundOnMessageHandlerParams) => Promise<void>;
bgOpenVault: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise<void>;
bgNeverSave: ({ sender }: BackgroundSenderParam) => Promise<void>;
bgUnlockPopoutOpened: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise<void>;

View File

@@ -823,6 +823,7 @@ describe("NotificationBackground", () => {
notificationBackground["notificationQueue"] = [queueMessage];
const cipherView = mock<CipherView>({
id: "testId",
name: "testItemName",
login: { username: "testUser" },
});
getDecryptedCipherByIdSpy.mockResolvedValueOnce(cipherView);
@@ -844,8 +845,9 @@ describe("NotificationBackground", () => {
sender.tab,
"saveCipherAttemptCompleted",
{
username: cipherView.login.username,
itemName: "testItemName",
cipherId: cipherView.id,
task: undefined,
},
);
});
@@ -899,7 +901,7 @@ describe("NotificationBackground", () => {
const cipherView = mock<CipherView>({
id: mockCipherId,
organizationId: mockOrgId,
login: { username: "testUser" },
name: "Test Item",
});
getDecryptedCipherByIdSpy.mockResolvedValueOnce(cipherView);
@@ -921,11 +923,11 @@ describe("NotificationBackground", () => {
"saveCipherAttemptCompleted",
{
cipherId: "testId",
itemName: "Test Item",
task: {
orgName: "Org Name, LLC",
remainingTasksCount: 1,
},
username: "testUser",
},
);
});
@@ -1074,6 +1076,7 @@ describe("NotificationBackground", () => {
notificationBackground["notificationQueue"] = [queueMessage];
const cipherView = mock<CipherView>({
id: "testId",
name: "testName",
login: { username: "test", password: "password" },
});
folderExistsSpy.mockResolvedValueOnce(false);
@@ -1097,8 +1100,8 @@ describe("NotificationBackground", () => {
sender.tab,
"saveCipherAttemptCompleted",
{
username: cipherView.login.username,
cipherId: cipherView.id,
itemName: cipherView.name,
},
);
expect(tabSendMessageSpy).toHaveBeenCalledWith(sender.tab, { command: "addedCipher" });

View File

@@ -41,7 +41,10 @@ import { SecurityTask } from "@bitwarden/common/vault/tasks/models/security-task
import { openUnlockPopout } from "../../auth/popup/utils/auth-popout-window";
import { BrowserApi } from "../../platform/browser/browser-api";
import { openAddEditVaultItemPopout } from "../../vault/popup/utils/vault-popout-window";
import {
openAddEditVaultItemPopout,
openViewVaultItemPopout,
} from "../../vault/popup/utils/vault-popout-window";
import {
OrganizationCategory,
OrganizationCategories,
@@ -67,6 +70,7 @@ import { OverlayBackgroundExtensionMessage } from "./abstractions/overlay.backgr
export default class NotificationBackground {
private openUnlockPopout = openUnlockPopout;
private openAddEditVaultItemPopout = openAddEditVaultItemPopout;
private openViewVaultItemPopout = openViewVaultItemPopout;
private notificationQueue: NotificationQueueMessageItem[] = [];
private allowedRetryCommands: Set<ExtensionCommandType> = new Set([
ExtensionCommand.AutofillLogin,
@@ -91,6 +95,7 @@ export default class NotificationBackground {
bgGetOrgData: () => this.getOrgData(),
bgNeverSave: ({ sender }) => this.saveNever(sender.tab),
bgOpenVault: ({ message, sender }) => this.openVault(message, sender.tab),
bgOpenViewVaultItemPopout: ({ message, sender }) => this.viewItem(message, sender.tab),
bgRemoveTabFromNotificationQueue: ({ sender }) =>
this.removeTabFromNotificationQueue(sender.tab),
bgReopenUnlockPopout: ({ sender }) => this.openUnlockPopout(sender.tab),
@@ -638,8 +643,8 @@ export default class NotificationBackground {
try {
await this.cipherService.createWithServer(cipher);
await BrowserApi.tabSendMessageData(tab, "saveCipherAttemptCompleted", {
username: queueMessage?.username && String(queueMessage.username),
cipherId: cipher?.id && String(cipher.id),
itemName: newCipher?.name && String(newCipher?.name),
cipherId: cipher?.id && String(cipher?.id),
});
await BrowserApi.tabSendMessage(tab, { command: "addedCipher" });
} catch (error) {
@@ -701,7 +706,7 @@ export default class NotificationBackground {
await this.cipherService.updateWithServer(cipher);
await BrowserApi.tabSendMessageData(tab, "saveCipherAttemptCompleted", {
username: cipherView?.login?.username && String(cipherView.login.username),
itemName: cipherView?.name && String(cipherView?.name),
cipherId: cipherView?.id && String(cipherView.id),
task: taskData,
});
@@ -754,6 +759,21 @@ export default class NotificationBackground {
await this.openAddEditVaultItemPopout(senderTab, { cipherId: message.cipherId });
}
private async viewItem(
message: NotificationBackgroundExtensionMessage,
senderTab: chrome.tabs.Tab,
) {
await Promise.all([
this.openViewVaultItemPopout(senderTab, {
cipherId: message.cipherId,
action: null,
}),
BrowserApi.tabSendMessageData(senderTab, "closeNotificationBar", {
fadeOutNotification: !!message.fadeOutNotification,
}),
]);
}
private async folderExists(folderId: string, userId: UserId) {
if (Utils.isNullOrWhitespace(folderId) || folderId === "null") {
return false;

View File

@@ -8,24 +8,24 @@ export function CipherAction({
handleAction = () => {
/* no-op */
},
i18n,
notificationType,
theme,
}: {
handleAction?: (e: Event) => void;
i18n: { [key: string]: string };
notificationType: typeof NotificationTypes.Change | typeof NotificationTypes.Add;
theme: Theme;
}) {
return notificationType === NotificationTypes.Change
? BadgeButton({
buttonAction: handleAction,
// @TODO localize
buttonText: "Update",
buttonText: i18n.notificationUpdate,
theme,
})
: EditButton({
buttonAction: handleAction,
// @TODO localize
buttonText: "Edit",
buttonText: i18n.notificationEdit,
theme,
});
}

View File

@@ -19,11 +19,13 @@ const cipherIconWidth = "24px";
export function CipherItem({
cipher,
handleAction,
i18n,
notificationType,
theme = ThemeTypes.Light,
}: {
cipher: NotificationCipherData;
handleAction?: (e: Event) => void;
i18n: { [key: string]: string };
notificationType?: NotificationType;
theme: Theme;
}) {
@@ -34,7 +36,7 @@ export function CipherItem({
if (notificationType === NotificationTypes.Change || notificationType === NotificationTypes.Add) {
cipherActionButton = html`<div>
${CipherAction({ handleAction, notificationType, theme })}
${CipherAction({ handleAction, i18n, notificationType, theme })}
</div>`;
}

View File

@@ -7,6 +7,7 @@ import { CipherAction } from "../../cipher/cipher-action";
type Args = {
handleAction?: (e: Event) => void;
i18n: { [key: string]: string };
notificationType: typeof NotificationTypes.Change | typeof NotificationTypes.Add;
theme: Theme;
};

View File

@@ -10,6 +10,7 @@ import { NotificationBody } from "../../notification/body";
type Args = {
ciphers: NotificationCipherData[];
i18n: { [key: string]: string };
notificationType: NotificationType;
theme: Theme;
handleEditOrUpdateAction: (e: Event) => void;

View File

@@ -17,12 +17,14 @@ const { css } = createEmotion({
export function NotificationBody({
ciphers = [],
i18n,
notificationType,
theme = ThemeTypes.Light,
handleEditOrUpdateAction,
}: {
ciphers?: NotificationCipherData[];
customClasses?: string[];
i18n: { [key: string]: string };
notificationType?: NotificationType;
theme: Theme;
handleEditOrUpdateAction: (e: Event) => void;
@@ -37,6 +39,7 @@ export function NotificationBody({
theme,
children: CipherItem({
cipher,
i18n,
notificationType,
theme,
handleAction: handleEditOrUpdateAction,

View File

@@ -22,17 +22,19 @@ function getVaultIconByProductTier(productTierType?: ProductTierType): Option["i
}
export type NotificationButtonRowProps = {
theme: Theme;
folders?: FolderView[];
i18n: { [key: string]: string };
organizations?: OrgView[];
primaryButton: {
text: string;
handlePrimaryButtonClick: (args: any) => void;
};
folders?: FolderView[];
organizations?: OrgView[];
theme: Theme;
};
export function NotificationButtonRow({
folders,
i18n,
organizations,
primaryButton,
theme,
@@ -40,7 +42,7 @@ export function NotificationButtonRow({
const currentUserVaultOption: Option = {
icon: User,
default: true,
text: "My vault", // @TODO localize
text: i18n.myVault,
value: "0",
};
const organizationOptions: Option[] = organizations?.length
@@ -84,7 +86,7 @@ export function NotificationButtonRow({
? [
{
id: "organization",
label: "Vault", // @TODO localize
label: i18n.vault,
options: organizationOptions,
},
]
@@ -93,7 +95,7 @@ export function NotificationButtonRow({
? [
{
id: "folder",
label: "Folder", // @TODO localize
label: i18n.folder,
options: folderOptions,
},
]

View File

@@ -16,16 +16,18 @@ const { css } = createEmotion({
export type NotificationConfirmationBodyProps = {
buttonText: string;
itemName: string;
confirmationMessage: string;
error?: string;
messageDetails?: string;
tasksAreComplete?: boolean;
theme: Theme;
handleOpenVault: (e: Event) => void;
handleOpenVault: () => void;
};
export function NotificationConfirmationBody({
buttonText,
itemName,
confirmationMessage,
error,
messageDetails,
@@ -43,6 +45,7 @@ export function NotificationConfirmationBody({
${showConfirmationMessage
? NotificationConfirmationMessage({
buttonText,
itemName,
message: confirmationMessage,
messageDetails,
theme,

View File

@@ -20,14 +20,14 @@ import { NotificationConfirmationFooter } from "./footer";
export type NotificationConfirmationContainerProps = NotificationBarIframeInitData & {
handleCloseNotification: (e: Event) => void;
handleOpenVault: (e: Event) => void;
handleOpenVault: () => void;
handleOpenTasks: (e: Event) => void;
} & {
error?: string;
i18n: { [key: string]: string };
itemName: string;
task?: NotificationTaskInfo;
type: NotificationType;
username: string;
};
export function NotificationConfirmationContainer({
@@ -36,13 +36,13 @@ export function NotificationConfirmationContainer({
handleOpenVault,
handleOpenTasks,
i18n,
itemName,
task,
theme = ThemeTypes.Light,
type,
username,
}: NotificationConfirmationContainerProps) {
const headerMessage = getHeaderMessage(i18n, type, error);
const confirmationMessage = getConfirmationMessage(i18n, username, type, error);
const confirmationMessage = getConfirmationMessage(i18n, itemName, type, error);
const buttonText = error ? i18n.newItem : i18n.view;
let messageDetails: string | undefined;
@@ -71,6 +71,7 @@ export function NotificationConfirmationContainer({
})}
${NotificationConfirmationBody({
buttonText,
itemName,
confirmationMessage,
tasksAreComplete,
messageDetails,
@@ -106,19 +107,17 @@ const notificationContainerStyles = (theme: Theme) => css`
function getConfirmationMessage(
i18n: { [key: string]: string },
username: string,
itemName: string,
type?: NotificationType,
error?: string,
) {
const loginSaveSuccessDetails = chrome.i18n.getMessage("loginSaveSuccessDetails", [username]);
const loginUpdatedSuccessDetails = chrome.i18n.getMessage("loginUpdatedSuccessDetails", [
username,
]);
const loginSaveConfirmation = chrome.i18n.getMessage("loginSaveConfirmation", [itemName]);
const loginUpdatedConfirmation = chrome.i18n.getMessage("loginUpdatedConfirmation", [itemName]);
if (error) {
return i18n.saveFailureDetails;
}
return type === "add" ? loginSaveSuccessDetails : loginUpdatedSuccessDetails;
return type === "add" ? loginSaveConfirmation : loginUpdatedConfirmation;
}
function getHeaderMessage(

View File

@@ -7,19 +7,23 @@ import { themes, typography } from "../../constants/styles";
export type NotificationConfirmationMessageProps = {
buttonText?: string;
itemName: string;
message?: string;
messageDetails?: string;
handleClick: (e: Event) => void;
handleClick: () => void;
theme: Theme;
};
export function NotificationConfirmationMessage({
buttonText,
itemName,
message,
messageDetails,
handleClick,
theme,
}: NotificationConfirmationMessageProps) {
const buttonAria = chrome.i18n.getMessage("notificationViewAria", [itemName]);
return html`
<div>
${message || buttonText
@@ -35,6 +39,10 @@ export function NotificationConfirmationMessage({
title=${buttonText}
class=${notificationConfirmationButtonTextStyles(theme)}
@click=${handleClick}
@keydown=${(e: KeyboardEvent) => handleButtonKeyDown(e, handleClick)}
aria-label=${buttonAria}
tabindex="0"
role="button"
>
${buttonText}
</a>
@@ -81,3 +89,10 @@ const AdditionalMessageStyles = ({ theme }: { theme: Theme }) => css`
font-size: 14px;
color: ${themes[theme].text.muted};
`;
function handleButtonKeyDown(event: KeyboardEvent, handleClick: () => void) {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
handleClick();
}
}

View File

@@ -59,6 +59,7 @@ export function NotificationContainer({
ciphers,
notificationType: type,
theme,
i18n,
})
: null}
${NotificationFooter({

View File

@@ -38,6 +38,7 @@ export function NotificationFooter({
? NotificationButtonRow({
folders,
organizations,
i18n,
primaryButton: {
handlePrimaryButtonClick: handleSaveAction,
text: primaryButtonText,

View File

@@ -33,7 +33,7 @@ type NotificationBarWindowMessage = {
data?: {
cipherId?: string;
task?: NotificationTaskInfo;
username?: string;
itemName?: string;
};
error?: string;
initData?: NotificationBarIframeInitData;

View File

@@ -53,23 +53,26 @@ function getI18n() {
return {
appName: chrome.i18n.getMessage("appName"),
close: chrome.i18n.getMessage("close"),
collection: chrome.i18n.getMessage("collection"),
folder: chrome.i18n.getMessage("folder"),
loginSaveSuccess: chrome.i18n.getMessage("loginSaveSuccess"),
loginSaveSuccessDetails: chrome.i18n.getMessage("loginSaveSuccessDetails"),
loginSaveConfirmation: chrome.i18n.getMessage("loginSaveConfirmation"),
loginUpdateSuccess: chrome.i18n.getMessage("loginUpdateSuccess"),
loginUpdateSuccessDetails: chrome.i18n.getMessage("loginUpdatedSuccessDetails"),
loginUpdateConfirmation: chrome.i18n.getMessage("loginUpdatedConfirmation"),
loginUpdateTaskSuccess: chrome.i18n.getMessage("loginUpdateTaskSuccess"),
loginUpdateTaskSuccessAdditional: chrome.i18n.getMessage("loginUpdateTaskSuccessAdditional"),
nextSecurityTaskAction: chrome.i18n.getMessage("nextSecurityTaskAction"),
newItem: chrome.i18n.getMessage("newItem"),
never: chrome.i18n.getMessage("never"),
myVault: chrome.i18n.getMessage("myVault"),
notificationAddDesc: chrome.i18n.getMessage("notificationAddDesc"),
notificationAddSave: chrome.i18n.getMessage("notificationAddSave"),
notificationChangeDesc: chrome.i18n.getMessage("notificationChangeDesc"),
notificationChangeSave: chrome.i18n.getMessage("notificationChangeSave"),
notificationUpdate: chrome.i18n.getMessage("notificationChangeSave"),
notificationEdit: chrome.i18n.getMessage("edit"),
notificationUnlock: chrome.i18n.getMessage("notificationUnlock"),
notificationUnlockDesc: chrome.i18n.getMessage("notificationUnlockDesc"),
notificationViewAria: chrome.i18n.getMessage("notificationViewAria"),
saveAction: chrome.i18n.getMessage("notificationAddSave"),
saveAsNewLoginAction: chrome.i18n.getMessage("saveAsNewLoginAction"),
saveFailure: chrome.i18n.getMessage("saveFailure"),
@@ -78,6 +81,7 @@ function getI18n() {
typeLogin: chrome.i18n.getMessage("typeLogin"),
updateLoginAction: chrome.i18n.getMessage("updateLoginAction"),
updateLoginPrompt: chrome.i18n.getMessage("updateLoginPrompt"),
vault: chrome.i18n.getMessage("vault"),
view: chrome.i18n.getMessage("view"),
};
}
@@ -200,7 +204,7 @@ async function initNotificationBar(message: NotificationBarWindowMessage) {
const changeTemplate = document.getElementById("template-change") as HTMLTemplateElement;
const changeButton = findElementById<HTMLSelectElement>(changeTemplate, "change-save");
changeButton.textContent = i18n.notificationChangeSave;
changeButton.textContent = i18n.notificationUpdate;
const changeEditButton = findElementById<HTMLButtonElement>(changeTemplate, "change-edit");
changeEditButton.textContent = i18n.notificationEdit;
@@ -346,10 +350,9 @@ function handleSaveCipherAttemptCompletedMessage(message: NotificationBarWindowM
);
}
function openViewVaultItemPopout(e: Event, cipherId: string) {
e.preventDefault();
function openViewVaultItemPopout(cipherId: string) {
sendPlatformMessage({
command: "bgOpenVault",
command: "bgOpenViewVaultItemPopout",
cipherId,
});
}
@@ -357,7 +360,7 @@ function openViewVaultItemPopout(e: Event, cipherId: string) {
function handleSaveCipherConfirmation(message: NotificationBarWindowMessage) {
const { theme, type } = notificationBarIframeInitData;
const { error, data } = message;
const { username, cipherId, task } = data || {};
const { cipherId, task, itemName } = data || {};
const i18n = getI18n();
const resolvedTheme = getResolvedTheme(theme ?? ThemeTypes.Light);
@@ -371,9 +374,9 @@ function handleSaveCipherConfirmation(message: NotificationBarWindowMessage) {
handleCloseNotification,
i18n,
error,
username: username ?? i18n.typeLogin,
itemName: itemName ?? i18n.typeLogin,
task,
handleOpenVault: (e) => cipherId && openViewVaultItemPopout(e, cipherId),
handleOpenVault: () => cipherId && openViewVaultItemPopout(cipherId),
handleOpenTasks: () => sendPlatformMessage({ command: "bgOpenAtRisksPasswords" }),
}),
document.body,

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { inject } from "@angular/core";
import { Router } from "@angular/router";

View File

@@ -14,6 +14,7 @@ import {
tdeDecryptionRequiredGuard,
unauthGuardFn,
} from "@bitwarden/angular/auth/guards";
import { featureFlaggedRoute } from "@bitwarden/angular/platform/utils/feature-flagged-route";
import { NewDeviceVerificationNoticeGuard } from "@bitwarden/angular/vault/guards";
import {
AnonLayoutWrapperComponent,
@@ -41,6 +42,7 @@ import {
NewDeviceVerificationComponent,
DeviceVerificationIcon,
} from "@bitwarden/auth/angular";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { LockComponent } from "@bitwarden/key-management-ui";
import {
NewDeviceVerificationNoticePageOneComponent,
@@ -53,6 +55,7 @@ import { maxAccountsGuardFn } from "../auth/guards/max-accounts.guard";
import { SetPasswordComponent } from "../auth/set-password.component";
import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component";
import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component";
import { VaultV2Component } from "../vault/app/vault/vault-v2.component";
import { VaultComponent } from "../vault/app/vault/vault.component";
import { Fido2PlaceholderComponent } from "./components/fido2placeholder.component";
@@ -132,11 +135,15 @@ const routes: Routes = [
},
],
},
{
path: "vault",
component: VaultComponent,
canActivate: [authGuard, NewDeviceVerificationNoticeGuard],
},
...featureFlaggedRoute({
defaultComponent: VaultComponent,
flaggedComponent: VaultV2Component,
featureFlag: FeatureFlag.PM18520_UpdateDesktopCipherForm,
routeOptions: {
path: "vault",
canActivate: [authGuard, NewDeviceVerificationNoticeGuard],
},
}),
{ path: "accessibility-cookie", component: AccessibilityCookieComponent },
{ path: "set-password", component: SetPasswordComponent },
{
@@ -359,7 +366,7 @@ const routes: Routes = [
imports: [
RouterModule.forRoot(routes, {
useHash: true,
/*enableTracing: true,*/
// enableTracing: true,
}),
],
exports: [RouterModule],

View File

@@ -9,7 +9,6 @@ import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { ColorPasswordCountPipe } from "@bitwarden/angular/pipes/color-password-count.pipe";
import { ColorPasswordPipe } from "@bitwarden/angular/pipes/color-password.pipe";
import { CalloutModule, DialogModule } from "@bitwarden/components";
import { DecryptionFailureDialogComponent } from "@bitwarden/vault";
import { AccessibilityCookieComponent } from "../auth/accessibility-cookie.component";
import { DeleteAccountComponent } from "../auth/delete-account.component";
@@ -28,6 +27,7 @@ import { PasswordHistoryComponent } from "../vault/app/vault/password-history.co
import { ShareComponent } from "../vault/app/vault/share.component";
import { VaultFilterModule } from "../vault/app/vault/vault-filter/vault-filter.module";
import { VaultItemsComponent } from "../vault/app/vault/vault-items.component";
import { VaultV2Component } from "../vault/app/vault/vault-v2.component";
import { VaultComponent } from "../vault/app/vault/vault.component";
import { ViewCustomFieldsComponent } from "../vault/app/vault/view-custom-fields.component";
import { ViewComponent } from "../vault/app/vault/view.component";
@@ -55,8 +55,8 @@ import { SharedModule } from "./shared/shared.module";
CalloutModule,
DeleteAccountComponent,
UserVerificationComponent,
DecryptionFailureDialogComponent,
NavComponent,
VaultV2Component,
],
declarations: [
AccessibilityCookieComponent,
@@ -65,7 +65,6 @@ import { SharedModule } from "./shared/shared.module";
AddEditCustomFieldsComponent,
AppComponent,
AttachmentsComponent,
VaultItemsComponent,
CollectionsComponent,
ColorPasswordPipe,
ColorPasswordCountPipe,
@@ -80,9 +79,10 @@ import { SharedModule } from "./shared/shared.module";
ShareComponent,
UpdateTempPasswordComponent,
VaultComponent,
VaultItemsComponent,
VaultTimeoutInputComponent,
ViewComponent,
ViewCustomFieldsComponent,
ViewComponent,
],
providers: [SshAgentService],
bootstrap: [AppComponent],

View File

@@ -393,6 +393,64 @@
"authenticatorKeyTotp": {
"message": "Authenticator key (TOTP)"
},
"authenticatorKey": {
"message": "Authenticator key"
},
"autofillOptions": {
"message": "Autofill options"
},
"websiteUri": {
"message": "Website (URI)"
},
"websiteUriCount": {
"message": "Website (URI) $COUNT$",
"description": "Label for an input field that contains a website URI. The input field is part of a list of fields, and the count indicates the position of the field in the list.",
"placeholders": {
"count": {
"content": "$1",
"example": "3"
}
}
},
"websiteAdded": {
"message": "Website added"
},
"addWebsite": {
"message": "Add website"
},
"deleteWebsite": {
"message": "Delete website"
},
"owner": {
"message": "Owner"
},
"addField": {
"message": "Add field"
},
"fieldType": {
"message": "Field type"
},
"fieldLabel": {
"message": "Field label"
},
"add": {
"message": "Add"
},
"textHelpText": {
"message": "Use text fields for data like security questions"
},
"hiddenHelpText": {
"message": "Use hidden fields for sensitive data like a password"
},
"checkBoxHelpText": {
"message": "Use checkboxes if you'd like to autofill a form's checkbox, like a remember email"
},
"linkedHelpText": {
"message": "Use a linked field when you are experiencing autofill issues for a specific website."
},
"linkedLabelHelpText": {
"message": "Enter the the field's html id, name, aria-label, or placeholder."
},
"folder": {
"message": "Folder"
},
@@ -418,6 +476,9 @@
"message": "Linked",
"description": "This describes a field that is 'linked' (related) to another field."
},
"cfTypeCheckbox": {
"message": "Checkbox"
},
"linkedValue": {
"message": "Linked value",
"description": "This describes a value that is 'linked' (related) to another value."
@@ -1915,6 +1976,43 @@
}
}
},
"cardDetails": {
"message": "Card details"
},
"cardBrandDetails": {
"message": "$BRAND$ details",
"placeholders": {
"brand": {
"content": "$1",
"example": "Visa"
}
}
},
"learnMoreAboutAuthenticators": {
"message": "Learn more about authenticators"
},
"copyTOTP": {
"message": "Copy Authenticator key (TOTP)"
},
"totpHelperTitle": {
"message": "Make 2-step verification seamless"
},
"totpHelper": {
"message": "Bitwarden can store and fill 2-step verification codes. Copy and paste the key into this field."
},
"totpHelperWithCapture": {
"message": "Bitwarden can store and fill 2-step verification codes. Select the camera icon to take a screenshot of this website's authenticator QR code, or copy and paste the key into this field."
},
"premium": {
"message": "Premium",
"description": "Premium membership"
},
"cardExpiredTitle": {
"message": "Expired card"
},
"cardExpiredMessage": {
"message": "If you've renewed it, update the card's information"
},
"verificationRequired": {
"message": "Verification required",
"description": "Default title for the user verification dialog."
@@ -2084,6 +2182,15 @@
"personalOwnershipPolicyInEffectImports": {
"message": "An organization policy has blocked importing items into your individual vault."
},
"personalDetails": {
"message": "Personal details"
},
"identification": {
"message": "Identification"
},
"contactInfo": {
"message": "Contact information"
},
"allSends": {
"message": "All Sends",
"description": "'Sends' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
@@ -2518,6 +2625,9 @@
"generateEmail": {
"message": "Generate email"
},
"usernameGenerator": {
"message": "Username generator"
},
"spinboxBoundariesHint": {
"message": "Value must be between $MIN$ and $MAX$.",
"description": "Explains spin box minimum and maximum values to the user",
@@ -3212,6 +3322,9 @@
"followTheStepsBelowToFinishLoggingIn": {
"message": "Follow the steps below to finish logging in."
},
"followTheStepsBelowToFinishLoggingInWithSecurityKey": {
"message": "Follow the steps below to finish logging in with your security key."
},
"launchDuo": {
"message": "Launch Duo in Browser"
},
@@ -3436,6 +3549,17 @@
"ssoError": {
"message": "No free ports could be found for the sso login."
},
"securePasswordGenerated": {
"message": "Secure password generated! Don't forget to also update your password on the website."
},
"useGeneratorHelpTextPartOne": {
"message": "Use the generator",
"description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'"
},
"useGeneratorHelpTextPartTwo": {
"message": "to create a strong unique password",
"description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'"
},
"biometricsStatusHelptextUnlockNeeded": {
"message": "Biometric unlock is unavailable because PIN or password unlock is required first."
},
@@ -3514,6 +3638,27 @@
"setupTwoStepLogin": {
"message": "Set up two-step login"
},
"itemDetails": {
"message": "Item details"
},
"itemName": {
"message": "Item name"
},
"loginCredentials": {
"message": "Login credentials"
},
"additionalOptions": {
"message": "Additional options"
},
"itemHistory": {
"message": "Item history"
},
"lastEdited": {
"message": "Last edited"
},
"upload": {
"message": "Upload"
},
"newDeviceVerificationNoticeContentPage1": {
"message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025."
},

View File

@@ -147,3 +147,8 @@ div:not(.modal)::-webkit-scrollbar-thumb,
.mx-auto {
margin-left: auto !important;
}
.vault-v2 button:not([bitbutton]):not([biticonbutton]) i.bwi,
a i.bwi {
margin-right: 0.25rem;
}

View File

@@ -162,3 +162,7 @@ app-root {
}
}
}
.vault-v2 > .details {
flex-direction: column-reverse;
}

View File

@@ -0,0 +1,32 @@
import { inject, Injectable } from "@angular/core";
import { firstValueFrom } from "rxjs";
import { DialogService } from "@bitwarden/components";
import { CipherFormGenerationService } from "@bitwarden/vault";
import { CredentialGeneratorDialogComponent } from "../vault/app/vault/credential-generator-dialog.component";
@Injectable()
export class DesktopCredentialGenerationService implements CipherFormGenerationService {
private dialogService = inject(DialogService);
async generatePassword(): Promise<string> {
return await this.generateCredential("password");
}
async generateUsername(uri: string): Promise<string> {
return await this.generateCredential("username", uri);
}
async generateCredential(type: "password" | "username", uri?: string): Promise<string> {
const dialogRef = CredentialGeneratorDialogComponent.open(this.dialogService, { type, uri });
const result = await firstValueFrom(dialogRef.closed);
if (!result || result.action === "canceled" || !result.generatedValue) {
return "";
}
return result.generatedValue;
}
}

View File

@@ -0,0 +1,30 @@
import { TestBed } from "@angular/core/testing";
import { mock, MockProxy } from "jest-mock-extended";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { DesktopPremiumUpgradePromptService } from "./desktop-premium-upgrade-prompt.service";
describe("DesktopPremiumUpgradePromptService", () => {
let service: DesktopPremiumUpgradePromptService;
let messager: MockProxy<MessagingService>;
beforeEach(async () => {
messager = mock<MessagingService>();
await TestBed.configureTestingModule({
providers: [
DesktopPremiumUpgradePromptService,
{ provide: MessagingService, useValue: messager },
],
}).compileComponents();
service = TestBed.inject(DesktopPremiumUpgradePromptService);
});
describe("promptForPremium", () => {
it("navigates to the premium update screen", async () => {
await service.promptForPremium();
expect(messager.send).toHaveBeenCalledWith("openPremium");
});
});
});

View File

@@ -0,0 +1,15 @@
import { inject } from "@angular/core";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
/**
* This class handles the premium upgrade process for the desktop.
*/
export class DesktopPremiumUpgradePromptService implements PremiumUpgradePromptService {
private messagingService = inject(MessagingService);
async promptForPremium() {
this.messagingService.send("openPremium");
}
}

View File

@@ -3,6 +3,7 @@
<ng-container bitDialogContent>
<vault-cipher-form-generator
[type]="data.type"
[uri]="data.uri"
(valueGenerated)="onCredentialGenerated($event)"
(algorithmSelected)="onAlgorithmSelected($event)"
/>
@@ -27,7 +28,6 @@
(click)="applyCredentials()"
appA11yTitle="{{ buttonLabel }}"
bitButton
bitDialogClose
[disabled]="!(buttonLabel && credentialValue)"
>
{{ buttonLabel }}

View File

@@ -10,6 +10,7 @@ import {
DialogService,
ItemModule,
LinkModule,
DialogRef,
} from "@bitwarden/components";
import {
CredentialGeneratorHistoryDialogComponent,
@@ -19,10 +20,22 @@ import { AlgorithmInfo } from "@bitwarden/generator-core";
import { CipherFormGeneratorComponent } from "@bitwarden/vault";
type CredentialGeneratorParams = {
onCredentialGenerated: (value?: string) => void;
/** @deprecated Prefer use of dialogRef.closed to retreive the generated value */
onCredentialGenerated?: (value?: string) => void;
type: "password" | "username";
uri?: string;
};
export interface CredentialGeneratorDialogResult {
action: CredentialGeneratorDialogAction;
generatedValue?: string;
}
export enum CredentialGeneratorDialogAction {
Selected = "selected",
Canceled = "canceled",
}
@Component({
standalone: true,
selector: "credential-generator-dialog",
@@ -45,6 +58,7 @@ export class CredentialGeneratorDialogComponent {
constructor(
@Inject(DIALOG_DATA) protected data: CredentialGeneratorParams,
private dialogService: DialogService,
private dialogRef: DialogRef<CredentialGeneratorDialogResult>,
private i18nService: I18nService,
) {}
@@ -59,11 +73,15 @@ export class CredentialGeneratorDialogComponent {
};
applyCredentials = () => {
this.data.onCredentialGenerated(this.credentialValue);
this.data.onCredentialGenerated?.(this.credentialValue);
this.dialogRef.close({
action: CredentialGeneratorDialogAction.Selected,
generatedValue: this.credentialValue,
});
};
clearCredentials = () => {
this.data.onCredentialGenerated();
this.data.onCredentialGenerated?.();
};
onCredentialGenerated = (value: string) => {
@@ -75,9 +93,12 @@ export class CredentialGeneratorDialogComponent {
this.dialogService.open(CredentialGeneratorHistoryDialogComponent);
};
static open = (dialogService: DialogService, data: CredentialGeneratorParams) => {
dialogService.open(CredentialGeneratorDialogComponent, {
data,
});
};
static open(dialogService: DialogService, data: CredentialGeneratorParams) {
return dialogService.open<CredentialGeneratorDialogResult, CredentialGeneratorParams>(
CredentialGeneratorDialogComponent,
{
data,
},
);
}
}

View File

@@ -0,0 +1,64 @@
<div class="footer">
<ng-container *ngIf="!cipher.decryptionFailure">
<button
#submitBtn
bitButton
form="cipherForm"
type="submit"
*ngIf="action !== 'view'"
class="primary"
appA11yTitle="{{ 'save' | i18n }}"
>
<i class="bwi bwi-save-changes bwi-lg bwi-fw" [hidden]="isSubmitting" aria-hidden="true"></i>
<i
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
[hidden]="!isSubmitting"
aria-hidden="true"
></i>
</button>
<button
type="button"
class="primary"
(click)="edit()"
appA11yTitle="{{ 'edit' | i18n }}"
*ngIf="!cipher.isDeleted && action === 'view'"
>
<i class="bwi bwi-pencil bwi-fw bwi-lg" aria-hidden="true"></i>
</button>
<button
*ngIf="action === 'edit' || action === 'clone' || action === 'add'"
type="button"
(click)="cancel()"
>
{{ "cancel" | i18n }}
</button>
<button
type="button"
class="primary"
(click)="restore()"
appA11yTitle="{{ 'restore' | i18n }}"
*ngIf="cipher.isDeleted"
>
<i class="bwi bwi-undo bwi-fw bwi-lg" aria-hidden="true"></i>
</button>
<button
type="button"
class="primary"
*ngIf="cipher.id && !cipher?.organizationId && !cipher.isDeleted && action !== 'clone'"
(click)="clone()"
appA11yTitle="{{ 'clone' | i18n }}"
>
<i class="bwi bwi-files bwi-fw bwi-lg" aria-hidden="true"></i>
</button>
</ng-container>
<div class="right" *ngIf="((canDeleteCipher$ | async) && action === 'edit') || action === 'view'">
<button
type="button"
(click)="delete()"
class="danger"
appA11yTitle="{{ (cipher.isDeleted ? 'permanentlyDelete' : 'delete') | i18n }}"
>
<i class="bwi bwi-trash bwi-lg bwi-fw" aria-hidden="true"></i>
</button>
</div>
</div>

View File

@@ -0,0 +1,159 @@
import { CommonModule } from "@angular/common";
import { Input, Output, EventEmitter, Component, OnInit, ViewChild } from "@angular/core";
import { Observable, firstValueFrom } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { CollectionId, UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherRepromptType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
import { ButtonComponent, ButtonModule, DialogService, ToastService } from "@bitwarden/components";
import { PasswordRepromptService } from "@bitwarden/vault";
@Component({
selector: "app-vault-item-footer",
templateUrl: "item-footer.component.html",
standalone: true,
imports: [ButtonModule, CommonModule, JslibModule],
})
export class ItemFooterComponent implements OnInit {
@Input({ required: true }) cipher: CipherView = new CipherView();
@Input() collectionId: string | null = null;
@Input({ required: true }) action: string = "view";
@Input() isSubmitting: boolean = false;
@Output() onEdit = new EventEmitter<CipherView>();
@Output() onClone = new EventEmitter<CipherView>();
@Output() onDelete = new EventEmitter<CipherView>();
@Output() onRestore = new EventEmitter<CipherView>();
@Output() onCancel = new EventEmitter<CipherView>();
@ViewChild("submitBtn", { static: false }) submitBtn: ButtonComponent | null = null;
canDeleteCipher$: Observable<boolean> = new Observable();
activeUserId: UserId | null = null;
private passwordReprompted = false;
constructor(
protected cipherService: CipherService,
protected dialogService: DialogService,
protected passwordRepromptService: PasswordRepromptService,
protected cipherAuthorizationService: CipherAuthorizationService,
protected accountService: AccountService,
protected toastService: ToastService,
protected i18nService: I18nService,
protected logService: LogService,
) {}
async ngOnInit() {
this.canDeleteCipher$ = this.cipherAuthorizationService.canDeleteCipher$(this.cipher, [
this.collectionId as CollectionId,
]);
this.activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
}
async clone() {
if (this.cipher.login?.hasFido2Credentials) {
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "passkeyNotCopied" },
content: { key: "passkeyNotCopiedAlert" },
type: "info",
});
if (!confirmed) {
return false;
}
}
if (await this.promptPassword()) {
this.onClone.emit(this.cipher);
return true;
}
return false;
}
protected edit() {
this.onEdit.emit(this.cipher);
}
cancel() {
this.onCancel.emit(this.cipher);
}
async delete(): Promise<boolean> {
if (!(await this.promptPassword())) {
return false;
}
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "deleteItem" },
content: {
key: this.cipher.isDeleted ? "permanentlyDeleteItemConfirmation" : "deleteItemConfirmation",
},
type: "warning",
});
if (!confirmed) {
return false;
}
try {
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
await this.deleteCipher(activeUserId);
this.toastService.showToast({
variant: "success",
message: this.i18nService.t(
this.cipher.isDeleted ? "permanentlyDeletedItem" : "deletedItem",
),
});
this.onDelete.emit(this.cipher);
} catch (e) {
this.logService.error(e);
}
return true;
}
async restore(): Promise<boolean> {
if (!this.cipher.isDeleted) {
return false;
}
try {
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
await this.restoreCipher(activeUserId);
this.toastService.showToast({
variant: "success",
message: this.i18nService.t("restoredItem"),
});
this.onRestore.emit(this.cipher);
} catch (e) {
this.logService.error(e);
}
return true;
}
protected deleteCipher(userId: UserId) {
return this.cipher.isDeleted
? this.cipherService.deleteWithServer(this.cipher.id, userId)
: this.cipherService.softDeleteWithServer(this.cipher.id, userId);
}
protected restoreCipher(userId: UserId) {
return this.cipherService.restoreWithServer(this.cipher.id, userId);
}
protected async promptPassword() {
if (this.cipher.reprompt === CipherRepromptType.None || this.passwordReprompted) {
return true;
}
return (this.passwordReprompted = await this.passwordRepromptService.showPasswordPrompt());
}
}

View File

@@ -0,0 +1,92 @@
<div class="container loading-spinner" *ngIf="!loaded">
<i class="bwi bwi-spinner bwi-spin bwi-3x" aria-hidden="true"></i>
</div>
<ng-container *ngIf="loaded">
<div class="content">
<cdk-virtual-scroll-viewport
itemSize="42"
minBufferPx="400"
maxBufferPx="600"
*ngIf="ciphers.length"
>
<div class="list">
<button
type="button"
*cdkVirtualFor="let c of ciphers; trackBy: trackByFn"
appStopClick
(click)="selectCipher(c)"
(contextmenu)="rightClickCipher(c)"
title="{{ 'viewItem' | i18n }}"
[ngClass]="{ active: c.id === activeCipherId }"
[attr.aria-pressed]="c.id === activeCipherId"
class="flex-list-item virtual-scroll-item"
>
<app-vault-icon [cipher]="c"></app-vault-icon>
<div class="flex-cipher-list-item">
<span class="text">
<span class="truncate-box">
<span class="truncate">{{ c.name }}</span>
<ng-container *ngIf="c.organizationId">
<i
class="bwi bwi-collection text-muted"
title="{{ 'shared' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "shared" | i18n }}</span>
</ng-container>
<ng-container *ngIf="c.hasAttachments">
<i
class="bwi bwi-paperclip text-muted"
title="{{ 'attachments' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "attachments" | i18n }}</span>
</ng-container>
</span>
</span>
<span *ngIf="c.subTitle" class="detail">{{ c.subTitle }}</span>
</div>
</button>
</div>
</cdk-virtual-scroll-viewport>
<div class="no-items" *ngIf="!ciphers.length">
<img class="no-items-image" aria-hidden="true" />
<p>{{ "noItemsInList" | i18n }}</p>
<ng-container *ngTemplateOutlet="addCipherButton"></ng-container>
</div>
<div class="footer">
<ng-container *ngTemplateOutlet="addCipherButton"></ng-container>
</div>
</div>
</ng-container>
<ng-template #addCipherButton>
<button
type="button"
class="block primary"
bitButton
appA11yTitle="{{ 'addItem' | i18n }}"
[disabled]="deleted"
[bitMenuTriggerFor]="addCipherMenu"
>
<i class="bwi bwi-plus bwi-lg" aria-hidden="true"></i>
</button>
<bit-menu #addCipherMenu>
<button type="button" bitMenuItem (click)="addCipher(CipherType.Login)">
<i class="bwi bwi-globe tw-mr-1" aria-hidden="true"></i>
{{ "typeLogin" | i18n }}
</button>
<button type="button" bitMenuItem (click)="addCipher(CipherType.Card)">
<i class="bwi bwi-credit-card tw-mr-1" aria-hidden="true"></i>
{{ "typeCard" | i18n }}
</button>
<button type="button" bitMenuItem (click)="addCipher(CipherType.Identity)">
<i class="bwi bwi-id-card tw-mr-1" aria-hidden="true"></i>
{{ "typeIdentity" | i18n }}
</button>
<button type="button" bitMenuItem (click)="addCipher(CipherType.SecureNote)">
<i class="bwi bwi-sticky-note tw-mr-1" aria-hidden="true"></i>
{{ "typeSecureNote" | i18n }}
</button>
</bit-menu>
</ng-template>

View File

@@ -0,0 +1,42 @@
import { ScrollingModule } from "@angular/cdk/scrolling";
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { distinctUntilChanged } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { VaultItemsComponent as BaseVaultItemsComponent } from "@bitwarden/angular/vault/components/vault-items.component";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { MenuModule } from "@bitwarden/components";
import { SearchBarService } from "../../../app/layout/search/search-bar.service";
@Component({
selector: "app-vault-items-v2",
templateUrl: "vault-items-v2.component.html",
standalone: true,
imports: [MenuModule, CommonModule, JslibModule, ScrollingModule],
})
export class VaultItemsV2Component extends BaseVaultItemsComponent {
constructor(
searchService: SearchService,
private readonly searchBarService: SearchBarService,
cipherService: CipherService,
accountService: AccountService,
) {
super(searchService, cipherService, accountService);
this.searchBarService.searchText$
.pipe(distinctUntilChanged(), takeUntilDestroyed())
.subscribe((searchText) => {
this.searchText = searchText!;
});
}
trackByFn(index: number, c: CipherView): string {
return c.id;
}
}

View File

@@ -0,0 +1,80 @@
<div id="vault" class="vault vault-v2" attr.aria-hidden="{{ showingModal }}">
<app-vault-items-v2
id="items"
class="items"
[activeCipherId]="cipherId"
(onCipherClicked)="viewCipher($event)"
(onCipherRightClicked)="viewCipherMenu($event)"
(onAddCipher)="addCipher($event)"
(onAddCipherOptions)="addCipherOptions()"
>
</app-vault-items-v2>
<div class="details" *ngIf="!!action">
<app-vault-item-footer
id="footer"
#footer
[cipher]="cipher"
[action]="action"
(onEdit)="editCipher($event)"
(onRestore)="restoreCipher()"
(onClone)="cloneCipher($event)"
(onDelete)="deleteCipher()"
(onCancel)="cancelCipher($event)"
></app-vault-item-footer>
<div class="content">
<div class="inner-content">
<div class="box">
<app-cipher-view *ngIf="action === 'view'" [cipher]="cipher" [collections]="collections">
</app-cipher-view>
<vault-cipher-form
#vaultForm
*ngIf="action === 'add' || action === 'edit' || action === 'clone'"
formId="cipherForm"
[config]="config"
(cipherSaved)="savedCipher($event)"
[submitBtn]="footer?.submitBtn"
>
<bit-item slot="attachment-button">
<button bit-item-content type="button" (click)="openAttachmentsDialog()">
<p class="tw-m-0">
{{ "attachments" | i18n }}
<span
*ngIf="!(canAccessAttachments$ | async)"
bitBadge
variant="success"
class="tw-ml-2"
>
{{ "premium" | i18n }}
</span>
</p>
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
</button>
</bit-item>
</vault-cipher-form>
</div>
</div>
</div>
</div>
<div
id="logo"
class="logo"
*ngIf="action !== 'add' && action !== 'edit' && action !== 'view' && action !== 'clone'"
>
<div class="content">
<div class="inner-content">
<img class="logo-image" alt="Bitwarden" aria-hidden="true" />
</div>
</div>
</div>
<div class="left-nav">
<app-vault-filter
class="vault-filters"
[activeFilter]="activeFilter"
(onFilterChange)="applyVaultFilter($event)"
(onAddFolder)="addFolder()"
(onEditFolder)="editFolder($event.id)"
></app-vault-filter>
<app-nav class="nav"></app-nav>
</div>
</div>
<ng-template #folderAddEdit></ng-template>

View File

@@ -0,0 +1,785 @@
import { CommonModule } from "@angular/common";
import {
ChangeDetectorRef,
Component,
NgZone,
OnDestroy,
OnInit,
ViewChild,
ViewContainerRef,
} from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { ActivatedRoute, Router } from "@angular/router";
import { firstValueFrom, Subject, takeUntil, switchMap } from "rxjs";
import { filter, map, take } from "rxjs/operators";
import { CollectionView } from "@bitwarden/admin-console/common";
import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { VaultViewPasswordHistoryService } from "@bitwarden/angular/services/view-password-history.service";
import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EventType } from "@bitwarden/common/enums";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { SyncService } from "@bitwarden/common/platform/sync";
import { CipherId, UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import {
BadgeModule,
ButtonModule,
DialogService,
ItemModule,
ToastService,
} from "@bitwarden/components";
import { I18nPipe } from "@bitwarden/ui-common";
import {
AttachmentDialogResult,
AttachmentsV2Component,
ChangeLoginPasswordService,
CipherFormConfig,
CipherFormConfigService,
CipherFormGenerationService,
CipherFormMode,
CipherFormModule,
CipherViewComponent,
DecryptionFailureDialogComponent,
DefaultChangeLoginPasswordService,
DefaultCipherFormConfigService,
PasswordRepromptService,
} from "@bitwarden/vault";
import { NavComponent } from "../../../app/layout/nav.component";
import { SearchBarService } from "../../../app/layout/search/search-bar.service";
import { DesktopCredentialGenerationService } from "../../../services/desktop-cipher-form-generator.service";
import { DesktopPremiumUpgradePromptService } from "../../../services/desktop-premium-upgrade-prompt.service";
import { invokeMenu, RendererMenuItem } from "../../../utils";
import { FolderAddEditComponent } from "./folder-add-edit.component";
import { ItemFooterComponent } from "./item-footer.component";
import { VaultFilterComponent } from "./vault-filter/vault-filter.component";
import { VaultFilterModule } from "./vault-filter/vault-filter.module";
import { VaultItemsV2Component } from "./vault-items-v2.component";
const BroadcasterSubscriptionId = "VaultComponent";
@Component({
selector: "app-vault",
templateUrl: "vault-v2.component.html",
standalone: true,
imports: [
BadgeModule,
CommonModule,
CipherFormModule,
CipherViewComponent,
ItemFooterComponent,
I18nPipe,
ItemModule,
ButtonModule,
NavComponent,
VaultFilterModule,
VaultItemsV2Component,
],
providers: [
{
provide: CipherFormConfigService,
useClass: DefaultCipherFormConfigService,
},
{
provide: ChangeLoginPasswordService,
useClass: DefaultChangeLoginPasswordService,
},
{
provide: ViewPasswordHistoryService,
useClass: VaultViewPasswordHistoryService,
},
{
provide: PremiumUpgradePromptService,
useClass: DesktopPremiumUpgradePromptService,
},
{ provide: CipherFormGenerationService, useClass: DesktopCredentialGenerationService },
],
})
export class VaultV2Component implements OnInit, OnDestroy {
@ViewChild(VaultItemsV2Component, { static: true })
vaultItemsComponent: VaultItemsV2Component | null = null;
@ViewChild(VaultFilterComponent, { static: true })
vaultFilterComponent: VaultFilterComponent | null = null;
@ViewChild("folderAddEdit", { read: ViewContainerRef, static: true })
folderAddEditModalRef: ViewContainerRef | null = null;
action: CipherFormMode | "view" | null = null;
cipherId: string | null = null;
favorites = false;
type: CipherType | null = null;
folderId: string | null = null;
collectionId: string | null = null;
organizationId: string | null = null;
myVaultOnly = false;
addType: CipherType | undefined = undefined;
addOrganizationId: string | null = null;
addCollectionIds: string[] | null = null;
showingModal = false;
deleted = false;
userHasPremiumAccess = false;
activeFilter: VaultFilter = new VaultFilter();
activeUserId: UserId | null = null;
cipherRepromptId: string | null = null;
cipher: CipherView | null = new CipherView();
collections: CollectionView[] | null = null;
config: CipherFormConfig | null = null;
protected canAccessAttachments$ = this.accountService.activeAccount$.pipe(
filter((account): account is Account => !!account),
switchMap((account) =>
this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id),
),
);
private modal: ModalRef | null = null;
private componentIsDestroyed$ = new Subject<boolean>();
constructor(
private route: ActivatedRoute,
private router: Router,
private i18nService: I18nService,
private modalService: ModalService,
private broadcasterService: BroadcasterService,
private changeDetectorRef: ChangeDetectorRef,
private ngZone: NgZone,
private syncService: SyncService,
private messagingService: MessagingService,
private platformUtilsService: PlatformUtilsService,
private eventCollectionService: EventCollectionService,
private totpService: TotpService,
private passwordRepromptService: PasswordRepromptService,
private searchBarService: SearchBarService,
private apiService: ApiService,
private dialogService: DialogService,
private billingAccountProfileStateService: BillingAccountProfileStateService,
private toastService: ToastService,
private accountService: AccountService,
private cipherService: CipherService,
private formConfigService: CipherFormConfigService,
private premiumUpgradePromptService: PremiumUpgradePromptService,
) {}
async ngOnInit() {
this.accountService.activeAccount$
.pipe(
filter((account): account is Account => !!account),
switchMap((account) =>
this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id),
),
takeUntil(this.componentIsDestroyed$),
)
.subscribe((canAccessPremium: boolean) => {
this.userHasPremiumAccess = canAccessPremium;
});
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
this.ngZone
.run(async () => {
let detectChanges = true;
try {
switch (message.command) {
case "newLogin":
await this.addCipher(CipherType.Login).catch(() => {});
break;
case "newCard":
await this.addCipher(CipherType.Card).catch(() => {});
break;
case "newIdentity":
await this.addCipher(CipherType.Identity).catch(() => {});
break;
case "newSecureNote":
await this.addCipher(CipherType.SecureNote).catch(() => {});
break;
case "focusSearch":
(document.querySelector("#search") as HTMLInputElement)?.select();
detectChanges = false;
break;
case "syncCompleted":
if (this.vaultItemsComponent) {
await this.vaultItemsComponent
.reload(this.activeFilter.buildFilter())
.catch(() => {});
}
if (this.vaultFilterComponent) {
await this.vaultFilterComponent
.reloadCollectionsAndFolders(this.activeFilter)
.catch(() => {});
await this.vaultFilterComponent.reloadOrganizations().catch(() => {});
}
break;
case "modalShown":
this.showingModal = true;
break;
case "modalClosed":
this.showingModal = false;
break;
case "copyUsername": {
if (this.cipher?.login?.username) {
this.copyValue(this.cipher, this.cipher?.login?.username, "username", "Username");
}
break;
}
case "copyPassword": {
if (this.cipher?.login?.password && this.cipher.viewPassword) {
this.copyValue(this.cipher, this.cipher.login.password, "password", "Password");
await this.eventCollectionService
.collect(EventType.Cipher_ClientCopiedPassword, this.cipher.id)
.catch(() => {});
}
break;
}
case "copyTotp": {
if (
this.cipher?.login?.hasTotp &&
(this.cipher.organizationUseTotp || this.userHasPremiumAccess)
) {
const value = await firstValueFrom(
this.totpService.getCode$(this.cipher.login.totp),
).catch(() => null);
if (value) {
this.copyValue(this.cipher, value.code, "verificationCodeTotp", "TOTP");
}
}
break;
}
default:
detectChanges = false;
break;
}
} catch {
// Ignore errors
}
if (detectChanges) {
this.changeDetectorRef.detectChanges();
}
})
.catch(() => {});
});
if (!this.syncService.syncInProgress) {
await this.load().catch(() => {});
}
this.searchBarService.setEnabled(true);
this.searchBarService.setPlaceholderText(this.i18nService.t("searchVault"));
const authRequest = await this.apiService.getLastAuthRequest().catch(() => null);
if (authRequest != null) {
this.messagingService.send("openLoginApproval", {
notificationId: authRequest.id,
});
}
this.activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(getUserId),
).catch(() => null);
if (this.activeUserId) {
this.cipherService
.failedToDecryptCiphers$(this.activeUserId)
.pipe(
map((ciphers) => ciphers?.filter((c) => !c.isDeleted) ?? []),
filter((ciphers) => ciphers.length > 0),
take(1),
takeUntil(this.componentIsDestroyed$),
)
.subscribe((ciphers) => {
DecryptionFailureDialogComponent.open(this.dialogService, {
cipherIds: ciphers.map((c) => c.id as CipherId),
});
});
}
}
ngOnDestroy() {
this.searchBarService.setEnabled(false);
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
this.componentIsDestroyed$.next(true);
this.componentIsDestroyed$.complete();
}
async load() {
const params = await firstValueFrom(this.route.queryParams).catch();
if (params.cipherId) {
const cipherView = new CipherView();
cipherView.id = params.cipherId;
if (params.action === "clone") {
await this.cloneCipher(cipherView).catch(() => {});
} else if (params.action === "edit") {
await this.editCipher(cipherView).catch(() => {});
} else {
await this.viewCipher(cipherView).catch(() => {});
}
} else if (params.action === "add") {
this.addType = Number(params.addType);
await this.addCipher(this.addType).catch(() => {});
}
this.activeFilter = new VaultFilter({
status: params.deleted ? "trash" : params.favorites ? "favorites" : "all",
cipherType:
params.action === "add" || params.type == null
? undefined
: (parseInt(params.type) as CipherType),
selectedFolderId: params.folderId,
selectedCollectionId: params.selectedCollectionId,
selectedOrganizationId: params.selectedOrganizationId,
myVaultOnly: params.myVaultOnly ?? false,
});
if (this.vaultItemsComponent) {
await this.vaultItemsComponent.reload(this.activeFilter.buildFilter()).catch(() => {});
}
}
async viewCipher(cipher: CipherView) {
if (await this.shouldReprompt(cipher, "view")) {
return;
}
this.cipherId = cipher.id;
this.cipher = cipher;
this.collections =
this.vaultFilterComponent?.collections.fullList.filter((c) =>
cipher.collectionIds.includes(c.id),
) ?? null;
this.action = "view";
await this.go().catch(() => {});
}
async openAttachmentsDialog() {
if (!this.userHasPremiumAccess) {
await this.premiumUpgradePromptService.promptForPremium();
return;
}
const dialogRef = AttachmentsV2Component.open(this.dialogService, {
cipherId: this.cipherId as CipherId,
});
const result = await firstValueFrom(dialogRef.closed).catch(() => null);
if (
result?.action === AttachmentDialogResult.Removed ||
result?.action === AttachmentDialogResult.Uploaded
) {
await this.vaultItemsComponent?.refresh().catch(() => {});
}
}
viewCipherMenu(cipher: CipherView) {
const menu: RendererMenuItem[] = [
{
label: this.i18nService.t("view"),
click: () => {
this.functionWithChangeDetection(() => {
this.viewCipher(cipher).catch(() => {});
});
},
},
];
if (cipher.decryptionFailure) {
invokeMenu(menu);
return;
}
if (!cipher.isDeleted) {
menu.push({
label: this.i18nService.t("edit"),
click: () => {
this.functionWithChangeDetection(() => {
this.editCipher(cipher).catch(() => {});
});
},
});
if (!cipher.organizationId) {
menu.push({
label: this.i18nService.t("clone"),
click: () => {
this.functionWithChangeDetection(() => {
this.cloneCipher(cipher).catch(() => {});
});
},
});
}
}
switch (cipher.type) {
case CipherType.Login:
if (
cipher.login.canLaunch ||
cipher.login.username != null ||
cipher.login.password != null
) {
menu.push({ type: "separator" });
}
if (cipher.login.canLaunch) {
menu.push({
label: this.i18nService.t("launch"),
click: () => this.platformUtilsService.launchUri(cipher.login.launchUri),
});
}
if (cipher.login.username != null) {
menu.push({
label: this.i18nService.t("copyUsername"),
click: () => this.copyValue(cipher, cipher.login.username, "username", "Username"),
});
}
if (cipher.login.password != null && cipher.viewPassword) {
menu.push({
label: this.i18nService.t("copyPassword"),
click: () => {
this.copyValue(cipher, cipher.login.password, "password", "Password");
this.eventCollectionService
.collect(EventType.Cipher_ClientCopiedPassword, cipher.id)
.catch(() => {});
},
});
}
if (cipher.login.hasTotp && (cipher.organizationUseTotp || this.userHasPremiumAccess)) {
menu.push({
label: this.i18nService.t("copyVerificationCodeTotp"),
click: async () => {
const value = await firstValueFrom(
this.totpService.getCode$(cipher.login.totp),
).catch(() => null);
if (value) {
this.copyValue(cipher, value.code, "verificationCodeTotp", "TOTP");
}
},
});
}
break;
case CipherType.Card:
if (cipher.card.number != null || cipher.card.code != null) {
menu.push({ type: "separator" });
}
if (cipher.card.number != null) {
menu.push({
label: this.i18nService.t("copyNumber"),
click: () => this.copyValue(cipher, cipher.card.number, "number", "Card Number"),
});
}
if (cipher.card.code != null) {
menu.push({
label: this.i18nService.t("copySecurityCode"),
click: () => {
this.copyValue(cipher, cipher.card.code, "securityCode", "Security Code");
this.eventCollectionService
.collect(EventType.Cipher_ClientCopiedCardCode, cipher.id)
.catch(() => {});
},
});
}
break;
default:
break;
}
invokeMenu(menu);
}
async shouldReprompt(cipher: CipherView, action: "edit" | "clone" | "view"): Promise<boolean> {
return !(await this.canNavigateAway(action, cipher)) || !(await this.passwordReprompt(cipher));
}
async buildFormConfig(action: CipherFormMode) {
this.config = await this.formConfigService
.buildConfig(action, this.cipherId as CipherId, this.addType)
.catch(() => null);
}
async editCipher(cipher: CipherView) {
if (await this.shouldReprompt(cipher, "edit")) {
return;
}
this.cipherId = cipher.id;
this.cipher = cipher;
await this.buildFormConfig("edit");
this.action = "edit";
await this.go().catch(() => {});
}
async cloneCipher(cipher: CipherView) {
if (await this.shouldReprompt(cipher, "clone")) {
return;
}
this.cipherId = cipher.id;
this.cipher = cipher;
await this.buildFormConfig("clone");
this.action = "clone";
await this.go().catch(() => {});
}
async addCipher(type: CipherType) {
this.addType = type || this.activeFilter.cipherType;
this.cipherId = null;
await this.buildFormConfig("add");
this.action = "add";
this.prefillCipherFromFilter();
await this.go().catch(() => {});
}
addCipherOptions() {
const menu: RendererMenuItem[] = [
{
label: this.i18nService.t("typeLogin"),
click: () => this.addCipherWithChangeDetection(CipherType.Login),
},
{
label: this.i18nService.t("typeCard"),
click: () => this.addCipherWithChangeDetection(CipherType.Card),
},
{
label: this.i18nService.t("typeIdentity"),
click: () => this.addCipherWithChangeDetection(CipherType.Identity),
},
{
label: this.i18nService.t("typeSecureNote"),
click: () => this.addCipherWithChangeDetection(CipherType.SecureNote),
},
];
invokeMenu(menu);
}
async savedCipher(cipher: CipherView) {
this.cipherId = null;
this.action = "view";
await this.vaultItemsComponent?.refresh().catch(() => {});
this.cipherId = cipher.id;
this.cipher = cipher;
if (this.activeUserId) {
await this.cipherService.clearCache(this.activeUserId).catch(() => {});
}
await this.vaultItemsComponent?.load(this.activeFilter.buildFilter()).catch(() => {});
await this.go().catch(() => {});
await this.vaultItemsComponent?.refresh().catch(() => {});
}
async deleteCipher() {
this.cipherId = null;
this.cipher = null;
this.action = null;
await this.go().catch(() => {});
await this.vaultItemsComponent?.refresh().catch(() => {});
}
async restoreCipher() {
this.cipherId = null;
this.action = null;
await this.go().catch(() => {});
await this.vaultItemsComponent?.refresh().catch(() => {});
}
async cancelCipher(cipher: CipherView) {
this.cipherId = cipher.id;
this.cipher = cipher;
this.action = this.cipherId != null ? "view" : null;
await this.go().catch(() => {});
}
async applyVaultFilter(vaultFilter: VaultFilter) {
this.searchBarService.setPlaceholderText(
this.i18nService.t(this.calculateSearchBarLocalizationString(vaultFilter)),
);
this.activeFilter = vaultFilter;
await this.vaultItemsComponent
?.reload(this.activeFilter.buildFilter(), vaultFilter.status === "trash")
.catch(() => {});
await this.go().catch(() => {});
}
private calculateSearchBarLocalizationString(vaultFilter: VaultFilter): string {
if (vaultFilter.status === "favorites") {
return "searchFavorites";
}
if (vaultFilter.status === "trash") {
return "searchTrash";
}
if (vaultFilter.cipherType != null) {
return "searchType";
}
if (vaultFilter.selectedFolderId != null && vaultFilter.selectedFolderId !== "none") {
return "searchFolder";
}
if (vaultFilter.selectedCollectionId != null) {
return "searchCollection";
}
if (vaultFilter.selectedOrganizationId != null) {
return "searchOrganization";
}
if (vaultFilter.myVaultOnly) {
return "searchMyVault";
}
return "searchVault";
}
async addFolder() {
this.messagingService.send("newFolder");
}
async editFolder(folderId: string) {
if (this.modal != null) {
this.modal.close();
}
if (this.folderAddEditModalRef == null) {
return;
}
const [modal, childComponent] = await this.modalService
.openViewRef(
FolderAddEditComponent,
this.folderAddEditModalRef,
(comp) => (comp.folderId = folderId),
)
.catch(() => [null, null] as any);
this.modal = modal;
if (childComponent) {
childComponent.onSavedFolder.subscribe(async (folder: FolderView) => {
this.modal?.close();
await this.vaultFilterComponent
?.reloadCollectionsAndFolders(this.activeFilter)
.catch(() => {});
});
childComponent.onDeletedFolder.subscribe(async (folder: FolderView) => {
this.modal?.close();
await this.vaultFilterComponent
?.reloadCollectionsAndFolders(this.activeFilter)
.catch(() => {});
});
}
if (this.modal) {
this.modal.onClosed.pipe(takeUntilDestroyed()).subscribe(() => {
this.modal = null;
});
}
}
private dirtyInput(): boolean {
return (
(this.action === "add" || this.action === "edit" || this.action === "clone") &&
document.querySelectorAll("vault-cipher-form .ng-dirty").length > 0
);
}
private async wantsToSaveChanges(): Promise<boolean> {
const confirmed = await this.dialogService
.openSimpleDialog({
title: { key: "unsavedChangesTitle" },
content: { key: "unsavedChangesConfirmation" },
type: "warning",
})
.catch(() => false);
return !confirmed;
}
private async go(queryParams: any = null) {
if (queryParams == null) {
queryParams = {
action: this.action,
cipherId: this.cipherId,
favorites: this.favorites ? true : null,
type: this.type,
folderId: this.folderId,
collectionId: this.collectionId,
deleted: this.deleted ? true : null,
organizationId: this.organizationId,
myVaultOnly: this.myVaultOnly,
};
}
this.router
.navigate([], {
relativeTo: this.route,
queryParams: queryParams,
replaceUrl: true,
})
.catch(() => {});
}
private addCipherWithChangeDetection(type: CipherType) {
this.functionWithChangeDetection(() => this.addCipher(type).catch(() => {}));
}
private copyValue(cipher: CipherView, value: string, labelI18nKey: string, aType: string) {
this.functionWithChangeDetection(() => {
(async () => {
if (
cipher.reprompt !== CipherRepromptType.None &&
this.passwordRepromptService.protectedFields().includes(aType) &&
!(await this.passwordReprompt(cipher))
) {
return;
}
this.platformUtilsService.copyToClipboard(value);
this.toastService.showToast({
variant: "info",
title: undefined,
message: this.i18nService.t("valueCopied", this.i18nService.t(labelI18nKey)),
});
if (this.action === "view") {
this.messagingService.send("minimizeOnCopy");
}
})().catch(() => {});
});
}
private functionWithChangeDetection(func: () => void) {
this.ngZone.run(() => {
func();
this.changeDetectorRef.detectChanges();
});
}
private prefillCipherFromFilter() {
if (this.activeFilter.selectedCollectionId != null && this.vaultFilterComponent != null) {
const collections = this.vaultFilterComponent.collections.fullList.filter(
(c) => c.id === this.activeFilter.selectedCollectionId,
);
if (collections.length > 0) {
this.addOrganizationId = collections[0].organizationId;
this.addCollectionIds = [this.activeFilter.selectedCollectionId];
}
} else if (this.activeFilter.selectedOrganizationId) {
this.addOrganizationId = this.activeFilter.selectedOrganizationId;
}
if (this.activeFilter.selectedFolderId && this.activeFilter.selectedFolder) {
this.folderId = this.activeFilter.selectedFolderId;
}
}
private async canNavigateAway(action: string, cipher?: CipherView) {
if (this.action === action && (!cipher || this.cipherId === cipher.id)) {
return false;
} else if (this.dirtyInput() && (await this.wantsToSaveChanges())) {
return false;
}
return true;
}
private async passwordReprompt(cipher: CipherView) {
if (cipher.reprompt === CipherRepromptType.None) {
this.cipherRepromptId = null;
return true;
}
if (this.cipherRepromptId === cipher.id) {
return true;
}
const repromptResult = await this.passwordRepromptService.showPasswordPrompt();
if (repromptResult) {
this.cipherRepromptId = cipher.id;
}
return repromptResult;
}
}

View File

@@ -69,6 +69,8 @@ import {
ToastService,
} from "@bitwarden/components";
import {
AttachmentDialogResult,
AttachmentsV2Component,
CipherFormConfig,
CipherFormConfigService,
CollectionAssignmentResult,
@@ -92,10 +94,6 @@ import {
} from "../../../vault/components/vault-item-dialog/vault-item-dialog.component";
import { VaultItemEvent } from "../../../vault/components/vault-items/vault-item-event";
import { VaultItemsModule } from "../../../vault/components/vault-items/vault-items.module";
import {
AttachmentDialogResult,
AttachmentsV2Component,
} from "../../../vault/individual-vault/attachments-v2.component";
import {
BulkDeleteDialogResult,
openBulkDeleteDialog,

View File

@@ -2,6 +2,7 @@ import { CommonModule } from "@angular/common";
import { Component, Inject } from "@angular/core";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { VaultViewPasswordHistoryService } from "@bitwarden/angular/services/view-password-history.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { EmergencyAccessId } from "@bitwarden/common/types/guid";
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
@@ -21,8 +22,6 @@ import {
DefaultChangeLoginPasswordService,
} from "@bitwarden/vault";
import { WebViewPasswordHistoryService } from "../../../../vault/services/web-view-password-history.service";
export interface EmergencyViewDialogParams {
/** The cipher being viewed. */
cipher: CipherView;
@@ -42,7 +41,7 @@ class PremiumUpgradePromptNoop implements PremiumUpgradePromptService {
standalone: true,
imports: [ButtonModule, CipherViewComponent, DialogModule, CommonModule, JslibModule],
providers: [
{ provide: ViewPasswordHistoryService, useClass: WebViewPasswordHistoryService },
{ provide: ViewPasswordHistoryService, useClass: VaultViewPasswordHistoryService },
{ provide: PremiumUpgradePromptService, useClass: PremiumUpgradePromptNoop },
{ provide: ChangeLoginPasswordService, useClass: DefaultChangeLoginPasswordService },
],

View File

@@ -1,17 +0,0 @@
<h1 class="tw-text-4xl !tw-text-alt2">The Bitwarden Password Manager</h1>
<div class="tw-pt-32">
<h2 class="tw-text-2xl">
Trusted by millions of individuals, teams, and organizations worldwide for secure password
storage and sharing.
</h2>
</div>
<ul class="tw-mt-12 tw-flex tw-flex-col tw-gap-10 tw-text-2xl tw-text-main">
<li>Store logins, secure notes, and more</li>
<li>Collaborate and share securely</li>
<li>Access anywhere on any device</li>
<li>Create your account to get started</li>
</ul>
<div class="tw-mt-28 tw-flex tw-flex-col tw-items-center tw-gap-5">
<app-logo-forbes></app-logo-forbes>
<app-logo-us-news></app-logo-us-news>
</div>

View File

@@ -1,7 +0,0 @@
import { Component } from "@angular/core";
@Component({
selector: "app-abm-enterprise-content",
templateUrl: "abm-enterprise-content.component.html",
})
export class AbmEnterpriseContentComponent {}

View File

@@ -1,17 +0,0 @@
<h1 class="tw-text-4xl !tw-text-alt2">The Bitwarden Password Manager</h1>
<div class="tw-pt-32">
<h2 class="tw-text-2xl">
Trusted by millions of individuals, teams, and organizations worldwide for secure password
storage and sharing.
</h2>
</div>
<ul class="tw-mt-12 tw-flex tw-flex-col tw-gap-10 tw-text-2xl tw-text-main">
<li>Store logins, secure notes, and more</li>
<li>Collaborate and share securely</li>
<li>Access anywhere on any device</li>
<li>Create your account to get started</li>
</ul>
<div class="tw-mt-28 tw-flex tw-flex-col tw-items-center tw-gap-5">
<app-logo-forbes></app-logo-forbes>
<app-logo-us-news></app-logo-us-news>
</div>

View File

@@ -1,7 +0,0 @@
import { Component } from "@angular/core";
@Component({
selector: "app-abm-teams-content",
templateUrl: "abm-teams-content.component.html",
})
export class AbmTeamsContentComponent {}

View File

@@ -1,17 +0,0 @@
<h1 class="tw-text-4xl !tw-text-alt2">Start Your Enterprise Free Trial Now</h1>
<div class="tw-pt-32">
<h2 class="tw-text-2xl">
Millions of individuals, teams, and organizations worldwide trust Bitwarden for secure password
storage and sharing.
</h2>
</div>
<ul class="tw-mt-12 tw-flex tw-flex-col tw-gap-10 tw-text-2xl tw-text-main">
<li>Collaborate and share securely</li>
<li>Deploy and manage quickly and easily</li>
<li>Access anywhere on any device</li>
<li>Create your account to get started</li>
</ul>
<div class="tw-mt-28 tw-flex tw-flex-col tw-items-center tw-gap-5">
<app-logo-cnet></app-logo-cnet>
<app-logo-us-news></app-logo-us-news>
</div>

View File

@@ -1,7 +0,0 @@
import { Component } from "@angular/core";
@Component({
selector: "app-cnet-enterprise-content",
templateUrl: "cnet-enterprise-content.component.html",
})
export class CnetEnterpriseContentComponent {}

View File

@@ -1,17 +0,0 @@
<h1 class="tw-text-4xl !tw-text-alt2">Start Your Premium Account Now</h1>
<div class="tw-pt-32">
<h2 class="tw-text-2xl">
Millions of individuals, teams, and organizations worldwide trust Bitwarden for secure password
storage and sharing.
</h2>
</div>
<ul class="tw-mt-12 tw-flex tw-flex-col tw-gap-10 tw-text-2xl tw-text-main">
<li>Store logins, secure notes, and more</li>
<li>Secure your account with advanced two-step login</li>
<li>Access anywhere on any device</li>
<li>Create your account to get started</li>
</ul>
<div class="tw-mt-28 tw-flex tw-flex-col tw-items-center tw-gap-5">
<app-logo-cnet></app-logo-cnet>
<app-logo-us-news></app-logo-us-news>
</div>

View File

@@ -1,7 +0,0 @@
import { Component } from "@angular/core";
@Component({
selector: "app-cnet-individual-content",
templateUrl: "cnet-individual-content.component.html",
})
export class CnetIndividualContentComponent {}

View File

@@ -1,17 +0,0 @@
<h1 class="tw-text-4xl !tw-text-alt2">Start Your Teams Free Trial Now</h1>
<div class="tw-pt-32">
<h2 class="tw-text-2xl">
Millions of individuals, teams, and organizations worldwide trust Bitwarden for secure password
storage and sharing.
</h2>
</div>
<ul class="tw-mt-12 tw-flex tw-flex-col tw-gap-10 tw-text-2xl tw-text-main">
<li>Collaborate and share securely</li>
<li>Deploy and manage quickly and easily</li>
<li>Access anywhere on any device</li>
<li>Create your account to get started</li>
</ul>
<div class="tw-mt-28 tw-flex tw-flex-col tw-items-center tw-gap-5">
<app-logo-cnet></app-logo-cnet>
<app-logo-us-news></app-logo-us-news>
</div>

View File

@@ -1,7 +0,0 @@
import { Component } from "@angular/core";
@Component({
selector: "app-cnet-teams-content",
templateUrl: "cnet-teams-content.component.html",
})
export class CnetTeamsContentComponent {}

View File

@@ -1,16 +0,0 @@
<h1 class="tw-text-4xl !tw-text-alt2">The Bitwarden Password Manager</h1>
<div class="tw-pt-32">
<h2 class="tw-text-2xl">
Trusted by millions of individuals, teams, and organizations worldwide for secure password
storage and sharing.
</h2>
</div>
<ul class="tw-mt-12 tw-flex tw-flex-col tw-gap-10 tw-text-2xl tw-text-main">
<li>Store logins, secure notes, and more</li>
<li>Collaborate and share securely</li>
<li>Access anywhere on any device</li>
<li>Create your account to get started</li>
</ul>
<div class="tw-mt-28">
<app-logo-company-testimonial></app-logo-company-testimonial>
</div>

View File

@@ -1,7 +0,0 @@
import { Component } from "@angular/core";
@Component({
selector: "app-default-content",
templateUrl: "default-content.component.html",
})
export class DefaultContentComponent {}

View File

@@ -1,44 +0,0 @@
<h1 class="tw-text-3xl !tw-text-alt2">Start your 7-day Enterprise free trial</h1>
<div class="tw-pt-20">
<h2 class="tw-text-2xl">
Bitwarden is the most trusted password manager designed for seamless administration and employee
usability.
</h2>
</div>
<ul class="tw-mt-12 tw-flex tw-flex-col tw-gap-10 tw-text-2xl tw-text-main tw-list-none tw-pl-0">
<li class="tw-flex tw-items-center">
<i class="bwi bwi-lg bwi-check-circle tw-mr-4 tw-flex-none"></i
><span class="tw-flex-auto"
>Instantly and securely share credentials with the groups and individuals who need them</span
>
</li>
<li class="tw-flex tw-items-center">
<i class="bwi bwi-lg bwi-check-circle tw-mr-4 tw-flex-none"></i
><span class="tw-flex-auto"
>Strengthen company-wide security through centralized administrative control and
policies</span
>
</li>
<li class="tw-flex tw-items-center">
<i class="bwi bwi-lg bwi-check-circle tw-mr-4 tw-flex-none"></i
><span class="tw-flex-auto"
>Streamline user onboarding and automate account provisioning with flexible SSO and SCIM
integrations</span
>
</li>
<li class="tw-flex tw-items-center">
<i class="bwi bwi-lg bwi-check-circle tw-mr-4 tw-flex-none"></i
><span class="tw-flex-auto"
>Migrate to Bitwarden in minutes with comprehensive import options</span
>
</li>
<li class="tw-flex tw-items-center">
<i class="bwi bwi-lg bwi-check-circle tw-mr-4 tw-flex-none"></i
><span class="tw-flex-auto"
>Give all Enterprise users the gift of 360º security with a free Families plan</span
>
</li>
</ul>
<div class="tw-mt-28 tw-flex tw-flex-col tw-items-center tw-gap-5">
<app-logo-badges></app-logo-badges>
</div>

View File

@@ -1,7 +0,0 @@
import { Component } from "@angular/core";
@Component({
selector: "app-enterprise-content",
templateUrl: "enterprise-content.component.html",
})
export class EnterpriseContentComponent {}

View File

@@ -1,44 +0,0 @@
<h1 class="tw-text-3xl !tw-text-alt2">Start your 7-day Enterprise free trial</h1>
<div class="tw-pt-20">
<h2 class="tw-text-2xl">
Bitwarden is the most trusted password manager designed for seamless administration and employee
usability.
</h2>
</div>
<ul class="tw-mt-12 tw-flex tw-flex-col tw-gap-10 tw-text-2xl tw-text-main tw-list-none tw-pl-0">
<li class="tw-flex tw-items-center">
<i class="bwi bwi-lg bwi-check-circle tw-mr-4 tw-flex-none"></i
><span class="tw-flex-auto"
>Instantly and securely share credentials with the groups and individuals who need them</span
>
</li>
<li class="tw-flex tw-items-center">
<i class="bwi bwi-lg bwi-check-circle tw-mr-4 tw-flex-none"></i
><span class="tw-flex-auto"
>Strengthen company-wide security through centralized administrative control and
policies</span
>
</li>
<li class="tw-flex tw-items-center">
<i class="bwi bwi-lg bwi-check-circle tw-mr-4 tw-flex-none"></i
><span class="tw-flex-auto"
>Streamline user onboarding and automate account provisioning with flexible SSO and SCIM
integrations</span
>
</li>
<li class="tw-flex tw-items-center">
<i class="bwi bwi-lg bwi-check-circle tw-mr-4 tw-flex-none"></i
><span class="tw-flex-auto"
>Migrate to Bitwarden in minutes with comprehensive import options</span
>
</li>
<li class="tw-flex tw-items-center">
<i class="bwi bwi-lg bwi-check-circle tw-mr-4 tw-flex-none"></i
><span class="tw-flex-auto"
>Give all Enterprise users the gift of 360º security with a free Families plan</span
>
</li>
</ul>
<div class="tw-mt-28 tw-flex tw-flex-col tw-items-center tw-gap-5">
<app-logo-badges></app-logo-badges>
</div>

View File

@@ -1,7 +0,0 @@
import { Component } from "@angular/core";
@Component({
selector: "app-enterprise1-content",
templateUrl: "enterprise1-content.component.html",
})
export class Enterprise1ContentComponent {}

View File

@@ -1,44 +0,0 @@
<h1 class="tw-text-3xl !tw-text-alt2">Start your 7-day Enterprise free trial</h1>
<div class="tw-pt-20">
<h2 class="tw-text-2xl">
Bitwarden is the most trusted password manager designed for seamless administration and employee
usability.
</h2>
</div>
<ul class="tw-mt-12 tw-flex tw-flex-col tw-gap-10 tw-text-2xl tw-text-main tw-list-none tw-pl-0">
<li class="tw-flex tw-items-center">
<i class="bwi bwi-lg bwi-check-circle tw-mr-4 tw-flex-none"></i
><span class="tw-flex-auto"
>Instantly and securely share credentials with the groups and individuals who need them</span
>
</li>
<li class="tw-flex tw-items-center">
<i class="bwi bwi-lg bwi-check-circle tw-mr-4 tw-flex-none"></i
><span class="tw-flex-auto"
>Strengthen company-wide security through centralized administrative control and
policies</span
>
</li>
<li class="tw-flex tw-items-center">
<i class="bwi bwi-lg bwi-check-circle tw-mr-4 tw-flex-none"></i
><span class="tw-flex-auto"
>Streamline user onboarding and automate account provisioning with flexible SSO and SCIM
integrations</span
>
</li>
<li class="tw-flex tw-items-center">
<i class="bwi bwi-lg bwi-check-circle tw-mr-4 tw-flex-none"></i
><span class="tw-flex-auto"
>Migrate to Bitwarden in minutes with comprehensive import options</span
>
</li>
<li class="tw-flex tw-items-center">
<i class="bwi bwi-lg bwi-check-circle tw-mr-4 tw-flex-none"></i
><span class="tw-flex-auto"
>Give all Enterprise users the gift of 360º security with a free Families plan</span
>
</li>
</ul>
<div class="tw-mt-28 tw-flex tw-flex-col tw-items-center tw-gap-5">
<app-logo-badges></app-logo-badges>
</div>

View File

@@ -1,7 +0,0 @@
import { Component } from "@angular/core";
@Component({
selector: "app-enterprise2-content",
templateUrl: "enterprise2-content.component.html",
})
export class Enterprise2ContentComponent {}

View File

@@ -1,11 +0,0 @@
<figure>
<figcaption>
<cite>
<img
src="../../images/register-layout/vault-signup-badges.png"
class="tw-mx-auto tw-block tw-w-full"
alt="third party awards"
/>
</cite>
</figcaption>
</figure>

View File

@@ -1,7 +0,0 @@
import { Component } from "@angular/core";
@Component({
selector: "app-logo-badges",
templateUrl: "logo-badges.component.html",
})
export class LogoBadgesComponent {}

View File

@@ -1,23 +0,0 @@
<figure>
<div class="tw-flex tw-justify-center tw-gap-4 tw-text-[#eab308] tw-text-5xl">
<i class="bwi bwi-star-f"></i>
<i class="bwi bwi-star-f"></i>
<i class="bwi bwi-star-f"></i>
<i class="bwi bwi-star-f"></i>
<i class="bwi bwi-star-f"></i>
</div>
<blockquote class="tw-mx-auto tw-my-2 tw-max-w-xl tw-px-4 tw-text-center">
“Bitwarden scores points for being fully open-source, secure and audited annually by third-party
cybersecurity firms, giving it a level of transparency that sets it apart from its peers.”
</blockquote>
<figcaption>
<cite>
<img
src="../../images/register-layout/cnet-logo.svg"
class="tw-mx-auto tw-block tw-w-40"
alt="CNET Logo"
/>
</cite>
<p class="tw-text-center tw-font-bold -tw-translate-y-4">Best Password Manager in 2024</p>
</figcaption>
</figure>

View File

@@ -1,7 +0,0 @@
import { Component } from "@angular/core";
@Component({
selector: "app-logo-cnet-5-stars",
templateUrl: "logo-cnet-5-stars.component.html",
})
export class LogoCnet5StarsComponent {}

View File

@@ -1,15 +0,0 @@
<figure>
<figcaption>
<cite>
<img
src="../../images/register-layout/cnet-logo.svg"
class="tw-mx-auto tw-block tw-w-40"
alt="CNET Logo"
/>
</cite>
</figcaption>
<blockquote class="tw-mx-auto tw-mt-2 tw-max-w-xl tw-px-4 tw-text-center">
"No more excuses; start using Bitwarden today. The identity you save could be your own. The
money definitely will be."
</blockquote>
</figure>

View File

@@ -1,7 +0,0 @@
import { Component } from "@angular/core";
@Component({
selector: "app-logo-cnet",
templateUrl: "logo-cnet.component.html",
})
export class LogoCnetComponent {}

View File

@@ -1,28 +0,0 @@
<figure class="tw-text-center">
<p class="tw-mx-auto tw-my-2 tw-max-w-xl tw-px-4 tw-text-center">
Recommended by industry experts
</p>
<div class="tw-flex tw-flex-wrap tw-gap-8 tw-items-center tw-justify-center tw-mb-4">
<div class="tw-flex tw-gap-8">
<img src="../../images/register-layout/cnet-logo.svg" class="tw-w-32" alt="CNET Logo" />
<img
src="../../images/register-layout/wired-logo.png"
class="tw-w-32 tw-object-contain"
alt="WIRED Logo"
/>
</div>
<div class="tw-flex tw-gap-8">
<img
src="../../images/register-layout/new-york-times-logo.svg"
class="tw-w-32"
alt="New York Times Logo"
/>
<img src="../../images/register-layout/pcmag-logo.svg" class="tw-w-32" alt="PC Mag Logo" />
</div>
</div>
<blockquote>
&ldquo;Bitwarden is currently CNET's top pick for the best password manager, thanks in part to
its commitment to transparency and its unbeatable free tier.&rdquo;
</blockquote>
<p class="tw-font-bold">Best Password Manager in 2024</p>
</figure>

View File

@@ -1,7 +0,0 @@
import { Component } from "@angular/core";
@Component({
selector: "app-logo-company-testimonial",
templateUrl: "logo-company-testimonial.component.html",
})
export class LogoCompanyTestimonialComponent {}

View File

@@ -1,15 +0,0 @@
<figure>
<figcaption>
<cite>
<img
src="../../images/register-layout/forbes-logo.svg"
class="tw-mx-auto tw-block tw-w-40"
alt="Forbes Logo"
/>
</cite>
</figcaption>
<blockquote class="tw-mx-auto tw-mt-2 tw-max-w-xl tw-px-4 tw-text-center">
“Bitwarden boasts the backing of some of the world's best security experts and an attractive,
easy-to-use interface”
</blockquote>
</figure>

View File

@@ -1,7 +0,0 @@
import { Component } from "@angular/core";
@Component({
selector: "app-logo-forbes",
templateUrl: "logo-forbes.component.html",
})
export class LogoForbesComponent {}

View File

@@ -1,5 +0,0 @@
<img
src="../../images/register-layout/usnews-360-badge.svg"
class="tw-mx-auto tw-block tw-w-48"
alt="US News 360 Reviews Best Password Manager"
/>

View File

@@ -1,7 +0,0 @@
import { Component } from "@angular/core";
@Component({
selector: "app-logo-us-news",
templateUrl: "logo-us-news.component.html",
})
export class LogoUSNewsComponent {}

View File

@@ -1,13 +0,0 @@
<figure>
<h2 class="tw-mx-auto tw-pb-2 tw-max-w-xl tw-font-semibold tw-text-center">
{{ header }}
</h2>
<blockquote class="tw-mx-auto tw-my-2 tw-max-w-xl tw-px-4 tw-text-center">
"{{ quote }}"
</blockquote>
<figcaption>
<cite>
<p class="tw-mx-auto tw-text-center tw-font-bold">{{ source }}</p>
</cite>
</figcaption>
</figure>

View File

@@ -1,13 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, Input } from "@angular/core";
@Component({
selector: "app-review-blurb",
templateUrl: "review-blurb.component.html",
})
export class ReviewBlurbComponent {
@Input() header: string;
@Input() quote: string;
@Input() source: string;
}

View File

@@ -1,18 +0,0 @@
<div class="tw-flex tw-flex-col tw-items-center tw-justify-center">
<img class="tw-mb-2" [ngClass]="logoClass" [src]="logoSrc" [alt]="logoAlt" />
<div class="tw-flex tw-items-center">
<div class="tw-flex tw-items-center tw-justify-center tw-text-[#eab308] tw-text-2xl">
<i class="bwi bwi-star-f"></i>
<i class="bwi bwi-star-f"></i>
<i class="bwi bwi-star-f"></i>
<i class="bwi bwi-star-f"></i>
<div class="tw-relative">
<div class="tw-absolute tw-inset-0 tw-w-3 tw-overflow-hidden">
<i class="bwi bwi-star-f"></i>
</div>
<i class="bwi bwi-star-f tw-text-[#cbd5e1]"></i>
</div>
</div>
<span class="tw-ml-2">4.7</span>
</div>
</div>

View File

@@ -1,13 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, Input } from "@angular/core";
@Component({
selector: "review-logo",
templateUrl: "review-logo.component.html",
})
export class ReviewLogoComponent {
@Input() logoClass: string;
@Input() logoSrc: string;
@Input() logoAlt: string;
}

View File

@@ -1,30 +0,0 @@
<h1 class="tw-text-4xl !tw-text-alt2">{{ header }}</h1>
<div class="tw-pt-16">
<h2 class="tw-text-2xl tw-font-semibold">
{{ headline }}
</h2>
</div>
<ul class="tw-mt-12 tw-flex tw-flex-col tw-gap-10 tw-text-2xl tw-text-main">
<li *ngFor="let primaryPoint of primaryPoints">
{{ primaryPoint }}
</li>
</ul>
<div class="tw-mt-12 tw-flex tw-flex-col">
<div class="tw-rounded-[32px] tw-bg-background">
<div class="tw-my-8 tw-mx-6">
<h2 class="tw-pl-5 tw-font-semibold">{{ calloutHeadline }}</h2>
<ul class="tw-space-y-4 tw-mt-4 tw-pl-10">
<li *ngFor="let callout of callouts">
{{ callout }}
</li>
</ul>
</div>
</div>
</div>
<div class="tw-mt-12 tw-flex tw-flex-col tw-items-center tw-gap-5">
<app-review-blurb
header="Businesses trust Bitwarden to secure their infrastructure"
quote="At this point, it would be almost impossible to leak our secrets. It's just one less thing we have to worry about."
source="Titanom Technologies"
></app-review-blurb>
</div>

View File

@@ -1,80 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, OnDestroy, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { Subject, takeUntil } from "rxjs";
@Component({
selector: "app-secrets-manager-content",
templateUrl: "secrets-manager-content.component.html",
})
export class SecretsManagerContentComponent implements OnInit, OnDestroy {
header: string;
headline =
"A simpler, faster way to secure and automate secrets across code and infrastructure deployments";
primaryPoints: string[];
calloutHeadline: string;
callouts: string[];
private paidPrimaryPoints = [
"Unlimited secrets, users, and projects",
"Simple and transparent pricing",
"Zero-knowledge, end-to-end encryption",
];
private paidCalloutHeadline = "Limited time offer";
private paidCallouts = [
"Sign up today and receive a complimentary 12-month subscription to Bitwarden Password Manager",
"Experience complete security across your organization",
"Secure all your sensitive credentials, from user applications to machine secrets",
];
private freePrimaryPoints = [
"Unlimited secrets",
"Simple and transparent pricing",
"Zero-knowledge, end-to-end encryption",
];
private freeCalloutHeadline = "Go beyond developer security!";
private freeCallouts = [
"Your Bitwarden account will also grant complimentary access to Bitwarden Password Manager",
"Extend end-to-end encryption to your personal passwords, addresses, credit cards and notes",
];
private destroy$ = new Subject<void>();
constructor(private activatedRoute: ActivatedRoute) {}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
ngOnInit(): void {
this.activatedRoute.queryParams.pipe(takeUntil(this.destroy$)).subscribe((queryParameters) => {
switch (queryParameters.org) {
case "enterprise":
this.header = "Secrets Manager for Enterprise";
this.primaryPoints = this.paidPrimaryPoints;
this.calloutHeadline = this.paidCalloutHeadline;
this.callouts = this.paidCallouts;
break;
case "free":
this.header = "Bitwarden Secrets Manager";
this.primaryPoints = this.freePrimaryPoints;
this.calloutHeadline = this.freeCalloutHeadline;
this.callouts = this.freeCallouts;
break;
case "teams":
case "teamsStarter":
this.header = "Secrets Manager for Teams";
this.primaryPoints = this.paidPrimaryPoints;
this.calloutHeadline = this.paidCalloutHeadline;
this.callouts = this.paidCallouts;
break;
}
});
}
}

View File

@@ -1,17 +0,0 @@
<h1 class="tw-text-4xl !tw-text-alt2">The Bitwarden Password Manager</h1>
<div class="tw-pt-32">
<h2 class="tw-text-2xl">
Trusted by millions of individuals, teams, and organizations worldwide for secure password
storage and sharing.
</h2>
</div>
<ul class="tw-mt-12 tw-flex tw-flex-col tw-gap-10 tw-text-2xl tw-text-main">
<li>Store logins, secure notes, and more</li>
<li>Collaborate and share securely</li>
<li>Access anywhere on any device</li>
<li>Create your account to get started</li>
</ul>
<div class="tw-mt-28 tw-flex tw-flex-col tw-items-center tw-gap-5">
<app-logo-forbes></app-logo-forbes>
<app-logo-us-news></app-logo-us-news>
</div>

View File

@@ -1,7 +0,0 @@
import { Component } from "@angular/core";
@Component({
selector: "app-teams-content",
templateUrl: "teams-content.component.html",
})
export class TeamsContentComponent {}

View File

@@ -1,35 +0,0 @@
<h1 class="tw-text-3xl !tw-text-alt2">Start your 7-day free trial for Teams</h1>
<div class="tw-pt-20">
<h2 class="tw-text-2xl">
Strengthen business security with an easy-to-use password manager your team will love.
</h2>
</div>
<ul class="tw-mt-12 tw-flex tw-flex-col tw-gap-10 tw-text-2xl tw-text-main tw-list-none tw-pl-0">
<li class="tw-flex tw-items-center">
<i class="bwi bwi-lg bwi-check-circle tw-mr-4 tw-flex-none"></i
><span class="tw-flex-auto"
>Instantly and securely share credentials with the groups and individuals who need them</span
>
</li>
<li class="tw-flex tw-items-center">
<i class="bwi bwi-lg bwi-check-circle tw-mr-4 tw-flex-none"></i
><span class="tw-flex-auto"
>Migrate to Bitwarden in minutes with comprehensive import options</span
>
</li>
<li class="tw-flex tw-items-center">
<i class="bwi bwi-lg bwi-check-circle tw-mr-4 tw-flex-none"></i
><span class="tw-flex-auto"
>Save time and increase productivity with autofill and instant device syncing</span
>
</li>
<li class="tw-flex tw-items-center">
<i class="bwi bwi-lg bwi-check-circle tw-mr-4 tw-flex-none"></i
><span class="tw-flex-auto"
>Enhance security practices across your team with easy user management</span
>
</li>
</ul>
<div class="tw-mt-28 tw-flex tw-flex-col tw-items-center tw-gap-5">
<app-logo-badges></app-logo-badges>
</div>

View File

@@ -1,7 +0,0 @@
import { Component } from "@angular/core";
@Component({
selector: "app-teams1-content",
templateUrl: "teams1-content.component.html",
})
export class Teams1ContentComponent {}

View File

@@ -1,35 +0,0 @@
<h1 class="tw-text-3xl !tw-text-alt2">Start your 7-day free trial for Teams</h1>
<div class="tw-pt-20">
<h2 class="tw-text-2xl">
Strengthen business security with an easy-to-use password manager your team will love.
</h2>
</div>
<ul class="tw-mt-12 tw-flex tw-flex-col tw-gap-10 tw-text-2xl tw-text-main tw-list-none tw-pl-0">
<li class="tw-flex tw-items-center">
<i class="bwi bwi-lg bwi-check-circle tw-mr-4 tw-flex-none"></i
><span class="tw-flex-auto"
>Instantly and securely share credentials with the groups and individuals who need them</span
>
</li>
<li class="tw-flex tw-items-center">
<i class="bwi bwi-lg bwi-check-circle tw-mr-4 tw-flex-none"></i
><span class="tw-flex-auto"
>Migrate to Bitwarden in minutes with comprehensive import options</span
>
</li>
<li class="tw-flex tw-items-center">
<i class="bwi bwi-lg bwi-check-circle tw-mr-4 tw-flex-none"></i
><span class="tw-flex-auto"
>Save time and increase productivity with autofill and instant device syncing</span
>
</li>
<li class="tw-flex tw-items-center">
<i class="bwi bwi-lg bwi-check-circle tw-mr-4 tw-flex-none"></i
><span class="tw-flex-auto"
>Enhance security practices across your team with easy user management</span
>
</li>
</ul>
<div class="tw-mt-28 tw-flex tw-flex-col tw-items-center tw-gap-5">
<app-logo-badges></app-logo-badges>
</div>

View File

@@ -1,7 +0,0 @@
import { Component } from "@angular/core";
@Component({
selector: "app-teams2-content",
templateUrl: "teams2-content.component.html",
})
export class Teams2ContentComponent {}

View File

@@ -1,26 +0,0 @@
<h1 class="tw-text-4xl !tw-text-alt2">Begin Teams Starter Free Trial Now</h1>
<div class="tw-pt-32">
<h2 class="tw-text-2xl">
Millions of individuals, teams, and organizations worldwide trust Bitwarden for secure password
storage and sharing.
</h2>
</div>
<ul class="tw-mt-12 tw-flex tw-flex-col tw-gap-10 tw-text-2xl tw-text-main">
<li>
Powerful security for up to 10 users
<div class="tw-mt-2 tw-text-base">
Have more than 10 users?
<a routerLink="/register" [queryParams]="{ org: 'teams', layout: 'teams1' }"
>Start a Teams trial</a
>
</div>
</li>
<li>Collaborate and share securely</li>
<li>Deploy and manage quickly and easily</li>
<li>Access anywhere on any device</li>
<li>Create your account to get started</li>
</ul>
<div class="tw-mt-28 tw-flex tw-flex-col tw-items-center tw-gap-5">
<app-logo-forbes></app-logo-forbes>
<app-logo-us-news></app-logo-us-news>
</div>

View File

@@ -1,7 +0,0 @@
import { Component } from "@angular/core";
@Component({
selector: "app-teams3-content",
templateUrl: "teams3-content.component.html",
})
export class Teams3ContentComponent {}

View File

@@ -1,45 +0,0 @@
<app-vertical-stepper #stepper linear>
<app-vertical-step
label="{{ 'organizationInformation' | i18n | titlecase }}"
[subLabel]="subLabels.organizationInfo"
>
<app-org-info [nameOnly]="true" [formGroup]="formGroup"> </app-org-info>
<button
type="button"
bitButton
buttonType="primary"
[disabled]="formGroup.get('name').invalid"
(click)="createOrganization()"
>
{{ "next" | i18n }}
</button>
</app-vertical-step>
<app-vertical-step label="{{ 'confirmationDetails' | i18n | titlecase }}">
<div class="tw-pb-6 tw-pl-6">
<p class="tw-text-xl">{{ "smFreeTrialThankYou" | i18n }}</p>
<ul class="tw-list-disc">
<li>
<p>
{{ "smFreeTrialConfirmationEmail" | i18n }}
<span class="tw-font-bold">{{ formGroup.get("email").value }}</span
>.
</p>
</li>
</ul>
</div>
<div class="tw-mb-3 tw-flex">
<button type="button" bitButton buttonType="primary" (click)="navigateToSecretsManager()">
{{ "getStarted" | i18n | titlecase }}
</button>
<button
type="button"
bitButton
buttonType="secondary"
(click)="navigateToMembers()"
class="tw-ml-3 tw-inline-flex tw-items-center tw-px-3"
>
{{ "inviteUsers" | i18n }}
</button>
</div>
</app-vertical-step>
</app-vertical-stepper>

View File

@@ -1,90 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, OnInit, ViewChild } from "@angular/core";
import { UntypedFormBuilder, Validators } from "@angular/forms";
import { Router } from "@angular/router";
import { OrganizationBillingServiceAbstraction as OrganizationBillingService } from "@bitwarden/common/billing/abstractions/organization-billing.service";
import { PlanType } from "@bitwarden/common/billing/enums";
import { ReferenceEventRequest } from "@bitwarden/common/models/request/reference-event.request";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { VerticalStepperComponent } from "../../trial-initiation/vertical-stepper/vertical-stepper.component";
@Component({
selector: "app-secrets-manager-trial-free-stepper",
templateUrl: "secrets-manager-trial-free-stepper.component.html",
})
export class SecretsManagerTrialFreeStepperComponent implements OnInit {
@ViewChild("stepper", { static: false }) verticalStepper: VerticalStepperComponent;
formGroup = this.formBuilder.group({
name: [
"",
{
validators: [Validators.required, Validators.maxLength(50)],
updateOn: "change",
},
],
email: [
"",
{
validators: [Validators.email],
},
],
});
subLabels = {
createAccount:
"Before creating your free organization, you first need to log in or create a personal account.",
organizationInfo: "Enter your organization information",
};
organizationId: string;
referenceEventRequest: ReferenceEventRequest;
constructor(
protected formBuilder: UntypedFormBuilder,
protected i18nService: I18nService,
protected organizationBillingService: OrganizationBillingService,
protected router: Router,
) {}
ngOnInit(): void {
this.referenceEventRequest = new ReferenceEventRequest();
this.referenceEventRequest.initiationPath = "Secrets Manager trial from marketing website";
}
accountCreated(email: string): void {
this.formGroup.get("email")?.setValue(email);
this.subLabels.createAccount = email;
this.verticalStepper.next();
}
async createOrganization(): Promise<void> {
const response = await this.organizationBillingService.startFree({
organization: {
name: this.formGroup.get("name").value,
billingEmail: this.formGroup.get("email").value,
},
plan: {
type: PlanType.Free,
subscribeToSecretsManager: true,
isFromSecretsManagerTrial: true,
},
});
this.organizationId = response.id;
this.subLabels.organizationInfo = response.name;
this.verticalStepper.next();
}
async navigateToMembers(): Promise<void> {
await this.router.navigate(["organizations", this.organizationId, "members"]);
}
async navigateToSecretsManager(): Promise<void> {
await this.router.navigate(["sm", this.organizationId]);
}
}

View File

@@ -1,67 +0,0 @@
<app-vertical-stepper #stepper linear>
<app-vertical-step
label="{{ 'organizationInformation' | i18n | titlecase }}"
[subLabel]="subLabels.organizationInfo"
>
<app-org-info [nameOnly]="true" [formGroup]="formGroup"></app-org-info>
<button
type="button"
bitButton
buttonType="primary"
[disabled]="formGroup.get('name').invalid"
[loading]="createOrganizationLoading"
(click)="createOrganizationOnTrial()"
*ngIf="enableTrialPayment$ | async"
>
{{ "startTrial" | i18n }}
</button>
<button
type="button"
bitButton
buttonType="primary"
[disabled]="formGroup.get('name').invalid"
[loading]="createOrganizationLoading"
cdkStepperNext
*ngIf="!(enableTrialPayment$ | async)"
>
{{ "next" | i18n }}
</button>
</app-vertical-step>
<app-vertical-step
label="{{ 'billing' | i18n | titlecase }}"
[subLabel]="billingSubLabel"
*ngIf="!(enableTrialPayment$ | async)"
>
<app-trial-billing-step
*ngIf="stepper.selectedIndex === 2"
[organizationInfo]="{
name: formGroup.get('name').value,
email: formGroup.get('email').value,
type: productType,
}"
[subscriptionProduct]="SubscriptionProduct.SecretsManager"
(steppedBack)="steppedBack()"
(organizationCreated)="organizationCreated($event)"
></app-trial-billing-step>
</app-vertical-step>
<app-vertical-step label="{{ 'confirmationDetails' | i18n | titlecase }}">
<app-trial-confirmation-details
[email]="formGroup.get('email').value"
[orgLabel]="organizationTypeQueryParameter"
></app-trial-confirmation-details>
<div class="tw-mb-3 tw-flex">
<button type="button" bitButton buttonType="primary" (click)="navigateToSecretsManager()">
{{ "getStarted" | i18n | titlecase }}
</button>
<button
type="button"
bitButton
buttonType="secondary"
(click)="navigateToMembers()"
class="tw-ml-3 tw-inline-flex tw-items-center tw-px-3"
>
{{ "inviteUsers" | i18n }}
</button>
</div>
</app-vertical-step>
</app-vertical-stepper>

View File

@@ -1,144 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, Input, OnInit, ViewChild } from "@angular/core";
import { UntypedFormBuilder } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router";
import { Subject, takeUntil } from "rxjs";
import { OrganizationBillingServiceAbstraction as OrganizationBillingService } from "@bitwarden/common/billing/abstractions/organization-billing.service";
import { PlanType, ProductTierType } from "@bitwarden/common/billing/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ReferenceEventRequest } from "@bitwarden/common/models/request/reference-event.request";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import {
OrganizationCreatedEvent,
SubscriptionProduct,
TrialOrganizationType,
} from "../../../billing/accounts/trial-initiation/trial-billing-step.component";
import { VerticalStepperComponent } from "../../trial-initiation/vertical-stepper/vertical-stepper.component";
import { SecretsManagerTrialFreeStepperComponent } from "../secrets-manager/secrets-manager-trial-free-stepper.component";
export enum ValidOrgParams {
families = "families",
enterprise = "enterprise",
teams = "teams",
teamsStarter = "teamsStarter",
individual = "individual",
premium = "premium",
free = "free",
}
const trialFlowOrgs = [
ValidOrgParams.teams,
ValidOrgParams.teamsStarter,
ValidOrgParams.enterprise,
ValidOrgParams.families,
];
@Component({
selector: "app-secrets-manager-trial-paid-stepper",
templateUrl: "secrets-manager-trial-paid-stepper.component.html",
})
export class SecretsManagerTrialPaidStepperComponent
extends SecretsManagerTrialFreeStepperComponent
implements OnInit
{
@ViewChild("stepper", { static: false }) verticalStepper: VerticalStepperComponent;
@Input() organizationTypeQueryParameter: string;
plan: PlanType;
createOrganizationLoading = false;
billingSubLabel = this.i18nService.t("billingTrialSubLabel");
organizationId: string;
private destroy$ = new Subject<void>();
protected enableTrialPayment$ = this.configService.getFeatureFlag$(
FeatureFlag.TrialPaymentOptional,
);
constructor(
private route: ActivatedRoute,
private configService: ConfigService,
protected formBuilder: UntypedFormBuilder,
protected i18nService: I18nService,
protected organizationBillingService: OrganizationBillingService,
protected router: Router,
) {
super(formBuilder, i18nService, organizationBillingService, router);
}
async ngOnInit(): Promise<void> {
this.referenceEventRequest = new ReferenceEventRequest();
this.referenceEventRequest.initiationPath = "Secrets Manager trial from marketing website";
this.route.queryParams.pipe(takeUntil(this.destroy$)).subscribe((qParams) => {
if (trialFlowOrgs.includes(qParams.org)) {
if (qParams.org === ValidOrgParams.teamsStarter) {
this.plan = PlanType.TeamsStarter;
} else if (qParams.org === ValidOrgParams.teams) {
this.plan = PlanType.TeamsAnnually;
} else if (qParams.org === ValidOrgParams.enterprise) {
this.plan = PlanType.EnterpriseAnnually;
}
}
});
}
organizationCreated(event: OrganizationCreatedEvent) {
this.organizationId = event.organizationId;
this.billingSubLabel = event.planDescription;
this.verticalStepper.next();
}
steppedBack() {
this.verticalStepper.previous();
}
async createOrganizationOnTrial(): Promise<void> {
this.createOrganizationLoading = true;
const response = await this.organizationBillingService.purchaseSubscriptionNoPaymentMethod({
organization: {
name: this.formGroup.get("name").value,
billingEmail: this.formGroup.get("email").value,
initiationPath: "Secrets Manager trial from marketing website",
},
plan: {
type: this.plan,
subscribeToSecretsManager: true,
isFromSecretsManagerTrial: true,
passwordManagerSeats: 1,
secretsManagerSeats: 1,
},
});
this.organizationId = response?.id;
this.subLabels.organizationInfo = response?.name;
this.createOrganizationLoading = false;
this.verticalStepper.next();
}
get createAccountLabel() {
const organizationType =
this.productType === ProductTierType.TeamsStarter
? "Teams Starter"
: ProductTierType[this.productType];
return `Before creating your ${organizationType} organization, you first need to log in or create a personal account.`;
}
get productType(): TrialOrganizationType {
switch (this.organizationTypeQueryParameter) {
case "enterprise":
return ProductTierType.Enterprise;
case "families":
return ProductTierType.Families;
case "teams":
return ProductTierType.Teams;
case "teamsStarter":
return ProductTierType.TeamsStarter;
}
}
protected readonly SubscriptionProduct = SubscriptionProduct;
}

View File

@@ -1,44 +0,0 @@
<!-- eslint-disable tailwindcss/no-custom-classname -->
<ng-container>
<div class="tw-absolute tw--z-10 tw--mt-48 tw-h-[28rem] tw-w-full tw-bg-background-alt2"></div>
<div class="tw-min-w-4xl tw-mx-auto tw-flex tw-max-w-screen-xl tw-gap-12 tw-px-4">
<div class="tw-w-1/2">
<img
alt="Bitwarden"
style="height: 50px; width: 335px"
class="tw-mt-6"
src="../../../../images/register-layout/logo-horizontal-white.svg"
/>
<div class="tw-pt-12">
<app-secrets-manager-content></app-secrets-manager-content>
</div>
</div>
<div class="tw-w-1/2">
<div class="tw-pt-44">
<div class="tw-rounded tw-border tw-border-solid tw-border-secondary-300 tw-bg-background">
<div
*ngIf="!freeOrganization"
class="tw-flex tw-h-auto tw-w-full tw-gap-5 tw-rounded-t tw-bg-secondary-100"
>
<h2 class="tw-pb-4 tw-pl-4 tw-pt-5 tw-text-base tw-font-bold tw-uppercase">
{{
"startYour7DayFreeTrialOfBitwardenSecretsManagerFor"
| i18n: organizationTypeQueryParameter
}}
</h2>
<environment-selector
class="tw-mr-4 tw-mt-6 tw-flex-shrink-0 tw-text-end"
></environment-selector>
</div>
<app-secrets-manager-trial-free-stepper
*ngIf="freeOrganization"
></app-secrets-manager-trial-free-stepper>
<app-secrets-manager-trial-paid-stepper
*ngIf="!freeOrganization"
[organizationTypeQueryParameter]="organizationTypeQueryParameter"
></app-secrets-manager-trial-paid-stepper>
</div>
</div>
</div>
</div>
</ng-container>

View File

@@ -1,32 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, OnDestroy, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { Subject, takeUntil } from "rxjs";
@Component({
selector: "app-secrets-manager-trial",
templateUrl: "secrets-manager-trial.component.html",
})
export class SecretsManagerTrialComponent implements OnInit, OnDestroy {
organizationTypeQueryParameter: string;
private destroy$ = new Subject<void>();
constructor(private route: ActivatedRoute) {}
ngOnInit(): void {
this.route.queryParams.pipe(takeUntil(this.destroy$)).subscribe((queryParameters) => {
this.organizationTypeQueryParameter = queryParameters.org;
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
get freeOrganization() {
return this.organizationTypeQueryParameter === "free";
}
}

View File

@@ -7,36 +7,10 @@ import { FormFieldModule } from "@bitwarden/components";
import { OrganizationCreateModule } from "../../admin-console/organizations/create/organization-create.module";
import { TrialBillingStepComponent } from "../../billing/accounts/trial-initiation/trial-billing-step.component";
import { SecretsManagerTrialFreeStepperComponent } from "../../billing/trial-initiation/secrets-manager/secrets-manager-trial-free-stepper.component";
import { SecretsManagerTrialPaidStepperComponent } from "../../billing/trial-initiation/secrets-manager/secrets-manager-trial-paid-stepper.component";
import { SecretsManagerTrialComponent } from "../../billing/trial-initiation/secrets-manager/secrets-manager-trial.component";
import { EnvironmentSelectorModule } from "../../components/environment-selector/environment-selector.module";
import { SharedModule } from "../../shared";
import { CompleteTrialInitiationComponent } from "./complete-trial-initiation/complete-trial-initiation.component";
import { ConfirmationDetailsComponent } from "./confirmation-details.component";
import { AbmEnterpriseContentComponent } from "./content/abm-enterprise-content.component";
import { AbmTeamsContentComponent } from "./content/abm-teams-content.component";
import { CnetEnterpriseContentComponent } from "./content/cnet-enterprise-content.component";
import { CnetIndividualContentComponent } from "./content/cnet-individual-content.component";
import { CnetTeamsContentComponent } from "./content/cnet-teams-content.component";
import { DefaultContentComponent } from "./content/default-content.component";
import { EnterpriseContentComponent } from "./content/enterprise-content.component";
import { Enterprise1ContentComponent } from "./content/enterprise1-content.component";
import { Enterprise2ContentComponent } from "./content/enterprise2-content.component";
import { LogoBadgesComponent } from "./content/logo-badges.component";
import { LogoCnet5StarsComponent } from "./content/logo-cnet-5-stars.component";
import { LogoCnetComponent } from "./content/logo-cnet.component";
import { LogoCompanyTestimonialComponent } from "./content/logo-company-testimonial.component";
import { LogoForbesComponent } from "./content/logo-forbes.component";
import { LogoUSNewsComponent } from "./content/logo-us-news.component";
import { ReviewBlurbComponent } from "./content/review-blurb.component";
import { ReviewLogoComponent } from "./content/review-logo.component";
import { SecretsManagerContentComponent } from "./content/secrets-manager-content.component";
import { TeamsContentComponent } from "./content/teams-content.component";
import { Teams1ContentComponent } from "./content/teams1-content.component";
import { Teams2ContentComponent } from "./content/teams2-content.component";
import { Teams3ContentComponent } from "./content/teams3-content.component";
import { VerticalStepperModule } from "./vertical-stepper/vertical-stepper.module";
@NgModule({
@@ -46,41 +20,10 @@ import { VerticalStepperModule } from "./vertical-stepper/vertical-stepper.modul
VerticalStepperModule,
FormFieldModule,
OrganizationCreateModule,
EnvironmentSelectorModule,
TrialBillingStepComponent,
InputPasswordComponent,
],
declarations: [
CompleteTrialInitiationComponent,
EnterpriseContentComponent,
TeamsContentComponent,
ConfirmationDetailsComponent,
DefaultContentComponent,
EnterpriseContentComponent,
Enterprise1ContentComponent,
Enterprise2ContentComponent,
TeamsContentComponent,
Teams1ContentComponent,
Teams2ContentComponent,
Teams3ContentComponent,
CnetEnterpriseContentComponent,
CnetIndividualContentComponent,
CnetTeamsContentComponent,
AbmEnterpriseContentComponent,
AbmTeamsContentComponent,
LogoBadgesComponent,
LogoCnet5StarsComponent,
LogoCompanyTestimonialComponent,
LogoCnetComponent,
LogoForbesComponent,
LogoUSNewsComponent,
ReviewLogoComponent,
SecretsManagerContentComponent,
ReviewBlurbComponent,
SecretsManagerTrialComponent,
SecretsManagerTrialFreeStepperComponent,
SecretsManagerTrialPaidStepperComponent,
],
declarations: [CompleteTrialInitiationComponent, ConfirmationDetailsComponent],
exports: [CompleteTrialInitiationComponent],
providers: [TitleCasePipe],
})

View File

@@ -7,6 +7,7 @@ import { firstValueFrom, Subject, switchMap } from "rxjs";
import { map } from "rxjs/operators";
import { CollectionView } from "@bitwarden/admin-console/common";
import { VaultViewPasswordHistoryService } from "@bitwarden/angular/services/view-password-history.service";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
@@ -39,6 +40,9 @@ import {
ToastService,
} from "@bitwarden/components";
import {
AttachmentDialogCloseResult,
AttachmentDialogResult,
AttachmentsV2Component,
ChangeLoginPasswordService,
CipherFormComponent,
CipherFormConfig,
@@ -50,16 +54,10 @@ import {
} from "@bitwarden/vault";
import { SharedModule } from "../../../shared/shared.module";
import {
AttachmentDialogCloseResult,
AttachmentDialogResult,
AttachmentsV2Component,
} from "../../individual-vault/attachments-v2.component";
import { WebVaultPremiumUpgradePromptService } from "../../../vault/services/web-premium-upgrade-prompt.service";
import { RoutedVaultFilterService } from "../../individual-vault/vault-filter/services/routed-vault-filter.service";
import { RoutedVaultFilterModel } from "../../individual-vault/vault-filter/shared/models/routed-vault-filter.model";
import { WebCipherFormGenerationService } from "../../services/web-cipher-form-generation.service";
import { WebVaultPremiumUpgradePromptService } from "../../services/web-premium-upgrade-prompt.service";
import { WebViewPasswordHistoryService } from "../../services/web-view-password-history.service";
export type VaultItemDialogMode = "view" | "form";
@@ -135,7 +133,7 @@ export enum VaultItemDialogResult {
],
providers: [
{ provide: PremiumUpgradePromptService, useClass: WebVaultPremiumUpgradePromptService },
{ provide: ViewPasswordHistoryService, useClass: WebViewPasswordHistoryService },
{ provide: ViewPasswordHistoryService, useClass: VaultViewPasswordHistoryService },
{ provide: CipherFormGenerationService, useClass: WebCipherFormGenerationService },
RoutedVaultFilterService,
{ provide: ChangeLoginPasswordService, useClass: DefaultChangeLoginPasswordService },

View File

@@ -21,6 +21,7 @@ import {
ItemModule,
} from "@bitwarden/components";
import {
AttachmentsV2Component,
CipherAttachmentsComponent,
CipherFormConfig,
CipherFormGenerationService,
@@ -31,8 +32,6 @@ import {
import { SharedModule } from "../../shared/shared.module";
import { WebCipherFormGenerationService } from "../services/web-cipher-form-generation.service";
import { AttachmentsV2Component } from "./attachments-v2.component";
/**
* The result of the AddEditCipherDialogV2 component.
*/

View File

@@ -69,6 +69,9 @@ import { DialogRef, DialogService, Icons, ToastService } from "@bitwarden/compon
import {
AddEditFolderDialogComponent,
AddEditFolderDialogResult,
AttachmentDialogCloseResult,
AttachmentDialogResult,
AttachmentsV2Component,
CipherFormConfig,
CollectionAssignmentResult,
DecryptionFailureDialogComponent,
@@ -96,11 +99,6 @@ import { VaultItem } from "../components/vault-items/vault-item";
import { VaultItemEvent } from "../components/vault-items/vault-item-event";
import { VaultItemsModule } from "../components/vault-items/vault-items.module";
import {
AttachmentDialogCloseResult,
AttachmentDialogResult,
AttachmentsV2Component,
} from "./attachments-v2.component";
import {
BulkDeleteDialogResult,
openBulkDeleteDialog,

View File

@@ -5,6 +5,7 @@ import { Component, EventEmitter, Inject, OnInit } from "@angular/core";
import { firstValueFrom, map, Observable } from "rxjs";
import { CollectionView } from "@bitwarden/admin-console/common";
import { VaultViewPasswordHistoryService } from "@bitwarden/angular/services/view-password-history.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
@@ -21,8 +22,8 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
import {
DIALOG_DATA,
DialogConfig,
DialogRef,
DialogConfig,
AsyncActionsModule,
DialogModule,
DialogService,
@@ -31,8 +32,7 @@ import {
import { CipherViewComponent } from "@bitwarden/vault";
import { SharedModule } from "../../shared/shared.module";
import { WebVaultPremiumUpgradePromptService } from "../services/web-premium-upgrade-prompt.service";
import { WebViewPasswordHistoryService } from "../services/web-view-password-history.service";
import { WebVaultPremiumUpgradePromptService } from "../../vault/services/web-premium-upgrade-prompt.service";
export interface ViewCipherDialogParams {
cipher: CipherView;
@@ -74,7 +74,7 @@ export interface ViewCipherDialogCloseResult {
standalone: true,
imports: [CipherViewComponent, CommonModule, AsyncActionsModule, DialogModule, SharedModule],
providers: [
{ provide: ViewPasswordHistoryService, useClass: WebViewPasswordHistoryService },
{ provide: ViewPasswordHistoryService, useClass: VaultViewPasswordHistoryService },
{ provide: PremiumUpgradePromptService, useClass: WebVaultPremiumUpgradePromptService },
],
})

View File

@@ -86,7 +86,7 @@ document.addEventListener("DOMContentLoaded", async () => {
titleForLargerScreens.innerText = localeService.t("verifyYourIdentity");
const subtitle = document.getElementById("subtitle");
subtitle.innerText = localeService.t("followTheStepsBelowToFinishLoggingIn");
subtitle.innerText = localeService.t("followTheStepsBelowToFinishLoggingInWithSecurityKey");
});
function start() {

View File

@@ -7280,6 +7280,9 @@
"followTheStepsBelowToFinishLoggingIn": {
"message": "Follow the steps below to finish logging in."
},
"followTheStepsBelowToFinishLoggingInWithSecurityKey": {
"message": "Follow the steps below to finish logging in with your security key."
},
"launchDuo": {
"message": "Launch Duo"
},

View File

@@ -3,17 +3,16 @@ import { TestBed } from "@angular/core/testing";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { DialogService } from "@bitwarden/components";
import { openPasswordHistoryDialog } from "@bitwarden/vault";
import { openPasswordHistoryDialog } from "../individual-vault/password-history.component";
import { VaultViewPasswordHistoryService } from "./view-password-history.service";
import { WebViewPasswordHistoryService } from "./web-view-password-history.service";
jest.mock("../individual-vault/password-history.component", () => ({
jest.mock("@bitwarden/vault", () => ({
openPasswordHistoryDialog: jest.fn(),
}));
describe("WebViewPasswordHistoryService", () => {
let service: WebViewPasswordHistoryService;
describe("VaultViewPasswordHistoryService", () => {
let service: VaultViewPasswordHistoryService;
let dialogService: DialogService;
beforeEach(async () => {
@@ -23,13 +22,13 @@ describe("WebViewPasswordHistoryService", () => {
await TestBed.configureTestingModule({
providers: [
WebViewPasswordHistoryService,
VaultViewPasswordHistoryService,
{ provide: DialogService, useValue: mockDialogService },
Overlay,
],
}).compileComponents();
service = TestBed.inject(WebViewPasswordHistoryService);
service = TestBed.inject(VaultViewPasswordHistoryService);
dialogService = TestBed.inject(DialogService);
});

View File

@@ -3,14 +3,13 @@ import { Injectable } from "@angular/core";
import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { DialogService } from "@bitwarden/components";
import { openPasswordHistoryDialog } from "../individual-vault/password-history.component";
import { openPasswordHistoryDialog } from "@bitwarden/vault";
/**
* This service is used to display the password history dialog in the web vault.
* This service is used to display the password history dialog in the vault.
*/
@Injectable()
export class WebViewPasswordHistoryService implements ViewPasswordHistoryService {
export class VaultViewPasswordHistoryService implements ViewPasswordHistoryService {
constructor(private dialogService: DialogService) {}
/**

View File

@@ -18,6 +18,7 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@Directive()
@@ -25,13 +26,14 @@ export class VaultItemsComponent implements OnInit, OnDestroy {
@Input() activeCipherId: string = null;
@Output() onCipherClicked = new EventEmitter<CipherView>();
@Output() onCipherRightClicked = new EventEmitter<CipherView>();
@Output() onAddCipher = new EventEmitter();
@Output() onAddCipher = new EventEmitter<CipherType | undefined>();
@Output() onAddCipherOptions = new EventEmitter();
loaded = false;
ciphers: CipherView[] = [];
deleted = false;
organization: Organization;
CipherType = CipherType;
protected searchPending = false;
@@ -109,8 +111,8 @@ export class VaultItemsComponent implements OnInit, OnDestroy {
this.onCipherRightClicked.emit(cipher);
}
addCipher() {
this.onAddCipher.emit();
addCipher(type?: CipherType) {
this.onAddCipher.emit(type);
}
addCipherOptions() {

View File

@@ -362,7 +362,7 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy {
break;
case TwoFactorProviderType.WebAuthn:
this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({
pageSubtitle: this.i18nService.t("followTheStepsBelowToFinishLoggingIn"),
pageSubtitle: this.i18nService.t("followTheStepsBelowToFinishLoggingInWithSecurityKey"),
pageIcon: TwoFactorAuthWebAuthnIcon,
});
break;

View File

@@ -57,6 +57,7 @@ export enum FeatureFlag {
VaultBulkManagementAction = "vault-bulk-management-action",
SecurityTasks = "security-tasks",
CipherKeyEncryption = "cipher-key-encryption",
PM18520_UpdateDesktopCipherForm = "pm-18520-desktop-cipher-forms",
EndUserNotifications = "pm-10609-end-user-notifications",
/* Platform */
@@ -110,6 +111,7 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.VaultBulkManagementAction]: FALSE,
[FeatureFlag.SecurityTasks]: FALSE,
[FeatureFlag.CipherKeyEncryption]: FALSE,
[FeatureFlag.PM18520_UpdateDesktopCipherForm]: FALSE,
[FeatureFlag.EndUserNotifications]: FALSE,
/* Auth */

View File

@@ -116,6 +116,12 @@ export class EncryptServiceImplementation implements EncryptService {
throw new Error("No encryption key provided.");
}
if (this.blockType0) {
if (key.inner().type === EncryptionType.AesCbc256_B64 || key.key.byteLength < 64) {
throw new Error("Type 0 encryption is not supported.");
}
}
if (plainValue == null) {
return Promise.resolve(null);
}

View File

@@ -55,6 +55,19 @@ describe("EncryptService", () => {
"No wrappingKey provided for wrapping.",
);
});
it("fails if type 0 key is provided with flag turned on", async () => {
(encryptService as any).blockType0 = true;
const mock32Key = mock<SymmetricCryptoKey>();
mock32Key.key = makeStaticByteArray(32);
mock32Key.inner.mockReturnValue({
type: 0,
encryptionKey: mock32Key.key,
});
await expect(encryptService.wrapSymmetricKey(mock32Key, mock32Key)).rejects.toThrow(
"Type 0 encryption is not supported.",
);
});
});
describe("wrapDecapsulationKey", () => {
@@ -83,6 +96,19 @@ describe("EncryptService", () => {
"No wrappingKey provided for wrapping.",
);
});
it("throws if type 0 key is provided with flag turned on", async () => {
(encryptService as any).blockType0 = true;
const mock32Key = mock<SymmetricCryptoKey>();
mock32Key.key = makeStaticByteArray(32);
mock32Key.inner.mockReturnValue({
type: 0,
encryptionKey: mock32Key.key,
});
await expect(
encryptService.wrapDecapsulationKey(new Uint8Array(200), mock32Key),
).rejects.toThrow("Type 0 encryption is not supported.");
});
});
describe("wrapEncapsulationKey", () => {
@@ -111,6 +137,19 @@ describe("EncryptService", () => {
"No wrappingKey provided for wrapping.",
);
});
it("throws if type 0 key is provided with flag turned on", async () => {
(encryptService as any).blockType0 = true;
const mock32Key = mock<SymmetricCryptoKey>();
mock32Key.key = makeStaticByteArray(32);
mock32Key.inner.mockReturnValue({
type: 0,
encryptionKey: mock32Key.key,
});
await expect(
encryptService.wrapEncapsulationKey(new Uint8Array(200), mock32Key),
).rejects.toThrow("Type 0 encryption is not supported.");
});
});
describe("onServerConfigChange", () => {

Some files were not shown because too many files have changed in this diff Show More