1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-08 20:50:28 +00:00

Merge remote-tracking branch 'origin' into auth/pm-18720/change-password-component-non-dialog-v2

This commit is contained in:
Patrick Pimentel
2025-05-13 15:22:11 -04:00
248 changed files with 3332 additions and 482 deletions

View File

@@ -312,6 +312,7 @@
"@angular/platform-browser",
"@angular/platform",
"@angular/router",
"axe-playwright",
"@compodoc/compodoc",
"@ng-select/ng-select",
"@storybook/addon-a11y",
@@ -320,6 +321,7 @@
"@storybook/addon-essentials",
"@storybook/addon-interactions",
"@storybook/addon-links",
"@storybook/test-runner",
"@storybook/addon-themes",
"@storybook/angular",
"@storybook/manager-api",

View File

@@ -26,6 +26,9 @@ const preview: Preview = {
wrapperDecorator,
],
parameters: {
a11y: {
element: "#storybook-root",
},
controls: {
matchers: {
color: /(background|color)$/i,

47
.storybook/test-runner.ts Normal file
View File

@@ -0,0 +1,47 @@
import { type TestRunnerConfig } from "@storybook/test-runner";
import { injectAxe, checkA11y } from "axe-playwright";
const testRunnerConfig: TestRunnerConfig = {
setup() {},
async preVisit(page, context) {
return await injectAxe(page);
},
async postVisit(page, context) {
await page.waitForSelector("#storybook-root");
// https://github.com/abhinaba-ghosh/axe-playwright#parameters-on-checka11y-axerun
await checkA11y(
// Playwright page instance.
page,
// context
"#storybook-root",
// axeOptions, see https://www.deque.com/axe/core-documentation/api-documentation/#parameters-axerun
{
detailedReport: true,
detailedReportOptions: {
// Includes the full html for invalid nodes
html: true,
},
verbose: false,
},
// skipFailures
false,
// reporter "v2" is terminal reporter, "html" writes results to file
"v2",
// axeHtmlReporterOptions
// NOTE: set reporter param (above) to "html" to activate these options
{
outputDir: "reports/a11y",
reportFileName: `${context.id}.html`,
},
);
},
};
export default testRunnerConfig;

View File

@@ -1088,22 +1088,13 @@
}
}
},
"loginSaveConfirmation": {
"message": "$ITEMNAME$ saved to Bitwarden.",
"placeholders": {
"itemName": {
"content": "$1"
}
},
"notificationLoginSaveConfirmation": {
"message": "saved to Bitwarden.",
"description": "Shown to user after item is saved."
},
"loginUpdatedConfirmation": {
"message": "$ITEMNAME$ updated in Bitwarden.",
"placeholders": {
"itemName": {
"content": "$1"
}
},
"notificationLoginUpdatedConfirmation": {
"message": "updated in Bitwarden.",
"description": "Shown to user after item is updated."
},
"saveAsNewLoginAction": {
@@ -5267,6 +5258,9 @@
"secureDevicesBody": {
"message": "Save unlimited passwords across unlimited devices with Bitwarden mobile, browser, and desktop apps."
},
"nudgeBadgeAria": {
"message": "1 notification"
},
"emptyVaultNudgeTitle": {
"message": "Import existing passwords"
},
@@ -5330,5 +5324,8 @@
"message": "Learn more about SSH agent",
"description": "Two part message",
"example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent"
},
"noPermissionsViewPage": {
"message": "You do not have permissions to view this page. Try logging in with a different account."
}
}

View File

@@ -17,6 +17,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
import { SelfHostedEnvironment } from "@bitwarden/common/platform/services/default-environment.service";
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
import { UserId } from "@bitwarden/common/types/guid";
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 { CipherService } from "@bitwarden/common/vault/services/cipher.service";
@@ -828,6 +829,7 @@ describe("NotificationBackground", () => {
id: "testId",
name: "testItemName",
login: { username: "testUser" },
reprompt: CipherRepromptType.None,
});
getDecryptedCipherByIdSpy.mockResolvedValueOnce(cipherView);
@@ -842,6 +844,7 @@ describe("NotificationBackground", () => {
message.edit,
sender.tab,
"testId",
false,
);
expect(updateWithServerSpy).toHaveBeenCalled();
expect(tabSendMessageDataSpy).toHaveBeenCalledWith(
@@ -855,6 +858,55 @@ describe("NotificationBackground", () => {
);
});
it("prompts the user for master password entry if the notification message type is for ChangePassword and the cipher reprompt is enabled", async () => {
const tab = createChromeTabMock({ id: 1, url: "https://example.com" });
const sender = mock<chrome.runtime.MessageSender>({ tab });
const message: NotificationBackgroundExtensionMessage = {
command: "bgSaveCipher",
edit: false,
folder: "folder-id",
};
const queueMessage = mock<AddChangePasswordQueueMessage>({
type: NotificationQueueMessageType.ChangePassword,
tab,
domain: "example.com",
newPassword: "newPassword",
});
notificationBackground["notificationQueue"] = [queueMessage];
const cipherView = mock<CipherView>({
id: "testId",
name: "testItemName",
login: { username: "testUser" },
reprompt: CipherRepromptType.Password,
});
getDecryptedCipherByIdSpy.mockResolvedValueOnce(cipherView);
sendMockExtensionMessage(message, sender);
await flushPromises();
expect(editItemSpy).not.toHaveBeenCalled();
expect(autofillService.isPasswordRepromptRequired).toHaveBeenCalled();
expect(createWithServerSpy).not.toHaveBeenCalled();
expect(updatePasswordSpy).toHaveBeenCalledWith(
cipherView,
queueMessage.newPassword,
message.edit,
sender.tab,
"testId",
false,
);
expect(updateWithServerSpy).not.toHaveBeenCalled();
expect(tabSendMessageDataSpy).not.toHaveBeenCalledWith(
sender.tab,
"saveCipherAttemptCompleted",
{
itemName: "testItemName",
cipherId: cipherView.id,
task: undefined,
},
);
});
it("completes password update notification with a security task notice if any are present for the cipher, and dismisses tasks for the updated cipher", async () => {
const mockCipherId = "testId";
const mockOrgId = "testOrgId";
@@ -905,6 +957,7 @@ describe("NotificationBackground", () => {
id: mockCipherId,
organizationId: mockOrgId,
name: "Test Item",
reprompt: CipherRepromptType.None,
});
getDecryptedCipherByIdSpy.mockResolvedValueOnce(cipherView);
@@ -919,6 +972,7 @@ describe("NotificationBackground", () => {
message.edit,
sender.tab,
mockCipherId,
false,
);
expect(updateWithServerSpy).toHaveBeenCalled();
expect(tabSendMessageDataSpy).toHaveBeenCalledWith(
@@ -1000,6 +1054,7 @@ describe("NotificationBackground", () => {
message.edit,
sender.tab,
"testId",
false,
);
expect(editItemSpy).toHaveBeenCalled();
expect(updateWithServerSpy).not.toHaveBeenCalled();
@@ -1170,7 +1225,7 @@ describe("NotificationBackground", () => {
newPassword: "newPassword",
});
notificationBackground["notificationQueue"] = [queueMessage];
const cipherView = mock<CipherView>();
const cipherView = mock<CipherView>({ reprompt: CipherRepromptType.None });
getDecryptedCipherByIdSpy.mockResolvedValueOnce(cipherView);
const errorMessage = "fetch error";
updateWithServerSpy.mockImplementation(() => {

View File

@@ -14,6 +14,7 @@ import {
ExtensionCommand,
ExtensionCommandType,
NOTIFICATION_BAR_LIFESPAN_MS,
UPDATE_PASSWORD,
} from "@bitwarden/common/autofill/constants";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { UserNotificationSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/user-notification-settings.service";
@@ -104,6 +105,8 @@ export default class NotificationBackground {
this.removeTabFromNotificationQueue(sender.tab),
bgReopenUnlockPopout: ({ sender }) => this.openUnlockPopout(sender.tab),
bgSaveCipher: ({ message, sender }) => this.handleSaveCipherMessage(message, sender),
bgHandleReprompt: ({ message, sender }: any) =>
this.handleCipherUpdateRepromptResponse(message),
bgUnlockPopoutOpened: ({ message, sender }) => this.unlockVault(message, sender.tab),
checkNotificationQueue: ({ sender }) => this.checkNotificationQueue(sender.tab),
collectPageDetailsResponse: ({ message }) =>
@@ -631,6 +634,17 @@ export default class NotificationBackground {
await this.saveOrUpdateCredentials(sender.tab, message.edit, message.folder);
}
async handleCipherUpdateRepromptResponse(message: NotificationBackgroundExtensionMessage) {
if (message.success) {
await this.saveOrUpdateCredentials(message.tab, false, undefined, true);
} else {
await BrowserApi.tabSendMessageData(message.tab, "saveCipherAttemptCompleted", {
error: "Password reprompt failed",
});
return;
}
}
/**
* Saves or updates credentials based on the message within the
* notification queue that is associated with the specified tab.
@@ -639,7 +653,12 @@ export default class NotificationBackground {
* @param edit - Identifies if the credentials should be edited or simply added
* @param folderId - The folder to add the cipher to
*/
private async saveOrUpdateCredentials(tab: chrome.tabs.Tab, edit: boolean, folderId?: string) {
private async saveOrUpdateCredentials(
tab: chrome.tabs.Tab,
edit: boolean,
folderId?: string,
skipReprompt: boolean = false,
) {
for (let i = this.notificationQueue.length - 1; i >= 0; i--) {
const queueMessage = this.notificationQueue[i];
if (
@@ -654,18 +673,26 @@ export default class NotificationBackground {
continue;
}
this.notificationQueue.splice(i, 1);
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(getOptionalUserId),
);
if (queueMessage.type === NotificationQueueMessageType.ChangePassword) {
const cipherView = await this.getDecryptedCipherById(queueMessage.cipherId, activeUserId);
await this.updatePassword(cipherView, queueMessage.newPassword, edit, tab, activeUserId);
await this.updatePassword(
cipherView,
queueMessage.newPassword,
edit,
tab,
activeUserId,
skipReprompt,
);
return;
}
this.notificationQueue.splice(i, 1);
// If the vault was locked, check if a cipher needs updating instead of creating a new one
if (queueMessage.wasVaultLocked) {
const allCiphers = await this.cipherService.getAllDecryptedForUrl(
@@ -725,6 +752,7 @@ export default class NotificationBackground {
edit: boolean,
tab: chrome.tabs.Tab,
userId: UserId,
skipReprompt: boolean = false,
) {
cipherView.login.password = newPassword;
@@ -758,6 +786,12 @@ export default class NotificationBackground {
}
: undefined;
if (cipherView.reprompt && !skipReprompt) {
await this.autofillService.isPasswordRepromptRequired(cipherView, tab, UPDATE_PASSWORD);
return;
}
await this.cipherService.updateWithServer(cipher);
await BrowserApi.tabSendMessageData(tab, "saveCipherAttemptCompleted", {

View File

@@ -107,9 +107,9 @@ export const mockI18n = {
collection: "Collection",
folder: "Folder",
loginSaveSuccess: "Login saved",
loginSaveConfirmation: "$ITEMNAME$ saved to Bitwarden.",
notificationLoginSaveConfirmation: "saved to Bitwarden.",
loginUpdateSuccess: "Login updated",
loginUpdatedConfirmation: "$ITEMNAME$ updated in Bitwarden.",
notificationLoginUpdatedConfirmation: "updated in Bitwarden.",
loginUpdateTaskSuccess:
"Great job! You took the steps to make you and $ORGANIZATION$ more secure.",
loginUpdateTaskSuccessAdditional:

View File

@@ -113,9 +113,14 @@ function getConfirmationMessage(i18n: I18n, type?: NotificationType, error?: str
if (error) {
return i18n.saveFailureDetails;
}
/* @TODO This partial string return and later concatenation with the cipher name is needed
* to handle cipher name overflow cases, but is risky for i18n concerns. Fix concatenation
* with cipher name overflow when a tag replacement solution is available.
*/
return type === NotificationTypes.Add
? i18n.loginSaveConfirmation
: i18n.loginUpdatedConfirmation;
? i18n.notificationLoginSaveConfirmation
: i18n.notificationLoginUpdatedConfirmation;
}
function getHeaderMessage(i18n: I18n, type?: NotificationType, error?: string) {

View File

@@ -28,28 +28,31 @@ export function NotificationConfirmationMessage({
<div class=${containerStyles}>
${message || buttonText
? html`
<span class=${itemNameStyles(theme)} title=${itemName}> ${itemName} </span>
<span
title=${message || buttonText}
class=${notificationConfirmationMessageStyles(theme)}
>
${message || nothing}
${buttonText
? html`
<a
title=${buttonText}
class=${notificationConfirmationButtonTextStyles(theme)}
@click=${handleClick}
@keydown=${(e: KeyboardEvent) => handleButtonKeyDown(e, () => handleClick(e))}
aria-label=${buttonAria}
tabindex="0"
role="button"
>
${buttonText}
</a>
`
: nothing}
</span>
<div class=${singleLineWrapperStyles}>
<span class=${itemNameStyles(theme)} title=${itemName}> ${itemName} </span>
<span
title=${message || buttonText}
class=${notificationConfirmationMessageStyles(theme)}
>
${message || nothing}
${buttonText
? html`
<a
title=${buttonText}
class=${notificationConfirmationButtonTextStyles(theme)}
@click=${handleClick}
@keydown=${(e: KeyboardEvent) =>
handleButtonKeyDown(e, () => handleClick(e))}
aria-label=${buttonAria}
tabindex="0"
role="button"
>
${buttonText}
</a>
`
: nothing}
</span>
</div>
`
: nothing}
${messageDetails
@@ -61,12 +64,17 @@ export function NotificationConfirmationMessage({
const containerStyles = css`
display: flex;
flex-wrap: wrap;
align-items: center;
flex-direction: column;
gap: ${spacing[1]};
width: 100%;
`;
const singleLineWrapperStyles = css`
display: inline;
white-space: normal;
word-break: break-word;
`;
const baseTextStyles = css`
overflow-x: hidden;
text-align: left;
@@ -81,6 +89,9 @@ const notificationConfirmationMessageStyles = (theme: Theme) => css`
color: ${themes[theme].text.main};
font-weight: 400;
white-space: normal;
word-break: break-word;
display: inline;
`;
const itemNameStyles = (theme: Theme) => css`
@@ -90,6 +101,10 @@ const itemNameStyles = (theme: Theme) => css`
font-weight: 400;
white-space: nowrap;
max-width: 300px;
overflow: hidden;
text-overflow: ellipsis;
display: inline-block;
vertical-align: bottom;
`;
const notificationConfirmationButtonTextStyles = (theme: Theme) => css`

View File

@@ -94,7 +94,7 @@ const notificationContainerStyles = (theme: Theme) => css`
}
[class*="${notificationBodyClassPrefix}-"] {
margin: ${spacing["3"]} 0 ${spacing["1.5"]} ${spacing["3"]};
margin: ${spacing["3"]} 0 0 ${spacing["3"]};
padding-right: ${spacing["3"]};
}
`;

View File

@@ -8,7 +8,7 @@ import {
NotificationTypes,
} from "../../../notification/abstractions/notification-bar";
import { OrgView, FolderView, I18n, CollectionView } from "../common-types";
import { spacing, themes } from "../constants/styles";
import { spacing } from "../constants/styles";
import { NotificationButtonRow } from "./button-row";
@@ -37,7 +37,7 @@ export function NotificationFooter({
const primaryButtonText = i18n.saveAction;
return html`
<div class=${notificationFooterStyles({ theme })}>
<div class=${notificationFooterStyles({ isChangeNotification })}>
${!isChangeNotification
? NotificationButtonRow({
collections,
@@ -56,13 +56,16 @@ export function NotificationFooter({
`;
}
const notificationFooterStyles = ({ theme }: { theme: Theme }) => css`
const notificationFooterStyles = ({
isChangeNotification,
}: {
isChangeNotification: boolean;
}) => css`
display: flex;
background-color: ${themes[theme].background.alt};
padding: 0 ${spacing[3]} ${spacing[3]} ${spacing[3]};
padding: ${spacing[2]} ${spacing[4]} ${isChangeNotification ? spacing[1] : spacing[4]}
${spacing[4]};
:last-child {
border-radius: 0 0 ${spacing["4"]} ${spacing["4"]};
padding-bottom: ${spacing[4]};
}
`;

View File

@@ -62,7 +62,7 @@ const buttonRowStyles = css`
> button {
max-width: min-content;
flex: 1 1 50%;
flex: 1 1 25%;
}
> div {

View File

@@ -21,6 +21,8 @@ export const RedirectFocusDirection = {
Next: "next",
} as const;
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum InlineMenuFillType {
AccountCreationUsername = 5,
PasswordGeneration = 6,

View File

@@ -5,6 +5,8 @@ import {
AssertCredentialResult,
} from "@bitwarden/common/platform/abstractions/fido2/fido2-client.service.abstraction";
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum MessageType {
CredentialCreationRequest,
CredentialCreationResponse,

View File

@@ -59,9 +59,7 @@ function getI18n() {
collection: chrome.i18n.getMessage("collection"),
folder: chrome.i18n.getMessage("folder"),
loginSaveSuccess: chrome.i18n.getMessage("loginSaveSuccess"),
loginSaveConfirmation: chrome.i18n.getMessage("loginSaveConfirmation"),
loginUpdateSuccess: chrome.i18n.getMessage("loginUpdateSuccess"),
loginUpdatedConfirmation: chrome.i18n.getMessage("loginUpdatedConfirmation"),
loginUpdateTaskSuccess: chrome.i18n.getMessage("loginUpdateTaskSuccess"),
loginUpdateTaskSuccessAdditional: chrome.i18n.getMessage("loginUpdateTaskSuccessAdditional"),
nextSecurityTaskAction: chrome.i18n.getMessage("nextSecurityTaskAction"),
@@ -74,6 +72,10 @@ function getI18n() {
notificationUpdate: chrome.i18n.getMessage("notificationChangeSave"),
notificationEdit: chrome.i18n.getMessage("edit"),
notificationEditTooltip: chrome.i18n.getMessage("notificationEditTooltip"),
notificationLoginSaveConfirmation: chrome.i18n.getMessage("notificationLoginSaveConfirmation"),
notificationLoginUpdatedConfirmation: chrome.i18n.getMessage(
"notificationLoginUpdatedConfirmation",
),
notificationUnlock: chrome.i18n.getMessage("notificationUnlock"),
notificationUnlockDesc: chrome.i18n.getMessage("notificationUnlockDesc"),
notificationViewAria: chrome.i18n.getMessage("notificationViewAria"),

View File

@@ -15,6 +15,7 @@ export type NotificationsExtensionMessage = {
typeData?: NotificationTypeData;
height?: number;
error?: string;
closedByUser?: boolean;
fadeOutNotification?: boolean;
};
};

View File

@@ -106,13 +106,15 @@ export class OverlayNotificationsContentService
* @param message - The message containing the data for closing the notification bar.
*/
private handleCloseNotificationBarMessage(message: NotificationsExtensionMessage) {
const closedByUser =
typeof message.data?.closedByUser === "boolean" ? message.data.closedByUser : true;
if (message.data?.fadeOutNotification) {
setElementStyles(this.notificationBarIframeElement, { opacity: "0" }, true);
globalThis.setTimeout(() => this.closeNotificationBar(true), 150);
globalThis.setTimeout(() => this.closeNotificationBar(closedByUser), 150);
return;
}
this.closeNotificationBar(true);
this.closeNotificationBar(closedByUser);
}
/**

View File

@@ -87,5 +87,9 @@ export abstract class AutofillService {
cipherType?: CipherType,
) => Promise<string | null>;
setAutoFillOnPageLoadOrgPolicy: () => Promise<void>;
isPasswordRepromptRequired: (cipher: CipherView, tab: chrome.tabs.Tab) => Promise<boolean>;
isPasswordRepromptRequired: (
cipher: CipherView,
tab: chrome.tabs.Tab,
action?: string,
) => Promise<boolean>;
}

View File

@@ -593,15 +593,20 @@ export default class AutofillService implements AutofillServiceInterface {
*
* @param cipher - The cipher to autofill
* @param tab - The tab to autofill
* @param action - override for default action once reprompt is completed successfully
*/
async isPasswordRepromptRequired(cipher: CipherView, tab: chrome.tabs.Tab): Promise<boolean> {
async isPasswordRepromptRequired(
cipher: CipherView,
tab: chrome.tabs.Tab,
action?: string,
): Promise<boolean> {
const userHasMasterPasswordAndKeyHash =
await this.userVerificationService.hasMasterPasswordAndMasterKeyHash();
if (cipher.reprompt === CipherRepromptType.Password && userHasMasterPasswordAndKeyHash) {
if (!this.isDebouncingPasswordRepromptPopout()) {
await this.openVaultItemPasswordRepromptPopout(tab, {
cipherId: cipher.id,
action: "autofill",
action: action ?? "autofill",
});
}

View File

@@ -664,6 +664,10 @@ export class BrowserApi {
* Identifies if the browser autofill settings are overridden by the extension.
*/
static async browserAutofillSettingsOverridden(): Promise<boolean> {
if (!(await BrowserApi.permissionsGranted(["privacy"]))) {
return false;
}
const checkOverrideStatus = (details: chrome.types.ChromeSettingGetResult<boolean>) =>
details.levelOfControl === "controlled_by_this_extension" && !details.value;

View File

@@ -50,17 +50,40 @@ export class PopupSizeService {
PopupSizeService.setStyle(width);
localStorage.setItem(PopupSizeService.LocalStorageKey, width);
});
}
async setHeight() {
const isInChromeTab = await BrowserPopupUtils.isInTab();
/**
* To support both browser default zoom and system default zoom, we need to take into account
* the full screen height. When system default zoom is >100%, window.innerHeight still outputs
* a height equivalent to what it would be at 100%, which can cause the extension window to
* render as too tall. So if the screen height is smaller than the max possible extension height,
* we should use that to set our extension height. Otherwise, we want to use the window.innerHeight
* to support browser zoom.
*
* This is basically a workaround for what we consider a bug with browsers reporting the wrong
* available innerHeight when system zoom is turned on. If that gets fixed, we can remove the code
* checking the screen height.
*/
const MAX_EXT_HEIGHT = 600;
const extensionInnerHeight = window.innerHeight;
// Use a 100px offset when calculating screen height to account for browser container elements
const screenAvailHeight = window.screen.availHeight - 100;
const availHeight =
screenAvailHeight < MAX_EXT_HEIGHT ? screenAvailHeight : extensionInnerHeight;
if (!BrowserPopupUtils.inPopup(window) || isInChromeTab) {
window.document.body.classList.add("body-full");
} else if (window.innerHeight < 400) {
window.document.body.classList.add("body-xxs");
} else if (window.innerHeight < 500) {
window.document.body.classList.add("body-xs");
} else if (window.innerHeight < 600) {
window.document.body.classList.add("body-sm");
window.document.documentElement.classList.add("body-full");
} else if (availHeight < 300) {
window.document.documentElement.classList.add("body-3xs");
} else if (availHeight < 400) {
window.document.documentElement.classList.add("body-xxs");
} else if (availHeight < 500) {
window.document.documentElement.classList.add("body-xs");
} else if (availHeight < 600) {
window.document.documentElement.classList.add("body-sm");
}
}

View File

@@ -19,7 +19,7 @@ import {
FormCacheOptions,
SignalCacheOptions,
ViewCacheService,
} from "@bitwarden/angular/platform/abstractions/view-cache.service";
} from "@bitwarden/angular/platform/view-cache";
import { MessageSender } from "@bitwarden/common/platform/messaging";
import { GlobalStateProvider } from "@bitwarden/common/platform/state";

View File

@@ -26,6 +26,7 @@ import {
import { BiometricsService, BiometricStateService } from "@bitwarden/key-management";
import { PopupCompactModeService } from "../platform/popup/layout/popup-compact-mode.service";
import { PopupSizeService } from "../platform/popup/layout/popup-size.service";
import { initPopupClosedListener } from "../platform/services/popup-view-cache-background.service";
import { VaultBrowserStateService } from "../vault/services/vault-browser-state.service";
@@ -71,6 +72,7 @@ export class AppComponent implements OnInit, OnDestroy {
private biometricStateService: BiometricStateService,
private biometricsService: BiometricsService,
private deviceTrustToastService: DeviceTrustToastService,
private popupSizeService: PopupSizeService,
) {
this.deviceTrustToastService.setupListeners$.pipe(takeUntilDestroyed()).subscribe();
}
@@ -79,6 +81,7 @@ export class AppComponent implements OnInit, OnDestroy {
initPopupClosedListener();
this.compactModeService.init();
await this.popupSizeService.setHeight();
// Component states must not persist between closing and reopening the popup, otherwise they become dead objects
// Clear them aggressively to make sure this doesn't occur

View File

@@ -8,6 +8,34 @@
html {
overflow: hidden;
min-height: 600px;
height: 100%;
&.body-sm {
min-height: 500px;
}
&.body-xs {
min-height: 400px;
}
&.body-xxs {
min-height: 300px;
}
&.body-3xs {
min-height: 240px;
}
&.body-full {
min-height: unset;
width: 100%;
height: 100%;
& body {
width: 100%;
}
}
}
html,
@@ -20,9 +48,9 @@ body {
body {
width: 380px;
height: 600px;
height: 100%;
position: relative;
min-height: 100vh;
min-height: inherit;
overflow: hidden;
color: $text-color;
background-color: $background-color;
@@ -31,23 +59,6 @@ body {
color: themed("textColor");
background-color: themed("backgroundColor");
}
&.body-sm {
height: 500px;
}
&.body-xs {
height: 400px;
}
&.body-xxs {
height: 300px;
}
&.body-full {
width: 100%;
height: 100%;
}
}
h1,

View File

@@ -5,9 +5,9 @@ import { Router } from "@angular/router";
import { merge, of, Subject } from "rxjs";
import { CollectionService } from "@bitwarden/admin-console/common";
import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service";
import { AngularThemingService } from "@bitwarden/angular/platform/services/theming/angular-theming.service";
import { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/safe-provider";
import { ViewCacheService } from "@bitwarden/angular/platform/view-cache";
import {
CLIENT_TYPE,
DEFAULT_VAULT_TIMEOUT,

View File

@@ -26,6 +26,8 @@ import { PopOutComponent } from "../../../platform/popup/components/pop-out.comp
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum SendState {
Empty,
NoResults,

View File

@@ -23,6 +23,7 @@
*ngIf="!isBrowserAutofillSettingOverridden && (showAutofillBadge$ | async)"
bitBadge
variant="notification"
[attr.aria-label]="'nudgeBadgeAria' | i18n"
>1</span
>
</div>
@@ -53,6 +54,7 @@
*ngIf="!(showVaultBadge$ | async)?.hasBadgeDismissed"
bitBadge
variant="notification"
[attr.aria-label]="'nudgeBadgeAria' | i18n"
>1</span
>
</div>
@@ -83,6 +85,7 @@
*ngIf="(downloadBitwardenNudgeStatus$ | async)?.hasBadgeDismissed === false"
bitBadge
variant="notification"
[attr.aria-label]="'nudgeBadgeAria' | i18n"
>1
</span>
</div>

View File

@@ -1,6 +1,6 @@
import { inject } from "@angular/core";
import { CanActivateFn } from "@angular/router";
import { switchMap, tap } from "rxjs";
import { CanActivateFn, Router } from "@angular/router";
import { map, switchMap } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -13,18 +13,22 @@ export const canAccessAtRiskPasswords: CanActivateFn = () => {
const taskService = inject(TaskService);
const toastService = inject(ToastService);
const i18nService = inject(I18nService);
const router = inject(Router);
return accountService.activeAccount$.pipe(
filterOutNullish(),
switchMap((user) => taskService.tasksEnabled$(user.id)),
tap((tasksEnabled) => {
map((tasksEnabled) => {
if (!tasksEnabled) {
toastService.showToast({
variant: "error",
title: "",
message: i18nService.t("accessDenied"),
message: i18nService.t("noPermissionsViewPage"),
});
return router.createUrlTree(["/tabs/vault"]);
}
return true;
}),
);
};

View File

@@ -10,6 +10,8 @@ import {
import { I18nPipe } from "@bitwarden/ui-common";
import { DarkImageSourceDirective, VaultCarouselModule } from "@bitwarden/vault";
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum AtRiskCarouselDialogResult {
Dismissed = "dismissed",
}

View File

@@ -30,6 +30,8 @@ export interface GeneratorDialogResult {
generatedValue?: string;
}
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum GeneratorDialogAction {
Selected = "selected",
Canceled = "canceled",

View File

@@ -55,6 +55,8 @@ import { VaultPageService } from "./vault-page.service";
import { AutofillVaultListItemsComponent, VaultListItemsContainerComponent } from ".";
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
enum VaultState {
Empty,
NoResults,

View File

@@ -19,6 +19,7 @@ import {
COPY_USERNAME_ID,
COPY_VERIFICATION_CODE_ID,
SHOW_AUTOFILL_BUTTON,
UPDATE_PASSWORD,
} from "@bitwarden/common/autofill/constants";
import { EventType } from "@bitwarden/common/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
@@ -49,6 +50,7 @@ import {
PasswordRepromptService,
} from "@bitwarden/vault";
import { sendExtensionMessage } from "../../../../../autofill/utils/index";
import { BrowserApi } from "../../../../../platform/browser/browser-api";
import BrowserPopupUtils from "../../../../../platform/popup/browser-popup-utils";
import { PopOutComponent } from "../../../../../platform/popup/components/pop-out.component";
@@ -72,7 +74,8 @@ type LoadAction =
| typeof SHOW_AUTOFILL_BUTTON
| typeof COPY_USERNAME_ID
| typeof COPY_PASSWORD_ID
| typeof COPY_VERIFICATION_CODE_ID;
| typeof COPY_VERIFICATION_CODE_ID
| typeof UPDATE_PASSWORD;
@Component({
selector: "app-view-v2",
@@ -294,7 +297,7 @@ export class ViewV2Component {
// Both vaultPopupAutofillService and copyCipherFieldService will perform password re-prompting internally.
switch (loadAction) {
case "show-autofill-button":
case SHOW_AUTOFILL_BUTTON:
// This action simply shows the cipher view, no need to do anything.
if (
this.cipher.reprompt !== CipherRepromptType.None &&
@@ -303,30 +306,42 @@ export class ViewV2Component {
await closeViewVaultItemPopout(`${VaultPopoutType.viewVaultItem}_${this.cipher.id}`);
}
return;
case "autofill":
case AUTOFILL_ID:
actionSuccess = await this.vaultPopupAutofillService.doAutofill(this.cipher, false);
break;
case "copy-username":
case COPY_USERNAME_ID:
actionSuccess = await this.copyCipherFieldService.copy(
this.cipher.login.username,
"username",
this.cipher,
);
break;
case "copy-password":
case COPY_PASSWORD_ID:
actionSuccess = await this.copyCipherFieldService.copy(
this.cipher.login.password,
"password",
this.cipher,
);
break;
case "copy-totp":
case COPY_VERIFICATION_CODE_ID:
actionSuccess = await this.copyCipherFieldService.copy(
this.cipher.login.totp,
"totp",
this.cipher,
);
break;
case UPDATE_PASSWORD: {
const repromptSuccess = await this.passwordRepromptService.showPasswordPrompt();
await sendExtensionMessage("bgHandleReprompt", {
tab: await chrome.tabs.get(senderTabId),
success: repromptSuccess,
});
await closeViewVaultItemPopout(`${VaultPopoutType.viewVaultItem}_${this.cipher.id}`);
break;
}
}
if (BrowserPopupUtils.inPopout(window)) {

View File

@@ -4,6 +4,7 @@ import { FormBuilder } from "@angular/forms";
import { BehaviorSubject, skipWhile } from "rxjs";
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common";
import { ViewCacheService } from "@bitwarden/angular/platform/view-cache";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
@@ -20,8 +21,6 @@ import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import { PopupViewCacheService } from "../../../platform/popup/view-cache/popup-view-cache.service";
import {
CachedFilterState,
MY_VAULT_ID,
@@ -123,7 +122,7 @@ describe("VaultPopupListFiltersService", () => {
useValue: accountService,
},
{
provide: PopupViewCacheService,
provide: ViewCacheService,
useValue: viewCacheService,
},
],

View File

@@ -15,6 +15,7 @@ import {
} from "rxjs";
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common";
import { ViewCacheService } from "@bitwarden/angular/platform/view-cache";
import { DynamicTreeNode } from "@bitwarden/angular/vault/vault-filter/models/dynamic-tree-node.model";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
@@ -40,8 +41,6 @@ import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import { ServiceUtils } from "@bitwarden/common/vault/service-utils";
import { ChipSelectOption } from "@bitwarden/components";
import { PopupViewCacheService } from "../../../platform/popup/view-cache/popup-view-cache.service";
const FILTER_VISIBILITY_KEY = new KeyDefinition<boolean>(VAULT_SETTINGS_DISK, "filterVisibility", {
deserializer: (obj) => obj,
});
@@ -178,7 +177,7 @@ export class VaultPopupListFiltersService {
private policyService: PolicyService,
private stateProvider: StateProvider,
private accountService: AccountService,
private viewCacheService: PopupViewCacheService,
private viewCacheService: ViewCacheService,
) {
this.filterForm.controls.organization.valueChanges
.pipe(takeUntilDestroyed())

View File

@@ -64,13 +64,14 @@
]
},
"dependencies": {
"@koa/multer": "3.0.2",
"@koa/multer": "3.1.0",
"@koa/router": "13.1.0",
"argon2": "0.41.1",
"big-integer": "1.6.52",
"browser-hrtime": "1.1.8",
"chalk": "4.1.2",
"commander": "11.1.0",
"core-js": "3.40.0",
"form-data": "4.0.1",
"https-proxy-agent": "7.0.6",
"inquirer": "8.2.6",
@@ -81,7 +82,7 @@
"koa-json": "2.0.2",
"lowdb": "1.0.0",
"lunr": "2.3.9",
"multer": "1.4.5-lts.1",
"multer": "1.4.5-lts.2",
"node-fetch": "2.6.12",
"node-forge": "1.3.1",
"open": "8.4.2",

View File

@@ -106,6 +106,8 @@ export class LoginCommand {
return Response.badRequest("client_secret is required.");
}
} else if (options.sso != null && this.canInteract) {
// If the optional Org SSO Identifier isn't provided, the option value is `true`.
const orgSsoIdentifier = options.sso === true ? null : options.sso;
const passwordOptions: any = {
type: "password",
length: 64,
@@ -119,7 +121,7 @@ export class LoginCommand {
const codeVerifierHash = await this.cryptoFunctionService.hash(ssoCodeVerifier, "sha256");
const codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash);
try {
const ssoParams = await this.openSsoPrompt(codeChallenge, state);
const ssoParams = await this.openSsoPrompt(codeChallenge, state, orgSsoIdentifier);
ssoCode = ssoParams.ssoCode;
orgIdentifier = ssoParams.orgIdentifier;
} catch {
@@ -664,6 +666,7 @@ export class LoginCommand {
private async openSsoPrompt(
codeChallenge: string,
state: string,
orgSsoIdentifier: string,
): Promise<{ ssoCode: string; orgIdentifier: string }> {
const env = await firstValueFrom(this.environmentService.environment$);
@@ -712,6 +715,8 @@ export class LoginCommand {
this.ssoRedirectUri,
state,
codeChallenge,
null,
orgSsoIdentifier,
);
this.platformUtilsService.launchUri(webAppSsoUrl);
});

View File

@@ -118,7 +118,10 @@ export class Program extends BaseProgram {
.description("Log into a user account.")
.option("--method <method>", "Two-step login method.")
.option("--code <code>", "Two-step login code.")
.option("--sso", "Log in with Single-Sign On.")
.option(
"--sso [identifier]",
"Log in with Single-Sign On with optional organization identifier.",
)
.option("--apikey", "Log in with an Api Key.")
.option("--passwordenv <passwordenv>", "Environment variable storing your password")
.option(

View File

@@ -15,6 +15,8 @@ const DEFAULT_MESSAGE_TIMEOUT = 10 * 1000; // 10 seconds
export type MessageHandler = (MessageCommon) => void;
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum IPCConnectionState {
Disconnected = "disconnected",
Connecting = "connecting",

View File

@@ -25,6 +25,8 @@ import { SearchBarService } from "../../layout/search/search-bar.service";
import { AddEditComponent } from "./add-edit.component";
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
enum Action {
None = "",
Add = "add",

View File

@@ -1,3 +1,5 @@
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum SshAgentPromptType {
Always = "always",
Never = "never",

View File

@@ -1,3 +1,5 @@
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum BiometricAction {
Authenticate = "authenticate",
GetStatus = "status",

View File

@@ -31,6 +31,8 @@ export interface CredentialGeneratorDialogResult {
generatedValue?: string;
}
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum CredentialGeneratorDialogAction {
Selected = "selected",
Canceled = "canceled",

View File

@@ -43,6 +43,8 @@ export interface BulkCollectionsDialogParams {
collections: CollectionView[];
}
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum BulkCollectionsDialogResult {
Saved = "saved",
Canceled = "canceled",

View File

@@ -132,6 +132,8 @@ import { VaultHeaderComponent } from "./vault-header/vault-header.component";
const BroadcasterSubscriptionId = "OrgVaultComponent";
const SearchTextDebounceInterval = 200;
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
enum AddAccessStatusType {
All = 0,
AddAccess = 1,

View File

@@ -58,6 +58,8 @@ import { AddEditGroupDetail } from "./../core/views/add-edit-group-detail";
/**
* Indices for the available tabs in the dialog
*/
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum GroupAddEditTabType {
Info = 0,
Members = 1,
@@ -82,6 +84,8 @@ export interface GroupAddEditDialogParams {
initialTab?: GroupAddEditTabType;
}
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum GroupAddEditDialogResultType {
Saved = "saved",
Canceled = "canceled",

View File

@@ -64,6 +64,8 @@ import { commaSeparatedEmails } from "./validators/comma-separated-emails.valida
import { inputEmailLimitValidator } from "./validators/input-email-limit.validator";
import { orgSeatLimitReachedValidator } from "./validators/org-seat-limit-reached.validator";
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum MemberDialogTab {
Role = 0,
Groups = 1,
@@ -92,6 +94,8 @@ export interface EditMemberDialogParams extends CommonMemberDialogParams {
export type MemberDialogParams = EditMemberDialogParams | AddMemberDialogParams;
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum MemberDialogResult {
Saved = "saved",
Canceled = "canceled",

View File

@@ -50,6 +50,8 @@ export type ResetPasswordDialogData = {
organizationId: string;
};
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum ResetPasswordDialogResult {
Ok = "ok",
}

View File

@@ -112,7 +112,7 @@ export class OrganizationUserResetPasswordService
if (orgSymKey == null) {
throw new Error("No org key found");
}
const decPrivateKey = await this.encryptService.decryptToBytes(
const decPrivateKey = await this.encryptService.unwrapDecapsulationKey(
new EncString(response.encryptedPrivateKey),
orgSymKey,
);

View File

@@ -41,6 +41,8 @@ export type PolicyEditDialogData = {
organizationId: string;
};
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum PolicyEditDialogResult {
Saved = "saved",
UpgradePlan = "upgrade-plan",

View File

@@ -71,6 +71,8 @@ export interface DeleteOrganizationDialogParams {
requestType: "InvalidFamiliesForEnterprise" | "RegularDelete";
}
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum DeleteOrganizationDialogResult {
Deleted = "deleted",
Canceled = "canceled",

View File

@@ -26,6 +26,8 @@ import {
Permission,
} from "./access-selector.models";
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum PermissionMode {
/**
* No permission controls or column present. No permission values are emitted.

View File

@@ -15,6 +15,8 @@ import { GroupView } from "../../../core";
/**
* Permission options that replace/correspond with manage, readOnly, and hidePassword server fields.
*/
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum CollectionPermission {
View = "view",
ViewExceptPass = "viewExceptPass",
@@ -23,6 +25,8 @@ export enum CollectionPermission {
Manage = "manage",
}
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum AccessItemType {
Collection,
Group,

View File

@@ -65,6 +65,8 @@ import {
} from "../access-selector/access-selector.models";
import { AccessSelectorModule } from "../access-selector/access-selector.module";
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum CollectionDialogTabType {
Info = 0,
Access = 1,
@@ -76,6 +78,8 @@ export enum CollectionDialogTabType {
* @readonly
* @enum {string}
*/
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
enum ButtonType {
/** Displayed when the user has reached the maximum number of collections allowed for the organization. */
Upgrade = "upgrade",
@@ -103,6 +107,8 @@ export interface CollectionDialogResult {
collection: CollectionResponse | CollectionView;
}
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum CollectionDialogAction {
Saved = "saved",
Canceled = "canceled",

View File

@@ -1,3 +1,5 @@
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum WebauthnLoginCredentialPrfStatus {
Enabled = 0,
Supported = 1,

View File

@@ -1,3 +1,5 @@
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum EmergencyAccessStatusType {
Invited = 0,
Accepted = 1,

View File

@@ -1,3 +1,5 @@
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum EmergencyAccessType {
View = 0,
Takeover = 1,

View File

@@ -8,6 +8,8 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
import { DialogConfig, DialogRef, DIALOG_DATA, DialogService } from "@bitwarden/components";
import { KeyService } from "@bitwarden/key-management";
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum EmergencyAccessConfirmDialogResult {
Confirmed = "confirmed",
}

View File

@@ -26,6 +26,8 @@ export type EmergencyAccessAddEditDialogData = {
readOnly: boolean;
};
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum EmergencyAccessAddEditDialogResult {
Saved = "saved",
Canceled = "canceled",

View File

@@ -24,6 +24,8 @@ import { KdfType, KdfConfigService, KeyService } from "@bitwarden/key-management
import { EmergencyAccessService } from "../../../emergency-access";
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum EmergencyAccessTakeoverResultType {
Done = "done",
}

View File

@@ -19,6 +19,8 @@ import { PendingWebauthnLoginCredentialView } from "../../../core/views/pending-
import { CreatePasskeyFailedIcon } from "./create-passkey-failed.icon";
import { CreatePasskeyIcon } from "./create-passkey.icon";
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum CreateCredentialDialogResult {
Success,
}

View File

@@ -51,8 +51,38 @@
<h2 class="tw-mb-3 tw-text-base tw-font-semibold">{{ "paymentType" | i18n }}</h2>
<app-payment [showAccountCredit]="false"></app-payment>
<app-manage-tax-information
(taxInformationChanged)="changedCountry()"
(taxInformationChanged)="onTaxInformationChanged()"
></app-manage-tax-information>
@if (trialLength === 0) {
@let priceLabel =
subscriptionProduct === SubscriptionProduct.PasswordManager
? "passwordManagerPlanPrice"
: "secretsManagerPlanPrice";
<div id="price" class="tw-my-4">
<div class="tw-text-muted tw-text-base">
{{ priceLabel | i18n }}: {{ getPriceFor(formGroup.value.cadence) | currency: "USD $" }}
<div>
{{ "estimatedTax" | i18n }}:
@if (fetchingTaxAmount) {
<ng-container *ngTemplateOutlet="loadingSpinner" />
} @else {
{{ taxAmount | currency: "USD $" }}
}
</div>
</div>
<hr class="tw-my-1 tw-grid tw-grid-cols-3 tw-ml-0" />
<p class="tw-text-lg">
<strong>{{ "total" | i18n }}: </strong>
@if (fetchingTaxAmount) {
<ng-container *ngTemplateOutlet="loadingSpinner" />
} @else {
{{ total | currency: "USD $" }}/{{ interval | i18n }}
}
</p>
</div>
}
</div>
<div class="tw-flex tw-space-x-2">
<button type="submit" buttonType="primary" bitButton [loading]="form.loading">
@@ -62,3 +92,12 @@
</div>
</div>
</form>
<ng-template #loadingSpinner>
<i
class="bwi bwi-spinner bwi-spin tw-text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
</ng-template>

View File

@@ -1,7 +1,16 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from "@angular/core";
import {
Component,
EventEmitter,
Input,
OnDestroy,
OnInit,
Output,
ViewChild,
} from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms";
import { from, Subject, switchMap, takeUntil } from "rxjs";
import { ManageTaxInformationComponent } from "@bitwarden/angular/billing/components";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
@@ -12,7 +21,14 @@ import {
PaymentInformation,
PlanInformation,
} from "@bitwarden/common/billing/abstractions/organization-billing.service";
import { PaymentMethodType, PlanType, ProductTierType } from "@bitwarden/common/billing/enums";
import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction";
import {
PaymentMethodType,
PlanType,
ProductTierType,
ProductType,
} from "@bitwarden/common/billing/enums";
import { PreviewTaxAmountForOrganizationTrialRequest } from "@bitwarden/common/billing/models/request/tax";
import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
@@ -34,11 +50,15 @@ export interface OrganizationCreatedEvent {
planDescription: string;
}
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
enum SubscriptionCadence {
Annual,
Monthly,
}
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum SubscriptionProduct {
PasswordManager,
SecretsManager,
@@ -50,7 +70,7 @@ export enum SubscriptionProduct {
imports: [BillingSharedModule],
standalone: true,
})
export class TrialBillingStepComponent implements OnInit {
export class TrialBillingStepComponent implements OnInit, OnDestroy {
@ViewChild(PaymentComponent) paymentComponent: PaymentComponent;
@ViewChild(ManageTaxInformationComponent) taxInfoComponent: ManageTaxInformationComponent;
@Input() organizationInfo: OrganizationInfo;
@@ -60,6 +80,7 @@ export class TrialBillingStepComponent implements OnInit {
@Output() organizationCreated = new EventEmitter<OrganizationCreatedEvent>();
loading = true;
fetchingTaxAmount = false;
annualCadence = SubscriptionCadence.Annual;
monthlyCadence = SubscriptionCadence.Monthly;
@@ -73,6 +94,12 @@ export class TrialBillingStepComponent implements OnInit {
annualPlan?: PlanResponse;
monthlyPlan?: PlanResponse;
taxAmount = 0;
private destroy$ = new Subject<void>();
protected readonly SubscriptionProduct = SubscriptionProduct;
constructor(
private apiService: ApiService,
private i18nService: I18nService,
@@ -80,6 +107,7 @@ export class TrialBillingStepComponent implements OnInit {
private messagingService: MessagingService,
private organizationBillingService: OrganizationBillingService,
private toastService: ToastService,
private taxService: TaxServiceAbstraction,
) {}
async ngOnInit(): Promise<void> {
@@ -87,9 +115,26 @@ export class TrialBillingStepComponent implements OnInit {
this.applicablePlans = plans.data.filter(this.isApplicable);
this.annualPlan = this.findPlanFor(SubscriptionCadence.Annual);
this.monthlyPlan = this.findPlanFor(SubscriptionCadence.Monthly);
if (this.trialLength === 0) {
this.formGroup.controls.cadence.valueChanges
.pipe(
switchMap((cadence) => from(this.previewTaxAmount(cadence))),
takeUntil(this.destroy$),
)
.subscribe((taxAmount) => {
this.taxAmount = taxAmount;
});
}
this.loading = false;
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
async submit(): Promise<void> {
if (!this.taxInfoComponent.validate()) {
return;
@@ -115,7 +160,11 @@ export class TrialBillingStepComponent implements OnInit {
this.messagingService.send("organizationCreated", { organizationId });
}
protected changedCountry() {
async onTaxInformationChanged() {
if (this.trialLength === 0) {
this.taxAmount = await this.previewTaxAmount(this.formGroup.value.cadence);
}
this.paymentComponent.showBankAccount =
this.taxInfoComponent.getTaxInformation().country === "US";
if (
@@ -170,6 +219,7 @@ export class TrialBillingStepComponent implements OnInit {
const payment: PaymentInformation = {
paymentMethod,
billing: this.getBillingInformationFromTaxInfoComponent(),
skipTrial: this.trialLength === 0,
};
const response = await this.organizationBillingService.purchaseSubscription({
@@ -250,4 +300,45 @@ export class TrialBillingStepComponent implements OnInit {
const notDisabledOrLegacy = !plan.disabled && !plan.legacyYear;
return hasCorrectProductType && notDisabledOrLegacy;
}
private previewTaxAmount = async (cadence: SubscriptionCadence): Promise<number> => {
this.fetchingTaxAmount = true;
if (!this.taxInfoComponent.validate()) {
return 0;
}
const plan = this.findPlanFor(cadence);
const productType =
this.subscriptionProduct === SubscriptionProduct.PasswordManager
? ProductType.PasswordManager
: ProductType.SecretsManager;
const taxInformation = this.taxInfoComponent.getTaxInformation();
const request: PreviewTaxAmountForOrganizationTrialRequest = {
planType: plan.type,
productType,
taxInformation: {
...taxInformation,
},
};
const response = await this.taxService.previewTaxAmountForOrganizationTrial(request);
this.fetchingTaxAmount = false;
return response.taxAmount;
};
get price() {
return this.getPriceFor(this.formGroup.value.cadence);
}
get total() {
return this.price + this.taxAmount;
}
get interval() {
return this.formGroup.value.cadence === SubscriptionCadence.Annual ? "year" : "month";
}
}

View File

@@ -179,7 +179,7 @@ export class FreeBitwardenFamiliesComponent implements OnInit {
return;
}
await this.apiService.deleteRevokeSponsorship(this.organizationId);
await this.organizationSponsorshipApiService.deleteRevokeSponsorship(this.organizationId, true);
this.toastService.showToast({
variant: "success",

View File

@@ -74,11 +74,15 @@ type ChangePlanDialogParams = {
productTierType: ProductTierType;
};
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum ChangePlanDialogResultType {
Closed = "closed",
Submitted = "submitted",
}
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum PlanCardState {
Selected = "selected",
NotSelected = "not_selected",

View File

@@ -7,6 +7,8 @@ import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-conso
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
import { DialogConfig, DIALOG_DATA, DialogRef, DialogService } from "@bitwarden/components";
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum DownloadLicenseDialogResult {
Cancelled = "cancelled",
Downloaded = "downloaded",

View File

@@ -27,6 +27,8 @@ import { DialogService, ToastService } from "@bitwarden/components";
import { BillingSyncKeyComponent } from "./billing-sync-key.component";
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
enum LicenseOptions {
SYNC = 0,
UPLOAD = 1,

View File

@@ -112,13 +112,15 @@ export class SponsoredFamiliesComponent implements OnInit, OnDestroy {
});
}
});
this.anyOrgsAvailable$ = this.availableSponsorshipOrgs$.pipe(map((orgs) => orgs.length > 0));
this.activeSponsorshipOrgs$ = this.organizationService
.organizations$(userId)
.pipe(map((orgs) => orgs.filter((o) => o.familySponsorshipFriendlyName !== null)));
.pipe(
map((orgs) =>
orgs.filter((o) => o.familySponsorshipFriendlyName !== null && !o.isAdminInitiated),
),
);
this.anyActiveSponsorships$ = this.activeSponsorshipOrgs$.pipe(map((orgs) => orgs.length > 0));
this.loading = false;

View File

@@ -109,7 +109,7 @@ export class SponsoringOrgRowComponent implements OnInit {
return;
}
await this.apiService.deleteRevokeSponsorship(this.sponsoringOrg.id);
await this.organizationSponsorshipApiService.deleteRevokeSponsorship(this.sponsoringOrg.id);
this.toastService.showToast({
variant: "success",
title: null,

View File

@@ -21,6 +21,8 @@ export interface AddCreditDialogData {
organizationId: string;
}
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum AddCreditDialogResult {
Added = "added",
Cancelled = "cancelled",

View File

@@ -30,6 +30,8 @@ export interface AdjustPaymentDialogParams {
providerId?: string;
}
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum AdjustPaymentDialogResultType {
Closed = "closed",
Submitted = "submitted",

View File

@@ -22,6 +22,8 @@ export interface AdjustStorageDialogParams {
organizationId?: string;
}
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum AdjustStorageDialogResultType {
Submitted = "submitted",
Closed = "closed",

View File

@@ -25,6 +25,8 @@ type OrganizationOffboardingParams = {
export type OffboardingSurveyDialogParams = UserOffboardingParams | OrganizationOffboardingParams;
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum OffboardingSurveyDialogResultType {
Closed = "closed",
Submitted = "submitted",

View File

@@ -1,3 +1,5 @@
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum UpdateLicenseDialogResult {
Updated = "updated",
Cancelled = "cancelled",

View File

@@ -7,6 +7,8 @@ import { ReportUnsecuredWebsites } from "./icons/report-unsecured-websites.icon"
import { ReportWeakPasswords } from "./icons/report-weak-passwords.icon";
import { ReportEntry } from "./shared";
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum ReportType {
ExposedPasswords = "exposedPasswords",
ReusedPasswords = "reusedPasswords",

View File

@@ -1,3 +1,5 @@
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum ReportVariant {
Enabled = "Enabled",
RequiresPremium = "RequiresPremium",

View File

@@ -22,6 +22,7 @@ import { DangerZoneComponent } from "../auth/settings/account/danger-zone.compon
import { DeauthorizeSessionsComponent } from "../auth/settings/account/deauthorize-sessions.component";
import { DeleteAccountDialogComponent } from "../auth/settings/account/delete-account-dialog.component";
import { ProfileComponent } from "../auth/settings/account/profile.component";
import { SelectableAvatarComponent } from "../auth/settings/account/selectable-avatar.component";
import { EmergencyAccessConfirmComponent } from "../auth/settings/emergency-access/confirm/emergency-access-confirm.component";
import { EmergencyAccessAddEditComponent } from "../auth/settings/emergency-access/emergency-access-add-edit.component";
import { EmergencyAccessComponent } from "../auth/settings/emergency-access/emergency-access.component";
@@ -39,7 +40,6 @@ import { VerifyRecoverDeleteComponent } from "../auth/verify-recover-delete.comp
import { SponsoredFamiliesComponent } from "../billing/settings/sponsored-families.component";
import { SponsoringOrgRowComponent } from "../billing/settings/sponsoring-org-row.component";
import { DynamicAvatarComponent } from "../components/dynamic-avatar.component";
import { SelectableAvatarComponent } from "../components/selectable-avatar.component";
import { ExposedPasswordsReportComponent as OrgExposedPasswordsReportComponent } from "../dirt/reports/pages/organizations/exposed-passwords-report.component";
import { InactiveTwoFactorReportComponent as OrgInactiveTwoFactorReportComponent } from "../dirt/reports/pages/organizations/inactive-two-factor-report.component";
import { ReusedPasswordsReportComponent as OrgReusedPasswordsReportComponent } from "../dirt/reports/pages/organizations/reused-passwords-report.component";

View File

@@ -95,6 +95,8 @@ export interface VaultItemDialogParams {
restore?: (c: CipherView) => Promise<boolean>;
}
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum VaultItemDialogResult {
/**
* A cipher was saved (created or updated).

View File

@@ -26,6 +26,8 @@ export interface WebVaultGeneratorDialogResult {
generatedValue?: string;
}
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum WebVaultGeneratorDialogAction {
Selected = "selected",
Canceled = "canceled",

View File

@@ -35,6 +35,8 @@ import { WebCipherFormGenerationService } from "../services/web-cipher-form-gene
/**
* The result of the AddEditCipherDialogV2 component.
*/
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum AddEditCipherDialogResult {
Edited = "edited",
Added = "added",

View File

@@ -29,6 +29,8 @@ export interface BulkDeleteDialogParams {
unassignedCiphers?: string[];
}
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum BulkDeleteDialogResult {
Deleted = "deleted",
Canceled = "canceled",

View File

@@ -23,6 +23,8 @@ export interface BulkMoveDialogParams {
cipherIds?: string[];
}
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum BulkMoveDialogResult {
Moved = "moved",
Canceled = "canceled",

View File

@@ -113,6 +113,8 @@ export interface FolderAddEditDialogParams {
folderId: string;
}
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum FolderAddEditDialogResult {
Deleted = "deleted",
Canceled = "canceled",

View File

@@ -17,6 +17,8 @@ import { UserId } from "@bitwarden/common/types/guid";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { PBKDF2KdfConfig, KdfConfigService, KdfType } from "@bitwarden/key-management";
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum VisibleVaultBanner {
KDFSettings = "kdf-settings",
OutdatedBrowser = "outdated-browser",

View File

@@ -15,6 +15,8 @@ export type VaultFilterType =
| FolderFilter
| CollectionFilter;
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum VaultFilterLabel {
OrganizationFilter = "organizationFilter",
TypeFilter = "typeFilter",

View File

@@ -54,6 +54,8 @@ export interface ViewCipherDialogParams {
disableEdit?: boolean;
}
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum ViewCipherDialogResult {
Edited = "edited",
Deleted = "deleted",

View File

@@ -7,6 +7,8 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { VaultMessages } from "@bitwarden/common/vault/enums/vault-messages.enum";
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum BrowserPromptState {
Loading = "loading",
Error = "error",

View File

@@ -1,3 +1,5 @@
import "core-js/proposals/explicit-resource-management";
import { program } from "commander";
import { registerOssPrograms } from "@bitwarden/cli/register-oss-programs";

View File

@@ -149,6 +149,8 @@ export interface PasswordHealthReportApplicationsRequest {
url: string;
}
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum DrawerType {
None = 0,
AppAtRiskMembers = 1,

View File

@@ -50,7 +50,7 @@ describe("RiskInsightsReportService", () => {
let testCase = testCaseResults[0];
expect(testCase).toBeTruthy();
expect(testCase.cipherMembers).toHaveLength(2);
expect(testCase.trimmedUris).toHaveLength(3);
expect(testCase.trimmedUris).toHaveLength(2);
expect(testCase.weakPasswordDetail).toBeTruthy();
expect(testCase.exposedPasswordDetail).toBeTruthy();
expect(testCase.reusedPasswordCount).toEqual(2);
@@ -69,7 +69,7 @@ describe("RiskInsightsReportService", () => {
it("should generate the raw data + uri report correctly", async () => {
const result = await firstValueFrom(service.generateRawDataUriReport$("orgId"));
expect(result).toHaveLength(9);
expect(result).toHaveLength(8);
// Two ciphers that have google.com as their uri. There should be 2 results
const googleResults = result.filter((x) => x.trimmedUri === "google.com");
@@ -88,7 +88,7 @@ describe("RiskInsightsReportService", () => {
it("should generate applications health report data correctly", async () => {
const result = await firstValueFrom(service.generateApplicationsReport$("orgId"));
expect(result).toHaveLength(6);
expect(result).toHaveLength(5);
// Two ciphers have google.com associated with them. The first cipher
// has 2 members and the second has 4. However, the 2 members in the first
@@ -132,7 +132,7 @@ describe("RiskInsightsReportService", () => {
expect(reportSummary.totalMemberCount).toEqual(7);
expect(reportSummary.totalAtRiskMemberCount).toEqual(6);
expect(reportSummary.totalApplicationCount).toEqual(6);
expect(reportSummary.totalAtRiskApplicationCount).toEqual(5);
expect(reportSummary.totalApplicationCount).toEqual(5);
expect(reportSummary.totalAtRiskApplicationCount).toEqual(4);
});
});

View File

@@ -428,7 +428,7 @@ export class RiskInsightsReportService {
const cipherUris: string[] = [];
const uris = cipher.login?.uris ?? [];
uris.map((u: { uri: string }) => {
const uri = Utils.getHostname(u.uri).replace("www.", "");
const uri = Utils.getDomain(u.uri);
if (!cipherUris.includes(uri)) {
cipherUris.push(uri);
}

View File

@@ -25,6 +25,8 @@ export type AddEditMemberDialogParams = {
};
};
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum AddEditMemberDialogResultType {
Closed = "closed",
Deleted = "deleted",

View File

@@ -74,7 +74,7 @@ export class WebProviderService {
const [publicKey, encryptedPrivateKey] = await this.keyService.makeKeyPair(organizationKey);
const encryptedCollectionName = await this.encryptService.encrypt(
const encryptedCollectionName = await this.encryptService.encryptString(
this.i18nService.t("defaultCollection"),
organizationKey,
);

View File

@@ -18,6 +18,8 @@ export type AddExistingOrganizationDialogParams = {
provider: Provider;
};
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum AddExistingOrganizationDialogResultType {
Closed = "closed",
Submitted = "submitted",

View File

@@ -22,6 +22,8 @@ type CreateClientDialogParams = {
plans: PlanResponse[];
};
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum CreateClientDialogResultType {
Closed = "closed",
Submitted = "submitted",

View File

@@ -23,6 +23,8 @@ type ManageClientNameDialogParams = {
};
};
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum ManageClientNameDialogResultType {
Closed = "closed",
Submitted = "submitted",

View File

@@ -18,6 +18,8 @@ type ManageClientSubscriptionDialogParams = {
provider: Provider;
};
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum ManageClientSubscriptionDialogResultType {
Closed = "closed",
Submitted = "submitted",

View File

@@ -37,6 +37,8 @@ import { PasswordHealthMembersURIComponent } from "./password-health-members-uri
import { PasswordHealthMembersComponent } from "./password-health-members.component";
import { PasswordHealthComponent } from "./password-health.component";
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum RiskInsightsTabType {
AllApps = 0,
CriticalApps = 1,

View File

@@ -11,6 +11,8 @@ import { DialogRef, DIALOG_DATA, BitValidators, ToastService } from "@bitwarden/
import { ProjectView } from "../../models/view/project.view";
import { ProjectService } from "../../projects/project.service";
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum OperationType {
Add,
Edit,

View File

@@ -44,11 +44,15 @@ import { SecretService } from "../secret.service";
import { SecretDeleteDialogComponent, SecretDeleteOperation } from "./secret-delete.component";
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum OperationType {
Add,
Edit,
}
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum SecretDialogTabType {
NameValuePair = 0,
People = 1,

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