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

Merge remote-tracking branch 'origin' into auth/pm-17111/add-browser-to-list-of-approving-clients-v2

This commit is contained in:
Patrick Pimentel
2025-05-13 14:57:26 -04:00
300 changed files with 4302 additions and 1136 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

@@ -58,7 +58,7 @@ jobs:
run: npm test -- --coverage --maxWorkers=3
- name: Report test results
uses: dorny/test-reporter@31a54ee7ebcacc03a09ea97a7e5465a47b84aea5 # v1.9.1
uses: dorny/test-reporter@6e6a65b7a0bd2c9197df7d0ae36ac5cee784230c # v2.0.0
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && !cancelled() }}
with:
name: Test Results

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": {
@@ -4551,6 +4542,12 @@
"downloadFromBitwardenNow": {
"message": "Download from bitwarden.com now"
},
"getItOnGooglePlay": {
"message": "Get it on Google Play"
},
"downloadOnTheAppStore": {
"message": "Download on the App Store"
},
"permanentlyDeleteAttachmentConfirmation": {
"message": "Are you sure you want to permanently delete this attachment?"
},
@@ -5261,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"
},
@@ -5324,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

@@ -144,17 +144,17 @@ export const border = {
export const typography = {
body1: `
line-height: 24px;
font-family: "DM Sans", sans-serif;
font-family: Roboto, sans-serif;
font-size: 16px;
`,
body2: `
line-height: 20px;
font-family: "DM Sans", sans-serif;
font-family: Roboto, sans-serif;
font-size: 14px;
`,
helperMedium: `
line-height: 16px;
font-family: "DM Sans", sans-serif;
font-family: Roboto, sans-serif;
font-size: 12px;
`,
};

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,18 +64,23 @@ 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;
text-overflow: ellipsis;
line-height: 24px;
font-family: "DM Sans", sans-serif;
font-family: Roboto, sans-serif;
font-size: 16px;
`;
@@ -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

@@ -19,7 +19,7 @@ const notificationHeaderMessageStyles = (theme: Theme) => css`
line-height: 28px;
white-space: nowrap;
color: ${themes[theme].text.main};
font-family: "DM Sans", sans-serif;
font-family: Roboto, sans-serif;
font-size: 18px;
font-weight: 600;
`;

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"),
loginUpdateConfirmation: 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

@@ -1,6 +1,6 @@
$dark-icon-themes: "theme_dark";
$font-family-sans-serif: "DM Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
$font-family-sans-serif: Roboto, "Helvetica Neue", Helvetica, Arial, sans-serif;
$font-family-source-code-pro: "Source Code Pro", monospace;
$font-size-base: 14px;

View File

@@ -720,6 +720,7 @@ export default class MainBackground {
this.logService,
(logoutReason: LogoutReason, userId?: UserId) => this.logout(logoutReason, userId),
this.vaultTimeoutSettingsService,
{ createRequest: (url, request) => new Request(url, request) },
);
this.fileUploadService = new FileUploadService(this.logService, this.apiService);

View File

@@ -357,7 +357,7 @@ export class NativeMessagingBackground {
await this.secureCommunication();
}
return await this.encryptService.encrypt(
return await this.encryptService.encryptString(
JSON.stringify(message),
this.secureChannel!.sharedSecret!,
);
@@ -401,10 +401,9 @@ export class NativeMessagingBackground {
return;
}
message = JSON.parse(
await this.encryptService.decryptToUtf8(
await this.encryptService.decryptString(
rawMessage as EncString,
this.secureChannel.sharedSecret,
"ipc-desktop-ipc-channel-key",
),
);
} else {

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

@@ -1,5 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 100% 100%">
<text fill="%23333333" x="50%" y="50%" font-family="\'DM Sans\', \'Helvetica Neue\', Helvetica, Arial, sans-serif"
<text fill="%23333333" x="50%" y="50%" font-family="\'Roboto\', \'Helvetica Neue\', Helvetica, Arial, sans-serif"
font-size="18" text-anchor="middle">
Loading...
</text>

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

@@ -1,6 +1,6 @@
$dark-icon-themes: "theme_dark";
$font-family-sans-serif: "DM Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
$font-family-sans-serif: Roboto, "Helvetica Neue", Helvetica, Arial, sans-serif;
$font-family-monospace: Menlo, Monaco, Consolas, "Courier New", monospace;
$font-size-base: 16px;
$font-size-large: 18px;

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

@@ -2,7 +2,6 @@
<popup-header slot="header" pageTitle="{{ 'downloadBitwarden' | i18n }}" showBackButton>
<ng-container slot="end">
<app-pop-out></app-pop-out>
<app-current-account></app-current-account>
</ng-container>
</popup-header>
<h2 bitTypography="h6">
@@ -20,16 +19,30 @@
/>
</div>
<div class="tw-flex tw-justify-center tw-gap-4">
<div class="tw-w-[43%]">
<a href="https://apps.apple.com/app/bitwarden-password-manager/id1137397744">
<img class="tw-w-full" src="../../../images/app-store.png" alt="" />
</a>
</div>
<div class="tw-w-[43%]">
<a href="https://play.google.com/store/apps/details?id=com.x8bit.bitwarden">
<img class="tw-w-full" src="../../../images/google-play.png" alt="" />
</a>
</div>
<a
class="tw-w-[43%] !tw-py-0"
target="_blank"
href="https://apps.apple.com/app/bitwarden-password-manager/id1137397744"
bitLink
>
<img
class="tw-w-full"
src="../../../images/app-store.png"
alt="{{ 'downloadOnTheAppStore' | i18n }}"
/>
</a>
<a
class="tw-w-[43%] !tw-py-0"
target="_blank"
href="https://play.google.com/store/apps/details?id=com.x8bit.bitwarden"
bitLink
>
<img
class="tw-w-full"
src="../../../images/google-play.png"
alt="{{ 'getItOnGooglePlay' | i18n }}"
/>
</a>
</div>
</bit-card>
@@ -41,6 +54,7 @@
<a
class="tw-text-primary-600 tw-mt-4 tw-flex tw-no-underline tw-gap-2 tw-items-center"
href="https://bitwarden.com/download/#downloads-desktop"
bitLink
target="_blank"
>
{{ "downloadFromBitwardenNow" | i18n }}

View File

@@ -6,7 +6,7 @@ import { 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 { CardComponent, TypographyModule } from "@bitwarden/components";
import { CardComponent, LinkModule, TypographyModule } from "@bitwarden/components";
import { VaultNudgesService, VaultNudgeType } from "@bitwarden/vault";
import { CurrentAccountComponent } from "../../../auth/popup/account-switching/current-account.component";
@@ -27,6 +27,7 @@ import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.co
CardComponent,
TypographyModule,
CurrentAccountComponent,
LinkModule,
],
})
export class DownloadBitwardenComponent implements OnInit {

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

@@ -39,6 +39,7 @@ export class NodeApiService extends ApiService {
logService,
logoutCallback,
vaultTimeoutSettingsService,
{ createRequest: (url, request) => new Request(url, request) },
customUserAgent,
);
}

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

@@ -120,9 +120,9 @@ checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7"
[[package]]
name = "arboard"
version = "3.4.1"
version = "3.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df099ccb16cd014ff054ac1bf392c67feeef57164b05c42f037cd40f5d4357f4"
checksum = "c1df21f715862ede32a0c525ce2ca4d52626bb0007f8c18b87a384503ac33e70"
dependencies = [
"clipboard-win",
"log",
@@ -130,6 +130,7 @@ dependencies = [
"objc2-app-kit",
"objc2-foundation",
"parking_lot",
"percent-encoding",
"wl-clipboard-rs",
"x11rb",
]
@@ -465,15 +466,6 @@ dependencies = [
"generic-array",
]
[[package]]
name = "block2"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f"
dependencies = [
"objc2",
]
[[package]]
name = "blocking"
version = "1.6.1"
@@ -565,12 +557,6 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cfg_aliases"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
[[package]]
name = "cfg_aliases"
version = "0.2.1"
@@ -867,17 +853,6 @@ dependencies = [
"powerfmt",
]
[[package]]
name = "derive-new"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d150dea618e920167e5973d70ae6ece4385b7164e0d799fe7c122dd0a5d912ad"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "desktop_core"
version = "0.0.0"
@@ -1007,6 +982,16 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "dispatch2"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec"
dependencies = [
"bitflags",
"objc2",
]
[[package]]
name = "displaydoc"
version = "0.2.5"
@@ -1409,7 +1394,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bdbbd5bc8c5749697ccaa352fa45aff8730cf21c68029c0eef1ffed7c3d6ba2"
dependencies = [
"cfg-if",
"nix 0.29.0",
"nix",
"widestring",
"windows 0.57.0",
]
@@ -1839,18 +1824,6 @@ dependencies = [
"libloading",
]
[[package]]
name = "nix"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
dependencies = [
"bitflags",
"cfg-if",
"cfg_aliases 0.1.1",
"libc",
]
[[package]]
name = "nix"
version = "0.29.0"
@@ -1859,7 +1832,7 @@ checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
dependencies = [
"bitflags",
"cfg-if",
"cfg_aliases 0.2.1",
"cfg_aliases",
"libc",
"memoffset",
]
@@ -1990,47 +1963,24 @@ dependencies = [
"libc",
]
[[package]]
name = "objc-sys"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310"
[[package]]
name = "objc2"
version = "0.5.2"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804"
checksum = "88c6597e14493ab2e44ce58f2fdecf095a51f12ca57bec060a11c57332520551"
dependencies = [
"objc-sys",
"objc2-encode",
]
[[package]]
name = "objc2-app-kit"
version = "0.2.2"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff"
checksum = "e6f29f568bec459b0ddff777cec4fe3fd8666d82d5a40ebd0ff7e66134f89bcc"
dependencies = [
"bitflags",
"block2",
"libc",
"objc2",
"objc2-core-data",
"objc2-core-image",
"objc2-foundation",
"objc2-quartz-core",
]
[[package]]
name = "objc2-core-data"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef"
dependencies = [
"bitflags",
"block2",
"objc2",
"objc2-core-graphics",
"objc2-foundation",
]
@@ -2041,18 +1991,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166"
dependencies = [
"bitflags",
"dispatch2",
"objc2",
]
[[package]]
name = "objc2-core-image"
version = "0.2.2"
name = "objc2-core-graphics"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80"
checksum = "989c6c68c13021b5c2d6b71456ebb0f9dc78d752e86a98da7c716f4f9470f5a4"
dependencies = [
"block2",
"bitflags",
"dispatch2",
"objc2",
"objc2-foundation",
"objc2-metal",
"objc2-core-foundation",
"objc2-io-surface",
]
[[package]]
@@ -2063,14 +2016,13 @@ checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
[[package]]
name = "objc2-foundation"
version = "0.2.2"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8"
checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c"
dependencies = [
"bitflags",
"block2",
"libc",
"objc2",
"objc2-core-foundation",
]
[[package]]
@@ -2084,28 +2036,14 @@ dependencies = [
]
[[package]]
name = "objc2-metal"
version = "0.2.2"
name = "objc2-io-surface"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6"
checksum = "7282e9ac92529fa3457ce90ebb15f4ecbc383e8338060960760fa2cf75420c3c"
dependencies = [
"bitflags",
"block2",
"objc2",
"objc2-foundation",
]
[[package]]
name = "objc2-quartz-core"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a"
dependencies = [
"bitflags",
"block2",
"objc2",
"objc2-foundation",
"objc2-metal",
"objc2-core-foundation",
]
[[package]]
@@ -3487,9 +3425,9 @@ dependencies = [
[[package]]
name = "wayland-protocols"
version = "0.31.2"
version = "0.32.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4"
checksum = "0781cf46869b37e36928f7b432273c0995aa8aed9552c556fb18754420541efc"
dependencies = [
"bitflags",
"wayland-backend",
@@ -3499,9 +3437,9 @@ dependencies = [
[[package]]
name = "wayland-protocols-wlr"
version = "0.2.0"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad1f61b76b6c2d8742e10f9ba5c3737f6530b4c243132c2a2ccc8aa96fe25cd6"
checksum = "248a02e6f595aad796561fa82d25601bd2c8c3b145b1c7453fc8f94c1a58f8b2"
dependencies = [
"bitflags",
"wayland-backend",
@@ -3982,15 +3920,14 @@ dependencies = [
[[package]]
name = "wl-clipboard-rs"
version = "0.8.1"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12b41773911497b18ca8553c3daaf8ec9fe9819caf93d451d3055f69de028adb"
checksum = "2a083daad7e8a4b8805ad73947ccadabe62afe37ce0e9787a56ff373d34762c7"
dependencies = [
"derive-new",
"libc",
"log",
"nix 0.28.0",
"os_pipe",
"rustix",
"tempfile",
"thiserror 1.0.69",
"tree_magic_mini",
@@ -4085,7 +4022,7 @@ dependencies = [
"futures-sink",
"futures-util",
"hex",
"nix 0.29.0",
"nix",
"ordered-stream",
"rand 0.8.5",
"serde",
@@ -4115,7 +4052,7 @@ dependencies = [
"futures-core",
"futures-lite",
"hex",
"nix 0.29.0",
"nix",
"ordered-stream",
"serde",
"serde_repr",

View File

@@ -11,7 +11,7 @@ publish = false
[workspace.dependencies]
aes = "=0.8.4"
anyhow = "=1.0.94"
arboard = { version = "=3.4.1", default-features = false }
arboard = { version = "=3.5.0", default-features = false }
argon2 = "=0.5.3"
base64 = "=0.22.1"
bindgen = "0.71.1"

View File

@@ -17,7 +17,7 @@
"yargs": "17.7.2"
},
"devDependencies": {
"@types/node": "22.14.1",
"@types/node": "22.15.3",
"typescript": "5.4.2"
}
},
@@ -101,9 +101,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "22.14.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz",
"integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==",
"version": "22.15.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.3.tgz",
"integrity": "sha512-lX7HFZeHf4QG/J7tBZqrCAXwz9J5RD56Y6MpP0eJkka8p+K0RY/yBTW7CYFJ4VGCclxqOLKmiGP5juQc6MKgcw==",
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"

View File

@@ -22,7 +22,7 @@
"yargs": "17.7.2"
},
"devDependencies": {
"@types/node": "22.14.1",
"@types/node": "22.15.3",
"typescript": "5.4.2"
},
"_moduleAliases": {

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

@@ -220,7 +220,7 @@ export default class NativeMessageService {
const sharedKey = await this.getSharedKeyForKey(key);
return this.encryptService.encrypt(commandDataString, sharedKey);
return this.encryptService.encryptString(commandDataString, sharedKey);
}
private async decryptResponsePayload(
@@ -228,11 +228,7 @@ export default class NativeMessageService {
key: string,
): Promise<DecryptedCommandData> {
const sharedKey = await this.getSharedKeyForKey(key);
const decrypted = await this.encryptService.decryptToUtf8(
payload,
sharedKey,
"native-messaging-session",
);
const decrypted = await this.encryptService.decryptString(payload, sharedKey);
return JSON.parse(decrypted);
}

View File

@@ -113,7 +113,7 @@ export class AvatarComponent implements OnChanges, OnInit {
textTag.setAttribute("fill", Utils.pickTextColorBasedOnBgColor(color, 135, true));
textTag.setAttribute(
"font-family",
'"DM Sans","Helvetica Neue",Helvetica,Arial,' +
'Roboto,"Helvetica Neue",Helvetica,Arial,' +
'sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"',
);
textTag.textContent = character;

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,5 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 100% 100%">
<text fill="%23333333" x="50%" y="50%" font-family="\'DM Sans\', \'Helvetica Neue\', Helvetica, Arial, sans-serif"
<text fill="%23333333" x="50%" y="50%" font-family="\'Roboto\', \'Helvetica Neue\', Helvetica, Arial, sans-serif"
font-size="18" text-anchor="middle">
Loading...
</text>

View File

@@ -1,6 +1,6 @@
$dark-icon-themes: "theme_dark";
$font-family-sans-serif: "DM Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
$font-family-sans-serif: Roboto, "Helvetica Neue", Helvetica, Arial, sans-serif;
$font-family-monospace: Menlo, Monaco, Consolas, "Courier New", monospace;
$font-size-base: 14px;
$font-size-large: 18px;

View File

@@ -347,7 +347,7 @@ describe("BiometricMessageHandlerService", () => {
trusted: false,
}),
);
encryptService.decryptToUtf8.mockResolvedValue(
encryptService.decryptString.mockResolvedValue(
JSON.stringify({
command: "biometricUnlock",
messageId: 0,
@@ -382,7 +382,7 @@ describe("BiometricMessageHandlerService", () => {
ngZone.run.mockReturnValue({
closed: of(true),
});
encryptService.decryptToUtf8.mockResolvedValue(
encryptService.decryptString.mockResolvedValue(
JSON.stringify({
command: BiometricsCommands.UnlockWithBiometricsForUser,
messageId: 0,
@@ -433,7 +433,7 @@ describe("BiometricMessageHandlerService", () => {
ngZone.run.mockReturnValue({
closed: of(false),
});
encryptService.decryptToUtf8.mockResolvedValue(
encryptService.decryptString.mockResolvedValue(
JSON.stringify({
command: BiometricsCommands.UnlockWithBiometricsForUser,
messageId: 0,
@@ -480,7 +480,7 @@ describe("BiometricMessageHandlerService", () => {
trusted: true,
}),
);
encryptService.decryptToUtf8.mockResolvedValue(
encryptService.decryptString.mockResolvedValue(
JSON.stringify({
command: BiometricsCommands.UnlockWithBiometricsForUser,
messageId: 0,

View File

@@ -175,7 +175,7 @@ export class BiometricMessageHandlerService {
}
const message: LegacyMessage = JSON.parse(
await this.encryptService.decryptToUtf8(
await this.encryptService.decryptString(
rawMessage as EncString,
SymmetricCryptoKey.fromString(sessionSecret),
),
@@ -365,7 +365,7 @@ export class BiometricMessageHandlerService {
throw new Error("Session secret is missing");
}
const encrypted = await this.encryptService.encrypt(
const encrypted = await this.encryptService.encryptString(
JSON.stringify(message),
SymmetricCryptoKey.fromString(sessionSecret),
);

View File

@@ -168,7 +168,7 @@ export class DuckDuckGoMessageHandlerService {
payload: DecryptedCommandData,
key: SymmetricCryptoKey,
): Promise<EncString> {
return await this.encryptService.encrypt(JSON.stringify(payload), key);
return await this.encryptService.encryptString(JSON.stringify(payload), key);
}
private async decryptPayload(message: EncryptedMessage): Promise<DecryptedCommandData> {
@@ -188,10 +188,9 @@ export class DuckDuckGoMessageHandlerService {
}
try {
let decryptedResult = await this.encryptService.decryptToUtf8(
let decryptedResult = await this.encryptService.decryptString(
message.encryptedCommand as EncString,
this.duckduckgoSharedSecret,
"ddg-shared-key",
);
decryptedResult = this.trimNullCharsFromMessage(decryptedResult);

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

@@ -514,6 +514,9 @@ export class VaultV2Component implements OnInit, OnDestroy {
this.cipherId = cipher.id;
this.cipher = cipher;
await this.buildFormConfig("edit");
if (!cipher.edit && this.config) {
this.config.mode = "partial-edit";
}
this.action = "edit";
await this.go().catch(() => {});
}

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

@@ -148,7 +148,7 @@ export class StripeService {
base: {
color: null,
fontFamily:
'"DM Sans", "Helvetica Neue", Helvetica, Arial, sans-serif, ' +
'Roboto, "Helvetica Neue", Helvetica, Arial, sans-serif, ' +
'"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"',
fontSize: "16px",
fontSmoothing: "antialiased",

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",

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