mirror of
https://github.com/bitwarden/browser
synced 2026-02-08 20:50:28 +00:00
Merge branch 'main' into auth/pm-9115/implement-view-data-persistence-in-2FA-flows
This commit is contained in:
@@ -1071,6 +1071,10 @@
|
||||
},
|
||||
"description": "Aria label for the view button in notification bar confirmation message"
|
||||
},
|
||||
"notificationEditTooltip": {
|
||||
"message": "Edit before saving",
|
||||
"description": "Tooltip and Aria label for edit button on cipher item"
|
||||
},
|
||||
"newNotification": {
|
||||
"message": "New notification"
|
||||
},
|
||||
@@ -1110,12 +1114,12 @@
|
||||
"message": "Update login",
|
||||
"description": "Button text for updating an existing login entry."
|
||||
},
|
||||
"saveLoginPrompt": {
|
||||
"message": "Save login?",
|
||||
"saveLogin": {
|
||||
"message": "Save login",
|
||||
"description": "Prompt asking the user if they want to save their login details."
|
||||
},
|
||||
"updateLoginPrompt": {
|
||||
"message": "Update existing login?",
|
||||
"updateLogin": {
|
||||
"message": "Update existing login",
|
||||
"description": "Prompt asking the user if they want to update an existing login entry."
|
||||
},
|
||||
"loginSaveSuccess": {
|
||||
@@ -1128,24 +1132,24 @@
|
||||
},
|
||||
"loginUpdateTaskSuccess": {
|
||||
"message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1"
|
||||
}
|
||||
},
|
||||
"description": "Shown to user after login is updated."
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1"
|
||||
}
|
||||
},
|
||||
"description": "Shown to user after login is updated."
|
||||
},
|
||||
"loginUpdateTaskSuccessAdditional": {
|
||||
"message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1"
|
||||
},
|
||||
"task_count": {
|
||||
"content": "$2"
|
||||
}
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1"
|
||||
},
|
||||
"description": "Shown to user after login is updated."
|
||||
"task_count": {
|
||||
"content": "$2"
|
||||
}
|
||||
},
|
||||
"description": "Shown to user after login is updated."
|
||||
},
|
||||
"nextSecurityTaskAction": {
|
||||
"message": "Change next password",
|
||||
@@ -2518,8 +2522,8 @@
|
||||
"example": "Acme Corp"
|
||||
},
|
||||
"count": {
|
||||
"content": "$2",
|
||||
"example": "2"
|
||||
"content": "$2",
|
||||
"example": "2"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -5224,4 +5228,4 @@
|
||||
"secureDevicesBody": {
|
||||
"message": "Save unlimited passwords across unlimited devices with Bitwarden mobile, browser, and desktop apps."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -163,6 +163,7 @@ export default class NotificationBackground {
|
||||
* Gets the current active tab and retrieves the relevant decrypted cipher
|
||||
* for the tab's URL. It constructs and returns an array of `NotificationCipherData` objects or a singular object.
|
||||
* If no active tab or URL is found, it returns an empty array.
|
||||
* If new login, returns a preview of the cipher.
|
||||
*
|
||||
* @returns {Promise<NotificationCipherData[]>}
|
||||
*/
|
||||
@@ -175,53 +176,94 @@ export default class NotificationBackground {
|
||||
firstValueFrom(this.accountService.activeAccount$.pipe(getOptionalUserId)),
|
||||
]);
|
||||
|
||||
if (!currentTab?.url || !activeUserId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const [decryptedCiphers, organizations] = await Promise.all([
|
||||
this.cipherService.getAllDecryptedForUrl(currentTab?.url, activeUserId),
|
||||
this.cipherService.getAllDecryptedForUrl(currentTab.url, activeUserId),
|
||||
firstValueFrom(this.organizationService.organizations$(activeUserId)),
|
||||
]);
|
||||
|
||||
const iconsServerUrl = env.getIconsUrl();
|
||||
|
||||
const toNotificationData = (view: CipherView): NotificationCipherData => {
|
||||
const { id, name, reprompt, favorite, login, organizationId } = view;
|
||||
const getOrganizationType = (orgId?: string) =>
|
||||
organizations.find((org) => org.id === orgId)?.productTierType;
|
||||
|
||||
const type = organizations.find((org) => org.id === organizationId)?.productTierType;
|
||||
const cipherQueueMessage = this.notificationQueue.find(
|
||||
(message): message is AddChangePasswordQueueMessage | AddLoginQueueMessage =>
|
||||
message.type === NotificationQueueMessageType.ChangePassword ||
|
||||
message.type === NotificationQueueMessageType.AddLogin,
|
||||
);
|
||||
|
||||
const organizationCategories: OrganizationCategory[] = [];
|
||||
if (cipherQueueMessage) {
|
||||
const cipherView =
|
||||
cipherQueueMessage.type === NotificationQueueMessageType.ChangePassword
|
||||
? await this.getDecryptedCipherById(cipherQueueMessage.cipherId, activeUserId)
|
||||
: this.convertAddLoginQueueMessageToCipherView(cipherQueueMessage);
|
||||
|
||||
const organizationType = getOrganizationType(cipherView.organizationId);
|
||||
return [
|
||||
this.convertToNotificationCipherData(
|
||||
cipherView,
|
||||
iconsServerUrl,
|
||||
showFavicons,
|
||||
organizationType,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
return decryptedCiphers.map((view) =>
|
||||
this.convertToNotificationCipherData(
|
||||
view,
|
||||
iconsServerUrl,
|
||||
showFavicons,
|
||||
getOrganizationType(view.organizationId),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a CipherView and organization type into a NotificationCipherData object
|
||||
* for use in the notification bar.
|
||||
*
|
||||
* @returns A NotificationCipherData object containing the relevant cipher information.
|
||||
*/
|
||||
|
||||
convertToNotificationCipherData(
|
||||
view: CipherView,
|
||||
iconsServerUrl: string,
|
||||
showFavicons: boolean,
|
||||
organizationType?: ProductTierType,
|
||||
): NotificationCipherData {
|
||||
const { id, name, reprompt, favorite, login } = view;
|
||||
|
||||
const organizationCategories: OrganizationCategory[] = [];
|
||||
|
||||
if (organizationType != null) {
|
||||
if (
|
||||
[ProductTierType.Teams, ProductTierType.Enterprise, ProductTierType.TeamsStarter].includes(
|
||||
type,
|
||||
organizationType,
|
||||
)
|
||||
) {
|
||||
organizationCategories.push(OrganizationCategories.business);
|
||||
}
|
||||
if ([ProductTierType.Families, ProductTierType.Free].includes(type)) {
|
||||
|
||||
if ([ProductTierType.Families, ProductTierType.Free].includes(organizationType)) {
|
||||
organizationCategories.push(OrganizationCategories.family);
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
type: CipherType.Login,
|
||||
reprompt,
|
||||
favorite,
|
||||
...(organizationCategories.length ? { organizationCategories } : {}),
|
||||
icon: buildCipherIcon(iconsServerUrl, view, showFavicons),
|
||||
login: login && { username: login.username },
|
||||
};
|
||||
};
|
||||
|
||||
const changeItem = this.notificationQueue.find(
|
||||
(message): message is AddChangePasswordQueueMessage =>
|
||||
message.type === NotificationQueueMessageType.ChangePassword,
|
||||
);
|
||||
|
||||
if (changeItem) {
|
||||
const cipherView = await this.getDecryptedCipherById(changeItem.cipherId, activeUserId);
|
||||
return [toNotificationData(cipherView)];
|
||||
}
|
||||
|
||||
return decryptedCiphers.map(toNotificationData);
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
type: CipherType.Login,
|
||||
reprompt,
|
||||
favorite,
|
||||
...(organizationCategories.length ? { organizationCategories } : {}),
|
||||
icon: buildCipherIcon(iconsServerUrl, view, showFavicons),
|
||||
login: login && { username: login.username },
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -21,6 +21,7 @@ export function EditButton({
|
||||
<button
|
||||
type="button"
|
||||
title=${buttonText}
|
||||
aria-label=${buttonText}
|
||||
class=${editButtonStyles({ disabled, theme })}
|
||||
@click=${(event: Event) => {
|
||||
if (!disabled) {
|
||||
|
||||
@@ -25,7 +25,7 @@ export function CipherAction({
|
||||
})
|
||||
: EditButton({
|
||||
buttonAction: handleAction,
|
||||
buttonText: i18n.notificationEdit,
|
||||
buttonText: i18n.notificationEditTooltip,
|
||||
theme,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ export function PencilSquare({ color, disabled, theme }: IconProps) {
|
||||
const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main;
|
||||
|
||||
return html`
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15" fill="none">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15" fill="none" aria-hidden="true">
|
||||
<path
|
||||
class=${css(buildIconColorRule(shapeColor, ruleNames.fill))}
|
||||
d="M11.013.677a1.75 1.75 0 0 1 2.474 0l.836.836a1.75 1.75 0 0 1 0 2.475L9.03 9.28a.75.75 0 0 1-.348.197l-3 .75a.75.75 0 0 1-.91-.91l.75-3a.75.75 0 0 1 .198-.348L11.013.677Zm1.414 1.06a.25.25 0 0 0-.354 0l-.646.647a.75.75 0 0 1 .103.086l1 1a.751.751 0 0 1 .087.103l.646-.646a.25.25 0 0 0 0-.353l-.836-.836Zm-.854 2.88a.752.752 0 0 1-.103-.087l-1-1a.756.756 0 0 1-.087-.103L6.928 6.884 6.531 8.47l1.586-.397 3.456-3.456Z"
|
||||
|
||||
@@ -96,9 +96,9 @@ const notificationContainerStyles = (theme: Theme) => css`
|
||||
function getHeaderMessage(i18n: { [key: string]: string }, type?: NotificationType) {
|
||||
switch (type) {
|
||||
case NotificationTypes.Add:
|
||||
return i18n.saveAsNewLoginAction;
|
||||
return i18n.saveLogin;
|
||||
case NotificationTypes.Change:
|
||||
return i18n.updateLoginPrompt;
|
||||
return i18n.updateLogin;
|
||||
case NotificationTypes.Unlock:
|
||||
return "";
|
||||
default:
|
||||
|
||||
@@ -70,6 +70,7 @@ function getI18n() {
|
||||
notificationChangeDesc: chrome.i18n.getMessage("notificationChangeDesc"),
|
||||
notificationUpdate: chrome.i18n.getMessage("notificationChangeSave"),
|
||||
notificationEdit: chrome.i18n.getMessage("edit"),
|
||||
notificationEditTooltip: chrome.i18n.getMessage("notificationEditTooltip"),
|
||||
notificationUnlock: chrome.i18n.getMessage("notificationUnlock"),
|
||||
notificationUnlockDesc: chrome.i18n.getMessage("notificationUnlockDesc"),
|
||||
notificationViewAria: chrome.i18n.getMessage("notificationViewAria"),
|
||||
@@ -77,10 +78,10 @@ function getI18n() {
|
||||
saveAsNewLoginAction: chrome.i18n.getMessage("saveAsNewLoginAction"),
|
||||
saveFailure: chrome.i18n.getMessage("saveFailure"),
|
||||
saveFailureDetails: chrome.i18n.getMessage("saveFailureDetails"),
|
||||
saveLoginPrompt: chrome.i18n.getMessage("saveLoginPrompt"),
|
||||
saveLogin: chrome.i18n.getMessage("saveLogin"),
|
||||
typeLogin: chrome.i18n.getMessage("typeLogin"),
|
||||
updateLoginAction: chrome.i18n.getMessage("updateLoginAction"),
|
||||
updateLoginPrompt: chrome.i18n.getMessage("updateLoginPrompt"),
|
||||
updateLogin: chrome.i18n.getMessage("updateLogin"),
|
||||
vault: chrome.i18n.getMessage("vault"),
|
||||
view: chrome.i18n.getMessage("view"),
|
||||
};
|
||||
|
||||
@@ -80,7 +80,7 @@ class VaultComponent {
|
||||
@Component({
|
||||
selector: "mock-add-button",
|
||||
template: `
|
||||
<button bitButton buttonType="primary" type="button">
|
||||
<button bitButton size="small" buttonType="primary" type="button">
|
||||
<i class="bwi bwi-plus-f" aria-hidden="true"></i>
|
||||
Add
|
||||
</button>
|
||||
|
||||
@@ -124,12 +124,8 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
* decryption is in progress. The latest decrypted ciphers will be emitted once decryption is complete.
|
||||
*/
|
||||
cipherViews$ = perUserCache$((userId: UserId): Observable<CipherView[] | null> => {
|
||||
return combineLatest([
|
||||
this.encryptedCiphersState(userId).state$,
|
||||
this.localData$(userId),
|
||||
this.keyService.cipherDecryptionKeys$(userId, true),
|
||||
]).pipe(
|
||||
filter(([ciphers, keys]) => ciphers != null && keys != null), // Skip if ciphers haven't been loaded yor synced yet
|
||||
return combineLatest([this.encryptedCiphersState(userId).state$, this.localData$(userId)]).pipe(
|
||||
filter(([ciphers]) => ciphers != null), // Skip if ciphers haven't been loaded yor synced yet
|
||||
switchMap(() => this.getAllDecrypted(userId)),
|
||||
);
|
||||
}, this.clearCipherViewsForUser$);
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { coerceBooleanProperty } from "@angular/cdk/coercion";
|
||||
import { NgClass } from "@angular/common";
|
||||
import { Input, HostBinding, Component, model, computed } from "@angular/core";
|
||||
import { Input, HostBinding, Component, model, computed, input } from "@angular/core";
|
||||
import { toObservable, toSignal } from "@angular/core/rxjs-interop";
|
||||
import { debounce, interval } from "rxjs";
|
||||
|
||||
import { ButtonLikeAbstraction, ButtonType } from "../shared/button-like.abstraction";
|
||||
import { ButtonLikeAbstraction, ButtonType, ButtonSize } from "../shared/button-like.abstraction";
|
||||
|
||||
const focusRing = [
|
||||
"focus-visible:tw-ring-2",
|
||||
@@ -15,6 +13,11 @@ const focusRing = [
|
||||
"focus-visible:tw-z-10",
|
||||
];
|
||||
|
||||
const buttonSizeStyles: Record<ButtonSize, string[]> = {
|
||||
small: ["tw-py-1", "tw-px-3", "tw-text-sm"],
|
||||
default: ["tw-py-1.5", "tw-px-3"],
|
||||
};
|
||||
|
||||
const buttonStyles: Record<ButtonType, string[]> = {
|
||||
primary: [
|
||||
"tw-border-primary-600",
|
||||
@@ -59,8 +62,6 @@ export class ButtonComponent implements ButtonLikeAbstraction {
|
||||
@HostBinding("class") get classList() {
|
||||
return [
|
||||
"tw-font-semibold",
|
||||
"tw-py-1.5",
|
||||
"tw-px-3",
|
||||
"tw-rounded-full",
|
||||
"tw-transition",
|
||||
"tw-border-2",
|
||||
@@ -85,7 +86,8 @@ export class ButtonComponent implements ButtonLikeAbstraction {
|
||||
"disabled:hover:tw-no-underline",
|
||||
]
|
||||
: [],
|
||||
);
|
||||
)
|
||||
.concat(buttonSizeStyles[this.size() || "default"]);
|
||||
}
|
||||
|
||||
protected disabledAttr = computed(() => {
|
||||
@@ -105,7 +107,9 @@ export class ButtonComponent implements ButtonLikeAbstraction {
|
||||
return this.showLoadingStyle() || (this.disabledAttr() && this.loading() === false);
|
||||
});
|
||||
|
||||
@Input() buttonType: ButtonType;
|
||||
@Input() buttonType: ButtonType = "secondary";
|
||||
|
||||
size = input<ButtonSize>("default");
|
||||
|
||||
private _block = false;
|
||||
|
||||
|
||||
@@ -9,6 +9,13 @@ export default {
|
||||
buttonType: "primary",
|
||||
disabled: false,
|
||||
loading: false,
|
||||
size: "default",
|
||||
},
|
||||
argTypes: {
|
||||
size: {
|
||||
options: ["small", "default"],
|
||||
control: { type: "radio" },
|
||||
},
|
||||
},
|
||||
parameters: {
|
||||
design: {
|
||||
@@ -24,19 +31,19 @@ export const Primary: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: /*html*/ `
|
||||
<div class="tw-flex tw-gap-4 tw-mb-6">
|
||||
<button bitButton [disabled]="disabled" [loading]="loading" [buttonType]="buttonType" [block]="block">Button</button>
|
||||
<button bitButton [disabled]="disabled" [loading]="loading" [buttonType]="buttonType" [block]="block" class="tw-test-hover">Button:hover</button>
|
||||
<button bitButton [disabled]="disabled" [loading]="loading" [buttonType]="buttonType" [block]="block" class="tw-test-focus-visible">Button:focus-visible</button>
|
||||
<button bitButton [disabled]="disabled" [loading]="loading" [buttonType]="buttonType" [block]="block" class="tw-test-hover tw-test-focus-visible">Button:hover:focus-visible</button>
|
||||
<button bitButton [disabled]="disabled" [loading]="loading" [buttonType]="buttonType" [block]="block" class="tw-test-active">Button:active</button>
|
||||
<div class="tw-flex tw-gap-4 tw-mb-6 tw-items-center">
|
||||
<button bitButton [disabled]="disabled" [loading]="loading" [buttonType]="buttonType" [size]="size" [block]="block">Button</button>
|
||||
<button bitButton [disabled]="disabled" [loading]="loading" [buttonType]="buttonType" [size]="size" [block]="block" class="tw-test-hover">Button:hover</button>
|
||||
<button bitButton [disabled]="disabled" [loading]="loading" [buttonType]="buttonType" [size]="size" [block]="block" class="tw-test-focus-visible">Button:focus-visible</button>
|
||||
<button bitButton [disabled]="disabled" [loading]="loading" [buttonType]="buttonType" [size]="size" [block]="block" class="tw-test-hover tw-test-focus-visible">Button:hover:focus-visible</button>
|
||||
<button bitButton [disabled]="disabled" [loading]="loading" [buttonType]="buttonType" [size]="size" [block]="block" class="tw-test-active">Button:active</button>
|
||||
</div>
|
||||
<div class="tw-flex tw-gap-4">
|
||||
<a href="#" bitButton [disabled]="disabled" [loading]="loading" [buttonType]="buttonType" [block]="block">Anchor</a>
|
||||
<a href="#" bitButton [disabled]="disabled" [loading]="loading" [buttonType]="buttonType" [block]="block" class="tw-test-hover">Anchor:hover</a>
|
||||
<a href="#" bitButton [disabled]="disabled" [loading]="loading" [buttonType]="buttonType" [block]="block" class="tw-test-focus-visible">Anchor:focus-visible</a>
|
||||
<a href="#" bitButton [disabled]="disabled" [loading]="loading" [buttonType]="buttonType" [block]="block" class="tw-test-hover tw-test-focus-visible">Anchor:hover:focus-visible</a>
|
||||
<a href="#" bitButton [disabled]="disabled" [loading]="loading" [buttonType]="buttonType" [block]="block" class="tw-test-active">Anchor:active</a>
|
||||
<div class="tw-flex tw-gap-4 tw-items-center">
|
||||
<a href="#" bitButton [disabled]="disabled" [loading]="loading" [buttonType]="buttonType" [size]="size" [block]="block">Anchor</a>
|
||||
<a href="#" bitButton [disabled]="disabled" [loading]="loading" [buttonType]="buttonType" [size]="size" [block]="block" class="tw-test-hover">Anchor:hover</a>
|
||||
<a href="#" bitButton [disabled]="disabled" [loading]="loading" [buttonType]="buttonType" [size]="size" [block]="block" class="tw-test-focus-visible">Anchor:focus-visible</a>
|
||||
<a href="#" bitButton [disabled]="disabled" [loading]="loading" [buttonType]="buttonType" [size]="size" [block]="block" class="tw-test-hover tw-test-focus-visible">Anchor:hover:focus-visible</a>
|
||||
<a href="#" bitButton [disabled]="disabled" [loading]="loading" [buttonType]="buttonType" [size]="size" [block]="block" class="tw-test-active">Anchor:active</a>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
@@ -59,6 +66,22 @@ export const Danger: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const Small: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: /*html*/ `
|
||||
<div class="tw-flex tw-gap-4 tw-mb-6 tw-items-center">
|
||||
<button bitButton [disabled]="disabled" [loading]="loading" [buttonType]="'primary'" [size]="size" [block]="block">Primary small</button>
|
||||
<button bitButton [disabled]="disabled" [loading]="loading" [buttonType]="'secondary'" [size]="size" [block]="block">Secondary small</button>
|
||||
<button bitButton [disabled]="disabled" [loading]="loading" [buttonType]="'danger'" [size]="size" [block]="block">Danger small</button>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
args: {
|
||||
size: "small",
|
||||
},
|
||||
};
|
||||
|
||||
export const Loading: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
|
||||
@@ -4,6 +4,8 @@ import { ModelSignal } from "@angular/core";
|
||||
// @ts-strict-ignore
|
||||
export type ButtonType = "primary" | "secondary" | "danger" | "unstyled";
|
||||
|
||||
export type ButtonSize = "default" | "small";
|
||||
|
||||
export abstract class ButtonLikeAbstraction {
|
||||
loading: ModelSignal<boolean>;
|
||||
disabled: ModelSignal<boolean>;
|
||||
|
||||
Reference in New Issue
Block a user