1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-28 02:23:25 +00:00

Merge branch 'main' into km/test-arm64

This commit is contained in:
Bernd Schoolmann
2025-01-16 21:59:12 +01:00
committed by GitHub
168 changed files with 5653 additions and 1126 deletions

View File

@@ -4007,6 +4007,9 @@
"passkeyRemoved": {
"message": "Passkey removed"
},
"autofillSuggestions": {
"message": "Autofill suggestions"
},
"itemSuggestions": {
"message": "Suggested items"
},
@@ -4586,12 +4589,6 @@
"textSends": {
"message": "Text Sends"
},
"bitwardenNewLook": {
"message": "Bitwarden has a new look!"
},
"bitwardenNewLookDesc": {
"message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!"
},
"accountActions": {
"message": "Account actions"
},

View File

@@ -20,7 +20,7 @@
{{ biometricUnavailabilityReason }}
</bit-hint>
</bit-form-control>
<bit-form-control class="tw-pl-5" *ngIf="this.form.value.biometric">
<bit-form-control class="tw-pl-5" *ngIf="this.form.value.biometric && showAutoPrompt">
<input
bitCheckbox
id="autoBiometricsPrompt"

View File

@@ -29,6 +29,7 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { DeviceType } from "@bitwarden/common/enums";
import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -106,6 +107,7 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
hasVaultTimeoutPolicy = false;
biometricUnavailabilityReason: string;
showChangeMasterPass = true;
showAutoPrompt = true;
form = this.formBuilder.group({
vaultTimeout: [null as VaultTimeout | null],
@@ -141,6 +143,11 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
) {}
async ngOnInit() {
// Firefox popup closes when unfocused by biometrics, blocking all unlock methods
if (this.platformUtilsService.getDevice() === DeviceType.FirefoxExtension) {
this.showAutoPrompt = false;
}
const hasMasterPassword = await this.userVerificationService.hasMasterPassword();
this.showMasterPasswordOnClientRestartOption = hasMasterPassword;
const maximumVaultTimeoutPolicy = this.policyService.get$(PolicyType.MaximumVaultTimeout);
@@ -514,6 +521,8 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
try {
const userKey = await this.biometricsService.unlockWithBiometricsForUser(userId);
result = await this.keyService.validateUserKey(userKey, userId);
// FIXME: Remove when updating file. Eslint update
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
result = false;
}

View File

@@ -0,0 +1,67 @@
import { dirname, join } from "path";
import path from "path";
import type { StorybookConfig } from "@storybook/web-components-webpack5";
import TsconfigPathsPlugin from "tsconfig-paths-webpack-plugin";
import remarkGfm from "remark-gfm";
const getAbsolutePath = (value: string): string =>
dirname(require.resolve(join(value, "package.json")));
const config: StorybookConfig = {
stories: ["../lit-stories/**/*.lit-stories.@(js|jsx|ts|tsx)"],
addons: [
getAbsolutePath("@storybook/addon-links"),
getAbsolutePath("@storybook/addon-essentials"),
getAbsolutePath("@storybook/addon-a11y"),
getAbsolutePath("@storybook/addon-designs"),
getAbsolutePath("@storybook/addon-interactions"),
{
name: "@storybook/addon-docs",
options: {
mdxPluginOptions: {
mdxCompileOptions: {
remarkPlugins: [remarkGfm],
},
},
},
},
],
framework: {
name: getAbsolutePath("@storybook/web-components-webpack5"),
options: {
legacyRootApi: true,
},
},
core: {
disableTelemetry: true,
},
env: (existingConfig) => ({
...existingConfig,
FLAGS: JSON.stringify({}),
}),
webpackFinal: async (config) => {
if (config.resolve) {
config.resolve.plugins = [
new TsconfigPathsPlugin({
configFile: path.resolve(__dirname, "../../../../../tsconfig.json"),
}),
] as any;
}
if (config.module && config.module.rules) {
config.module.rules.push({
test: /\.(ts|tsx)$/,
exclude: /node_modules/,
use: [
{
loader: require.resolve("ts-loader"),
},
],
});
}
return config;
},
docs: {},
};
export default config;

View File

@@ -0,0 +1,34 @@
import { Meta, StoryObj } from "@storybook/web-components";
import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
import { ActionButton } from "../../buttons/action-button";
type Args = {
buttonText: string;
disabled: boolean;
theme: Theme;
buttonAction: (e: Event) => void;
};
export default {
title: "Components/Buttons/Action Button",
argTypes: {
buttonText: { control: "text" },
disabled: { control: "boolean" },
theme: { control: "select", options: [...Object.values(ThemeTypes)] },
buttonAction: { control: false },
},
args: {
buttonText: "Click Me",
disabled: false,
theme: ThemeTypes.Light,
buttonAction: () => alert("Clicked"),
},
} as Meta<Args>;
const Template = (args: Args) => ActionButton({ ...args });
export const Default: StoryObj<Args> = {
render: Template,
};

View File

@@ -0,0 +1,34 @@
import { Meta, StoryObj } from "@storybook/web-components";
import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
import { BadgeButton } from "../../buttons/badge-button";
type Args = {
buttonAction: (e: Event) => void;
buttonText: string;
disabled?: boolean;
theme: Theme;
};
export default {
title: "Components/Buttons/Badge Button",
argTypes: {
buttonText: { control: "text" },
disabled: { control: "boolean" },
theme: { control: "select", options: [...Object.values(ThemeTypes)] },
buttonAction: { control: false },
},
args: {
buttonText: "Click Me",
disabled: false,
theme: ThemeTypes.Light,
buttonAction: () => alert("Clicked"),
},
} as Meta<Args>;
const Template = (args: Args) => BadgeButton({ ...args });
export const Default: StoryObj<Args> = {
render: Template,
};

View File

@@ -0,0 +1,29 @@
import { Meta, StoryObj } from "@storybook/web-components";
import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
import { CloseButton } from "../../buttons/close-button";
type Args = {
handleCloseNotification: (e: Event) => void;
theme: Theme;
};
export default {
title: "Components/Buttons/Close Button",
argTypes: {
theme: { control: "select", options: [...Object.values(ThemeTypes)] },
handleCloseNotification: { control: false },
},
args: {
theme: ThemeTypes.Light,
handleCloseNotification: () => {
alert("Close button clicked!");
},
},
} as Meta<Args>;
const Template = (args: Args) => CloseButton({ ...args });
export const Default: StoryObj<Args> = {
render: Template,
};

View File

@@ -0,0 +1,33 @@
import { Meta, StoryObj } from "@storybook/web-components";
import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
import { EditButton } from "../../buttons/edit-button";
type Args = {
buttonAction: (e: Event) => void;
buttonText: string;
disabled?: boolean;
theme: Theme;
};
export default {
title: "Components/Buttons/Edit Button",
argTypes: {
buttonText: { control: "text" },
disabled: { control: "boolean" },
theme: { control: "select", options: [...Object.values(ThemeTypes)] },
buttonAction: { control: false },
},
args: {
buttonText: "Click Me",
disabled: false,
theme: ThemeTypes.Light,
buttonAction: () => alert("Clicked"),
},
} as Meta<Args>;
const Template = (args: Args) => EditButton({ ...args });
export const Default: StoryObj<Args> = {
render: Template,
};

View File

@@ -0,0 +1,36 @@
import { Meta, StoryObj } from "@storybook/web-components";
import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
import { NotificationTypes } from "../../../../notification/abstractions/notification-bar";
import { CipherAction } from "../../cipher/cipher-action";
type Args = {
handleAction?: (e: Event) => void;
notificationType: typeof NotificationTypes.Change | typeof NotificationTypes.Add;
theme: Theme;
};
export default {
title: "Components/Ciphers/Cipher Action",
argTypes: {
theme: { control: "select", options: [...Object.values(ThemeTypes)] },
notificationType: {
control: "select",
options: [NotificationTypes.Change, NotificationTypes.Add],
},
handleAction: { control: false },
},
args: {
theme: ThemeTypes.Light,
notificationType: NotificationTypes.Change,
handleAction: () => {
alert("Action triggered!");
},
},
} as Meta<Args>;
const Template = (args: Args) => CipherAction({ ...args });
export const Default: StoryObj<Args> = {
render: Template,
};

View File

@@ -0,0 +1,40 @@
import { Meta, StoryObj } from "@storybook/web-components";
import { html } from "lit";
import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
import { CipherIcon } from "../../cipher/cipher-icon";
type Args = {
color: string;
size: string;
theme: Theme;
uri?: string;
};
export default {
title: "Components/Ciphers/Cipher Icon",
argTypes: {
color: { control: "color" },
size: { control: "text" },
theme: { control: "select", options: [...Object.values(ThemeTypes)] },
uri: { control: "text" },
},
args: {
size: "50px",
theme: ThemeTypes.Light,
uri: "",
},
} as Meta<Args>;
const Template = (args: Args) => {
return html`
<div style="width: ${args.size}; height: ${args.size}; overflow: hidden;">
${CipherIcon({ ...args })}
</div>
`;
};
export const Default: StoryObj<Args> = {
render: Template,
};

View File

@@ -0,0 +1,33 @@
import { Meta, StoryObj } from "@storybook/web-components";
import { html } from "lit";
import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
import { CipherInfoIndicatorIcons } from "../../cipher/cipher-indicator-icons";
type Args = {
isBusinessOrg?: boolean;
isFamilyOrg?: boolean;
theme: Theme;
};
export default {
title: "Components/Ciphers/Cipher Indicator Icon",
argTypes: {
isBusinessOrg: { control: "boolean" },
isFamilyOrg: { control: "boolean" },
theme: { control: "select", options: [...Object.values(ThemeTypes)] },
},
args: {
theme: ThemeTypes.Light,
isBusinessOrg: true,
isFamilyOrg: false,
},
} as Meta<Args>;
const Template: StoryObj<Args>["render"] = (args) =>
html`<div>${CipherInfoIndicatorIcons({ ...args })}</div>`;
export const Default: StoryObj<Args> = {
render: Template,
};

View File

@@ -0,0 +1,66 @@
import { Meta, StoryObj } from "@storybook/web-components";
import { html } from "lit";
import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
import * as Icons from "../../icons";
type Args = {
color?: string;
disabled?: boolean;
theme: Theme;
size: number;
iconLink: URL;
};
export default {
title: "Components/Icons/Icons",
argTypes: {
iconLink: { control: "text" },
color: { control: "color" },
disabled: { control: "boolean" },
theme: { control: "select", options: [...Object.values(ThemeTypes)] },
size: { control: "number", min: 10, max: 100, step: 1 },
},
args: {
iconLink: new URL("https://bitwarden.com"),
disabled: false,
theme: ThemeTypes.Light,
size: 50,
},
} as Meta<Args>;
const Template = (args: Args, IconComponent: (props: Args) => ReturnType<typeof html>) => html`
<div
style="width: ${args.size}px; height: ${args.size}px; display: flex; align-items: center; justify-content: center;"
>
${IconComponent({ ...args })}
</div>
`;
const createIconStory = (iconName: keyof typeof Icons): StoryObj<Args> => {
const story = {
render: (args) => Template(args, Icons[iconName]),
} as StoryObj<Args>;
if (iconName !== "BrandIconContainer") {
story.argTypes = {
iconLink: { table: { disable: true } },
};
}
return story;
};
export const AngleDownIcon = createIconStory("AngleDown");
export const BusinessIcon = createIconStory("Business");
export const BrandIcon = createIconStory("BrandIconContainer");
export const CloseIcon = createIconStory("Close");
export const ExclamationTriangleIcon = createIconStory("ExclamationTriangle");
export const FamilyIcon = createIconStory("Family");
export const FolderIcon = createIconStory("Folder");
export const GlobeIcon = createIconStory("Globe");
export const PartyHornIcon = createIconStory("PartyHorn");
export const PencilSquareIcon = createIconStory("PencilSquare");
export const ShieldIcon = createIconStory("Shield");
export const UserIcon = createIconStory("User");

View File

@@ -0,0 +1,53 @@
import { Meta, StoryObj } from "@storybook/web-components";
import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
import { NotificationType } from "../../../../notification/abstractions/notification-bar";
import { CipherData } from "../../cipher/types";
import { NotificationBody } from "../../notification/body";
type Args = {
ciphers: CipherData[];
notificationType: NotificationType;
theme: Theme;
};
export default {
title: "Components/Notifications/Notification Body",
argTypes: {
ciphers: { control: "object" },
theme: { control: "select", options: [...Object.values(ThemeTypes)] },
notificationType: {
control: "select",
options: ["add", "change", "unlock", "fileless-import"],
},
},
args: {
ciphers: [
{
id: "1",
name: "Example Cipher",
type: CipherType.Login,
favorite: false,
reprompt: CipherRepromptType.None,
icon: {
imageEnabled: true,
image: "",
fallbackImage: "https://example.com/fallback.png",
icon: "icon-class",
},
login: { username: "user@example.com", passkey: null },
},
],
theme: ThemeTypes.Light,
notificationType: "change",
},
} as Meta<Args>;
const Template = (args: Args) => NotificationBody({ ...args });
export const Default: StoryObj<Args> = {
render: Template,
};

View File

@@ -0,0 +1,32 @@
import { Meta, StoryObj } from "@storybook/web-components";
import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
import { NotificationType } from "../../../../notification/abstractions/notification-bar";
import { NotificationFooter } from "../../notification/footer";
type Args = {
notificationType: NotificationType;
theme: Theme;
};
export default {
title: "Components/Notifications/Notification Footer",
argTypes: {
theme: { control: "select", options: [...Object.values(ThemeTypes)] },
notificationType: {
control: "select",
options: ["add", "change", "unlock", "fileless-import"],
},
},
args: {
theme: ThemeTypes.Light,
notificationType: "add",
},
} as Meta<Args>;
const Template = (args: Args) => NotificationFooter({ ...args });
export const Default: StoryObj<Args> = {
render: Template,
};

View File

@@ -0,0 +1,33 @@
import { Meta, StoryObj } from "@storybook/web-components";
import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
import { NotificationHeader } from "../../notification/header";
type Args = {
message: string;
standalone: boolean;
theme: Theme;
handleCloseNotification: (e: Event) => void;
};
export default {
title: "Components/Notifications/Notification Header",
argTypes: {
message: { control: "text" },
standalone: { control: "boolean" },
theme: { control: "select", options: [...Object.values(ThemeTypes)] },
},
args: {
message: "This is a notification message",
standalone: true,
theme: ThemeTypes.Light,
handleCloseNotification: () => alert("Close Clicked"),
},
} as Meta<Args>;
const Template = (args: Args) => NotificationHeader({ ...args });
export const Default: StoryObj<Args> = {
render: Template,
};

View File

@@ -0,0 +1,31 @@
import { Meta, StoryObj } from "@storybook/web-components";
import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
import { ActionRow } from "../../rows/action-row";
type Args = {
itemText: string;
handleAction: (e: Event) => void;
theme: Theme;
};
export default {
title: "Components/Rows/Action Row",
argTypes: {
itemText: { control: "text" },
theme: { control: "select", options: [...Object.values(ThemeTypes)] },
handleAction: { control: false },
},
args: {
itemText: "Action Item",
theme: ThemeTypes.Light,
handleAction: () => alert("Action triggered"),
},
} as Meta<Args>;
const Template = (args: Args) => ActionRow({ ...args });
export const Default: StoryObj<Args> = {
render: Template,
};

View File

@@ -0,0 +1,25 @@
import { Meta, StoryObj } from "@storybook/web-components";
import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
import { ButtonRow } from "../../rows/button-row";
type Args = {
theme: Theme;
};
export default {
title: "Components/Rows/Button Row",
argTypes: {
theme: { control: "select", options: [...Object.values(ThemeTypes)] },
},
args: {
theme: ThemeTypes.Light,
},
} as Meta<Args>;
const Template = (args: Args) => ButtonRow({ ...args });
export const Default: StoryObj<Args> = {
render: Template,
};

View File

@@ -0,0 +1,28 @@
import { Meta, StoryObj } from "@storybook/web-components";
import { TemplateResult } from "lit";
import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
import { ItemRow } from "../../rows/item-row";
type Args = {
theme: Theme;
children: TemplateResult | TemplateResult[];
};
export default {
title: "Components/Rows/Item Row",
argTypes: {
theme: { control: "select", options: [...Object.values(ThemeTypes)] },
children: { control: "object" },
},
args: {
theme: ThemeTypes.Light,
},
} as Meta<Args>;
const Template = (args: Args) => ItemRow({ ...args });
export const Default: StoryObj<Args> = {
render: Template,
};

View File

@@ -0,0 +1,7 @@
{
"name": "@bitwarden/lit-components",
"version": "2025.1.1",
"scripts": {
"storybook:lit": "storybook dev -p 6006 -c ./.lit-storybook"
}
}

View File

@@ -665,6 +665,7 @@ export default class MainBackground {
this.logService,
this.keyService,
this.biometricStateService,
this.messagingService,
);
this.appIdService = new AppIdService(this.storageService, this.logService);

View File

@@ -1,6 +1,7 @@
import { Injectable } from "@angular/core";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { UserId } from "@bitwarden/common/types/guid";
@@ -23,6 +24,7 @@ export class BackgroundBrowserBiometricsService extends BiometricsService {
private logService: LogService,
private keyService: KeyService,
private biometricStateService: BiometricStateService,
private messagingService: MessagingService,
) {
super();
}
@@ -96,8 +98,9 @@ export class BackgroundBrowserBiometricsService extends BiometricsService {
const userKey = new SymmetricCryptoKey(decodedUserkey) as UserKey;
if (await this.keyService.validateUserKey(userKey, userId)) {
await this.biometricStateService.setBiometricUnlockEnabled(true);
await this.biometricStateService.setFingerprintValidated(true);
this.keyService.setUserKey(userKey, userId);
await this.keyService.setUserKey(userKey, userId);
// to update badge and other things
this.messagingService.send("switchAccount", { userId });
return userKey;
}
} else {
@@ -114,8 +117,9 @@ export class BackgroundBrowserBiometricsService extends BiometricsService {
const userKey = new SymmetricCryptoKey(decodedUserkey) as UserKey;
if (await this.keyService.validateUserKey(userKey, userId)) {
await this.biometricStateService.setBiometricUnlockEnabled(true);
await this.biometricStateService.setFingerprintValidated(true);
this.keyService.setUserKey(userKey, userId);
await this.keyService.setUserKey(userKey, userId);
// to update badge and other things
this.messagingService.send("switchAccount", { userId });
return userKey;
}
} else {

View File

@@ -137,8 +137,8 @@ export class AppComponent implements OnInit, OnDestroy {
this.toastService._showToast(msg);
} else if (msg.command === "reloadProcess") {
if (this.platformUtilsService.isSafari()) {
window.setTimeout(() => {
this.biometricStateService.updateLastProcessReload();
window.setTimeout(async () => {
await this.biometricStateService.updateLastProcessReload();
window.location.reload();
}, 2000);
}

View File

@@ -1,7 +1,7 @@
<app-vault-list-items-container
*ngIf="autofillCiphers$ | async as ciphers"
[ciphers]="ciphers"
[title]="'itemSuggestions' | i18n"
[title]="((currentURIIsBlocked$ | async) ? 'itemSuggestions' : 'autofillSuggestions') | i18n"
[showRefresh]="showRefresh"
(onRefresh)="refreshCurrentTab()"
[description]="(showEmptyAutofillTip$ | async) ? ('autofillSuggestionsTip' | i18n) : null"

View File

@@ -65,6 +65,12 @@ export class AutofillVaultListItemsComponent implements OnInit {
),
);
/**
* Flag indicating that the current tab location is blocked
*/
currentURIIsBlocked$: Observable<boolean> =
this.vaultPopupAutofillService.currentTabIsOnBlocklist$;
constructor(
private vaultPopupItemsService: VaultPopupItemsService,
private vaultPopupAutofillService: VaultPopupAutofillService,

View File

@@ -152,7 +152,7 @@ describe("VaultHeaderV2Component", () => {
it("defaults the initial state to true", (done) => {
// The initial value of the `state$` variable above is undefined
component["initialDisclosureVisibility$"].subscribe((initialVisibility) => {
expect(initialVisibility).toBeTrue();
expect(initialVisibility).toBe(true);
done();
});

View File

@@ -1,79 +0,0 @@
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import {
ButtonModule,
DialogModule,
DialogService,
IconModule,
svgIcon,
} from "@bitwarden/components";
const announcementIcon = svgIcon`
<svg xmlns="http://www.w3.org/2000/svg" width="86" height="74" fill="none">
<g fill-rule="evenodd" clip-path="url(#a)" clip-rule="evenodd">
<path class="tw-fill-art-primary" d="m17.477 51.274 2.472 17.441a3.779 3.779 0 0 0 4.583 3.154l1.497-.342a3.779 3.779 0 0 0 2.759-4.831L23.44 49.91l1.8-.573 5.348 16.784a5.668 5.668 0 0 1-4.138 7.247l-1.497.341a5.668 5.668 0 0 1-6.874-4.73l-2.473-17.44 1.871-.266Z"/>
<path class="tw-fill-art-accent" d="m55.063 27.1-1.38.316-.211-.92 1.381-.316a3.306 3.306 0 0 1 3.96 2.486l1.052 4.605a3.306 3.306 0 0 1-2.487 3.96l-.92.21-.211-.92.92-.211a2.362 2.362 0 0 0 1.777-2.828l-1.052-4.605a2.362 2.362 0 0 0-2.829-1.777Z"/>
<path class="tw-fill-art-primary" d="M49.79 12.5a.18.18 0 0 0-.272-.11L21.855 29.438a.181.181 0 0 0-.058.055l-.208.323-10.947 2.5a.457.457 0 0 0-.139.064.664.664 0 0 0-.15.135.343.343 0 0 0-.06.095l.499 2.182-4.36.996c-1.873.428-3.086 2.465-2.64 4.417l1.5 6.566c.446 1.951 2.423 3.26 4.296 2.832l4.36-.996.499 2.182c.012.012.04.034.095.06a.658.658 0 0 0 .194.055c.07.009.122.004.152-.003l10.947-2.501.328.2a.18.18 0 0 0 .075.025l32.324 3.344a.18.18 0 0 0 .196-.218L49.79 12.5Zm-1.263-1.72a2.07 2.07 0 0 1 3.104 1.299L60.6 51.332a2.07 2.07 0 0 1-2.233 2.517l-32.323-3.343a2.072 2.072 0 0 1-.474-.106l-10.26 2.344a2.474 2.474 0 0 1-1.571-.184c-.463-.217-.973-.643-1.127-1.32l-.085-.37-2.518.576c-2.975.68-5.9-1.37-6.559-4.253l-1.5-6.566c-.659-2.883 1.086-6 4.061-6.68l2.518-.575-.084-.37c-.155-.677.12-1.282.442-1.678.325-.4.803-.727 1.334-.848l10.262-2.345c.113-.113.24-.214.38-.3l27.664-17.05Z"/>
<path class="tw-fill-art-accent" d="m10.792 34.793 3.156 13.814-.92.21L9.87 35.004l.921-.21ZM21.59 29.817l4.246 18.578-.508.12-.512.118L20.68 30.02l.91-.203Z"/>
<path class="tw-fill-art-primary" d="M64.287.59A.945.945 0 0 1 65.58.248c8.784 5.11 15.628 14.039 18.166 25.145 2.537 11.105.25 22.12-5.443 30.538a.945.945 0 0 1-1.565-1.059c5.398-7.98 7.587-18.46 5.166-29.058C79.48 15.215 72.958 6.726 64.629 1.882A.945.945 0 0 1 64.287.59Z"/>
<path class="tw-fill-art-accent" d="M61.6 6.385a.472.472 0 0 1 .643-.18c7.245 4.067 12.949 11.44 15.055 20.66s.171 18.338-4.588 25.149a.472.472 0 0 1-.774-.542c4.603-6.587 6.49-15.431 4.441-24.397-2.048-8.965-7.59-16.113-14.596-20.047a.472.472 0 0 1-.18-.643Z"/>
<path class="tw-fill-art-primary" d="M57.804 11.193a.472.472 0 0 1 .604-.285c6.11 2.186 11.426 8.739 13.364 17.22 1.938 8.48-.006 16.693-4.56 21.315a.472.472 0 1 1-.672-.663c4.27-4.335 6.197-12.187 4.311-20.442-1.886-8.254-7.032-14.49-12.761-16.54a.472.472 0 0 1-.286-.605Z"/>
</g>
<defs>
<clipPath id="a">
<path fill="#fff" d="M0 0h86v74H0z"/>
</clipPath>
</defs>
</svg>
`;
@Component({
standalone: true,
selector: "app-vault-ui-onboarding",
template: `
<bit-simple-dialog>
<div bitDialogIcon>
<bit-icon [icon]="icon"></bit-icon>
</div>
<span bitDialogTitle>
{{ "bitwardenNewLook" | i18n }}
</span>
<span bitDialogContent>
{{ "bitwardenNewLookDesc" | i18n }}
</span>
<ng-container bitDialogFooter>
<button
bitButton
type="button"
buttonType="primary"
(click)="navigateToLink()"
bitDialogClose
>
{{ "learnMore" | i18n }}
<i class="bwi bwi-external-link bwi-fw" aria-hidden="true"></i>
</button>
<button bitButton type="button" buttonType="secondary" bitDialogClose>
{{ "close" | i18n }}
</button>
</ng-container>
</bit-simple-dialog>
`,
imports: [CommonModule, DialogModule, ButtonModule, JslibModule, IconModule],
})
export class VaultUiOnboardingComponent {
icon = announcementIcon;
static open(dialogService: DialogService) {
return dialogService.open<boolean>(VaultUiOnboardingComponent);
}
navigateToLink = async () => {
window.open(
"https://bitwarden.com/blog/bringing-intuitive-workflows-and-visual-updates-to-the-bitwarden-browser/",
"_blank",
);
};
}

View File

@@ -19,7 +19,6 @@ import { PopupHeaderComponent } from "../../../../platform/popup/layout/popup-he
import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page.component";
import { VaultPopupItemsService } from "../../services/vault-popup-items.service";
import { VaultPopupListFiltersService } from "../../services/vault-popup-list-filters.service";
import { VaultUiOnboardingService } from "../../services/vault-ui-onboarding.service";
import { BlockedInjectionBanner } from "./blocked-injection-banner/blocked-injection-banner.component";
import {
@@ -58,7 +57,6 @@ enum VaultState {
VaultHeaderV2Component,
DecryptionFailureDialogComponent,
],
providers: [VaultUiOnboardingService],
})
export class VaultV2Component implements OnInit, OnDestroy {
cipherType = CipherType;
@@ -93,7 +91,6 @@ export class VaultV2Component implements OnInit, OnDestroy {
constructor(
private vaultPopupItemsService: VaultPopupItemsService,
private vaultPopupListFiltersService: VaultPopupListFiltersService,
private vaultUiOnboardingService: VaultUiOnboardingService,
private destroyRef: DestroyRef,
private cipherService: CipherService,
private dialogService: DialogService,
@@ -123,8 +120,6 @@ export class VaultV2Component implements OnInit, OnDestroy {
}
async ngOnInit() {
await this.vaultUiOnboardingService.showOnboardingDialog();
this.cipherService.failedToDecryptCiphers$
.pipe(
map((ciphers) => ciphers.filter((c) => !c.isDeleted)),

View File

@@ -179,7 +179,7 @@ describe("ViewV2Component", () => {
flush(); // Resolve all promises
expect(doAutofill).toHaveBeenCalledOnce();
expect(doAutofill).toHaveBeenCalledTimes(1);
}));
it('invokes `copy` when action="copy-username"', fakeAsync(() => {
@@ -187,7 +187,7 @@ describe("ViewV2Component", () => {
flush(); // Resolve all promises
expect(copy).toHaveBeenCalledOnce();
expect(copy).toHaveBeenCalledTimes(1);
}));
it('invokes `copy` when action="copy-password"', fakeAsync(() => {
@@ -195,7 +195,7 @@ describe("ViewV2Component", () => {
flush(); // Resolve all promises
expect(copy).toHaveBeenCalledOnce();
expect(copy).toHaveBeenCalledTimes(1);
}));
it('invokes `copy` when action="copy-totp"', fakeAsync(() => {
@@ -203,7 +203,7 @@ describe("ViewV2Component", () => {
flush(); // Resolve all promises
expect(copy).toHaveBeenCalledOnce();
expect(copy).toHaveBeenCalledTimes(1);
}));
it("closes the popout after a load action", fakeAsync(() => {
@@ -218,9 +218,9 @@ describe("ViewV2Component", () => {
flush(); // Resolve all promises
expect(doAutofill).toHaveBeenCalledOnce();
expect(doAutofill).toHaveBeenCalledTimes(1);
expect(focusSpy).toHaveBeenCalledWith(99);
expect(closeSpy).toHaveBeenCalledOnce();
expect(closeSpy).toHaveBeenCalledTimes(1);
}));
});
});

View File

@@ -127,6 +127,8 @@ export class VaultPopupAutofillService {
[currentTabHostname as string]: { bannerIsDismissed: true },
});
}
// FIXME: Remove when updating file. Eslint update
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
throw new Error(
"There was a problem dismissing the blocked interaction URI notification banner",

View File

@@ -488,7 +488,7 @@ describe("VaultPopupListFiltersService", () => {
state$.next(true);
service.filterVisibilityState$.subscribe((filterVisibility) => {
expect(filterVisibility).toBeTrue();
expect(filterVisibility).toBe(true);
done();
});
});
@@ -496,7 +496,7 @@ describe("VaultPopupListFiltersService", () => {
it("updates stored filter state", async () => {
await service.updateFilterVisibility(false);
expect(update).toHaveBeenCalledOnce();
expect(update).toHaveBeenCalledTimes(1);
// Get callback passed to `update`
const updateCallback = update.mock.calls[0][0];
expect(updateCallback()).toBe(false);

View File

@@ -1,89 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Injectable } from "@angular/core";
import { firstValueFrom, map } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import {
GlobalState,
KeyDefinition,
StateProvider,
VAULT_BROWSER_UI_ONBOARDING,
} from "@bitwarden/common/platform/state";
import { DialogService } from "@bitwarden/components";
import { VaultUiOnboardingComponent } from "../components/vault-v2/vault-ui-onboarding/vault-ui-onboarding.component";
// Key definition for the Vault UI onboarding state.
// This key is used to store the state of the new UI information dialog.
export const GLOBAL_VAULT_UI_ONBOARDING = new KeyDefinition<boolean>(
VAULT_BROWSER_UI_ONBOARDING,
"dialogState",
{
deserializer: (obj) => obj,
},
);
@Injectable()
export class VaultUiOnboardingService {
private onboardingUiReleaseDate = new Date("2024-12-10");
private vaultUiOnboardingState: GlobalState<boolean> = this.stateProvider.getGlobal(
GLOBAL_VAULT_UI_ONBOARDING,
);
private readonly vaultUiOnboardingState$ = this.vaultUiOnboardingState.state$.pipe(
map((x) => x ?? false),
);
constructor(
private stateProvider: StateProvider,
private dialogService: DialogService,
private apiService: ApiService,
) {}
/**
* Checks whether the onboarding dialog should be shown and opens it if necessary.
* The dialog is shown if the user has not previously viewed it and is not a new account.
*/
async showOnboardingDialog(): Promise<void> {
const hasViewedDialog = await this.getVaultUiOnboardingState();
if (!hasViewedDialog && !(await this.isNewAccount())) {
await this.openVaultUiOnboardingDialog();
}
}
private async openVaultUiOnboardingDialog(): Promise<boolean> {
const dialogRef = VaultUiOnboardingComponent.open(this.dialogService);
const result = firstValueFrom(dialogRef.closed);
// Update the onboarding state when the dialog is closed
await this.setVaultUiOnboardingState(true);
return result;
}
private async isNewAccount(): Promise<boolean> {
const userProfile = await this.apiService.getProfile();
const profileCreationDate = new Date(userProfile.creationDate);
return profileCreationDate > this.onboardingUiReleaseDate;
}
/**
* Updates and saves the state indicating whether the user has viewed
* the new UI onboarding information dialog.
*/
private async setVaultUiOnboardingState(value: boolean): Promise<void> {
await this.vaultUiOnboardingState.update(() => value);
}
/**
* Retrieves the current state indicating whether the user has viewed
* the new UI onboarding information dialog.s
*/
private async getVaultUiOnboardingState(): Promise<boolean> {
return await firstValueFrom(this.vaultUiOnboardingState$);
}
}

View File

@@ -63,7 +63,7 @@
"browser-hrtime": "1.1.8",
"chalk": "4.1.2",
"commander": "11.1.0",
"form-data": "4.0.0",
"form-data": "4.0.1",
"https-proxy-agent": "7.0.5",
"inquirer": "8.2.6",
"jsdom": "25.0.1",

View File

@@ -13,7 +13,7 @@ use windows::{
const CRED_FLAGS_NONE: u32 = 0;
pub async fn get_password<'a>(service: &str, account: &str) -> Result<String> {
pub async fn get_password(service: &str, account: &str) -> Result<String> {
let target_name = U16CString::from_str(target_name(service, account))?;
let mut credential: *mut CREDENTIALW = std::ptr::null_mut();

View File

@@ -30,21 +30,24 @@ void runSync(void* context, NSDictionary *params) {
[mappedCredentials addObject:credential];
}
if ([type isEqualToString:@"fido2"]) {
NSString *cipherId = credential[@"cipherId"];
NSString *rpId = credential[@"rpId"];
NSString *userName = credential[@"userName"];
NSData *credentialId = decodeBase64URL(credential[@"credentialId"]);
NSData *userHandle = decodeBase64URL(credential[@"userHandle"]);
if (@available(macos 14, *)) {
if ([type isEqualToString:@"fido2"]) {
NSString *cipherId = credential[@"cipherId"];
NSString *rpId = credential[@"rpId"];
NSString *userName = credential[@"userName"];
NSData *credentialId = decodeBase64URL(credential[@"credentialId"]);
NSData *userHandle = decodeBase64URL(credential[@"userHandle"]);
ASPasskeyCredentialIdentity *credential = [[ASPasskeyCredentialIdentity alloc]
initWithRelyingPartyIdentifier:rpId
userName:userName
credentialID:credentialId
userHandle:userHandle
recordIdentifier:cipherId];
Class passkeyCredentialIdentityClass = NSClassFromString(@"ASPasskeyCredentialIdentity");
id credential = [[passkeyCredentialIdentityClass alloc]
initWithRelyingPartyIdentifier:rpId
userName:userName
credentialID:credentialId
userHandle:userHandle
recordIdentifier:cipherId];
[mappedCredentials addObject:credential];
[mappedCredentials addObject:credential];
}
}
}

View File

@@ -1,7 +1,7 @@
{
"name": "@bitwarden/desktop",
"description": "A secure and free password manager for all of your devices.",
"version": "2025.1.1",
"version": "2025.1.3",
"keywords": [
"bitwarden",
"password",
@@ -35,7 +35,7 @@
"clean:dist": "rimraf ./dist",
"pack:dir": "npm run clean:dist && electron-builder --dir -p never",
"pack:lin:flatpak": "npm run clean:dist && electron-builder --dir -p never && flatpak-builder --repo=build/.repo build/.flatpak ./resources/com.bitwarden.desktop.devel.yaml --install-deps-from=flathub --force-clean && flatpak build-bundle ./build/.repo/ ./dist/com.bitwarden.desktop.flatpak com.bitwarden.desktop",
"pack:lin": "npm run clean:dist && electron-builder --linux --x64 -p never && export SNAP_FILE=$(realpath ./dist/bitwarden_*.snap) && unsquashfs -d ./dist/tmp-snap/ $SNAP_FILE && mkdir -p ./dist/tmp-snap/meta/polkit/ && cp ./resources/com.bitwarden.desktop.policy ./dist/tmp-snap/meta/polkit/polkit.com.bitwarden.desktop.policy && rm $SNAP_FILE && mksquashfs ./dist/tmp-snap/ $SNAP_FILE -noappend -comp lzo -no-fragments && rm -rf ./dist/tmp-snap/",
"pack:lin": "npm run clean:dist && electron-builder --linux --x64 -p never && export SNAP_FILE=$(realpath ./dist/bitwarden_*.snap) && unsquashfs -d ./dist/tmp-snap/ $SNAP_FILE && mkdir -p ./dist/tmp-snap/meta/polkit/ && cp ./resources/com.bitwarden.desktop.policy ./dist/tmp-snap/meta/polkit/polkit.com.bitwarden.desktop.policy && rm $SNAP_FILE && snapcraft pack ./dist/tmp-snap/ && mv ./*.snap ./dist/ && rm -rf ./dist/tmp-snap/",
"pack:lin:arm64": "npm run clean:dist && electron-builder --dir -p never && tar -czvf ./dist/bitwarden_desktop_arm64.tar.gz ./dist/linux-arm64-unpacked/",
"pack:mac": "npm run clean:dist && electron-builder --mac --universal -p never",
"pack:mac:arm64": "npm run clean:dist && electron-builder --mac --arm64 -p never",

View File

@@ -362,14 +362,24 @@ export class SettingsComponent implements OnInit, OnDestroy {
}
});
this.supportsBiometric =
(await this.biometricsService.getBiometricsStatus()) === BiometricsStatus.Available;
this.supportsBiometric = this.shouldAllowBiometricSetup(
await this.biometricsService.getBiometricsStatus(),
);
this.timerId = setInterval(async () => {
this.supportsBiometric =
(await this.biometricsService.getBiometricsStatus()) === BiometricsStatus.Available;
this.supportsBiometric = this.shouldAllowBiometricSetup(
await this.biometricsService.getBiometricsStatus(),
);
}, 1000);
}
private shouldAllowBiometricSetup(biometricStatus: BiometricsStatus): boolean {
return [
BiometricsStatus.Available,
BiometricsStatus.AutoSetupNeeded,
BiometricsStatus.ManualSetupNeeded,
].includes(biometricStatus);
}
async saveVaultTimeout(newValue: VaultTimeout) {
if (newValue === VaultTimeoutStringType.Never) {
const confirmed = await this.dialogService.openSimpleDialog({
@@ -650,7 +660,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
const skipSupportedPlatformCheck =
ipc.platform.allowBrowserintegrationOverride || ipc.platform.isDev;
if (skipSupportedPlatformCheck) {
if (!skipSupportedPlatformCheck) {
if (
ipc.platform.deviceType === DeviceType.MacOsDesktop &&
!this.platformUtilsService.isMacAppStore()

View File

@@ -117,15 +117,15 @@ describe("biometrics tests", function () {
const testCases = [
// happy path
[true, false, false, BiometricsStatus.Available],
[false, true, true, BiometricsStatus.AutoSetupNeeded],
[false, true, false, BiometricsStatus.ManualSetupNeeded],
[false, false, false, BiometricsStatus.HardwareUnavailable],
[false, true, true, BiometricsStatus.HardwareUnavailable],
[true, true, true, BiometricsStatus.AutoSetupNeeded],
[true, true, false, BiometricsStatus.ManualSetupNeeded],
// should not happen
[false, false, true, BiometricsStatus.HardwareUnavailable],
[true, true, true, BiometricsStatus.Available],
[true, true, false, BiometricsStatus.Available],
[true, false, true, BiometricsStatus.Available],
[false, true, false, BiometricsStatus.HardwareUnavailable],
[false, false, false, BiometricsStatus.HardwareUnavailable],
];
for (const [supportsBiometric, needsSetup, canAutoSetup, expected] of testCases) {

View File

@@ -60,6 +60,8 @@ export class MainBiometricsService extends DesktopBiometricsService {
*/
async getBiometricsStatus(): Promise<BiometricsStatus> {
if (!(await this.osBiometricsService.osSupportsBiometric())) {
return BiometricsStatus.HardwareUnavailable;
} else {
if (await this.osBiometricsService.osBiometricsNeedsSetup()) {
if (await this.osBiometricsService.osBiometricsCanAutoSetup()) {
return BiometricsStatus.AutoSetupNeeded;
@@ -67,8 +69,6 @@ export class MainBiometricsService extends DesktopBiometricsService {
return BiometricsStatus.ManualSetupNeeded;
}
}
return BiometricsStatus.HardwareUnavailable;
}
return BiometricsStatus.Available;
}

View File

@@ -1,12 +1,12 @@
{
"name": "@bitwarden/desktop",
"version": "2025.1.1",
"version": "2025.1.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@bitwarden/desktop",
"version": "2025.1.1",
"version": "2025.1.3",
"license": "GPL-3.0",
"dependencies": {
"@bitwarden/desktop-napi": "file:../desktop_native/napi"

View File

@@ -2,7 +2,7 @@
"name": "@bitwarden/desktop",
"productName": "Bitwarden",
"description": "A secure and free password manager for all of your devices.",
"version": "2025.1.1",
"version": "2025.1.3",
"author": "Bitwarden Inc. <hello@bitwarden.com> (https://bitwarden.com)",
"homepage": "https://bitwarden.com",
"license": "GPL-3.0",

View File

@@ -77,10 +77,7 @@ export class OrganizationLayoutComponent implements OnInit {
filter((org) => org != null),
);
this.canAccessExport$ = combineLatest([
this.organization$,
this.configService.getFeatureFlag$(FeatureFlag.PM11360RemoveProviderExportPermission),
]).pipe(map(([org, removeProviderExport]) => org.canAccessExport(removeProviderExport)));
this.canAccessExport$ = this.organization$.pipe(map((org) => org.canAccessExport));
this.showPaymentAndHistory$ = this.organization$.pipe(
map(

View File

@@ -1,13 +1,8 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { inject, NgModule } from "@angular/core";
import { CanMatchFn, RouterModule, Routes } from "@angular/router";
import { map } from "rxjs";
import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
import { canAccessSettingsTab } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { organizationPermissionsGuard } from "../../organizations/guards/org-permissions.guard";
import { organizationRedirectGuard } from "../../organizations/guards/org-redirect.guard";
@@ -16,11 +11,6 @@ import { PoliciesComponent } from "../../organizations/policies";
import { AccountComponent } from "./account.component";
import { TwoFactorSetupComponent } from "./two-factor-setup.component";
const removeProviderExportPermission$: CanMatchFn = () =>
inject(ConfigService)
.getFeatureFlag$(FeatureFlag.PM11360RemoveProviderExportPermission)
.pipe(map((removeProviderExport) => removeProviderExport === true));
const routes: Routes = [
{
path: "",
@@ -68,27 +58,13 @@ const routes: Routes = [
titleId: "importData",
},
},
// Export routing is temporarily duplicated to set the flag value passed into org.canAccessExport
{
path: "export",
loadComponent: () =>
import("../tools/vault-export/org-vault-export.component").then(
(mod) => mod.OrganizationVaultExportComponent,
),
canMatch: [removeProviderExportPermission$], // if this matches, the flag is ON
canActivate: [organizationPermissionsGuard((org) => org.canAccessExport(true))],
data: {
titleId: "exportVault",
},
},
{
path: "export",
loadComponent: () =>
import("../tools/vault-export/org-vault-export.component").then(
(mod) => mod.OrganizationVaultExportComponent,
),
canActivate: [organizationPermissionsGuard((org) => org.canAccessExport(false))],
canActivate: [organizationPermissionsGuard((org) => org.canAccessExport)],
data: {
titleId: "exportVault",
},
@@ -118,7 +94,8 @@ function getSettingsRoute(organization: Organization) {
if (organization.canManageDeviceApprovals) {
return "device-approvals";
}
return undefined;
return "/";
}
@NgModule({

View File

@@ -1062,7 +1062,11 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
}
private refreshSalesTax(): void {
if (!this.taxInformation.country || !this.taxInformation.postalCode) {
if (
this.taxInformation === undefined ||
!this.taxInformation.country ||
!this.taxInformation.postalCode
) {
return;
}

View File

@@ -193,7 +193,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
this.billing = await this.organizationApiService.getBilling(this.organizationId);
this.sub = await this.organizationApiService.getSubscription(this.organizationId);
this.taxInformation = await this.organizationApiService.getTaxInfo(this.organizationId);
} else {
} else if (!this.selfHosted) {
this.taxInformation = await this.apiService.getTaxInfo();
}

View File

@@ -157,7 +157,6 @@ export const SMAvailable: Story = {
canManageUsers: false,
canAccessSecretsManager: true,
enabled: true,
canAccessExport: (_) => false,
},
] as Organization[],
mockProviders: [],
@@ -173,7 +172,6 @@ export const SMAndACAvailable: Story = {
canManageUsers: true,
canAccessSecretsManager: true,
enabled: true,
canAccessExport: (_) => false,
},
] as Organization[],
mockProviders: [],
@@ -189,7 +187,6 @@ export const WithAllOptions: Story = {
canManageUsers: true,
canAccessSecretsManager: true,
enabled: true,
canAccessExport: (_) => false,
},
] as Organization[],
mockProviders: [{ id: "provider-a" }] as Provider[],

View File

@@ -176,7 +176,6 @@ export const WithSM: Story = {
canManageUsers: false,
canAccessSecretsManager: true,
enabled: true,
canAccessExport: (_) => false,
},
] as Organization[],
mockProviders: [],
@@ -192,7 +191,6 @@ export const WithSMAndAC: Story = {
canManageUsers: true,
canAccessSecretsManager: true,
enabled: true,
canAccessExport: (_) => false,
},
] as Organization[],
mockProviders: [],
@@ -208,7 +206,6 @@ export const WithAllOptions: Story = {
canManageUsers: true,
canAccessSecretsManager: true,
enabled: true,
canAccessExport: (_) => false,
},
] as Organization[],
mockProviders: [{ id: "provider-a" }] as Provider[],

View File

@@ -116,7 +116,6 @@ describe("ProductSwitcherService", () => {
id: "1234",
canAccessSecretsManager: true,
enabled: true,
canAccessExport: (_) => true,
},
] as Organization[]);
@@ -232,14 +231,12 @@ describe("ProductSwitcherService", () => {
canAccessSecretsManager: true,
enabled: true,
name: "Org 2",
canAccessExport: (_) => true,
},
{
id: "4243",
canAccessSecretsManager: true,
enabled: true,
name: "Org 32",
canAccessExport: (_) => true,
},
] as Organization[]);

View File

@@ -122,6 +122,39 @@
}
}
},
"atRiskApplicationsWithCount": {
"message": "At-risk applications ($COUNT$)",
"placeholders": {
"count": {
"content": "$1",
"example": "3"
}
}
},
"atRiskMembersDescription": {
"message": "These members are logging into applications with weak, exposed, or reused passwords."
},
"atRiskApplicationsDescription": {
"message": "These applications have weak, exposed, or reused passwords."
},
"atRiskMembersDescriptionWithApp": {
"message": "These members are logging into $APPNAME$ with weak, exposed, or reused passwords.",
"placeholders": {
"appname": {
"content": "$1",
"example": "Salesforce"
}
}
},
"atRiskMembersWithCount": {
"message": "At-risk members ($COUNT$)",
"placeholders": {
"count": {
"content": "$1",
"example": "3"
}
}
},
"atRiskMembersDescription": {
"message": "These members are logging into applications with weak, exposed, or reused passwords."
},
@@ -8256,33 +8289,33 @@
"trustedDevices": {
"message": "Trusted devices"
},
"memberDecryptionOptionTdeDescriptionPartOne": {
"message": "Once authenticated, members will decrypt vault data using a key stored on their device. The",
"description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'"
"memberDecryptionOptionTdeDescPart1": {
"message": "Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The",
"description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'"
},
"memberDecryptionOptionTdeDescriptionLinkOne": {
"memberDecryptionOptionTdeDescLink1": {
"message": "single organization",
"description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'"
"description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'"
},
"memberDecryptionOptionTdeDescriptionPartTwo": {
"memberDecryptionOptionTdeDescPart2": {
"message": "policy,",
"description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'"
"description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'"
},
"memberDecryptionOptionTdeDescriptionLinkTwo": {
"memberDecryptionOptionTdeDescLink2": {
"message": "SSO required",
"description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'"
"description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'"
},
"memberDecryptionOptionTdeDescriptionPartThree": {
"memberDecryptionOptionTdeDescPart3": {
"message": "policy, and",
"description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'"
"description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'"
},
"memberDecryptionOptionTdeDescriptionLinkThree": {
"memberDecryptionOptionTdeDescLink3": {
"message": "account recovery administration",
"description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'"
"description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'"
},
"memberDecryptionOptionTdeDescriptionPartFour": {
"message": "policy with automatic enrollment will turn on when this option is used.",
"description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'"
"memberDecryptionOptionTdeDescPart4": {
"message": "policy will turn on when this option is used.",
"description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'"
},
"orgPermissionsUpdatedMustSetPassword": {
"message": "Your organization permissions were updated, requiring you to set a master password.",