Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[AC-2436] Show unassigned items banner in browser #8656

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 { Subject, firstValueFrom, from } from "rxjs";
import { debounceTime, switchMap, takeUntil } from "rxjs/operators";

import { UnassignedItemsBannerService } from "@bitwarden/angular/services/unassigned-items-banner.service";

Check warning on line 6 in apps/browser/src/vault/popup/components/vault/current-tab.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/browser/src/vault/popup/components/vault/current-tab.component.ts#L6

Added line #L6 was not covered by tests
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";

Check warning on line 11 in apps/browser/src/vault/popup/components/vault/current-tab.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/browser/src/vault/popup/components/vault/current-tab.component.ts#L11

Added line #L11 was not covered by tests
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";

Check warning on line 13 in apps/browser/src/vault/popup/components/vault/current-tab.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/browser/src/vault/popup/components/vault/current-tab.component.ts#L13

Added line #L13 was not covered by tests
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 @@
private loadedTimeout: number;
private searchTimeout: number;

protected unassignedItemsBannerEnabled$ = this.configService.getFeatureFlag$(

Check warning on line 60 in apps/browser/src/vault/popup/components/vault/current-tab.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/browser/src/vault/popup/components/vault/current-tab.component.ts#L60

Added line #L60 was not covered by tests
FeatureFlag.UnassignedItemsBanner,
);

constructor(
private platformUtilsService: PlatformUtilsService,
private cipherService: CipherService,
Expand All @@ -70,6 +77,8 @@
private organizationService: OrganizationService,
private vaultFilterService: VaultFilterService,
private vaultSettingsService: VaultSettingsService,
private configService: ConfigService,
protected unassignedItemsBannerService: UnassignedItemsBannerService,

Check warning on line 81 in apps/browser/src/vault/popup/components/vault/current-tab.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/browser/src/vault/popup/components/vault/current-tab.component.ts#L81

Added line #L81 was not covered by tests
) {}

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,

Check warning on line 15 in libs/angular/src/services/unassigned-items-banner.service.ts

View check run for this annotation

Codecov / codecov/patch

libs/angular/src/services/unassigned-items-banner.service.ts#L15

Added line #L15 was not covered by tests
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);

Check warning on line 44 in libs/angular/src/services/unassigned-items-banner.service.ts

View check run for this annotation

Codecov / codecov/patch

libs/angular/src/services/unassigned-items-banner.service.ts#L44

Added line #L44 was not covered by tests
}
}
Loading