diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index ea9e62916a..ce73bd99d3 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -2486,6 +2486,36 @@ "changeAtRiskPasswordsFasterDesc": { "message": "Update your settings so you can quickly autofill your passwords and generate new ones" }, + "reviewAtRiskLogins": { + "message": "Review at-risk logins" + }, + "reviewAtRiskPasswords": { + "message": "Review at-risk passwords" + }, + "reviewAtRiskLoginsSlideDesc": { + "message": "Your organization passwords are at-risk because they are weak, reused, and/or exposed.", + "description": "Description of the review at-risk login slide on the at-risk password page carousel" + }, + "reviewAtRiskLoginSlideImgAlt": { + "message": "Illustration of a list of logins that are at-risk" + }, + "generatePasswordSlideDesc": { + "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", + "description": "Description of the generate password slide on the at-risk password page carousel" + }, + "generatePasswordSlideImgAlt": { + "message": "Illustration of the Bitwarden autofill menu displaying a generated password" + }, + "updateInBitwarden": { + "message": "Update in Bitwarden" + }, + "updateInBitwardenSlideDesc": { + "message": "Bitwarden will then prompt you to update the password in the password manager.", + "description": "Description of the update in Bitwarden slide on the at-risk password page carousel" + }, + "updateInBitwardenSlideImgAlt": { + "message": "Illustration of a Bitwarden’s notification prompting the user to update the login" + }, "turnOnAutofill": { "message": "Turn on autofill" }, diff --git a/apps/browser/src/images/at-risk-password-carousel/generate_password.dark.png b/apps/browser/src/images/at-risk-password-carousel/generate_password.dark.png new file mode 100644 index 0000000000..8d0fbbdec5 Binary files /dev/null and b/apps/browser/src/images/at-risk-password-carousel/generate_password.dark.png differ diff --git a/apps/browser/src/images/at-risk-password-carousel/generate_password.light.png b/apps/browser/src/images/at-risk-password-carousel/generate_password.light.png new file mode 100644 index 0000000000..4e46ce2adc Binary files /dev/null and b/apps/browser/src/images/at-risk-password-carousel/generate_password.light.png differ diff --git a/apps/browser/src/images/at-risk-password-carousel/review_at-risk_logins.dark.png b/apps/browser/src/images/at-risk-password-carousel/review_at-risk_logins.dark.png new file mode 100644 index 0000000000..47e1d40bd4 Binary files /dev/null and b/apps/browser/src/images/at-risk-password-carousel/review_at-risk_logins.dark.png differ diff --git a/apps/browser/src/images/at-risk-password-carousel/review_at-risk_logins.light.png b/apps/browser/src/images/at-risk-password-carousel/review_at-risk_logins.light.png new file mode 100644 index 0000000000..a248f04c97 Binary files /dev/null and b/apps/browser/src/images/at-risk-password-carousel/review_at-risk_logins.light.png differ diff --git a/apps/browser/src/images/at-risk-password-carousel/update_login.dark.png b/apps/browser/src/images/at-risk-password-carousel/update_login.dark.png new file mode 100644 index 0000000000..a04d3e369b Binary files /dev/null and b/apps/browser/src/images/at-risk-password-carousel/update_login.dark.png differ diff --git a/apps/browser/src/images/at-risk-password-carousel/update_login.light.png b/apps/browser/src/images/at-risk-password-carousel/update_login.light.png new file mode 100644 index 0000000000..3bc176f8ac Binary files /dev/null and b/apps/browser/src/images/at-risk-password-carousel/update_login.light.png differ diff --git a/apps/browser/src/vault/popup/components/at-risk-carousel-dialog/at-risk-carousel-dialog.component.html b/apps/browser/src/vault/popup/components/at-risk-carousel-dialog/at-risk-carousel-dialog.component.html new file mode 100644 index 0000000000..aee456a8f2 --- /dev/null +++ b/apps/browser/src/vault/popup/components/at-risk-carousel-dialog/at-risk-carousel-dialog.component.html @@ -0,0 +1,54 @@ + +
+ + + +

{{ "reviewAtRiskLogins" | i18n }}

+

