{
authRequestAnsweringService = mock
();
- configService = mock();
- configService.getFeatureFlag$.mockImplementation((flag: FeatureFlag) => {
- const flagValueByFlag: Partial> = {
- [FeatureFlag.InactiveUserServerNotification]: true,
- [FeatureFlag.PushNotificationsWhenLocked]: true,
- };
- return new BehaviorSubject(flagValueByFlag[flag] ?? false) as any;
- });
-
policyService = mock();
defaultServerNotificationsService = new DefaultServerNotificationsService(
diff --git a/libs/common/src/platform/server-notifications/internal/default-server-notifications.service.ts b/libs/common/src/platform/server-notifications/internal/default-server-notifications.service.ts
index 5ee288351d5..5b026add1a2 100644
--- a/libs/common/src/platform/server-notifications/internal/default-server-notifications.service.ts
+++ b/libs/common/src/platform/server-notifications/internal/default-server-notifications.service.ts
@@ -71,48 +71,20 @@ export class DefaultServerNotificationsService implements ServerNotificationsSer
private readonly configService: ConfigService,
private readonly policyService: InternalPolicyService,
) {
- this.notifications$ = this.configService
- .getFeatureFlag$(FeatureFlag.InactiveUserServerNotification)
- .pipe(
- distinctUntilChanged(),
- switchMap((inactiveUserServerNotificationEnabled) => {
- if (inactiveUserServerNotificationEnabled) {
- return this.accountService.accounts$.pipe(
- map((accounts: Record): Set => {
- const validUserIds = Object.entries(accounts)
- .filter(
- ([_, accountInfo]) => accountInfo.email !== "" || accountInfo.emailVerified,
- )
- .map(([userId, _]) => userId as UserId);
- return new Set(validUserIds);
- }),
- trackedMerge((id: UserId) => {
- return this.userNotifications$(id as UserId).pipe(
- map(
- (notification: NotificationResponse) => [notification, id as UserId] as const,
- ),
- );
- }),
- );
- }
-
- return this.accountService.activeAccount$.pipe(
- map((account) => account?.id),
- distinctUntilChanged(),
- switchMap((activeAccountId) => {
- if (activeAccountId == null) {
- // We don't emit server-notifications for inactive accounts currently
- return EMPTY;
- }
-
- return this.userNotifications$(activeAccountId).pipe(
- map((notification) => [notification, activeAccountId] as const),
- );
- }),
- );
- }),
- share(), // Multiple subscribers should only create a single connection to the server
- );
+ this.notifications$ = this.accountService.accounts$.pipe(
+ map((accounts: Record): Set => {
+ const validUserIds = Object.entries(accounts)
+ .filter(([_, accountInfo]) => accountInfo.email !== "" || accountInfo.emailVerified)
+ .map(([userId, _]) => userId as UserId);
+ return new Set(validUserIds);
+ }),
+ trackedMerge((id: UserId) => {
+ return this.userNotifications$(id as UserId).pipe(
+ map((notification: NotificationResponse) => [notification, id as UserId] as const),
+ );
+ }),
+ share(), // Multiple subscribers should only create a single connection to the server
+ );
}
/**
@@ -175,25 +147,13 @@ export class DefaultServerNotificationsService implements ServerNotificationsSer
}
private hasAccessToken$(userId: UserId) {
- return this.configService.getFeatureFlag$(FeatureFlag.PushNotificationsWhenLocked).pipe(
+ return this.authService.authStatusFor$(userId).pipe(
+ map(
+ (authStatus) =>
+ authStatus === AuthenticationStatus.Locked ||
+ authStatus === AuthenticationStatus.Unlocked,
+ ),
distinctUntilChanged(),
- switchMap((featureFlagEnabled) => {
- if (featureFlagEnabled) {
- return this.authService.authStatusFor$(userId).pipe(
- map(
- (authStatus) =>
- authStatus === AuthenticationStatus.Locked ||
- authStatus === AuthenticationStatus.Unlocked,
- ),
- distinctUntilChanged(),
- );
- } else {
- return this.authService.authStatusFor$(userId).pipe(
- map((authStatus) => authStatus === AuthenticationStatus.Unlocked),
- distinctUntilChanged(),
- );
- }
- }),
);
}
@@ -208,19 +168,13 @@ export class DefaultServerNotificationsService implements ServerNotificationsSer
return;
}
- if (
- await firstValueFrom(
- this.configService.getFeatureFlag$(FeatureFlag.InactiveUserServerNotification),
- )
- ) {
- const activeAccountId = await firstValueFrom(
- this.accountService.activeAccount$.pipe(map((a) => a?.id)),
- );
+ const activeAccountId = await firstValueFrom(
+ this.accountService.activeAccount$.pipe(map((a) => a?.id)),
+ );
- const isActiveUser = activeAccountId === userId;
- if (!isActiveUser && !AllowedMultiUserNotificationTypes.has(notification.type)) {
- return;
- }
+ const notificationIsForActiveUser = activeAccountId === userId;
+ if (!notificationIsForActiveUser && !AllowedMultiUserNotificationTypes.has(notification.type)) {
+ return;
}
switch (notification.type) {
diff --git a/libs/common/src/vault/abstractions/cipher.service.ts b/libs/common/src/vault/abstractions/cipher.service.ts
index 8472a359c51..0d3a0b99fcb 100644
--- a/libs/common/src/vault/abstractions/cipher.service.ts
+++ b/libs/common/src/vault/abstractions/cipher.service.ts
@@ -207,9 +207,13 @@ export abstract class CipherService implements UserKeyRotationDataProvider>;
+ abstract upsert(
+ cipher: CipherData | CipherData[],
+ userId?: UserId,
+ ): Promise>;
abstract replace(ciphers: { [id: string]: CipherData }, userId: UserId): Promise;
abstract clear(userId?: string): Promise;
abstract moveManyWithServer(ids: string[], folderId: string, userId: UserId): Promise;
diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts
index 402b8ed1030..3c44b854de7 100644
--- a/libs/common/src/vault/services/cipher.service.ts
+++ b/libs/common/src/vault/services/cipher.service.ts
@@ -1196,12 +1196,15 @@ export class CipherService implements CipherServiceAbstraction {
await this.encryptedCiphersState(userId).update(() => ciphers);
}
- async upsert(cipher: CipherData | CipherData[]): Promise> {
+ async upsert(
+ cipher: CipherData | CipherData[],
+ userId?: UserId,
+ ): Promise> {
const ciphers = cipher instanceof CipherData ? [cipher] : cipher;
const res = await this.updateEncryptedCipherState((current) => {
ciphers.forEach((c) => (current[c.id as CipherId] = c));
return current;
- });
+ }, userId);
// Some state storage providers (e.g. Electron) don't update the state immediately, wait for next tick
// Otherwise, subscribers to cipherViews$ can get stale data
await new Promise((resolve) => setTimeout(resolve, 0));
diff --git a/libs/common/src/vault/services/default-cipher-archive.service.spec.ts b/libs/common/src/vault/services/default-cipher-archive.service.spec.ts
index 807311ca851..2f5e69d65ed 100644
--- a/libs/common/src/vault/services/default-cipher-archive.service.spec.ts
+++ b/libs/common/src/vault/services/default-cipher-archive.service.spec.ts
@@ -219,7 +219,7 @@ describe("DefaultCipherArchiveService", () => {
} as any,
}),
);
- mockCipherService.replace.mockResolvedValue(undefined);
+ mockCipherService.upsert.mockResolvedValue(undefined);
});
it("should archive single cipher", async () => {
@@ -233,13 +233,13 @@ describe("DefaultCipherArchiveService", () => {
true,
);
expect(mockCipherService.ciphers$).toHaveBeenCalledWith(userId);
- expect(mockCipherService.replace).toHaveBeenCalledWith(
- expect.objectContaining({
- [cipherId]: expect.objectContaining({
+ expect(mockCipherService.upsert).toHaveBeenCalledWith(
+ [
+ expect.objectContaining({
archivedDate: "2024-01-15T10:30:00.000Z",
revisionDate: "2024-01-15T10:31:00.000Z",
}),
- }),
+ ],
userId,
);
});
@@ -282,7 +282,7 @@ describe("DefaultCipherArchiveService", () => {
} as any,
}),
);
- mockCipherService.replace.mockResolvedValue(undefined);
+ mockCipherService.upsert.mockResolvedValue(undefined);
});
it("should unarchive single cipher", async () => {
@@ -296,12 +296,12 @@ describe("DefaultCipherArchiveService", () => {
true,
);
expect(mockCipherService.ciphers$).toHaveBeenCalledWith(userId);
- expect(mockCipherService.replace).toHaveBeenCalledWith(
- expect.objectContaining({
- [cipherId]: expect.objectContaining({
+ expect(mockCipherService.upsert).toHaveBeenCalledWith(
+ [
+ expect.objectContaining({
revisionDate: "2024-01-15T10:31:00.000Z",
}),
- }),
+ ],
userId,
);
});
diff --git a/libs/common/src/vault/services/default-cipher-archive.service.ts b/libs/common/src/vault/services/default-cipher-archive.service.ts
index 8076735c9e2..c1daade0dad 100644
--- a/libs/common/src/vault/services/default-cipher-archive.service.ts
+++ b/libs/common/src/vault/services/default-cipher-archive.service.ts
@@ -95,7 +95,7 @@ export class DefaultCipherArchiveService implements CipherArchiveService {
localCipher.revisionDate = cipher.revisionDate;
}
- await this.cipherService.replace(currentCiphers, userId);
+ await this.cipherService.upsert(Object.values(currentCiphers), userId);
}
async unarchiveWithServer(ids: CipherId | CipherId[], userId: UserId): Promise {
@@ -116,6 +116,6 @@ export class DefaultCipherArchiveService implements CipherArchiveService {
localCipher.revisionDate = cipher.revisionDate;
}
- await this.cipherService.replace(currentCiphers, userId);
+ await this.cipherService.upsert(Object.values(currentCiphers), userId);
}
}
diff --git a/libs/components/src/dialog/dialog.service.stories.ts b/libs/components/src/dialog/dialog.service.stories.ts
index 3b5bdc4d4e9..4e5c718e494 100644
--- a/libs/components/src/dialog/dialog.service.stories.ts
+++ b/libs/components/src/dialog/dialog.service.stories.ts
@@ -6,13 +6,14 @@ import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/an
import { getAllByRole, userEvent } from "storybook/test";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
+import { GlobalStateProvider } from "@bitwarden/state";
import { ButtonModule } from "../button";
import { IconButtonModule } from "../icon-button";
import { LayoutComponent } from "../layout";
import { SharedModule } from "../shared";
import { positionFixedWrapperDecorator } from "../stories/storybook-decorators";
-import { I18nMockService } from "../utils/i18n-mock.service";
+import { I18nMockService, StorybookGlobalStateProvider } from "../utils";
import { DialogModule } from "./dialog.module";
import { DialogService } from "./dialog.service";
@@ -161,6 +162,10 @@ export default {
});
},
},
+ {
+ provide: GlobalStateProvider,
+ useClass: StorybookGlobalStateProvider,
+ },
],
}),
],
diff --git a/libs/components/src/drawer/drawer.stories.ts b/libs/components/src/drawer/drawer.stories.ts
index 727d16b5481..9904b77ee9f 100644
--- a/libs/components/src/drawer/drawer.stories.ts
+++ b/libs/components/src/drawer/drawer.stories.ts
@@ -1,7 +1,8 @@
import { RouterTestingModule } from "@angular/router/testing";
-import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
+import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/angular";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
+import { GlobalStateProvider } from "@bitwarden/state";
import { ButtonModule } from "../button";
import { CalloutModule } from "../callout";
@@ -9,7 +10,7 @@ import { LayoutComponent } from "../layout";
import { mockLayoutI18n } from "../layout/mocks";
import { positionFixedWrapperDecorator } from "../stories/storybook-decorators";
import { TypographyModule } from "../typography";
-import { I18nMockService } from "../utils";
+import { I18nMockService, StorybookGlobalStateProvider } from "../utils";
import { DrawerBodyComponent } from "./drawer-body.component";
import { DrawerHeaderComponent } from "./drawer-header.component";
@@ -47,6 +48,14 @@ export default {
},
],
}),
+ applicationConfig({
+ providers: [
+ {
+ provide: GlobalStateProvider,
+ useClass: StorybookGlobalStateProvider,
+ },
+ ],
+ }),
],
} as Meta;
diff --git a/libs/components/src/item/item.stories.ts b/libs/components/src/item/item.stories.ts
index d2c197d0088..9498c163da7 100644
--- a/libs/components/src/item/item.stories.ts
+++ b/libs/components/src/item/item.stories.ts
@@ -1,16 +1,23 @@
import { ScrollingModule } from "@angular/cdk/scrolling";
import { CommonModule } from "@angular/common";
import { RouterTestingModule } from "@angular/router/testing";
-import { Meta, StoryObj, componentWrapperDecorator, moduleMetadata } from "@storybook/angular";
+import {
+ Meta,
+ StoryObj,
+ applicationConfig,
+ componentWrapperDecorator,
+ moduleMetadata,
+} from "@storybook/angular";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
+import { GlobalStateProvider } from "@bitwarden/state";
import { AvatarModule } from "../avatar";
import { BadgeModule } from "../badge";
import { IconButtonModule } from "../icon-button";
import { LayoutComponent } from "../layout";
import { TypographyModule } from "../typography";
-import { I18nMockService } from "../utils/i18n-mock.service";
+import { I18nMockService, StorybookGlobalStateProvider } from "../utils";
import { ItemActionComponent } from "./item-action.component";
import { ItemContentComponent } from "./item-content.component";
@@ -50,6 +57,14 @@ export default {
},
],
}),
+ applicationConfig({
+ providers: [
+ {
+ provide: GlobalStateProvider,
+ useClass: StorybookGlobalStateProvider,
+ },
+ ],
+ }),
componentWrapperDecorator((story) => `${story}
`),
],
parameters: {
diff --git a/libs/components/src/layout/layout.stories.ts b/libs/components/src/layout/layout.stories.ts
index a059fd61b92..59770c21d2e 100644
--- a/libs/components/src/layout/layout.stories.ts
+++ b/libs/components/src/layout/layout.stories.ts
@@ -1,13 +1,15 @@
import { RouterTestingModule } from "@angular/router/testing";
-import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
+import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/angular";
import { userEvent } from "storybook/test";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
+import { GlobalStateProvider } from "@bitwarden/state";
import { CalloutModule } from "../callout";
import { NavigationModule } from "../navigation";
import { positionFixedWrapperDecorator } from "../stories/storybook-decorators";
import { I18nMockService } from "../utils/i18n-mock.service";
+import { StorybookGlobalStateProvider } from "../utils/state-mock";
import { LayoutComponent } from "./layout.component";
import { mockLayoutI18n } from "./mocks";
@@ -28,6 +30,14 @@ export default {
},
],
}),
+ applicationConfig({
+ providers: [
+ {
+ provide: GlobalStateProvider,
+ useClass: StorybookGlobalStateProvider,
+ },
+ ],
+ }),
],
parameters: {
chromatic: { viewports: [640, 1280] },
diff --git a/libs/components/src/layout/mocks.ts b/libs/components/src/layout/mocks.ts
index 8b001eb8fd1..15b126ca718 100644
--- a/libs/components/src/layout/mocks.ts
+++ b/libs/components/src/layout/mocks.ts
@@ -5,4 +5,5 @@ export const mockLayoutI18n = {
submenu: "submenu",
toggleCollapse: "toggle collapse",
loading: "Loading",
+ resizeSideNavigation: "Resize side navigation",
};
diff --git a/libs/components/src/navigation/nav-group.stories.ts b/libs/components/src/navigation/nav-group.stories.ts
index c0111c23fc1..fa1cb06dbfe 100644
--- a/libs/components/src/navigation/nav-group.stories.ts
+++ b/libs/components/src/navigation/nav-group.stories.ts
@@ -3,11 +3,13 @@ import { RouterModule } from "@angular/router";
import { StoryObj, Meta, moduleMetadata, applicationConfig } from "@storybook/angular";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
+import { GlobalStateProvider } from "@bitwarden/state";
import { LayoutComponent } from "../layout";
import { SharedModule } from "../shared/shared.module";
import { positionFixedWrapperDecorator } from "../stories/storybook-decorators";
import { I18nMockService } from "../utils/i18n-mock.service";
+import { StorybookGlobalStateProvider } from "../utils/state-mock";
import { NavGroupComponent } from "./nav-group.component";
import { NavigationModule } from "./navigation.module";
@@ -42,6 +44,7 @@ export default {
toggleSideNavigation: "Toggle side navigation",
skipToContent: "Skip to content",
loading: "Loading",
+ resizeSideNavigation: "Resize side navigation",
});
},
},
@@ -58,6 +61,10 @@ export default {
{ useHash: true },
),
),
+ {
+ provide: GlobalStateProvider,
+ useClass: StorybookGlobalStateProvider,
+ },
],
}),
],
diff --git a/libs/components/src/navigation/nav-item.stories.ts b/libs/components/src/navigation/nav-item.stories.ts
index 131dacc8142..3036ab26348 100644
--- a/libs/components/src/navigation/nav-item.stories.ts
+++ b/libs/components/src/navigation/nav-item.stories.ts
@@ -1,12 +1,14 @@
import { RouterTestingModule } from "@angular/router/testing";
-import { StoryObj, Meta, moduleMetadata } from "@storybook/angular";
+import { StoryObj, Meta, moduleMetadata, applicationConfig } from "@storybook/angular";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
+import { GlobalStateProvider } from "@bitwarden/state";
import { IconButtonModule } from "../icon-button";
import { LayoutComponent } from "../layout";
import { positionFixedWrapperDecorator } from "../stories/storybook-decorators";
import { I18nMockService } from "../utils/i18n-mock.service";
+import { StorybookGlobalStateProvider } from "../utils/state-mock";
import { NavItemComponent } from "./nav-item.component";
import { NavigationModule } from "./navigation.module";
@@ -31,11 +33,20 @@ export default {
toggleSideNavigation: "Toggle side navigation",
skipToContent: "Skip to content",
loading: "Loading",
+ resizeSideNavigation: "Resize side navigation",
});
},
},
],
}),
+ applicationConfig({
+ providers: [
+ {
+ provide: GlobalStateProvider,
+ useClass: StorybookGlobalStateProvider,
+ },
+ ],
+ }),
],
parameters: {
design: {
diff --git a/libs/components/src/navigation/side-nav.component.html b/libs/components/src/navigation/side-nav.component.html
index c8b20ecba77..84c7e3e7298 100644
--- a/libs/components/src/navigation/side-nav.component.html
+++ b/libs/components/src/navigation/side-nav.component.html
@@ -5,47 +5,64 @@
};
as data
) {
-