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:
@@ -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",
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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" });
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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>`;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ export function NotificationContainer({
|
||||
ciphers,
|
||||
notificationType: type,
|
||||
theme,
|
||||
i18n,
|
||||
})
|
||||
: null}
|
||||
${NotificationFooter({
|
||||
|
||||
@@ -38,6 +38,7 @@ export function NotificationFooter({
|
||||
? NotificationButtonRow({
|
||||
folders,
|
||||
organizations,
|
||||
i18n,
|
||||
primaryButton: {
|
||||
handlePrimaryButtonClick: handleSaveAction,
|
||||
text: primaryButtonText,
|
||||
|
||||
@@ -33,7 +33,7 @@ type NotificationBarWindowMessage = {
|
||||
data?: {
|
||||
cipherId?: string;
|
||||
task?: NotificationTaskInfo;
|
||||
username?: string;
|
||||
itemName?: string;
|
||||
};
|
||||
error?: string;
|
||||
initData?: NotificationBarIframeInitData;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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."
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -162,3 +162,7 @@ app-root {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.vault-v2 > .details {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
64
apps/desktop/src/vault/app/vault/item-footer.component.html
Normal file
64
apps/desktop/src/vault/app/vault/item-footer.component.html
Normal 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>
|
||||
159
apps/desktop/src/vault/app/vault/item-footer.component.ts
Normal file
159
apps/desktop/src/vault/app/vault/item-footer.component.ts
Normal 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());
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
42
apps/desktop/src/vault/app/vault/vault-items-v2.component.ts
Normal file
42
apps/desktop/src/vault/app/vault/vault-items-v2.component.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
80
apps/desktop/src/vault/app/vault/vault-v2.component.html
Normal file
80
apps/desktop/src/vault/app/vault/vault-v2.component.html
Normal 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>
|
||||
785
apps/desktop/src/vault/app/vault/vault-v2.component.ts
Normal file
785
apps/desktop/src/vault/app/vault/vault-v2.component.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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 },
|
||||
],
|
||||
|
||||
@@ -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>
|
||||
@@ -1,7 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
@Component({
|
||||
selector: "app-abm-enterprise-content",
|
||||
templateUrl: "abm-enterprise-content.component.html",
|
||||
})
|
||||
export class AbmEnterpriseContentComponent {}
|
||||
@@ -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>
|
||||
@@ -1,7 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
@Component({
|
||||
selector: "app-abm-teams-content",
|
||||
templateUrl: "abm-teams-content.component.html",
|
||||
})
|
||||
export class AbmTeamsContentComponent {}
|
||||
@@ -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>
|
||||
@@ -1,7 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
@Component({
|
||||
selector: "app-cnet-enterprise-content",
|
||||
templateUrl: "cnet-enterprise-content.component.html",
|
||||
})
|
||||
export class CnetEnterpriseContentComponent {}
|
||||
@@ -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>
|
||||
@@ -1,7 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
@Component({
|
||||
selector: "app-cnet-individual-content",
|
||||
templateUrl: "cnet-individual-content.component.html",
|
||||
})
|
||||
export class CnetIndividualContentComponent {}
|
||||
@@ -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>
|
||||
@@ -1,7 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
@Component({
|
||||
selector: "app-cnet-teams-content",
|
||||
templateUrl: "cnet-teams-content.component.html",
|
||||
})
|
||||
export class CnetTeamsContentComponent {}
|
||||
@@ -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>
|
||||
@@ -1,7 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
@Component({
|
||||
selector: "app-default-content",
|
||||
templateUrl: "default-content.component.html",
|
||||
})
|
||||
export class DefaultContentComponent {}
|
||||
@@ -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>
|
||||
@@ -1,7 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
@Component({
|
||||
selector: "app-enterprise-content",
|
||||
templateUrl: "enterprise-content.component.html",
|
||||
})
|
||||
export class EnterpriseContentComponent {}
|
||||
@@ -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>
|
||||
@@ -1,7 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
@Component({
|
||||
selector: "app-enterprise1-content",
|
||||
templateUrl: "enterprise1-content.component.html",
|
||||
})
|
||||
export class Enterprise1ContentComponent {}
|
||||
@@ -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>
|
||||
@@ -1,7 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
@Component({
|
||||
selector: "app-enterprise2-content",
|
||||
templateUrl: "enterprise2-content.component.html",
|
||||
})
|
||||
export class Enterprise2ContentComponent {}
|
||||
@@ -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>
|
||||
@@ -1,7 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
@Component({
|
||||
selector: "app-logo-badges",
|
||||
templateUrl: "logo-badges.component.html",
|
||||
})
|
||||
export class LogoBadgesComponent {}
|
||||
@@ -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>
|
||||
@@ -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 {}
|
||||
@@ -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>
|
||||
@@ -1,7 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
@Component({
|
||||
selector: "app-logo-cnet",
|
||||
templateUrl: "logo-cnet.component.html",
|
||||
})
|
||||
export class LogoCnetComponent {}
|
||||
@@ -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>
|
||||
“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.”
|
||||
</blockquote>
|
||||
<p class="tw-font-bold">Best Password Manager in 2024</p>
|
||||
</figure>
|
||||
@@ -1,7 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
@Component({
|
||||
selector: "app-logo-company-testimonial",
|
||||
templateUrl: "logo-company-testimonial.component.html",
|
||||
})
|
||||
export class LogoCompanyTestimonialComponent {}
|
||||
@@ -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>
|
||||
@@ -1,7 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
@Component({
|
||||
selector: "app-logo-forbes",
|
||||
templateUrl: "logo-forbes.component.html",
|
||||
})
|
||||
export class LogoForbesComponent {}
|
||||
@@ -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"
|
||||
/>
|
||||
@@ -1,7 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
@Component({
|
||||
selector: "app-logo-us-news",
|
||||
templateUrl: "logo-us-news.component.html",
|
||||
})
|
||||
export class LogoUSNewsComponent {}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -1,7 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
@Component({
|
||||
selector: "app-teams-content",
|
||||
templateUrl: "teams-content.component.html",
|
||||
})
|
||||
export class TeamsContentComponent {}
|
||||
@@ -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>
|
||||
@@ -1,7 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
@Component({
|
||||
selector: "app-teams1-content",
|
||||
templateUrl: "teams1-content.component.html",
|
||||
})
|
||||
export class Teams1ContentComponent {}
|
||||
@@ -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>
|
||||
@@ -1,7 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
@Component({
|
||||
selector: "app-teams2-content",
|
||||
templateUrl: "teams2-content.component.html",
|
||||
})
|
||||
export class Teams2ContentComponent {}
|
||||
@@ -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>
|
||||
@@ -1,7 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
@Component({
|
||||
selector: "app-teams3-content",
|
||||
templateUrl: "teams3-content.component.html",
|
||||
})
|
||||
export class Teams3ContentComponent {}
|
||||
@@ -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>
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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],
|
||||
})
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 },
|
||||
],
|
||||
})
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
/**
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user