Skip to content

Commit

Permalink
[AC-2436] Show unassigned items banner in browser (#8656)
Browse files Browse the repository at this point in the history
* Boostrap basic banner, show for all admins

* Remove UI banner, fix method calls

* Invert showBanner -> hideBanner

* Add api call

* Minor tweaks and wording

* Change to active user state

* Add tests

* Fix mixed up names

* Simplify logic

* Add feature flag

* Do not clear on logout

* Show banner in browser as well

* Update apps/browser/src/_locales/en/messages.json

* Update copy

---------

Co-authored-by: Addison Beck <[email protected]>
Co-authored-by: Addison Beck <[email protected]>
(cherry picked from commit 98ed744)
  • Loading branch information
eliykat committed Apr 10, 2024
1 parent 3268a06 commit 4962794
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 6 deletions.
3 changes: 3 additions & 0 deletions apps/browser/src/_locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -3005,5 +3005,8 @@
},
"passkeyRemoved": {
"message": "Passkey removed"
},
"unassignedItemsBanner": {
"message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible."
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,32 @@ <h1 class="sr-only">{{ "currentTab" | i18n }}</h1>
</div>
<ng-container *ngIf="loaded">
<app-vault-select (onVaultSelectionChanged)="load()"></app-vault-select>
<app-callout *ngIf="showHowToAutofill" type="info" title="{{ 'howToAutofill' | i18n }}">
<p>{{ autofillCalloutText }}</p>
<app-callout
*ngIf="
(unassignedItemsBannerEnabled$ | async) &&
(unassignedItemsBannerService.showBanner$ | async)
"
type="info"
>
<p>
{{ "unassignedItemsBanner" | i18n }}
<a
href="https://bitwarden.com/help/unassigned-vault-items-moved-to-admin-console"
bitLink
linkType="contrast"
target="_blank"
rel="noreferrer"
>{{ "learnMore" | i18n }}</a
>
</p>
<button
type="button"
class="btn primary callout-half"
appStopClick
(click)="dismissCallout()"
(click)="unassignedItemsBannerService.hideBanner()"
>
{{ "gotIt" | i18n }}
</button>
<button type="button" class="btn callout-half" appStopClick (click)="goToSettings()">
{{ "autofillSettings" | i18n }}
</button>
</app-callout>
<div class="box list" *ngIf="loginCiphers">
<h2 class="box-header">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@ import { Router } from "@angular/router";
import { Subject, firstValueFrom } from "rxjs";
import { debounceTime, takeUntil } from "rxjs/operators";

import { UnassignedItemsBannerService } from "@bitwarden/angular/services/unassigned-items-banner.service";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants";
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
Expand Down Expand Up @@ -54,6 +57,10 @@ export class CurrentTabComponent implements OnInit, OnDestroy {
private loadedTimeout: number;
private searchTimeout: number;

protected unassignedItemsBannerEnabled$ = this.configService.getFeatureFlag$(
FeatureFlag.UnassignedItemsBanner,
);

constructor(
private platformUtilsService: PlatformUtilsService,
private cipherService: CipherService,
Expand All @@ -70,6 +77,8 @@ export class CurrentTabComponent implements OnInit, OnDestroy {
private organizationService: OrganizationService,
private vaultFilterService: VaultFilterService,
private vaultSettingsService: VaultSettingsService,
private configService: ConfigService,
protected unassignedItemsBannerService: UnassignedItemsBannerService,
) {}

async ngOnInit() {
Expand Down
53 changes: 53 additions & 0 deletions libs/angular/src/services/unassigned-items-banner.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { MockProxy, mock } from "jest-mock-extended";
import { firstValueFrom, skip } from "rxjs";

import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { FakeStateProvider, mockAccountServiceWith } from "@bitwarden/common/spec";
import { UserId } from "@bitwarden/common/types/guid";

import { SHOW_BANNER_KEY, UnassignedItemsBannerService } from "./unassigned-items-banner.service";

describe("UnassignedItemsBanner", () => {
let stateProvider: FakeStateProvider;
let apiService: MockProxy<ApiService>;

const sutFactory = () => new UnassignedItemsBannerService(stateProvider, apiService);

beforeEach(() => {
const fakeAccountService = mockAccountServiceWith("userId" as UserId);
stateProvider = new FakeStateProvider(fakeAccountService);
apiService = mock();
});

it("shows the banner if showBanner local state is true", async () => {
const showBanner = stateProvider.activeUser.getFake(SHOW_BANNER_KEY);
showBanner.nextState(true);

const sut = sutFactory();
expect(await firstValueFrom(sut.showBanner$)).toBe(true);
expect(apiService.getShowUnassignedCiphersBanner).not.toHaveBeenCalled();
});

it("does not show the banner if showBanner local state is false", async () => {
const showBanner = stateProvider.activeUser.getFake(SHOW_BANNER_KEY);
showBanner.nextState(false);

const sut = sutFactory();
expect(await firstValueFrom(sut.showBanner$)).toBe(false);
expect(apiService.getShowUnassignedCiphersBanner).not.toHaveBeenCalled();
});

it("fetches from server if local state has not been set yet", async () => {
apiService.getShowUnassignedCiphersBanner.mockResolvedValue(true);

const showBanner = stateProvider.activeUser.getFake(SHOW_BANNER_KEY);
showBanner.nextState(undefined);

const sut = sutFactory();
// skip first value so we get the recomputed value after the server call
expect(await firstValueFrom(sut.showBanner$.pipe(skip(1)))).toBe(true);
// Expect to have updated local state
expect(await firstValueFrom(showBanner.state$)).toBe(true);
expect(apiService.getShowUnassignedCiphersBanner).toHaveBeenCalledTimes(1);
});
});
46 changes: 46 additions & 0 deletions libs/angular/src/services/unassigned-items-banner.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Injectable } from "@angular/core";
import { EMPTY, concatMap } from "rxjs";

import { ApiService } from "@bitwarden/common/abstractions/api.service";
import {
StateProvider,
UNASSIGNED_ITEMS_BANNER_DISK,
UserKeyDefinition,
} from "@bitwarden/common/platform/state";

export const SHOW_BANNER_KEY = new UserKeyDefinition<boolean>(
UNASSIGNED_ITEMS_BANNER_DISK,
"showBanner",
{
deserializer: (b) => b,
clearOn: [],
},
);

/** Displays a banner that tells users how to move their unassigned items into a collection. */
@Injectable({ providedIn: "root" })
export class UnassignedItemsBannerService {
private _showBanner = this.stateProvider.getActive(SHOW_BANNER_KEY);

showBanner$ = this._showBanner.state$.pipe(
concatMap(async (showBanner) => {
// null indicates that the user has not seen or dismissed the banner yet - get the flag from server
if (showBanner == null) {
const showBannerResponse = await this.apiService.getShowUnassignedCiphersBanner();
await this._showBanner.update(() => showBannerResponse);
return EMPTY; // complete the inner observable without emitting any value; the update on the previous line will trigger another run
}

return showBanner;
}),
);

constructor(
private stateProvider: StateProvider,
private apiService: ApiService,
) {}

async hideBanner() {
await this._showBanner.update(() => false);
}
}

0 comments on commit 4962794

Please sign in to comment.