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 web #8655

Merged
12 changes: 7 additions & 5 deletions apps/web/src/app/layouts/header/web-header.component.html
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
<bit-banner
class="-tw-m-6 tw-flex tw-flex-col tw-pb-6"
(onClose)="webLayoutMigrationBannerService.hideBanner()"
*ngIf="webLayoutMigrationBannerService.showBanner$ | async"
(onClose)="webUnassignedItemsBannerService.hideBanner()"
*ngIf="
(unassignedItemsBannerEnabled$ | async) && (webUnassignedItemsBannerService.showBanner$ | async)
"
>
{{ "newWebApp" | i18n }}
{{ "unassignedItemsBanner" | i18n }}
<a
href="https://bitwarden.com/blog/bitwarden-design-updating-the-navigation-in-the-web-app"
href="https://bitwarden.com/help/unassigned-vault-items-moved-to-admin-console"
bitLink
linkType="contrast"
target="_blank"
rel="noreferrer"
>{{ "releaseBlog" | i18n }}</a
>{{ "learnMore" | i18n }}</a
>
</bit-banner>
<header
Expand Down
10 changes: 8 additions & 2 deletions apps/web/src/app/layouts/header/web-header.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
import { combineLatest, map, Observable } from "rxjs";

import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { AccountProfile } from "@bitwarden/common/platform/models/domain/account";

import { WebLayoutMigrationBannerService } from "./web-layout-migration-banner.service";
import { WebUnassignedItemsBannerService } from "./web-unassigned-items-banner.service";

@Component({
selector: "app-header",
Expand All @@ -31,14 +33,18 @@
protected canLock$: Observable<boolean>;
protected selfHosted: boolean;
protected hostname = location.hostname;
protected unassignedItemsBannerEnabled$ = this.configService.getFeatureFlag$(

Check warning on line 36 in apps/web/src/app/layouts/header/web-header.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/layouts/header/web-header.component.ts#L36

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

constructor(
private route: ActivatedRoute,
private stateService: StateService,
private platformUtilsService: PlatformUtilsService,
private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
private messagingService: MessagingService,
protected webLayoutMigrationBannerService: WebLayoutMigrationBannerService,
protected webUnassignedItemsBannerService: WebUnassignedItemsBannerService,
private configService: ConfigService,

Check warning on line 47 in apps/web/src/app/layouts/header/web-header.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/layouts/header/web-header.component.ts#L47

Added line #L47 was not covered by tests
) {
this.routeData$ = this.route.data.pipe(
map((params) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
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,
WebUnassignedItemsBannerService,
} from "./web-unassigned-items-banner.service";

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

const sutFactory = () => new WebUnassignedItemsBannerService(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);
});
});
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 apps/web/src/app/layouts/header/web-unassigned-items-banner.service.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/layouts/header/web-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 WebUnassignedItemsBannerService {
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 apps/web/src/app/layouts/header/web-unassigned-items-banner.service.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/layouts/header/web-unassigned-items-banner.service.ts#L44

Added line #L44 was not covered by tests
}
}
3 changes: 3 additions & 0 deletions apps/web/src/locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -7899,5 +7899,8 @@
},
"machineAccountAccessUpdated": {
"message": "Machine account access updated"
},
"unassignedItemsBanner": {
"message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible."
}
}
1 change: 1 addition & 0 deletions libs/common/src/abstractions/api.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ export abstract class ApiService {
emergencyAccessId?: string,
) => Promise<AttachmentResponse>;
getCiphersOrganization: (organizationId: string) => Promise<ListResponse<CipherResponse>>;
getShowUnassignedCiphersBanner: () => Promise<boolean>;
postCipher: (request: CipherRequest) => Promise<CipherResponse>;
postCipherCreate: (request: CipherCreateRequest) => Promise<CipherResponse>;
postCipherAdmin: (request: CipherCreateRequest) => Promise<CipherResponse>;
Expand Down
1 change: 1 addition & 0 deletions libs/common/src/enums/feature-flag.enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
ShowPaymentMethodWarningBanners = "show-payment-method-warning-banners",
EnableConsolidatedBilling = "enable-consolidated-billing",
AC1795_UpdatedSubscriptionStatusSection = "AC-1795_updated-subscription-status-section",
UnassignedItemsBanner = "unassigned-items-banner",

Check warning on line 12 in libs/common/src/enums/feature-flag.enum.ts

View check run for this annotation

Codecov / codecov/patch

libs/common/src/enums/feature-flag.enum.ts#L12

Added line #L12 was not covered by tests
}

// Replace this with a type safe lookup of the feature flag values in PM-2282
Expand Down
4 changes: 4 additions & 0 deletions libs/common/src/platform/state/state-definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ export const NEW_WEB_LAYOUT_BANNER_DISK = new StateDefinition("newWebLayoutBanne
web: "disk-local",
});

export const UNASSIGNED_ITEMS_BANNER_DISK = new StateDefinition("unassignedItemsBanner", "disk", {
web: "disk-local",
});

// Platform

export const APPLICATION_ID_DISK = new StateDefinition("applicationId", "disk", {
Expand Down
5 changes: 5 additions & 0 deletions libs/common/src/services/api.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { firstValueFrom } from "rxjs";

Check notice on line 1 in libs/common/src/services/api.service.ts

View check run for this annotation

CodeScene Delta Analysis / CodeScene Cloud Delta Analysis (main)

ℹ Getting worse: Lines of Code in a Single File

The lines of code increases from 1361 to 1365, improve code health by reducing it to 1000. The number of Lines of Code in a single file. More Lines of Code lowers the code health.

Check notice on line 1 in libs/common/src/services/api.service.ts

View check run for this annotation

CodeScene Delta Analysis / CodeScene Cloud Delta Analysis (main)

ℹ Getting worse: Number of Functions in a Single Module

The number of functions increases from 181 to 182, threshold = 75. This file contains too many functions. Beyond a certain threshold, more functions lower the code health.

import { ApiService as ApiServiceAbstraction } from "../abstractions/api.service";
import { OrganizationConnectionType } from "../admin-console/enums";
Expand Down Expand Up @@ -506,6 +506,11 @@
return new ListResponse(r, CipherResponse);
}

async getShowUnassignedCiphersBanner(): Promise<boolean> {
const r = await this.send("GET", "/ciphers/has-unassigned-ciphers", null, true, true);
return r;

Check warning on line 511 in libs/common/src/services/api.service.ts

View check run for this annotation

Codecov / codecov/patch

libs/common/src/services/api.service.ts#L510-L511

Added lines #L510 - L511 were not covered by tests
}

async postCipher(request: CipherRequest): Promise<CipherResponse> {
const r = await this.send("POST", "/ciphers", request, true, true);
return new CipherResponse(r);
Expand Down
Loading