1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +00:00

Merge branch 'main' into PM-26250-Explore-options-to-enable-direct-importer-for-mac-app-store-build

This commit is contained in:
John Harrington
2025-11-25 07:57:16 -07:00
committed by GitHub
56 changed files with 985 additions and 307 deletions

View File

@@ -4902,6 +4902,9 @@
"premium": {
"message": "Premium"
},
"unlockFeaturesWithPremium": {
"message": "Unlock reporting, emergency access, and more security features with Premium."
},
"freeOrgsCannotUseAttachments": {
"message": "Free organizations cannot use attachments"
},

View File

@@ -726,17 +726,6 @@ export default class MainBackground {
const pinStateService = new PinStateService(this.stateProvider);
this.pinService = new PinService(
this.accountService,
this.encryptService,
this.kdfConfigService,
this.keyGenerationService,
this.logService,
this.keyService,
this.sdkService,
pinStateService,
);
this.appIdService = new AppIdService(this.storageService, this.logService);
this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider);
@@ -756,16 +745,6 @@ export default class MainBackground {
VaultTimeoutStringType.OnRestart, // default vault timeout
);
this.biometricsService = new BackgroundBrowserBiometricsService(
runtimeNativeMessagingBackground,
this.logService,
this.keyService,
this.biometricStateService,
this.messagingService,
this.vaultTimeoutSettingsService,
this.pinService,
);
this.apiService = new ApiService(
this.tokenService,
this.platformUtilsService,
@@ -849,6 +828,27 @@ export default class MainBackground {
this.configService,
);
this.pinService = new PinService(
this.accountService,
this.encryptService,
this.kdfConfigService,
this.keyGenerationService,
this.logService,
this.keyService,
this.sdkService,
pinStateService,
);
this.biometricsService = new BackgroundBrowserBiometricsService(
runtimeNativeMessagingBackground,
this.logService,
this.keyService,
this.biometricStateService,
this.messagingService,
this.vaultTimeoutSettingsService,
this.pinService,
);
this.passwordStrengthService = new PasswordStrengthService();
this.passwordGenerationService = legacyPasswordGenerationServiceFactory(

View File

@@ -293,14 +293,24 @@ export default class RuntimeBackground {
case "openPopup":
await this.openPopup();
break;
case VaultMessages.OpenAtRiskPasswords:
case VaultMessages.OpenAtRiskPasswords: {
if (await this.shouldRejectManyOriginMessage(msg)) {
return;
}
await this.main.openAtRisksPasswordsPage();
this.announcePopupOpen();
break;
case VaultMessages.OpenBrowserExtensionToUrl:
}
case VaultMessages.OpenBrowserExtensionToUrl: {
if (await this.shouldRejectManyOriginMessage(msg)) {
return;
}
await this.main.openTheExtensionToPage(msg.url);
this.announcePopupOpen();
break;
}
case "bgUpdateContextMenu":
case "editedCipher":
case "addedCipher":
@@ -312,10 +322,7 @@ export default class RuntimeBackground {
break;
}
case "authResult": {
const env = await firstValueFrom(this.environmentService.environment$);
const vaultUrl = env.getWebVaultUrl();
if (msg.referrer == null || Utils.getHostname(vaultUrl) !== msg.referrer) {
if (!(await this.isValidVaultReferrer(msg.referrer))) {
return;
}
@@ -334,10 +341,7 @@ export default class RuntimeBackground {
break;
}
case "webAuthnResult": {
const env = await firstValueFrom(this.environmentService.environment$);
const vaultUrl = env.getWebVaultUrl();
if (msg.referrer == null || Utils.getHostname(vaultUrl) !== msg.referrer) {
if (!(await this.isValidVaultReferrer(msg.referrer))) {
return;
}
@@ -372,6 +376,48 @@ export default class RuntimeBackground {
}
}
/**
* For messages that can originate from a vault host page or extension, validate referrer or external
*
* @param message
* @returns true if message fails validation
*/
private async shouldRejectManyOriginMessage(message: {
webExtSender: chrome.runtime.MessageSender;
}): Promise<boolean> {
const isValidVaultReferrer = await this.isValidVaultReferrer(
Utils.getHostname(message?.webExtSender?.origin),
);
if (isValidVaultReferrer) {
return false;
}
return isExternalMessage(message);
}
/**
* Validates a message's referrer matches the configured web vault hostname.
*
* @param referrer - hostname from message source
* @returns true if referrer matches web vault
*/
private async isValidVaultReferrer(referrer: string | null | undefined): Promise<boolean> {
if (!referrer) {
return false;
}
const env = await firstValueFrom(this.environmentService.environment$);
const vaultUrl = env.getWebVaultUrl();
const vaultHostname = Utils.getHostname(vaultUrl);
if (!vaultHostname) {
return false;
}
return vaultHostname === referrer;
}
private async autofillPage(tabToAutoFill: chrome.tabs.Tab) {
const totpCode = await this.autofillService.doAutoFill({
tab: tabToAutoFill,

View File

@@ -1,8 +1,5 @@
// eslint-disable-next-line no-restricted-imports
import { CommonModule } from "@angular/common";
// eslint-disable-next-line no-restricted-imports
import { Component, inject } from "@angular/core";
// eslint-disable-next-line no-restricted-imports
import { ActivatedRoute, RouterModule } from "@angular/router";
import { firstValueFrom, map } from "rxjs";

View File

@@ -1,5 +1,3 @@
// TODO: This needs to be dealt with by moving this folder or updating the lint rule.
/* eslint-disable no-restricted-imports */
import { ActivatedRoute, RouterModule } from "@angular/router";
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
import { BehaviorSubject, of } from "rxjs";

View File

@@ -1,6 +1,4 @@
// eslint-disable-next-line no-restricted-imports
import { CommonModule } from "@angular/common";
// eslint-disable-next-line no-restricted-imports
import { Component } from "@angular/core";
import { JslibModule } from "@bitwarden/angular/jslib.module";

View File

@@ -56,8 +56,8 @@ import { BlockedDomainsComponent } from "../autofill/popup/settings/blocked-doma
import { ExcludedDomainsComponent } from "../autofill/popup/settings/excluded-domains.component";
import { NotificationsSettingsComponent } from "../autofill/popup/settings/notifications.component";
import { PremiumV2Component } from "../billing/popup/settings/premium-v2.component";
import { PhishingWarning } from "../dirt/phishing-detection/pages/phishing-warning.component";
import { ProtectedByComponent } from "../dirt/phishing-detection/pages/protected-by-component";
import { PhishingWarning } from "../dirt/phishing-detection/popup/phishing-warning.component";
import { ProtectedByComponent } from "../dirt/phishing-detection/popup/protected-by-component";
import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component";
import BrowserPopupUtils from "../platform/browser/browser-popup-utils";
import { popupRouterCacheGuard } from "../platform/popup/view-cache/popup-router-cache.service";

View File

@@ -1,4 +1,19 @@
<popup-page>
<bit-spotlight *ngIf="!(hasPremium$ | async)" persistent>
<span class="tw-text-xs"
>{{ "unlockFeaturesWithPremium" | i18n }}
<button
bitLink
buttonType="primary"
class="tw-text-xs"
type="button"
(click)="openUpgradeDialog()"
[title]="'upgradeNow' | i18n"
>
{{ "upgradeNow" | i18n }}
</button>
</span>
</bit-spotlight>
<popup-header slot="header" pageTitle="{{ 'settings' | i18n }}">
<ng-container slot="end">
<app-pop-out></app-pop-out>
@@ -20,7 +35,7 @@
<div class="tw-flex tw-items-center tw-justify-center">
<p class="tw-pr-2">{{ "autofill" | i18n }}</p>
<span
*ngIf="!isBrowserAutofillSettingOverridden && (showAutofillBadge$ | async)"
*ngIf="!(isBrowserAutofillSettingOverridden$ | async) && (showAutofillBadge$ | async)"
bitBadge
variant="notification"
[attr.aria-label]="'nudgeBadgeAria' | i18n"

View File

@@ -0,0 +1,260 @@
import { ChangeDetectionStrategy, Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
import { TestBed, waitForAsync } from "@angular/core/testing";
import { RouterTestingModule } from "@angular/router/testing";
import { mock, MockProxy } from "jest-mock-extended";
import { BehaviorSubject, firstValueFrom, of, Subject } from "rxjs";
import { PremiumUpgradeDialogComponent } from "@bitwarden/angular/billing/components";
import { NudgesService, NudgeType } from "@bitwarden/angular/vault";
import { AutofillBrowserSettingsService } from "@bitwarden/browser/autofill/services/autofill-browser-settings.service";
import { BrowserApi } from "@bitwarden/browser/platform/browser/browser-api";
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { DialogService } from "@bitwarden/components";
import { GlobalStateProvider } from "@bitwarden/state";
import { FakeGlobalStateProvider } from "@bitwarden/state-test-utils";
import { CurrentAccountComponent } from "../../../auth/popup/account-switching/current-account.component";
import { SettingsV2Component } from "./settings-v2.component";
@Component({
selector: "app-current-account",
standalone: true,
template: "",
changeDetection: ChangeDetectionStrategy.OnPush,
})
class CurrentAccountStubComponent {}
describe("SettingsV2Component", () => {
let account$: BehaviorSubject<Account | null>;
let mockAccountService: Partial<AccountService>;
let mockBillingState: { hasPremiumFromAnySource$: jest.Mock };
let mockNudges: {
showNudgeBadge$: jest.Mock;
dismissNudge: jest.Mock;
};
let mockAutofillSettings: {
defaultBrowserAutofillDisabled$: Subject<boolean>;
isBrowserAutofillSettingOverridden: jest.Mock<Promise<boolean>>;
};
let dialogService: MockProxy<DialogService>;
let openSpy: jest.SpyInstance;
beforeEach(waitForAsync(async () => {
dialogService = mock<DialogService>();
account$ = new BehaviorSubject<Account | null>(null);
mockAccountService = {
activeAccount$: account$ as unknown as AccountService["activeAccount$"],
};
mockBillingState = {
hasPremiumFromAnySource$: jest.fn().mockReturnValue(of(false)),
};
mockNudges = {
showNudgeBadge$: jest.fn().mockImplementation(() => of(false)),
dismissNudge: jest.fn().mockResolvedValue(undefined),
};
mockAutofillSettings = {
defaultBrowserAutofillDisabled$: new BehaviorSubject<boolean>(false),
isBrowserAutofillSettingOverridden: jest.fn().mockResolvedValue(false),
};
jest.spyOn(BrowserApi, "getBrowserClientVendor").mockReturnValue("Chrome");
const cfg = TestBed.configureTestingModule({
imports: [SettingsV2Component, RouterTestingModule],
providers: [
{ provide: AccountService, useValue: mockAccountService },
{ provide: BillingAccountProfileStateService, useValue: mockBillingState },
{ provide: NudgesService, useValue: mockNudges },
{ provide: AutofillBrowserSettingsService, useValue: mockAutofillSettings },
{ provide: DialogService, useValue: dialogService },
{ provide: I18nService, useValue: { t: jest.fn((key: string) => key) } },
{ provide: GlobalStateProvider, useValue: new FakeGlobalStateProvider() },
{ provide: PlatformUtilsService, useValue: mock<PlatformUtilsService>() },
{ provide: AvatarService, useValue: mock<AvatarService>() },
{ provide: AuthService, useValue: mock<AuthService>() },
],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
});
TestBed.overrideComponent(SettingsV2Component, {
add: {
imports: [CurrentAccountStubComponent],
providers: [{ provide: DialogService, useValue: dialogService }],
},
remove: {
imports: [CurrentAccountComponent],
},
});
await cfg.compileComponents();
}));
afterEach(() => {
jest.resetAllMocks();
});
function pushActiveAccount(id = "user-123"): Account {
const acct = { id } as Account;
account$.next(acct);
return acct;
}
it("shows the premium spotlight when user does NOT have premium", async () => {
mockBillingState.hasPremiumFromAnySource$.mockReturnValue(of(false));
pushActiveAccount();
const fixture = TestBed.createComponent(SettingsV2Component);
fixture.detectChanges();
await fixture.whenStable();
const el: HTMLElement = fixture.nativeElement;
expect(el.querySelector("bit-spotlight")).toBeTruthy();
});
it("hides the premium spotlight when user HAS premium", async () => {
mockBillingState.hasPremiumFromAnySource$.mockReturnValue(of(true));
pushActiveAccount();
const fixture = TestBed.createComponent(SettingsV2Component);
fixture.detectChanges();
await fixture.whenStable();
const el: HTMLElement = fixture.nativeElement;
expect(el.querySelector("bit-spotlight")).toBeFalsy();
});
it("openUpgradeDialog calls PremiumUpgradeDialogComponent.open with the DialogService", async () => {
openSpy = jest.spyOn(PremiumUpgradeDialogComponent, "open").mockImplementation();
mockBillingState.hasPremiumFromAnySource$.mockReturnValue(of(false));
pushActiveAccount();
const fixture = TestBed.createComponent(SettingsV2Component);
const component = fixture.componentInstance;
fixture.detectChanges();
await fixture.whenStable();
component["openUpgradeDialog"]();
expect(openSpy).toHaveBeenCalledTimes(1);
expect(openSpy).toHaveBeenCalledWith(dialogService);
});
it("isBrowserAutofillSettingOverridden$ emits the value from the AutofillBrowserSettingsService", async () => {
pushActiveAccount();
mockAutofillSettings.isBrowserAutofillSettingOverridden.mockResolvedValue(true);
const fixture = TestBed.createComponent(SettingsV2Component);
const component = fixture.componentInstance;
fixture.detectChanges();
await fixture.whenStable();
const value = await firstValueFrom(component["isBrowserAutofillSettingOverridden$"]);
expect(value).toBe(true);
mockAutofillSettings.isBrowserAutofillSettingOverridden.mockResolvedValue(false);
const fixture2 = TestBed.createComponent(SettingsV2Component);
const component2 = fixture2.componentInstance;
fixture2.detectChanges();
await fixture2.whenStable();
const value2 = await firstValueFrom(component2["isBrowserAutofillSettingOverridden$"]);
expect(value2).toBe(false);
});
it("showAutofillBadge$ emits true when default autofill is NOT disabled and nudge is true", async () => {
pushActiveAccount();
mockNudges.showNudgeBadge$.mockImplementation((type: NudgeType) =>
of(type === NudgeType.AutofillNudge),
);
const fixture = TestBed.createComponent(SettingsV2Component);
const component = fixture.componentInstance;
fixture.detectChanges();
await fixture.whenStable();
mockAutofillSettings.defaultBrowserAutofillDisabled$.next(false);
const value = await firstValueFrom(component.showAutofillBadge$);
expect(value).toBe(true);
});
it("showAutofillBadge$ emits false when default autofill IS disabled even if nudge is true", async () => {
pushActiveAccount();
mockNudges.showNudgeBadge$.mockImplementation((type: NudgeType) =>
of(type === NudgeType.AutofillNudge),
);
const fixture = TestBed.createComponent(SettingsV2Component);
const component = fixture.componentInstance;
fixture.detectChanges();
await fixture.whenStable();
mockAutofillSettings.defaultBrowserAutofillDisabled$.next(true);
const value = await firstValueFrom(component.showAutofillBadge$);
expect(value).toBe(false);
});
it("dismissBadge dismisses when showVaultBadge$ emits true", async () => {
const acct = pushActiveAccount();
mockNudges.showNudgeBadge$.mockImplementation((type: NudgeType) => {
return of(type === NudgeType.EmptyVaultNudge);
});
const fixture = TestBed.createComponent(SettingsV2Component);
const component = fixture.componentInstance;
fixture.detectChanges();
await fixture.whenStable();
await component.dismissBadge(NudgeType.EmptyVaultNudge);
expect(mockNudges.dismissNudge).toHaveBeenCalledTimes(1);
expect(mockNudges.dismissNudge).toHaveBeenCalledWith(NudgeType.EmptyVaultNudge, acct.id, true);
});
it("dismissBadge does nothing when showVaultBadge$ emits false", async () => {
pushActiveAccount();
mockNudges.showNudgeBadge$.mockReturnValue(of(false));
const fixture = TestBed.createComponent(SettingsV2Component);
const component = fixture.componentInstance;
fixture.detectChanges();
await fixture.whenStable();
await component.dismissBadge(NudgeType.EmptyVaultNudge);
expect(mockNudges.dismissNudge).not.toHaveBeenCalled();
});
it("showDownloadBitwardenNudge$ proxies to nudges service for the active account", async () => {
const acct = pushActiveAccount("user-xyz");
mockNudges.showNudgeBadge$.mockImplementation((type: NudgeType) =>
of(type === NudgeType.DownloadBitwarden),
);
const fixture = TestBed.createComponent(SettingsV2Component);
const component = fixture.componentInstance;
fixture.detectChanges();
await fixture.whenStable();
const val = await firstValueFrom(component.showDownloadBitwardenNudge$);
expect(val).toBe(true);
expect(mockNudges.showNudgeBadge$).toHaveBeenCalledWith(NudgeType.DownloadBitwarden, acct.id);
});
});

View File

@@ -1,21 +1,31 @@
import { CommonModule } from "@angular/common";
import { Component, OnInit } from "@angular/core";
import { ChangeDetectionStrategy, Component } from "@angular/core";
import { RouterModule } from "@angular/router";
import {
combineLatest,
filter,
firstValueFrom,
from,
map,
Observable,
shareReplay,
switchMap,
} from "rxjs";
import { PremiumUpgradeDialogComponent } from "@bitwarden/angular/billing/components";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { NudgesService, NudgeType } from "@bitwarden/angular/vault";
import { SpotlightComponent } from "@bitwarden/angular/vault/components/spotlight/spotlight.component";
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
import { UserId } from "@bitwarden/common/types/guid";
import { BadgeComponent, ItemModule } from "@bitwarden/components";
import {
BadgeComponent,
DialogService,
ItemModule,
LinkModule,
TypographyModule,
} from "@bitwarden/components";
import { CurrentAccountComponent } from "../../../auth/popup/account-switching/current-account.component";
import { AutofillBrowserSettingsService } from "../../../autofill/services/autofill-browser-settings.service";
@@ -24,8 +34,6 @@ import { PopOutComponent } from "../../../platform/popup/components/pop-out.comp
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
@Component({
templateUrl: "settings-v2.component.html",
imports: [
@@ -38,18 +46,30 @@ import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.co
ItemModule,
CurrentAccountComponent,
BadgeComponent,
SpotlightComponent,
TypographyModule,
LinkModule,
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SettingsV2Component implements OnInit {
export class SettingsV2Component {
NudgeType = NudgeType;
activeUserId: UserId | null = null;
protected isBrowserAutofillSettingOverridden = false;
protected isBrowserAutofillSettingOverridden$ = from(
this.autofillBrowserSettingsService.isBrowserAutofillSettingOverridden(
BrowserApi.getBrowserClientVendor(window),
),
);
private authenticatedAccount$: Observable<Account> = this.accountService.activeAccount$.pipe(
filter((account): account is Account => account !== null),
shareReplay({ bufferSize: 1, refCount: true }),
);
protected hasPremium$ = this.authenticatedAccount$.pipe(
switchMap((account) => this.accountProfileStateService.hasPremiumFromAnySource$(account.id)),
);
showDownloadBitwardenNudge$: Observable<boolean> = this.authenticatedAccount$.pipe(
switchMap((account) =>
this.nudgesService.showNudgeBadge$(NudgeType.DownloadBitwarden, account.id),
@@ -79,13 +99,12 @@ export class SettingsV2Component implements OnInit {
private readonly nudgesService: NudgesService,
private readonly accountService: AccountService,
private readonly autofillBrowserSettingsService: AutofillBrowserSettingsService,
private readonly accountProfileStateService: BillingAccountProfileStateService,
private readonly dialogService: DialogService,
) {}
async ngOnInit() {
this.isBrowserAutofillSettingOverridden =
await this.autofillBrowserSettingsService.isBrowserAutofillSettingOverridden(
BrowserApi.getBrowserClientVendor(window),
);
protected openUpgradeDialog() {
PremiumUpgradeDialogComponent.open(this.dialogService);
}
async dismissBadge(type: NudgeType) {

View File

@@ -6,12 +6,6 @@
</popup-header>
<bit-item-group>
<bit-item *ngIf="!(canAccessPremium$ | async)">
<a type="button" bit-item-content routerLink="/premium">
{{ "premiumMembership" | i18n }}
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
</a>
</bit-item>
<bit-item
*ngIf="
(familySponsorshipAvailable$ | async) &&

View File

@@ -1,13 +1,12 @@
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { RouterModule } from "@angular/router";
import { Observable, firstValueFrom, of, switchMap } from "rxjs";
import { Observable, firstValueFrom, switchMap } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { DialogService, ItemModule } from "@bitwarden/components";
@@ -32,14 +31,12 @@ import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.co
],
})
export class MoreFromBitwardenPageV2Component {
canAccessPremium$: Observable<boolean>;
protected familySponsorshipAvailable$: Observable<boolean>;
protected isFreeFamilyPolicyEnabled$: Observable<boolean>;
protected hasSingleEnterpriseOrg$: Observable<boolean>;
constructor(
private dialogService: DialogService,
private billingAccountProfileStateService: BillingAccountProfileStateService,
private environmentService: EnvironmentService,
private organizationService: OrganizationService,
private familiesPolicyService: FamiliesPolicyService,
@@ -48,13 +45,6 @@ export class MoreFromBitwardenPageV2Component {
this.familySponsorshipAvailable$ = getUserId(this.accountService.activeAccount$).pipe(
switchMap((userId) => this.organizationService.familySponsorshipAvailable$(userId)),
);
this.canAccessPremium$ = this.accountService.activeAccount$.pipe(
switchMap((account) =>
account
? this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id)
: of(false),
),
);
this.hasSingleEnterpriseOrg$ = this.familiesPolicyService.hasSingleEnterpriseOrg$();
this.isFreeFamilyPolicyEnabled$ = this.familiesPolicyService.isFreeFamilyPolicyEnabled$();
}

View File

@@ -120,9 +120,9 @@ checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7"
[[package]]
name = "arboard"
version = "3.6.0"
version = "3.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55f533f8e0af236ffe5eb979b99381df3258853f00ba2e44b6e1955292c75227"
checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf"
dependencies = [
"clipboard-win",
"log",
@@ -131,6 +131,7 @@ dependencies = [
"objc2-foundation",
"parking_lot",
"percent-encoding",
"windows-sys 0.60.2",
"wl-clipboard-rs",
"x11rb",
]

View File

@@ -22,7 +22,7 @@ publish = false
aes = "=0.8.4"
aes-gcm = "=0.10.3"
anyhow = "=1.0.94"
arboard = { version = "=3.6.0", default-features = false }
arboard = { version = "=3.6.1", default-features = false }
ashpd = "=0.11.0"
base64 = "=0.22.1"
bitwarden-russh = { git = "https://github.com/bitwarden/bitwarden-russh.git", rev = "a641316227227f8777fdf56ac9fa2d6b5f7fe662" }

View File

@@ -154,45 +154,15 @@
},
"configurations": {
"oss": {
"buildTarget": "web:build:oss"
},
"oss-dev": {
"buildTarget": "web:build:oss-dev"
},
"commercial": {
"buildTarget": "web:build:commercial"
},
"commercial-dev": {
"buildTarget": "web:build:commercial-dev"
},
"commercial-qa": {
"buildTarget": "web:build:commercial-qa"
},
"commercial-cloud": {
"buildTarget": "web:build:commercial-cloud"
},
"commercial-euprd": {
"buildTarget": "web:build:commercial-euprd"
},
"commercial-euqa": {
"buildTarget": "web:build:commercial-euqa"
},
"commercial-usdev": {
"buildTarget": "web:build:commercial-usdev"
},
"commercial-ee": {
"buildTarget": "web:build:commercial-ee"
},
"oss-selfhost": {
"buildTarget": "web:build:oss-selfhost"
},
"oss-selfhost-dev": {
"buildTarget": "web:build:oss-selfhost-dev"
},
"commercial-selfhost": {
"buildTarget": "web:build:commercial-selfhost"
},
"commercial-selfhost-dev": {
"buildTarget": "web:build:commercial-selfhost-dev"
}
}

View File

@@ -19,6 +19,10 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { DialogService } from "@bitwarden/components";
import { PasswordRepromptService, CipherFormConfigService } from "@bitwarden/vault";
import { HeaderModule } from "../../../../layouts/header/header.module";
import { SharedModule } from "../../../../shared";
import { OrganizationBadgeModule } from "../../../../vault/individual-vault/organization-badge/organization-badge.module";
import { PipesModule } from "../../../../vault/individual-vault/pipes/pipes.module";
import { RoutedVaultFilterBridgeService } from "../../../../vault/individual-vault/vault-filter/services/routed-vault-filter-bridge.service";
import { RoutedVaultFilterService } from "../../../../vault/individual-vault/vault-filter/services/routed-vault-filter.service";
import { AdminConsoleCipherFormConfigService } from "../../../../vault/org-vault/services/admin-console-cipher-form-config.service";
@@ -38,7 +42,7 @@ import { ExposedPasswordsReportComponent as BaseExposedPasswordsReportComponent
RoutedVaultFilterService,
RoutedVaultFilterBridgeService,
],
standalone: false,
imports: [SharedModule, HeaderModule, OrganizationBadgeModule, PipesModule],
})
export class ExposedPasswordsReportComponent
extends BaseExposedPasswordsReportComponent

View File

@@ -14,6 +14,10 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { DialogService } from "@bitwarden/components";
import { CipherFormConfigService, PasswordRepromptService } from "@bitwarden/vault";
import { HeaderModule } from "../../../../layouts/header/header.module";
import { SharedModule } from "../../../../shared";
import { OrganizationBadgeModule } from "../../../../vault/individual-vault/organization-badge/organization-badge.module";
import { PipesModule } from "../../../../vault/individual-vault/pipes/pipes.module";
import { RoutedVaultFilterBridgeService } from "../../../../vault/individual-vault/vault-filter/services/routed-vault-filter-bridge.service";
import { RoutedVaultFilterService } from "../../../../vault/individual-vault/vault-filter/services/routed-vault-filter.service";
import { AdminConsoleCipherFormConfigService } from "../../../../vault/org-vault/services/admin-console-cipher-form-config.service";
@@ -32,7 +36,7 @@ import { InactiveTwoFactorReportComponent as BaseInactiveTwoFactorReportComponen
RoutedVaultFilterService,
RoutedVaultFilterBridgeService,
],
standalone: false,
imports: [SharedModule, HeaderModule, OrganizationBadgeModule, PipesModule],
})
export class InactiveTwoFactorReportComponent
extends BaseInactiveTwoFactorReportComponent

View File

@@ -18,6 +18,10 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { DialogService } from "@bitwarden/components";
import { CipherFormConfigService, PasswordRepromptService } from "@bitwarden/vault";
import { HeaderModule } from "../../../../layouts/header/header.module";
import { SharedModule } from "../../../../shared";
import { OrganizationBadgeModule } from "../../../../vault/individual-vault/organization-badge/organization-badge.module";
import { PipesModule } from "../../../../vault/individual-vault/pipes/pipes.module";
import { RoutedVaultFilterBridgeService } from "../../../../vault/individual-vault/vault-filter/services/routed-vault-filter-bridge.service";
import { RoutedVaultFilterService } from "../../../../vault/individual-vault/vault-filter/services/routed-vault-filter.service";
import { AdminConsoleCipherFormConfigService } from "../../../../vault/org-vault/services/admin-console-cipher-form-config.service";
@@ -37,7 +41,7 @@ import { ReusedPasswordsReportComponent as BaseReusedPasswordsReportComponent }
RoutedVaultFilterService,
RoutedVaultFilterBridgeService,
],
standalone: false,
imports: [SharedModule, HeaderModule, OrganizationBadgeModule, PipesModule],
})
export class ReusedPasswordsReportComponent
extends BaseReusedPasswordsReportComponent

View File

@@ -18,6 +18,10 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { DialogService } from "@bitwarden/components";
import { CipherFormConfigService, PasswordRepromptService } from "@bitwarden/vault";
import { HeaderModule } from "../../../../layouts/header/header.module";
import { SharedModule } from "../../../../shared";
import { OrganizationBadgeModule } from "../../../../vault/individual-vault/organization-badge/organization-badge.module";
import { PipesModule } from "../../../../vault/individual-vault/pipes/pipes.module";
import { RoutedVaultFilterBridgeService } from "../../../../vault/individual-vault/vault-filter/services/routed-vault-filter-bridge.service";
import { RoutedVaultFilterService } from "../../../../vault/individual-vault/vault-filter/services/routed-vault-filter.service";
import { AdminConsoleCipherFormConfigService } from "../../../../vault/org-vault/services/admin-console-cipher-form-config.service";
@@ -37,7 +41,7 @@ import { UnsecuredWebsitesReportComponent as BaseUnsecuredWebsitesReportComponen
RoutedVaultFilterService,
RoutedVaultFilterBridgeService,
],
standalone: false,
imports: [SharedModule, HeaderModule, OrganizationBadgeModule, PipesModule],
})
export class UnsecuredWebsitesReportComponent
extends BaseUnsecuredWebsitesReportComponent

View File

@@ -19,6 +19,10 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { DialogService } from "@bitwarden/components";
import { CipherFormConfigService, PasswordRepromptService } from "@bitwarden/vault";
import { HeaderModule } from "../../../../layouts/header/header.module";
import { SharedModule } from "../../../../shared";
import { OrganizationBadgeModule } from "../../../../vault/individual-vault/organization-badge/organization-badge.module";
import { PipesModule } from "../../../../vault/individual-vault/pipes/pipes.module";
import { RoutedVaultFilterBridgeService } from "../../../../vault/individual-vault/vault-filter/services/routed-vault-filter-bridge.service";
import { RoutedVaultFilterService } from "../../../../vault/individual-vault/vault-filter/services/routed-vault-filter.service";
import { AdminConsoleCipherFormConfigService } from "../../../../vault/org-vault/services/admin-console-cipher-form-config.service";
@@ -38,7 +42,7 @@ import { WeakPasswordsReportComponent as BaseWeakPasswordsReportComponent } from
RoutedVaultFilterService,
RoutedVaultFilterBridgeService,
],
standalone: false,
imports: [SharedModule, HeaderModule, OrganizationBadgeModule, PipesModule],
})
export class WeakPasswordsReportComponent
extends BaseWeakPasswordsReportComponent

View File

@@ -30,6 +30,8 @@ import { NavigationProductSwitcherComponent } from "./navigation-switcher.compon
selector: "[mockOrgs]",
standalone: false,
})
// FIXME(https://bitwarden.atlassian.net/browse/PM-28232): Use Directive suffix
// eslint-disable-next-line @angular-eslint/directive-class-suffix
class MockOrganizationService implements Partial<OrganizationService> {
private static _orgs = new BehaviorSubject<Organization[]>([]);
@@ -49,6 +51,8 @@ class MockOrganizationService implements Partial<OrganizationService> {
selector: "[mockProviders]",
standalone: false,
})
// FIXME(https://bitwarden.atlassian.net/browse/PM-28232): Use Directive suffix
// eslint-disable-next-line @angular-eslint/directive-class-suffix
class MockProviderService implements Partial<ProviderService> {
private static _providers = new BehaviorSubject<Provider[]>([]);

View File

@@ -30,6 +30,8 @@ import { ProductSwitcherService } from "./shared/product-switcher.service";
selector: "[mockOrgs]",
standalone: false,
})
// FIXME(https://bitwarden.atlassian.net/browse/PM-28232): Use Directive suffix
// eslint-disable-next-line @angular-eslint/directive-class-suffix
class MockOrganizationService implements Partial<OrganizationService> {
private static _orgs = new BehaviorSubject<Organization[]>([]);
@@ -49,6 +51,8 @@ class MockOrganizationService implements Partial<OrganizationService> {
selector: "[mockProviders]",
standalone: false,
})
// FIXME(https://bitwarden.atlassian.net/browse/PM-28232): Use Directive suffix
// eslint-disable-next-line @angular-eslint/directive-class-suffix
class MockProviderService implements Partial<ProviderService> {
private static _providers = new BehaviorSubject<Provider[]>([]);

View File

@@ -7,16 +7,6 @@ import { VerifyRecoverDeleteComponent } from "../auth/verify-recover-delete.comp
import { FreeBitwardenFamiliesComponent } from "../billing/members/free-bitwarden-families.component";
import { SponsoredFamiliesComponent } from "../billing/settings/sponsored-families.component";
import { SponsoringOrgRowComponent } from "../billing/settings/sponsoring-org-row.component";
// eslint-disable-next-line no-restricted-imports -- Temporarily disabled until DIRT refactors these out of this module
import { ExposedPasswordsReportComponent as OrgExposedPasswordsReportComponent } from "../dirt/reports/pages/organizations/exposed-passwords-report.component";
// eslint-disable-next-line no-restricted-imports -- Temporarily disabled until DIRT refactors these out of this module
import { InactiveTwoFactorReportComponent as OrgInactiveTwoFactorReportComponent } from "../dirt/reports/pages/organizations/inactive-two-factor-report.component";
// eslint-disable-next-line no-restricted-imports -- Temporarily disabled until DIRT refactors these out of this module
import { ReusedPasswordsReportComponent as OrgReusedPasswordsReportComponent } from "../dirt/reports/pages/organizations/reused-passwords-report.component";
// eslint-disable-next-line no-restricted-imports -- Temporarily disabled until DIRT refactors these out of this module
import { UnsecuredWebsitesReportComponent as OrgUnsecuredWebsitesReportComponent } from "../dirt/reports/pages/organizations/unsecured-websites-report.component";
// eslint-disable-next-line no-restricted-imports -- Temporarily disabled until DIRT refactors these out of this module
import { WeakPasswordsReportComponent as OrgWeakPasswordsReportComponent } from "../dirt/reports/pages/organizations/weak-passwords-report.component";
import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component";
import { HeaderModule } from "../layouts/header/header.module";
import { OrganizationBadgeModule } from "../vault/individual-vault/organization-badge/organization-badge.module";
@@ -29,11 +19,6 @@ import { SharedModule } from "./shared.module";
@NgModule({
imports: [SharedModule, HeaderModule, OrganizationBadgeModule, PipesModule],
declarations: [
OrgExposedPasswordsReportComponent,
OrgInactiveTwoFactorReportComponent,
OrgReusedPasswordsReportComponent,
OrgUnsecuredWebsitesReportComponent,
OrgWeakPasswordsReportComponent,
RecoverDeleteComponent,
RecoverTwoFactorComponent,
RemovePasswordComponent,

View File

@@ -109,10 +109,12 @@
<i class="bwi bwi-fw bwi-clone" aria-hidden="true"></i>
{{ "copyUsername" | i18n }}
</button>
<button bitMenuItem type="button" appCopyField="password" [cipher]="cipher">
<i class="bwi bwi-fw bwi-clone" aria-hidden="true"></i>
{{ "copyPassword" | i18n }}
</button>
@if (cipher.viewPassword) {
<button bitMenuItem type="button" appCopyField="password" [cipher]="cipher">
<i class="bwi bwi-fw bwi-clone" aria-hidden="true"></i>
{{ "copyPassword" | i18n }}
</button>
}
<button bitMenuItem type="button" appCopyField="totp" [cipher]="cipher">
<i class="bwi bwi-fw bwi-clone" aria-hidden="true"></i>
{{ "copyVerificationCode" | i18n }}

View File

@@ -0,0 +1,144 @@
import { OverlayContainer } from "@angular/cdk/overlay";
import { CommonModule } from "@angular/common";
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { RouterModule } from "@angular/router";
import { mock } from "jest-mock-extended";
import { BehaviorSubject } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
import { CipherViewLike } from "@bitwarden/common/vault/utils/cipher-view-like-utils";
import { IconButtonModule, MenuModule } from "@bitwarden/components";
import { CopyCipherFieldDirective, CopyCipherFieldService } from "@bitwarden/vault";
import { OrganizationNameBadgeComponent } from "../../individual-vault/organization-badge/organization-name-badge.component";
import { VaultCipherRowComponent } from "./vault-cipher-row.component";
// eslint-disable-next-line no-console
const originalError = console.error;
// eslint-disable-next-line no-console
console.error = (...args) => {
if (
typeof args[0] === "object" &&
(args[0] as Error).message.includes("Could not parse CSS stylesheet")
) {
// Opening the overlay container in tests causes stylesheets to be parsed,
// which can lead to JSDOM unable to parse CSS errors. These can be ignored safely.
return;
}
originalError(...args);
};
describe("VaultCipherRowComponent", () => {
let component: VaultCipherRowComponent<CipherViewLike>;
let fixture: ComponentFixture<VaultCipherRowComponent<CipherViewLike>>;
let overlayContainer: OverlayContainer;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [VaultCipherRowComponent, OrganizationNameBadgeComponent],
imports: [
CommonModule,
RouterModule.forRoot([]),
MenuModule,
IconButtonModule,
JslibModule,
CopyCipherFieldDirective,
],
providers: [
{ provide: I18nService, useValue: { t: (key: string) => key } },
{
provide: EnvironmentService,
useValue: { environment$: new BehaviorSubject({}).asObservable() },
},
{
provide: DomainSettingsService,
useValue: { showFavicons$: new BehaviorSubject(false).asObservable() },
},
{ provide: CopyCipherFieldService, useValue: mock<CopyCipherFieldService>() },
{ provide: AccountService, useValue: mock<AccountService>() },
{ provide: CipherService, useValue: mock<CipherService>() },
],
}).compileComponents();
fixture = TestBed.createComponent(VaultCipherRowComponent);
component = fixture.componentInstance;
overlayContainer = TestBed.inject(OverlayContainer);
});
afterEach(() => {
overlayContainer?.ngOnDestroy();
});
afterAll(() => {
// eslint-disable-next-line no-console
console.error = originalError;
});
describe("copy password visibility", () => {
let loginCipher: CipherView;
beforeEach(() => {
loginCipher = new CipherView();
loginCipher.id = "cipher-1";
loginCipher.name = "Test Login";
loginCipher.type = CipherType.Login;
loginCipher.login = new LoginView();
loginCipher.login.password = "test-password";
loginCipher.organizationId = undefined;
loginCipher.deletedDate = null;
loginCipher.archivedDate = null;
component.cipher = loginCipher;
component.disabled = false;
});
const openMenuAndGetContent = (): string => {
fixture.detectChanges();
const menuTrigger = fixture.nativeElement.querySelector(
'button[biticonbutton="bwi-ellipsis-v"]',
) as HTMLButtonElement;
expect(menuTrigger).toBeTruthy();
menuTrigger.click();
fixture.detectChanges();
return overlayContainer.getContainerElement().innerHTML;
};
it("renders copy password button in menu when viewPassword is true", () => {
component.cipher.viewPassword = true;
const overlayContent = openMenuAndGetContent();
expect(overlayContent).toContain('appcopyfield="password"');
expect(overlayContent).toContain("copyPassword");
});
it("does not render copy password button in menu when viewPassword is false", () => {
component.cipher.viewPassword = false;
const overlayContent = openMenuAndGetContent();
expect(overlayContent).not.toContain('appcopyfield="password"');
});
it("does not render copy password button in menu when viewPassword is undefined", () => {
component.cipher.viewPassword = undefined;
const overlayContent = openMenuAndGetContent();
expect(overlayContent).not.toContain('appcopyfield="password"');
});
});
});

View File

@@ -13,9 +13,11 @@ const config = require(path.resolve(__dirname, "config.js"));
const pjson = require(path.resolve(__dirname, "package.json"));
module.exports.getEnv = function getEnv(params) {
const ENV = params.env || (process.env.ENV == null ? "development" : process.env.ENV);
const NODE_ENV = process.env.NODE_ENV == null ? "development" : process.env.NODE_ENV;
const LOGGING = process.env.LOGGING != "false";
const ENV = params.env?.ENV ?? process.env?.ENV ?? "development";
const NODE_ENV = params.env?.NODE_ENV ?? process.env?.NODE_ENV ?? "development";
const LOGGING =
params.env?.LOGGING ??
(process.env?.LOGGING === undefined ? true : process.env.LOGGING !== "false");
return { ENV, NODE_ENV, LOGGING };
};
@@ -35,7 +37,11 @@ const DEFAULT_PARAMS = {
* tsConfig: string;
* outputPath?: string;
* mode?: string;
* env?: string;
* env?: {
* ENV?: string;
* NODE_ENV?: string;
* LOGGING?: boolean;
* };
* importAliases?: import("webpack").ResolveOptions["alias"];
* }} params
*/

View File

@@ -15,6 +15,7 @@ module.exports = (webpackConfig, context) => {
},
tsConfig: "apps/web/tsconfig.build.json",
outputPath: path.resolve(context.context.root, context.options.outputPath),
env: context.options.env,
});
} else {
return buildConfig({

View File

@@ -3,6 +3,7 @@ const { buildConfig } = require(path.resolve(__dirname, "../../apps/web/webpack.
module.exports = (webpackConfig, context) => {
const isNxBuild = context && context.options;
if (isNxBuild) {
return buildConfig({
configName: "Commercial",
@@ -23,6 +24,7 @@ module.exports = (webpackConfig, context) => {
alias: "@bitwarden/commercial-sdk-internal",
},
],
env: context.options.env,
});
} else {
return buildConfig({

View File

@@ -63,7 +63,7 @@ export default tseslint.config(
// TODO: Enable these.
"@angular-eslint/component-class-suffix": "error",
"@angular-eslint/contextual-lifecycle": "error",
"@angular-eslint/directive-class-suffix": 0,
"@angular-eslint/directive-class-suffix": "error",
"@angular-eslint/no-empty-lifecycle-method": 0,
"@angular-eslint/no-input-rename": 0,
"@angular-eslint/no-inputs-metadata-property": "error",

View File

@@ -24,6 +24,8 @@ import { KeyService } from "@bitwarden/key-management";
selector: "app-user-verification",
standalone: false,
})
// FIXME(https://bitwarden.atlassian.net/browse/PM-28232): Use Directive suffix
// eslint-disable-next-line @angular-eslint/directive-class-suffix
export class UserVerificationComponent implements ControlValueAccessor, OnInit, OnDestroy {
private _invalidSecret = false;
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals

View File

@@ -45,6 +45,8 @@ export function _cipherListVirtualScrollStrategyFactory(cipherListDir: CipherLis
},
],
})
// FIXME(https://bitwarden.atlassian.net/browse/PM-28232): Use Directive suffix
// eslint-disable-next-line @angular-eslint/directive-class-suffix
export class CipherListVirtualScroll extends CdkFixedSizeVirtualScroll {
_scrollStrategy: CipherListVirtualScrollStrategy;

View File

@@ -3,20 +3,20 @@
>
<div class="tw-flex tw-justify-between tw-items-start tw-flex-grow">
<div>
<h2 bitTypography="h4" class="tw-font-medium !tw-mb-1">{{ title }}</h2>
<h2 *ngIf="title()" bitTypography="h4" class="tw-font-medium !tw-mb-1">{{ title() }}</h2>
<p
*ngIf="subtitle"
*ngIf="subtitle()"
class="tw-text-main tw-mb-0"
bitTypography="body2"
[innerHTML]="subtitle"
[innerHTML]="subtitle()"
></p>
<ng-content *ngIf="!subtitle"></ng-content>
<ng-content *ngIf="!subtitle()"></ng-content>
</div>
<button
type="button"
bitIconButton="bwi-close"
size="small"
*ngIf="!persistent"
*ngIf="!persistent()"
(click)="handleDismiss()"
class="-tw-me-2"
[label]="'close' | i18n"
@@ -28,10 +28,10 @@
bitButton
type="button"
buttonType="primary"
*ngIf="buttonText"
*ngIf="buttonText()"
(click)="handleButtonClick($event)"
>
{{ buttonText }}
<i *ngIf="buttonIcon" [ngClass]="buttonIcon" class="bwi tw-ml-1" aria-hidden="true"></i>
{{ buttonText() }}
<i *ngIf="buttonIcon()" [ngClass]="buttonIcon()" class="bwi tw-ml-1" aria-hidden="true"></i>
</button>
</div>

View File

@@ -0,0 +1,208 @@
import { CommonModule } from "@angular/common";
import { ChangeDetectionStrategy, Component } from "@angular/core";
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { By } from "@angular/platform-browser";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { SpotlightComponent } from "./spotlight.component";
describe("SpotlightComponent", () => {
let fixture: ComponentFixture<SpotlightComponent>;
let component: SpotlightComponent;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [SpotlightComponent],
providers: [{ provide: I18nService, useValue: { t: (key: string) => key } }],
}).compileComponents();
fixture = TestBed.createComponent(SpotlightComponent);
component = fixture.componentInstance;
});
function detect(): void {
fixture.detectChanges();
}
it("should create", () => {
expect(component).toBeTruthy();
});
describe("rendering when inputs are null", () => {
it("should render without crashing when inputs are null/undefined", () => {
// Explicitly drive the inputs to null to exercise template null branches
fixture.componentRef.setInput("title", null);
fixture.componentRef.setInput("subtitle", null);
fixture.componentRef.setInput("buttonText", null);
fixture.componentRef.setInput("buttonIcon", null);
// persistent has a default, but drive it as well for coverage sanity
fixture.componentRef.setInput("persistent", false);
expect(() => detect()).not.toThrow();
const root = fixture.debugElement.nativeElement as HTMLElement;
expect(root).toBeTruthy();
});
});
describe("close button visibility based on persistent", () => {
it("should show the close button when persistent is false", () => {
fixture.componentRef.setInput("persistent", false);
detect();
// Assumes dismiss uses bitIconButton
const dismissButton = fixture.debugElement.query(By.css("button[bitIconButton]"));
expect(dismissButton).toBeTruthy();
});
it("should hide the close button when persistent is true", () => {
fixture.componentRef.setInput("persistent", true);
detect();
const dismissButton = fixture.debugElement.query(By.css("button[bitIconButton]"));
expect(dismissButton).toBeNull();
});
});
describe("event emission", () => {
it("should emit onButtonClick when CTA button is clicked", () => {
const clickSpy = jest.fn();
component.onButtonClick.subscribe(clickSpy);
fixture.componentRef.setInput("buttonText", "Click me");
detect();
const buttonDe = fixture.debugElement.query(By.css("button[bitButton]"));
expect(buttonDe).toBeTruthy();
const event = new MouseEvent("click");
buttonDe.triggerEventHandler("click", event);
expect(clickSpy).toHaveBeenCalledTimes(1);
expect(clickSpy.mock.calls[0][0]).toBeInstanceOf(MouseEvent);
});
it("should emit onDismiss when close button is clicked", () => {
const dismissSpy = jest.fn();
component.onDismiss.subscribe(dismissSpy);
fixture.componentRef.setInput("persistent", false);
detect();
const dismissButton = fixture.debugElement.query(By.css("button[bitIconButton]"));
expect(dismissButton).toBeTruthy();
dismissButton.triggerEventHandler("click", new MouseEvent("click"));
expect(dismissSpy).toHaveBeenCalledTimes(1);
});
it("handleButtonClick should emit via onButtonClick()", () => {
const clickSpy = jest.fn();
component.onButtonClick.subscribe(clickSpy);
const event = new MouseEvent("click");
component.handleButtonClick(event);
expect(clickSpy).toHaveBeenCalledTimes(1);
expect(clickSpy.mock.calls[0][0]).toBe(event);
});
it("handleDismiss should emit via onDismiss()", () => {
const dismissSpy = jest.fn();
component.onDismiss.subscribe(dismissSpy);
component.handleDismiss();
expect(dismissSpy).toHaveBeenCalledTimes(1);
});
});
describe("content projection behavior", () => {
@Component({
standalone: true,
imports: [SpotlightComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<bit-spotlight>
<span class="tw-text-sm">Projected content</span>
</bit-spotlight>
`,
})
class HostWithProjectionComponent {}
let hostFixture: ComponentFixture<HostWithProjectionComponent>;
beforeEach(async () => {
hostFixture = TestBed.createComponent(HostWithProjectionComponent);
});
it("should render projected content inside the spotlight", () => {
hostFixture.detectChanges();
const projected = hostFixture.debugElement.query(By.css(".tw-text-sm"));
expect(projected).toBeTruthy();
expect(projected.nativeElement.textContent.trim()).toBe("Projected content");
});
});
describe("boolean attribute transform for persistent", () => {
@Component({
standalone: true,
imports: [CommonModule, SpotlightComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- bare persistent attribute -->
<bit-spotlight *ngIf="mode === 'bare'" persistent></bit-spotlight>
<!-- no persistent attribute -->
<bit-spotlight *ngIf="mode === 'none'"></bit-spotlight>
<!-- explicit persistent="false" -->
<bit-spotlight *ngIf="mode === 'falseStr'" persistent="false"></bit-spotlight>
`,
})
class BooleanHostComponent {
mode: "bare" | "none" | "falseStr" = "bare";
}
let boolFixture: ComponentFixture<BooleanHostComponent>;
let boolHost: BooleanHostComponent;
beforeEach(async () => {
boolFixture = TestBed.createComponent(BooleanHostComponent);
boolHost = boolFixture.componentInstance;
});
function getSpotlight(): SpotlightComponent {
const de = boolFixture.debugElement.query(By.directive(SpotlightComponent));
return de.componentInstance as SpotlightComponent;
}
it("treats bare 'persistent' attribute as true via booleanAttribute", () => {
boolHost.mode = "bare";
boolFixture.detectChanges();
const spotlight = getSpotlight();
expect(spotlight.persistent()).toBe(true);
});
it("uses default false when 'persistent' is omitted", () => {
boolHost.mode = "none";
boolFixture.detectChanges();
const spotlight = getSpotlight();
expect(spotlight.persistent()).toBe(false);
});
it('treats persistent="false" as false', () => {
boolHost.mode = "falseStr";
boolFixture.detectChanges();
const spotlight = getSpotlight();
expect(spotlight.persistent()).toBe(false);
});
});
});

View File

@@ -1,43 +1,28 @@
import { CommonModule } from "@angular/common";
import { Component, EventEmitter, Input, Output } from "@angular/core";
import { booleanAttribute, ChangeDetectionStrategy, Component, input, output } from "@angular/core";
import { ButtonModule, IconButtonModule, TypographyModule } from "@bitwarden/components";
import { I18nPipe } from "@bitwarden/ui-common";
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
@Component({
selector: "bit-spotlight",
templateUrl: "spotlight.component.html",
imports: [ButtonModule, CommonModule, IconButtonModule, I18nPipe, TypographyModule],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SpotlightComponent {
// The title of the component
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-signals
@Input({ required: true }) title: string | null = null;
readonly title = input<string>();
// The subtitle of the component
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-signals
@Input() subtitle?: string | null = null;
readonly subtitle = input<string>();
// The text to display on the button
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-signals
@Input() buttonText?: string;
// Wheter the component can be dismissed, if true, the component will not show a close button
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-signals
@Input() persistent = false;
readonly buttonText = input<string>();
// Whether the component can be dismissed, if true, the component will not show a close button
readonly persistent = input(false, { transform: booleanAttribute });
// Optional icon to display on the button
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-signals
@Input() buttonIcon: string | null = null;
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref
@Output() onDismiss = new EventEmitter<void>();
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref
@Output() onButtonClick = new EventEmitter();
readonly buttonIcon = input<string>();
readonly onDismiss = output<void>();
readonly onButtonClick = output<MouseEvent>();
handleButtonClick(event: MouseEvent): void {
this.onButtonClick.emit(event);

View File

@@ -1,11 +1,11 @@
import { BitwardenClient } from "@bitwarden/sdk-internal";
import { PasswordManagerClient } from "@bitwarden/sdk-internal";
import { SdkClientFactory } from "../src/platform/abstractions/sdk/sdk-client-factory";
export class DefaultSdkClientFactory implements SdkClientFactory {
createSdkClient(
...args: ConstructorParameters<typeof BitwardenClient>
): Promise<BitwardenClient> {
...args: ConstructorParameters<typeof PasswordManagerClient>
): Promise<PasswordManagerClient> {
throw new Error("Method not implemented.");
}
}

View File

@@ -1,14 +1,14 @@
import type { BitwardenClient } from "@bitwarden/sdk-internal";
import type { PasswordManagerClient } from "@bitwarden/sdk-internal";
/**
* Factory for creating SDK clients.
*/
export abstract class SdkClientFactory {
/**
* Creates a new BitwardenClient. Assumes the SDK is already loaded.
* @param args Bitwarden client constructor parameters
* Creates a new Password Manager client. Assumes the SDK is already loaded.
* @param args Password Manager client constructor parameters
*/
abstract createSdkClient(
...args: ConstructorParameters<typeof BitwardenClient>
): Promise<BitwardenClient>;
...args: ConstructorParameters<typeof PasswordManagerClient>
): Promise<PasswordManagerClient>;
}

View File

@@ -1,6 +1,6 @@
import { Observable } from "rxjs";
import { BitwardenClient, Uuid } from "@bitwarden/sdk-internal";
import { PasswordManagerClient, Uuid } from "@bitwarden/sdk-internal";
import { UserId } from "../../../types/guid";
import { Rc } from "../../misc/reference-counting/rc";
@@ -46,7 +46,7 @@ export abstract class SdkService {
* Retrieve a client initialized without a user.
* This client can only be used for operations that don't require a user context.
*/
abstract client$: Observable<BitwardenClient>;
abstract client$: Observable<PasswordManagerClient>;
/**
* Retrieve a client initialized for a specific user.
@@ -64,7 +64,7 @@ export abstract class SdkService {
*
* @param userId The user id for which to retrieve the client
*/
abstract userClient$(userId: UserId): Observable<Rc<BitwardenClient>>;
abstract userClient$(userId: UserId): Observable<Rc<PasswordManagerClient>>;
/**
* This method is used during/after an authentication procedure to set a new client for a specific user.
@@ -75,5 +75,5 @@ export abstract class SdkService {
* @param userId The user id for which to set the client
* @param client The client to set for the user. If undefined, the client will be unset.
*/
abstract setClient(userId: UserId, client: BitwardenClient | undefined): void;
abstract setClient(userId: UserId, client: PasswordManagerClient | undefined): void;
}

View File

@@ -7,13 +7,13 @@ import { SdkClientFactory } from "../../abstractions/sdk/sdk-client-factory";
*/
export class DefaultSdkClientFactory implements SdkClientFactory {
/**
* Initializes a Bitwarden client. Assumes the SDK is already loaded.
* @param args Bitwarden client constructor parameters
* @returns A BitwardenClient
* Initializes a Password Manager client. Assumes the SDK is already loaded.
* @param args Password Manager client constructor parameters
* @returns A PasswordManagerClient
*/
async createSdkClient(
...args: ConstructorParameters<typeof sdk.BitwardenClient>
): Promise<sdk.BitwardenClient> {
return Promise.resolve(new sdk.BitwardenClient(...args));
...args: ConstructorParameters<typeof sdk.PasswordManagerClient>
): Promise<sdk.PasswordManagerClient> {
return Promise.resolve(new sdk.PasswordManagerClient(...args));
}
}

View File

@@ -5,7 +5,7 @@ import { SecurityStateService } from "@bitwarden/common/key-management/security-
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
// eslint-disable-next-line no-restricted-imports
import { KdfConfigService, KeyService, PBKDF2KdfConfig } from "@bitwarden/key-management";
import { BitwardenClient } from "@bitwarden/sdk-internal";
import { PasswordManagerClient } from "@bitwarden/sdk-internal";
import {
ObservableTracker,
@@ -109,7 +109,7 @@ describe("DefaultSdkService", () => {
});
describe("given no client override has been set for the user", () => {
let mockClient!: MockProxy<BitwardenClient>;
let mockClient!: MockProxy<PasswordManagerClient>;
beforeEach(() => {
mockClient = createMockClient();
@@ -123,8 +123,8 @@ describe("DefaultSdkService", () => {
});
it("does not create an SDK client when called the second time with same userId", async () => {
const subject_1 = new BehaviorSubject<Rc<BitwardenClient> | undefined>(undefined);
const subject_2 = new BehaviorSubject<Rc<BitwardenClient> | undefined>(undefined);
const subject_1 = new BehaviorSubject<Rc<PasswordManagerClient> | undefined>(undefined);
const subject_2 = new BehaviorSubject<Rc<PasswordManagerClient> | undefined>(undefined);
// Use subjects to ensure the subscription is kept alive
service.userClient$(userId).subscribe(subject_1);
@@ -139,8 +139,8 @@ describe("DefaultSdkService", () => {
});
it("destroys the internal SDK client when all subscriptions are closed", async () => {
const subject_1 = new BehaviorSubject<Rc<BitwardenClient> | undefined>(undefined);
const subject_2 = new BehaviorSubject<Rc<BitwardenClient> | undefined>(undefined);
const subject_1 = new BehaviorSubject<Rc<PasswordManagerClient> | undefined>(undefined);
const subject_2 = new BehaviorSubject<Rc<PasswordManagerClient> | undefined>(undefined);
const subscription_1 = service.userClient$(userId).subscribe(subject_1);
const subscription_2 = service.userClient$(userId).subscribe(subject_2);
await new Promise(process.nextTick);
@@ -170,7 +170,7 @@ describe("DefaultSdkService", () => {
describe("given overrides are used", () => {
it("does not create a new client and emits the override client when a client override has already been set ", async () => {
const mockClient = mock<BitwardenClient>();
const mockClient = mock<PasswordManagerClient>();
service.setClient(userId, mockClient);
const userClientTracker = new ObservableTracker(service.userClient$(userId), false);
await userClientTracker.pauseUntilReceived(1);
@@ -242,8 +242,8 @@ describe("DefaultSdkService", () => {
});
});
function createMockClient(): MockProxy<BitwardenClient> {
const client = mock<BitwardenClient>();
function createMockClient(): MockProxy<PasswordManagerClient> {
const client = mock<PasswordManagerClient>();
client.crypto.mockReturnValue(mock());
client.platform.mockReturnValue({
state: jest.fn().mockReturnValue(mock()),

View File

@@ -20,7 +20,7 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co
// eslint-disable-next-line no-restricted-imports
import { KeyService, KdfConfigService, KdfConfig, KdfType } from "@bitwarden/key-management";
import {
BitwardenClient,
PasswordManagerClient,
ClientSettings,
DeviceType as SdkDeviceType,
TokenProvider,
@@ -70,9 +70,9 @@ class JsTokenProvider implements TokenProvider {
export class DefaultSdkService implements SdkService {
private sdkClientOverrides = new BehaviorSubject<{
[userId: UserId]: Rc<BitwardenClient> | typeof UnsetClient;
[userId: UserId]: Rc<PasswordManagerClient> | typeof UnsetClient;
}>({});
private sdkClientCache = new Map<UserId, Observable<Rc<BitwardenClient>>>();
private sdkClientCache = new Map<UserId, Observable<Rc<PasswordManagerClient>>>();
client$ = this.environmentService.environment$.pipe(
concatMap(async (env) => {
@@ -107,14 +107,14 @@ export class DefaultSdkService implements SdkService {
private userAgent: string | null = null,
) {}
userClient$(userId: UserId): Observable<Rc<BitwardenClient>> {
userClient$(userId: UserId): Observable<Rc<PasswordManagerClient>> {
return this.sdkClientOverrides.pipe(
takeWhile((clients) => clients[userId] !== UnsetClient, false),
map((clients) => {
if (clients[userId] === UnsetClient) {
throw new Error("Encountered UnsetClient even though it should have been filtered out");
}
return clients[userId] as Rc<BitwardenClient>;
return clients[userId] as Rc<PasswordManagerClient>;
}),
distinctUntilChanged(),
switchMap((clientOverride) => {
@@ -129,7 +129,7 @@ export class DefaultSdkService implements SdkService {
);
}
setClient(userId: UserId, client: BitwardenClient | undefined) {
setClient(userId: UserId, client: PasswordManagerClient | undefined) {
const previousValue = this.sdkClientOverrides.value[userId];
this.sdkClientOverrides.next({
@@ -149,7 +149,7 @@ export class DefaultSdkService implements SdkService {
* @param userId The user id for which to create the client
* @returns An observable that emits the client for the user
*/
private internalClient$(userId: UserId): Observable<Rc<BitwardenClient>> {
private internalClient$(userId: UserId): Observable<Rc<PasswordManagerClient>> {
const cached = this.sdkClientCache.get(userId);
if (cached !== undefined) {
return cached;
@@ -187,7 +187,7 @@ export class DefaultSdkService implements SdkService {
switchMap(
([env, account, kdfParams, privateKey, userKey, signingKey, orgKeys, securityState]) => {
// Create our own observable to be able to implement clean-up logic
return new Observable<Rc<BitwardenClient>>((subscriber) => {
return new Observable<Rc<PasswordManagerClient>>((subscriber) => {
const createAndInitializeClient = async () => {
if (env == null || kdfParams == null || privateKey == null || userKey == null) {
return undefined;
@@ -214,7 +214,7 @@ export class DefaultSdkService implements SdkService {
return client;
};
let client: Rc<BitwardenClient> | undefined;
let client: Rc<PasswordManagerClient> | undefined;
createAndInitializeClient()
.then((c) => {
client = c === undefined ? undefined : new Rc(c);
@@ -239,7 +239,7 @@ export class DefaultSdkService implements SdkService {
private async initializeClient(
userId: UserId,
client: BitwardenClient,
client: PasswordManagerClient,
account: AccountInfo,
kdfParams: KdfConfig,
privateKey: EncryptedString,
@@ -281,7 +281,7 @@ export class DefaultSdkService implements SdkService {
await this.loadFeatureFlags(client);
}
private async loadFeatureFlags(client: BitwardenClient) {
private async loadFeatureFlags(client: PasswordManagerClient) {
const serverConfig = await firstValueFrom(this.configService.serverConfig$);
const featureFlagMap = new Map(

View File

@@ -1,4 +1,4 @@
import type { BitwardenClient } from "@bitwarden/sdk-internal";
import type { PasswordManagerClient } from "@bitwarden/sdk-internal";
import { SdkClientFactory } from "../../abstractions/sdk/sdk-client-factory";
@@ -9,8 +9,8 @@ import { SdkClientFactory } from "../../abstractions/sdk/sdk-client-factory";
*/
export class NoopSdkClientFactory implements SdkClientFactory {
createSdkClient(
...args: ConstructorParameters<typeof BitwardenClient>
): Promise<BitwardenClient> {
...args: ConstructorParameters<typeof PasswordManagerClient>
): Promise<PasswordManagerClient> {
return Promise.reject(new Error("SDK not available"));
}
}

View File

@@ -7,7 +7,7 @@ import {
throwIfEmpty,
} from "rxjs";
import { BitwardenClient } from "@bitwarden/sdk-internal";
import { PasswordManagerClient } from "@bitwarden/sdk-internal";
import { UserId } from "../../types/guid";
import { SdkService, UserNotLoggedInError } from "../abstractions/sdk/sdk.service";
@@ -17,18 +17,18 @@ import { DeepMockProxy, mockDeep } from "./mock-deep";
export class MockSdkService implements SdkService {
private userClients$ = new BehaviorSubject<{
[userId: UserId]: Rc<BitwardenClient> | undefined;
[userId: UserId]: Rc<PasswordManagerClient> | undefined;
}>({});
private _client$ = new BehaviorSubject(mockDeep<BitwardenClient>());
private _client$ = new BehaviorSubject(mockDeep<PasswordManagerClient>());
client$ = this._client$.asObservable();
version$ = new BehaviorSubject("0.0.1-test").asObservable();
userClient$(userId: UserId): Observable<Rc<BitwardenClient>> {
userClient$(userId: UserId): Observable<Rc<PasswordManagerClient>> {
return this.userClients$.pipe(
takeWhile((clients) => clients[userId] !== undefined, false),
map((clients) => clients[userId] as Rc<BitwardenClient>),
map((clients) => clients[userId] as Rc<PasswordManagerClient>),
distinctUntilChanged(),
throwIfEmpty(() => new UserNotLoggedInError(userId)),
);
@@ -42,7 +42,7 @@ export class MockSdkService implements SdkService {
* Returns the non-user scoped client mock.
* This is what is returned by the `client$` observable.
*/
get client(): DeepMockProxy<BitwardenClient> {
get client(): DeepMockProxy<PasswordManagerClient> {
return this._client$.value;
}
@@ -55,7 +55,7 @@ export class MockSdkService implements SdkService {
* @returns A user-scoped mock for the user.
*/
userLogin: (userId: UserId) => {
const client = mockDeep<BitwardenClient>();
const client = mockDeep<PasswordManagerClient>();
this.userClients$.next({
...this.userClients$.getValue(),
[userId]: new Rc(client),

View File

@@ -54,6 +54,14 @@ const buttonStyles: Record<ButtonType, string[]> = {
"hover:!tw-text-contrast",
...focusRing,
],
dangerPrimary: [
"tw-border-danger-600",
"tw-bg-danger-600",
"!tw-text-contrast",
"hover:tw-bg-danger-700",
"hover:tw-border-danger-700",
...focusRing,
],
unstyled: [],
};

View File

@@ -62,6 +62,13 @@ export const Primary: Story = {
},
};
export const DangerPrimary: Story = {
...Default,
args: {
buttonType: "dangerPrimary",
},
};
export const Danger: Story = {
...Default,
args: {
@@ -77,6 +84,7 @@ export const Small: Story = {
<button type="button" bitButton [disabled]="disabled" [loading]="loading" [buttonType]="'primary'" [size]="size" [block]="block">Primary small</button>
<button type="button" bitButton [disabled]="disabled" [loading]="loading" [buttonType]="'secondary'" [size]="size" [block]="block">Secondary small</button>
<button type="button" bitButton [disabled]="disabled" [loading]="loading" [buttonType]="'danger'" [size]="size" [block]="block">Danger small</button>
<button type="button" bitButton [disabled]="disabled" [loading]="loading" [buttonType]="'dangerPrimary'" [size]="size" [block]="block">Danger Primary small</button>
</div>
`,
}),

View File

@@ -1,11 +1,11 @@
import { NgModule } from "@angular/core";
import { FormControlComponent } from "./form-control.component";
import { BitHintComponent } from "./hint.component";
import { BitHintDirective } from "./hint.directive";
import { BitLabelComponent } from "./label.component";
@NgModule({
imports: [BitLabelComponent, FormControlComponent, BitHintComponent],
exports: [FormControlComponent, BitLabelComponent, BitHintComponent],
imports: [BitLabelComponent, FormControlComponent, BitHintDirective],
exports: [FormControlComponent, BitLabelComponent, BitHintDirective],
})
export class FormControlModule {}

View File

@@ -9,6 +9,6 @@ let nextId = 0;
class: "tw-text-muted tw-font-normal tw-inline-block tw-mt-1 tw-text-xs",
},
})
export class BitHintComponent {
export class BitHintDirective {
@HostBinding() id = `bit-hint-${nextId++}`;
}

View File

@@ -15,7 +15,7 @@ import {
import { I18nPipe } from "@bitwarden/ui-common";
import { BitHintComponent } from "../form-control/hint.component";
import { BitHintDirective } from "../form-control/hint.directive";
import { BitLabelComponent } from "../form-control/label.component";
import { inputBorderClasses } from "../input/input.directive";
@@ -31,7 +31,7 @@ import { BitFormFieldControl } from "./form-field-control";
})
export class BitFormFieldComponent implements AfterContentChecked {
readonly input = contentChild.required(BitFormFieldControl);
readonly hint = contentChild(BitHintComponent);
readonly hint = contentChild(BitHintDirective);
readonly label = contentChild(BitLabelComponent);
readonly prefixContainer = viewChild<ElementRef<HTMLDivElement>>("prefixContainer");

View File

@@ -1,6 +1,6 @@
import { ModelSignal } from "@angular/core";
export type ButtonType = "primary" | "secondary" | "danger" | "unstyled";
export type ButtonType = "primary" | "secondary" | "danger" | "dangerPrimary" | "unstyled";
export type ButtonSize = "default" | "small";

View File

@@ -13,7 +13,7 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms";
import { AriaDisableDirective } from "../a11y";
import { FormControlModule } from "../form-control/form-control.module";
import { BitHintComponent } from "../form-control/hint.component";
import { BitHintDirective } from "../form-control/hint.directive";
import { BitLabelComponent } from "../form-control/label.component";
let nextId = 0;
@@ -56,7 +56,7 @@ export class SwitchComponent implements ControlValueAccessor, AfterViewInit {
protected readonly disabled = model(false);
protected readonly disabledReasonText = input<string | null>(null);
private readonly hintComponent = contentChild<BitHintComponent>(BitHintComponent);
private readonly hintComponent = contentChild<BitHintDirective>(BitHintDirective);
protected readonly disabledReasonTextId = `bit-switch-disabled-text-${nextId++}`;

View File

@@ -35,7 +35,7 @@ import { TableComponent } from "./table.component";
@Directive({
selector: "[bitRowDef]",
})
export class BitRowDef {
export class BitRowDefDirective {
constructor(public template: TemplateRef<any>) {}
}
@@ -69,7 +69,7 @@ export class TableScrollComponent
/** Optional trackBy function. */
readonly trackBy = input<TrackByFunction<any> | undefined>();
protected readonly rowDef = contentChild(BitRowDef);
protected readonly rowDef = contentChild(BitRowDefDirective);
/**
* Height of the thead element (in pixels).

View File

@@ -5,14 +5,14 @@ import { NgModule } from "@angular/core";
import { CellDirective } from "./cell.directive";
import { RowDirective } from "./row.directive";
import { SortableComponent } from "./sortable.component";
import { BitRowDef, TableScrollComponent } from "./table-scroll.component";
import { BitRowDefDirective, TableScrollComponent } from "./table-scroll.component";
import { TableBodyDirective, TableComponent } from "./table.component";
@NgModule({
imports: [
CommonModule,
ScrollingModule,
BitRowDef,
BitRowDefDirective,
CellDirective,
RowDirective,
SortableComponent,
@@ -21,7 +21,7 @@ import { TableBodyDirective, TableComponent } from "./table.component";
TableScrollComponent,
],
exports: [
BitRowDef,
BitRowDefDirective,
CellDirective,
RowDirective,
SortableComponent,

View File

@@ -8,6 +8,8 @@ import { firstValueFrom } from "rxjs";
providers: [TextFieldModule],
hostDirectives: [CdkTextareaAutosize],
})
// FIXME(https://bitwarden.atlassian.net/browse/PM-28232): Use Directive suffix
// eslint-disable-next-line @angular-eslint/directive-class-suffix
export class VaultAutosizeReadOnlyTextArea implements AfterViewInit {
constructor(
@Host() private autosize: CdkTextareaAutosize,

156
package-lock.json generated
View File

@@ -23,8 +23,8 @@
"@angular/platform-browser": "19.2.14",
"@angular/platform-browser-dynamic": "19.2.14",
"@angular/router": "19.2.14",
"@bitwarden/commercial-sdk-internal": "0.2.0-main.375",
"@bitwarden/sdk-internal": "0.2.0-main.375",
"@bitwarden/commercial-sdk-internal": "0.2.0-main.395",
"@bitwarden/sdk-internal": "0.2.0-main.395",
"@electron/fuses": "1.8.0",
"@emotion/css": "11.13.5",
"@koa/multer": "4.0.0",
@@ -38,7 +38,7 @@
"@nx/js": "21.6.8",
"@nx/webpack": "21.6.8",
"big-integer": "1.6.52",
"braintree-web-drop-in": "1.44.0",
"braintree-web-drop-in": "1.46.0",
"buffer": "6.0.3",
"bufferutil": "4.0.9",
"chalk": "4.1.2",
@@ -84,7 +84,7 @@
"@compodoc/compodoc": "1.1.26",
"@electron/notarize": "3.0.1",
"@electron/rebuild": "4.0.1",
"@eslint/compat": "1.2.9",
"@eslint/compat": "2.0.0",
"@lit-labs/signals": "0.1.2",
"@ngtools/webpack": "19.2.14",
"@storybook/addon-a11y": "8.6.12",
@@ -4620,9 +4620,9 @@
"link": true
},
"node_modules/@bitwarden/commercial-sdk-internal": {
"version": "0.2.0-main.375",
"resolved": "https://registry.npmjs.org/@bitwarden/commercial-sdk-internal/-/commercial-sdk-internal-0.2.0-main.375.tgz",
"integrity": "sha512-UMVfLjMh79+5et1if7qqOi+pSGP5Ay3AcGp4E5oLZ0p0yFsN2Q54UFv+SLju0/oI0qTvVZP1RkEtTJXHdNrpTg==",
"version": "0.2.0-main.395",
"resolved": "https://registry.npmjs.org/@bitwarden/commercial-sdk-internal/-/commercial-sdk-internal-0.2.0-main.395.tgz",
"integrity": "sha512-DrxL3iA29hzWpyxPyZjiXx0m+EHOgk4CVb+BAi2SoxsacmyHYuTgXuASFMieRz2rv85wS3UR0N64Ok9lC+xNYA==",
"license": "BITWARDEN SOFTWARE DEVELOPMENT KIT LICENSE AGREEMENT",
"dependencies": {
"type-fest": "^4.41.0"
@@ -4725,9 +4725,9 @@
"link": true
},
"node_modules/@bitwarden/sdk-internal": {
"version": "0.2.0-main.375",
"resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.375.tgz",
"integrity": "sha512-kf2SKFkAdSmV2/ORo6u1eegwYW2ha62NHUsx2ij2uPWmm7mzXUoNa7z8mqhJV1ozg5o7yBqBuXd6Wqo9Ww+/RA==",
"version": "0.2.0-main.395",
"resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.395.tgz",
"integrity": "sha512-biExeL2Grp11VQjjK6QM16+WOYk87mTgUhYKFm+Bu/A0zZBzhL/6AocpA9h2T5M8rLCGVVJVUMaXUW3YrSTqEA==",
"license": "GPL-3.0",
"dependencies": {
"type-fest": "^4.41.0"
@@ -4798,15 +4798,15 @@
"link": true
},
"node_modules/@braintree/asset-loader": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@braintree/asset-loader/-/asset-loader-2.0.1.tgz",
"integrity": "sha512-OGAoBA5MRVsr5qg0sXM6NMJbqHnYZhBudtM6WGgpQnoX42fjUYbE6Y6qFuuerD5z3lsOAjnu80DooBs1VBuh5Q==",
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@braintree/asset-loader/-/asset-loader-2.0.3.tgz",
"integrity": "sha512-uREap1j30wKRlC0mK99nNPMpEp77NtB6XixpDfFJPZHmkrmw7IB4skKe+26LZBK1H6oSainFhAyKoP7x3eyOKA==",
"license": "MIT"
},
"node_modules/@braintree/browser-detection": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@braintree/browser-detection/-/browser-detection-2.0.1.tgz",
"integrity": "sha512-wpRI7AXEUh6o3ILrJbpNOYE7ItfjX/S8JZP7Z5FF66ULngBGYOqE8SeLlLKXG69Nc07HtlL/6nk/h539iz9hcQ==",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@braintree/browser-detection/-/browser-detection-2.0.2.tgz",
"integrity": "sha512-Zrv/pyodvwv/hsjsBKXKVcwHZOkx4A/5Cy2hViXtqghAhLd3483bYUIfHZJE5JKTrd018ny1FI5pN1PHFtW7vw==",
"license": "MIT"
},
"node_modules/@braintree/event-emitter": {
@@ -4822,9 +4822,9 @@
"license": "MIT"
},
"node_modules/@braintree/iframer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@braintree/iframer/-/iframer-2.0.0.tgz",
"integrity": "sha512-x1kHOyIJNDvi4P1s6pVBZhqhBa1hqDG9+yzcsCR1oNVC0LxH9CAP8bKxioT8/auY1sUyy+D8T4Vp/jv7QqSqLQ==",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@braintree/iframer/-/iframer-2.0.1.tgz",
"integrity": "sha512-t1zJX5+f1yxHAzBJPaQT/XVMocKodUqjTE+hYvuxxWjqEZIbH8/eT5b5n767jY16mYw3+XiDkKHqcp4Pclq1wg==",
"license": "MIT"
},
"node_modules/@braintree/sanitize-url": {
@@ -4834,9 +4834,9 @@
"license": "MIT"
},
"node_modules/@braintree/uuid": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@braintree/uuid/-/uuid-1.0.0.tgz",
"integrity": "sha512-AtI5hfttWSuWAgcwLUZdcZ7Fp/8jCCUf9JTs7+Xow9ditU28zuoBovqq083yph2m3SxPYb84lGjOq+cXlXBvJg==",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@braintree/uuid/-/uuid-1.0.1.tgz",
"integrity": "sha512-Tgu5GoODkf4oj4aLlVIapEPEfjitIHrg5ftqY6pa5Ghr4ZUA9XtZIIZ6ZPdP9x8/X0lt/FB8tRq83QuCQCwOrA==",
"license": "ISC"
},
"node_modules/@braintree/wrap-promise": {
@@ -6559,16 +6559,19 @@
}
},
"node_modules/@eslint/compat": {
"version": "1.2.9",
"resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.2.9.tgz",
"integrity": "sha512-gCdSY54n7k+driCadyMNv8JSPzYLeDVM/ikZRtvtROBpRdFSkS8W9A82MqsaY7lZuwL0wiapgD0NT1xT0hyJsA==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-2.0.0.tgz",
"integrity": "sha512-T9AfE1G1uv4wwq94ozgTGio5EUQBqAVe1X9qsQtSNVEYW6j3hvtZVm8Smr4qL1qDPFg+lOB2cL5RxTRMzq4CTA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@eslint/core": "^1.0.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
"node": "^20.19.0 || ^22.13.0 || >=24"
},
"peerDependencies": {
"eslint": "^9.10.0"
"eslint": "^8.40 || 9"
},
"peerDependenciesMeta": {
"eslint": {
@@ -6576,6 +6579,19 @@
}
}
},
"node_modules/@eslint/compat/node_modules/@eslint/core": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.0.0.tgz",
"integrity": "sha512-PRfWP+8FOldvbApr6xL7mNCw4cJcSTq4GA7tYbgq15mRb0kWKO/wEB2jr+uwjFH3sZvEZneZyCUGTxsv4Sahyw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@types/json-schema": "^7.0.15"
},
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=24"
}
},
"node_modules/@eslint/config-array": {
"version": "0.20.1",
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.1.tgz",
@@ -17762,40 +17778,40 @@
}
},
"node_modules/braintree-web": {
"version": "3.113.0",
"resolved": "https://registry.npmjs.org/braintree-web/-/braintree-web-3.113.0.tgz",
"integrity": "sha512-qykYxZyld4X1tRNgXZQ3ZGzmhDGTBTRQ6Q24KaG9PuYqo+P2TVDEDOVC6tRbkx2RUIdXLv2M6WpkG7oLqEia9Q==",
"version": "3.123.2",
"resolved": "https://registry.npmjs.org/braintree-web/-/braintree-web-3.123.2.tgz",
"integrity": "sha512-N4IH75vKY67eONc0Ao4e7F+XagFW+3ok+Nfs/eOjw5D/TUt03diMAQ8woOwJghi2ql6/yjqNzZi2zE/sTWXmJg==",
"license": "MIT",
"dependencies": {
"@braintree/asset-loader": "2.0.1",
"@braintree/browser-detection": "2.0.1",
"@braintree/asset-loader": "2.0.3",
"@braintree/browser-detection": "2.0.2",
"@braintree/event-emitter": "0.4.1",
"@braintree/extended-promise": "1.0.0",
"@braintree/iframer": "2.0.0",
"@braintree/iframer": "2.0.1",
"@braintree/sanitize-url": "7.0.4",
"@braintree/uuid": "1.0.0",
"@braintree/uuid": "1.0.1",
"@braintree/wrap-promise": "2.1.0",
"@paypal/accelerated-checkout-loader": "1.1.0",
"card-validator": "10.0.0",
"credit-card-type": "10.0.1",
"framebus": "6.0.0",
"inject-stylesheet": "6.0.1",
"card-validator": "10.0.3",
"credit-card-type": "10.0.2",
"framebus": "6.0.3",
"inject-stylesheet": "6.0.2",
"promise-polyfill": "8.2.3",
"restricted-input": "3.0.5"
"restricted-input": "4.0.3"
}
},
"node_modules/braintree-web-drop-in": {
"version": "1.44.0",
"resolved": "https://registry.npmjs.org/braintree-web-drop-in/-/braintree-web-drop-in-1.44.0.tgz",
"integrity": "sha512-maOq9SwiXztIzixJhOras7K44x4UIqqnkyQMYAJqxQ8WkADv9AkflCu2j3IeVYCus/Th9gWWFHcBugn3C4sZGw==",
"version": "1.46.0",
"resolved": "https://registry.npmjs.org/braintree-web-drop-in/-/braintree-web-drop-in-1.46.0.tgz",
"integrity": "sha512-KxCjJpaigoMajYD/iIA+ohXaI6Olt2Bj/Yu45WpJOjolKO9n1UmXl9bsq9UIiGOFIGqi/JWva1wI4cIHHvcI1A==",
"license": "MIT",
"dependencies": {
"@braintree/asset-loader": "2.0.1",
"@braintree/browser-detection": "2.0.1",
"@braintree/asset-loader": "2.0.3",
"@braintree/browser-detection": "2.0.2",
"@braintree/event-emitter": "0.4.1",
"@braintree/uuid": "1.0.0",
"@braintree/uuid": "1.0.1",
"@braintree/wrap-promise": "2.1.0",
"braintree-web": "3.113.0"
"braintree-web": "3.123.2"
}
},
"node_modules/browser-assert": {
@@ -18444,20 +18460,14 @@
"license": "CC-BY-4.0"
},
"node_modules/card-validator": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/card-validator/-/card-validator-10.0.0.tgz",
"integrity": "sha512-2fLyCBOxO7/b56sxoYav8FeJqv9bWpZSyKq8sXKxnpxTGXHnM/0c8WEKG+ZJ+OXFcabnl98pD0EKBtTn+Tql0g==",
"version": "10.0.3",
"resolved": "https://registry.npmjs.org/card-validator/-/card-validator-10.0.3.tgz",
"integrity": "sha512-xOEDsK3hojV0OIpmrR64eZGpngnOqRDEP20O+WSRtvjLSW6nyekW4i2N9SzYg679uFO3RyHcFHxb+mml5tXc4A==",
"license": "MIT",
"dependencies": {
"credit-card-type": "^9.1.0"
"credit-card-type": "^10.0.2"
}
},
"node_modules/card-validator/node_modules/credit-card-type": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/credit-card-type/-/credit-card-type-9.1.0.tgz",
"integrity": "sha512-CpNFuLxiPFxuZqhSKml3M+t0K/484pMAnfYWH14JoD7OZMnmC0Lmo+P7JX9SobqFpRoo7ifA18kOHdxJywYPEA==",
"license": "MIT"
},
"node_modules/case-sensitive-paths-webpack-plugin": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz",
@@ -19692,9 +19702,9 @@
"license": "MIT"
},
"node_modules/credit-card-type": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/credit-card-type/-/credit-card-type-10.0.1.tgz",
"integrity": "sha512-vQOuWmBgsgG1ovGeDi8m6Zeu1JaqH/JncrxKmaqMbv/LunyOQdLiQhPHtOsNlbUI05TocR5nod/Mbs3HYtr6sQ==",
"version": "10.0.2",
"resolved": "https://registry.npmjs.org/credit-card-type/-/credit-card-type-10.0.2.tgz",
"integrity": "sha512-vt/iQokU0mtrT7ceRU75FSmWnIh5JFpLsUUUWYRmztYekOGm0ZbCuzwFTbNkq41k92y+0B8ChscFhRN9DhVZEA==",
"license": "MIT"
},
"node_modules/cross-dirname": {
@@ -23410,20 +23420,14 @@
}
},
"node_modules/framebus": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/framebus/-/framebus-6.0.0.tgz",
"integrity": "sha512-bL9V68hVaVBCY9rveoWbPFFI9hAXIJtESs51B+9XmzvMt38+wP8b4VdiJsavjMS6NfPZ/afQ/jc2qaHmSGI1kQ==",
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/framebus/-/framebus-6.0.3.tgz",
"integrity": "sha512-G/N2p+kFZ1xPBge7tbtTq2KcTR1kSKs1rVbTqH//WdtvJSexS33fsTTOq3yfUWvUczqhujyaFc+omawC9YyRBg==",
"license": "MIT",
"dependencies": {
"@braintree/uuid": "^0.1.0"
"@braintree/uuid": "^1.0.0"
}
},
"node_modules/framebus/node_modules/@braintree/uuid": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/@braintree/uuid/-/uuid-0.1.0.tgz",
"integrity": "sha512-YvZJdlNcK5EnR+7M8AjgEAf4Qx696+FOSYlPfy5ePn80vODtVAUU0FxHnzKZC0og1VbDNQDDiwhthR65D4Na0g==",
"license": "ISC"
},
"node_modules/fresh": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
@@ -24987,9 +24991,9 @@
}
},
"node_modules/inject-stylesheet": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/inject-stylesheet/-/inject-stylesheet-6.0.1.tgz",
"integrity": "sha512-2fvune1D4+8mvJoLVo95ncY4HrDkIaYIReRzXv8tkWFgdG9iuc5QuX57gtSDPWTWQI/f5BGwwtH85wxHouzucg==",
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/inject-stylesheet/-/inject-stylesheet-6.0.2.tgz",
"integrity": "sha512-sswMueya1LXEfwcy7KXPuq3zAW6HvgAeViApEhIaCviCkP4XYoKrQj8ftEmxPmIHn88X4R3xOAsnN/QCPvVKWw==",
"license": "MIT"
},
"node_modules/inquirer": {
@@ -36154,12 +36158,12 @@
"license": "ISC"
},
"node_modules/restricted-input": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/restricted-input/-/restricted-input-3.0.5.tgz",
"integrity": "sha512-lUuXZ3wUnHURRarj5/0C8vomWIfWJO+p7T6RYwB46v7Oyuyr3yyupU+i7SjqUv4S6RAeAAZt1C/QCLJ9xhQBow==",
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/restricted-input/-/restricted-input-4.0.3.tgz",
"integrity": "sha512-VpkwT5Fr3DhvoRZfPnmHDhnYAYETjjNzDlvA4NlW0iknFS47C5X4OCHEpOOxaPjvmka5V8d1ty1jVVoorZKvHg==",
"license": "MIT",
"dependencies": {
"@braintree/browser-detection": "^1.12.1"
"@braintree/browser-detection": "^1.17.2"
}
},
"node_modules/restricted-input/node_modules/@braintree/browser-detection": {

View File

@@ -47,7 +47,7 @@
"@compodoc/compodoc": "1.1.26",
"@electron/notarize": "3.0.1",
"@electron/rebuild": "4.0.1",
"@eslint/compat": "1.2.9",
"@eslint/compat": "2.0.0",
"@lit-labs/signals": "0.1.2",
"@ngtools/webpack": "19.2.14",
"@storybook/addon-a11y": "8.6.12",
@@ -160,8 +160,8 @@
"@angular/platform-browser": "19.2.14",
"@angular/platform-browser-dynamic": "19.2.14",
"@angular/router": "19.2.14",
"@bitwarden/sdk-internal": "0.2.0-main.375",
"@bitwarden/commercial-sdk-internal": "0.2.0-main.375",
"@bitwarden/sdk-internal": "0.2.0-main.395",
"@bitwarden/commercial-sdk-internal": "0.2.0-main.395",
"@electron/fuses": "1.8.0",
"@emotion/css": "11.13.5",
"@koa/multer": "4.0.0",
@@ -175,7 +175,7 @@
"@nx/js": "21.6.8",
"@nx/webpack": "21.6.8",
"big-integer": "1.6.52",
"braintree-web-drop-in": "1.44.0",
"braintree-web-drop-in": "1.46.0",
"buffer": "6.0.3",
"bufferutil": "4.0.9",
"chalk": "4.1.2",