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:
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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");
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "@bitwarden/lit-components",
|
||||
"version": "2025.1.1",
|
||||
"scripts": {
|
||||
"storybook:lit": "storybook dev -p 6006 -c ./.lit-storybook"
|
||||
}
|
||||
}
|
||||
@@ -665,6 +665,7 @@ export default class MainBackground {
|
||||
this.logService,
|
||||
this.keyService,
|
||||
this.biometricStateService,
|
||||
this.messagingService,
|
||||
);
|
||||
|
||||
this.appIdService = new AppIdService(this.storageService, this.logService);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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)),
|
||||
|
||||
@@ -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);
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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$);
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
4
apps/desktop/src/package-lock.json
generated
4
apps/desktop/src/package-lock.json
generated
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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[],
|
||||
|
||||
@@ -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[],
|
||||
|
||||
@@ -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[]);
|
||||
|
||||
|
||||
@@ -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.",
|
||||
|
||||
Reference in New Issue
Block a user