+ {{ "reviewAtRiskLoginsSlideDesc" | i18n }} +

+
+ + +

{{ "generatePassword" | i18n }}

+

+ {{ "generatePasswordSlideDesc" | i18n }} +

+
+ + +

{{ "updateInBitwarden" | i18n }}

+

+ {{ "updateInBitwardenSlideDesc" | i18n }} +

+
+
+
+
+ +
+
diff --git a/apps/browser/src/vault/popup/components/at-risk-carousel-dialog/at-risk-carousel-dialog.component.ts b/apps/browser/src/vault/popup/components/at-risk-carousel-dialog/at-risk-carousel-dialog.component.ts new file mode 100644 index 0000000000..9af9f0aeda --- /dev/null +++ b/apps/browser/src/vault/popup/components/at-risk-carousel-dialog/at-risk-carousel-dialog.component.ts @@ -0,0 +1,46 @@ +import { DialogRef } from "@angular/cdk/dialog"; +import { Component, inject, signal } from "@angular/core"; + +import { ButtonModule, DialogModule, DialogService, TypographyModule } from "@bitwarden/components"; +import { I18nPipe } from "@bitwarden/ui-common"; +import { DarkImageSourceDirective, VaultCarouselModule } from "@bitwarden/vault"; + +export enum AtRiskCarouselDialogResult { + Dismissed = "dismissed", +} + +@Component({ + selector: "vault-at-risk-carousel-dialog", + templateUrl: "./at-risk-carousel-dialog.component.html", + imports: [ + DialogModule, + VaultCarouselModule, + TypographyModule, + ButtonModule, + DarkImageSourceDirective, + I18nPipe, + ], + standalone: true, +}) +export class AtRiskCarouselDialogComponent { + private dialogRef = inject(DialogRef); + + protected dismissBtnEnabled = signal(false); + + protected async dismiss() { + this.dialogRef.close(AtRiskCarouselDialogResult.Dismissed); + } + + protected onSlideChange(slideIndex: number) { + // Only enable the dismiss button on the last slide + if (slideIndex === 2) { + this.dismissBtnEnabled.set(true); + } + } + + static open(dialogService: DialogService) { + return dialogService.open(AtRiskCarouselDialogComponent, { + disableClose: true, + }); + } +} diff --git a/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-password-page.service.ts b/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-password-page.service.ts index f8cd4a6065..4f3c235dc3 100644 --- a/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-password-page.service.ts +++ b/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-password-page.service.ts @@ -2,18 +2,27 @@ import { inject, Injectable } from "@angular/core"; import { map, Observable } from "rxjs"; import { - BANNERS_DISMISSED_DISK, + AT_RISK_PASSWORDS_PAGE_DISK, StateProvider, UserKeyDefinition, } from "@bitwarden/common/platform/state"; import { UserId } from "@bitwarden/common/types/guid"; -export const AT_RISK_PASSWORD_AUTOFILL_CALLOUT_DISMISSED_KEY = new UserKeyDefinition( - BANNERS_DISMISSED_DISK, - "atRiskPasswordAutofillBannerDismissed", +const AUTOFILL_CALLOUT_DISMISSED_KEY = new UserKeyDefinition( + AT_RISK_PASSWORDS_PAGE_DISK, + "autofillCalloutDismissed", { deserializer: (bannersDismissed) => bannersDismissed, - clearOn: [], // Do not clear dismissed banners + clearOn: [], // Do not clear dismissed callout + }, +); + +const GETTING_STARTED_CAROUSEL_DISMISSED_KEY = new UserKeyDefinition( + AT_RISK_PASSWORDS_PAGE_DISK, + "gettingStartedCarouselDismissed", + { + deserializer: (bannersDismissed) => bannersDismissed, + clearOn: [], // Do not clear dismissed carousel }, ); @@ -23,13 +32,23 @@ export class AtRiskPasswordPageService { isCalloutDismissed(userId: UserId): Observable { return this.stateProvider - .getUser(userId, AT_RISK_PASSWORD_AUTOFILL_CALLOUT_DISMISSED_KEY) + .getUser(userId, AUTOFILL_CALLOUT_DISMISSED_KEY) .state$.pipe(map((dismissed) => !!dismissed)); } async dismissCallout(userId: UserId): Promise { + await this.stateProvider.getUser(userId, AUTOFILL_CALLOUT_DISMISSED_KEY).update(() => true); + } + + isGettingStartedDismissed(userId: UserId): Observable { + return this.stateProvider + .getUser(userId, GETTING_STARTED_CAROUSEL_DISMISSED_KEY) + .state$.pipe(map((dismissed) => !!dismissed)); + } + + async dismissGettingStarted(userId: UserId): Promise { await this.stateProvider - .getUser(userId, AT_RISK_PASSWORD_AUTOFILL_CALLOUT_DISMISSED_KEY) + .getUser(userId, GETTING_STARTED_CAROUSEL_DISMISSED_KEY) .update(() => true); } } diff --git a/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-passwords.component.spec.ts b/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-passwords.component.spec.ts index 39b9650538..c719618b33 100644 --- a/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-passwords.component.spec.ts +++ b/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-passwords.component.spec.ts @@ -16,7 +16,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { ToastService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { ChangeLoginPasswordService, DefaultChangeLoginPasswordService, @@ -28,6 +28,7 @@ import { import { PopupHeaderComponent } from "../../../../platform/popup/layout/popup-header.component"; import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page.component"; +import { AtRiskCarouselDialogResult } from "../at-risk-carousel-dialog/at-risk-carousel-dialog.component"; import { AtRiskPasswordPageService } from "./at-risk-password-page.service"; import { AtRiskPasswordsComponent } from "./at-risk-passwords.component"; @@ -73,6 +74,7 @@ describe("AtRiskPasswordsComponent", () => { const mockToastService = mock(); const mockAtRiskPasswordPageService = mock(); const mockChangeLoginPasswordService = mock(); + const mockDialogService = mock(); beforeEach(async () => { mockTasks$ = new BehaviorSubject([ @@ -109,6 +111,7 @@ describe("AtRiskPasswordsComponent", () => { calloutDismissed$ = new BehaviorSubject(false); setInlineMenuVisibility.mockClear(); mockToastService.showToast.mockClear(); + mockDialogService.open.mockClear(); mockAtRiskPasswordPageService.isCalloutDismissed.mockReturnValue(calloutDismissed$); await TestBed.configureTestingModule({ @@ -162,6 +165,7 @@ describe("AtRiskPasswordsComponent", () => { providers: [ AtRiskPasswordPageService, { provide: ChangeLoginPasswordService, useClass: DefaultChangeLoginPasswordService }, + DialogService, ], }, add: { @@ -169,6 +173,7 @@ describe("AtRiskPasswordsComponent", () => { providers: [ { provide: AtRiskPasswordPageService, useValue: mockAtRiskPasswordPageService }, { provide: ChangeLoginPasswordService, useValue: mockChangeLoginPasswordService }, + { provide: DialogService, useValue: mockDialogService }, ], }, }) @@ -269,4 +274,31 @@ describe("AtRiskPasswordsComponent", () => { }); }); }); + + describe("getting started carousel", () => { + it("should open the carousel automatically if the user has not dismissed it", async () => { + mockAtRiskPasswordPageService.isGettingStartedDismissed.mockReturnValue(of(false)); + mockDialogService.open.mockReturnValue({ closed: of(undefined) } as any); + await component.ngOnInit(); + expect(mockDialogService.open).toHaveBeenCalled(); + }); + + it("should not open the carousel automatically if the user has already dismissed it", async () => { + mockDialogService.open.mockClear(); // Need to clear the mock since the component is already initialized once + mockAtRiskPasswordPageService.isGettingStartedDismissed.mockReturnValue(of(true)); + mockDialogService.open.mockReturnValue({ closed: of(undefined) } as any); + await component.ngOnInit(); + expect(mockDialogService.open).not.toHaveBeenCalled(); + }); + + it("should mark the carousel as dismissed when the user dismisses it", async () => { + mockAtRiskPasswordPageService.isGettingStartedDismissed.mockReturnValue(of(false)); + mockDialogService.open.mockReturnValue({ + closed: of(AtRiskCarouselDialogResult.Dismissed), + } as any); + await component.ngOnInit(); + expect(mockDialogService.open).toHaveBeenCalled(); + expect(mockAtRiskPasswordPageService.dismissGettingStarted).toHaveBeenCalled(); + }); + }); }); diff --git a/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-passwords.component.ts b/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-passwords.component.ts index 4753bc77ec..471bdfeed1 100644 --- a/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-passwords.component.ts +++ b/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-passwords.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from "@angular/common"; -import { Component, inject, signal } from "@angular/core"; +import { Component, inject, OnInit, signal } from "@angular/core"; import { Router } from "@angular/router"; import { combineLatest, firstValueFrom, map, of, shareReplay, startWith, switchMap } from "rxjs"; @@ -19,6 +19,8 @@ import { BadgeModule, ButtonModule, CalloutModule, + DialogModule, + DialogService, ItemModule, ToastService, TypographyModule, @@ -30,11 +32,16 @@ import { PasswordRepromptService, SecurityTaskType, TaskService, + VaultCarouselModule, } from "@bitwarden/vault"; import { PopOutComponent } from "../../../../platform/popup/components/pop-out.component"; import { PopupHeaderComponent } from "../../../../platform/popup/layout/popup-header.component"; import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page.component"; +import { + AtRiskCarouselDialogComponent, + AtRiskCarouselDialogResult, +} from "../at-risk-carousel-dialog/at-risk-carousel-dialog.component"; import { AtRiskPasswordPageService } from "./at-risk-password-page.service"; @@ -50,6 +57,8 @@ import { AtRiskPasswordPageService } from "./at-risk-password-page.service"; CalloutModule, ButtonModule, BadgeModule, + DialogModule, + VaultCarouselModule, ], providers: [ AtRiskPasswordPageService, @@ -59,7 +68,7 @@ import { AtRiskPasswordPageService } from "./at-risk-password-page.service"; standalone: true, templateUrl: "./at-risk-passwords.component.html", }) -export class AtRiskPasswordsComponent { +export class AtRiskPasswordsComponent implements OnInit { private taskService = inject(TaskService); private organizationService = inject(OrganizationService); private cipherService = inject(CipherService); @@ -72,6 +81,7 @@ export class AtRiskPasswordsComponent { private atRiskPasswordPageService = inject(AtRiskPasswordPageService); private changeLoginPasswordService = inject(ChangeLoginPasswordService); private platformUtilsService = inject(PlatformUtilsService); + private dialogService = inject(DialogService); /** * The cipher that is currently being launched. Used to show a loading spinner on the badge button. @@ -141,6 +151,21 @@ export class AtRiskPasswordsComponent { }), ); + async ngOnInit() { + const { userId } = await firstValueFrom(this.activeUserData$); + const gettingStartedDismissed = await firstValueFrom( + this.atRiskPasswordPageService.isGettingStartedDismissed(userId), + ); + if (!gettingStartedDismissed) { + const ref = AtRiskCarouselDialogComponent.open(this.dialogService); + + const result = await firstValueFrom(ref.closed); + if (result === AtRiskCarouselDialogResult.Dismissed) { + await this.atRiskPasswordPageService.dismissGettingStarted(userId); + } + } + } + async viewCipher(cipher: CipherView) { const repromptPassed = await this.passwordRepromptService.passwordRepromptCheck(cipher); if (!repromptPassed) { diff --git a/libs/common/src/platform/state/state-definitions.ts b/libs/common/src/platform/state/state-definitions.ts index c7901bc34e..84d5b81ee7 100644 --- a/libs/common/src/platform/state/state-definitions.ts +++ b/libs/common/src/platform/state/state-definitions.ts @@ -199,3 +199,4 @@ export const NEW_DEVICE_VERIFICATION_NOTICE = new StateDefinition( ); export const VAULT_APPEARANCE = new StateDefinition("vaultAppearance", "disk"); export const SECURITY_TASKS_DISK = new StateDefinition("securityTasks", "disk"); +export const AT_RISK_PASSWORDS_PAGE_DISK = new StateDefinition("atRiskPasswordsPage", "disk"); diff --git a/libs/components/src/dialog/simple-dialog/simple-dialog.component.html b/libs/components/src/dialog/simple-dialog/simple-dialog.component.html index 1f154a8d54..d810838cab 100644 --- a/libs/components/src/dialog/simple-dialog/simple-dialog.component.html +++ b/libs/components/src/dialog/simple-dialog/simple-dialog.component.html @@ -3,10 +3,12 @@ @fadeIn >
- @if (hasIcon) { - - } @else { - + @if (!hideIcon()) { + @if (hasIcon) { + + } @else { + + } }

({ + props: args, + template: ` + + Premium Subscription Available + Message Content + + + + + + `, + }), +}; + export const ScrollingContent: Story = { - render: (args: SimpleDialogComponent) => ({ + render: (args) => ({ props: args, template: ` diff --git a/libs/vault/src/components/carousel/carousel.component.html b/libs/vault/src/components/carousel/carousel.component.html index b3e124e02b..04c6e55905 100644 --- a/libs/vault/src/components/carousel/carousel.component.html +++ b/libs/vault/src/components/carousel/carousel.component.html @@ -20,6 +20,6 @@ >

- +
diff --git a/libs/vault/src/components/carousel/carousel.component.ts b/libs/vault/src/components/carousel/carousel.component.ts index ab6d0a38f3..2346ee2990 100644 --- a/libs/vault/src/components/carousel/carousel.component.ts +++ b/libs/vault/src/components/carousel/carousel.component.ts @@ -8,12 +8,12 @@ import { ContentChildren, ElementRef, EventEmitter, + inject, Input, Output, QueryList, ViewChild, ViewChildren, - inject, } from "@angular/core"; import { ButtonModule } from "@bitwarden/components"; @@ -89,7 +89,7 @@ export class VaultCarouselComponent implements AfterViewInit { this.slideChange.emit(index); } - ngAfterViewInit(): void { + async ngAfterViewInit() { this.keyManager = new FocusKeyManager(this.carouselButtons) .withHorizontalOrientation("ltr") .withWrap() @@ -98,7 +98,7 @@ export class VaultCarouselComponent implements AfterViewInit { // Set the first carousel button as active, this avoids having to double tab the arrow keys on initial focus. this.keyManager.setFirstItemActive(); - this.setMinHeightOfCarousel(); + await this.setMinHeightOfCarousel(); } /** @@ -106,7 +106,7 @@ export class VaultCarouselComponent implements AfterViewInit { * Render each slide in a temporary portal outlet to get the height of each slide * and store the tallest slide height. */ - private setMinHeightOfCarousel() { + private async setMinHeightOfCarousel() { // Store the height of the carousel button element. const heightOfButtonsPx = this.carouselButtonWrapper.nativeElement.offsetHeight; @@ -121,13 +121,14 @@ export class VaultCarouselComponent implements AfterViewInit { // to determine the height of the first slide. let tallestSlideHeightPx = containerHeight - heightOfButtonsPx; - this.slides.forEach((slide, index) => { - // Skip the first slide, the height is accounted for above. - if (index === this.selectedIndex) { - return; + for (let i = 0; i < this.slides.length; i++) { + if (i === this.selectedIndex) { + continue; } + this.tempSlideOutlet.attach(this.slides.get(i)!.content); - this.tempSlideOutlet.attach(slide.content); + // Wait for the slide to render. Otherwise, the previous slide may not have been removed from the DOM yet. + await new Promise(requestAnimationFrame); // Store the height of the current slide if is larger than the current stored height; if (this.tempSlideContainer.nativeElement.offsetHeight > tallestSlideHeightPx) { @@ -136,8 +137,7 @@ export class VaultCarouselComponent implements AfterViewInit { // cleanup the outlet this.tempSlideOutlet.detach(); - }); - + } // Set the min height of the entire carousel based on the largest slide. this.minHeight = `${tallestSlideHeightPx + heightOfButtonsPx}px`; this.changeDetectorRef.detectChanges(); diff --git a/libs/vault/src/components/carousel/carousel.module.ts b/libs/vault/src/components/carousel/carousel.module.ts new file mode 100644 index 0000000000..c426e7f89c --- /dev/null +++ b/libs/vault/src/components/carousel/carousel.module.ts @@ -0,0 +1,10 @@ +import { NgModule } from "@angular/core"; + +import { VaultCarouselSlideComponent } from "./carousel-slide/carousel-slide.component"; +import { VaultCarouselComponent } from "./carousel.component"; + +@NgModule({ + imports: [VaultCarouselComponent, VaultCarouselSlideComponent], + exports: [VaultCarouselComponent, VaultCarouselSlideComponent], +}) +export class VaultCarouselModule {} diff --git a/libs/vault/src/components/carousel/index.ts b/libs/vault/src/components/carousel/index.ts index a785c26102..b2fcfb087f 100644 --- a/libs/vault/src/components/carousel/index.ts +++ b/libs/vault/src/components/carousel/index.ts @@ -1 +1 @@ -export { VaultCarouselComponent } from "./carousel.component"; +export { VaultCarouselModule } from "./carousel.module"; diff --git a/libs/vault/src/components/dark-image-source.directive.ts b/libs/vault/src/components/dark-image-source.directive.ts new file mode 100644 index 0000000000..6f3e03ef91 --- /dev/null +++ b/libs/vault/src/components/dark-image-source.directive.ts @@ -0,0 +1,62 @@ +import { + DestroyRef, + Directive, + ElementRef, + HostBinding, + inject, + input, + OnInit, +} from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { combineLatest, Observable } from "rxjs"; + +import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens"; +import { Theme } from "@bitwarden/common/platform/enums"; +import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; + +/** + * Directive that will switch the image source based on the currently applied theme. + * + * @example + * ```html + * + * ``` + */ +@Directive({ + selector: "[appDarkImgSrc]", + standalone: true, +}) +export class DarkImageSourceDirective implements OnInit { + private themeService = inject(ThemeStateService); + private systemTheme$: Observable = inject(SYSTEM_THEME_OBSERVABLE); + private el = inject(ElementRef); + private destroyRef = inject(DestroyRef); + + /** + * The image source to use when the light theme is applied. Automatically assigned the value + * of the `` src attribute. + */ + protected lightImgSrc: string | undefined; + + /** + * The image source to use when the dark theme is applied. + */ + darkImgSrc = input.required({ alias: "appDarkImgSrc" }); + + @HostBinding("attr.src") src: string | undefined; + + ngOnInit() { + // Set the light image source from the element's current src attribute + this.lightImgSrc = this.el.nativeElement.getAttribute("src"); + + // Update the image source based on the active theme + combineLatest([this.themeService.selectedTheme$, this.systemTheme$]) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(([theme, systemTheme]) => { + const appliedTheme = theme === "system" ? systemTheme : theme; + const isDark = + appliedTheme === "dark" || appliedTheme === "nord" || appliedTheme === "solarizedDark"; + this.src = isDark ? this.darkImgSrc() : this.lightImgSrc; + }); + } +} diff --git a/libs/vault/src/index.ts b/libs/vault/src/index.ts index ac905c1f5e..e4857411d0 100644 --- a/libs/vault/src/index.ts +++ b/libs/vault/src/index.ts @@ -4,6 +4,7 @@ export { CopyCipherFieldService, CopyAction } from "./services/copy-cipher-field export { CopyCipherFieldDirective } from "./components/copy-cipher-field.directive"; export { OrgIconDirective } from "./components/org-icon.directive"; export { CanDeleteCipherDirective } from "./components/can-delete-cipher.directive"; +export { DarkImageSourceDirective } from "./components/dark-image-source.directive"; export * from "./utils/observable-utilities"; @@ -21,6 +22,7 @@ export { NewDeviceVerificationNoticePageOneComponent } from "./components/new-de export { NewDeviceVerificationNoticePageTwoComponent } from "./components/new-device-verification-notice/new-device-verification-notice-page-two.component"; export { DecryptionFailureDialogComponent } from "./components/decryption-failure-dialog/decryption-failure-dialog.component"; export * from "./components/add-edit-folder-dialog/add-edit-folder-dialog.component"; +export * from "./components/carousel"; export * as VaultIcons from "./icons";