Skip to content
Open
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
1 change: 1 addition & 0 deletions apps/browser/src/background/main.background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1351,6 +1351,7 @@ export default class MainBackground {
this.policyService,
this.newPolicyService,
this.autoConfirmService,
this.billingAccountProfileStateService,
);

this.fido2UserInterfaceService = new BrowserFido2UserInterfaceService(this.authService);
Expand Down
1 change: 1 addition & 0 deletions libs/angular/src/services/jslib-services.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1172,6 +1172,7 @@ const safeProviders: SafeProvider[] = [
InternalPolicyService,
InternalNewPolicyService,
AutomaticUserConfirmationService,
BillingAccountProfileStateService,
],
}),
safeProvider({
Expand Down
2 changes: 2 additions & 0 deletions libs/common/src/enums/notification-type.enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,6 @@ export enum NotificationType {

SyncPolicy = 25,
AutoConfirmMember = 26,

PremiumStatusChanged = 27,
}
14 changes: 14 additions & 0 deletions libs/common/src/models/response/notification.response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ export class NotificationResponse extends BaseResponse {
case NotificationType.AutoConfirmMember:
this.payload = new AutoConfirmMemberNotification(payload);
break;
case NotificationType.PremiumStatusChanged:
this.payload = new PremiumStatusChangedNotification(payload);
break;
default:
break;
}
Expand Down Expand Up @@ -228,3 +231,14 @@ export class AutoConfirmMemberNotification extends BaseResponse {
this.organizationId = this.getResponseProperty("OrganizationId");
}
}

export class PremiumStatusChangedNotification extends BaseResponse {
userId: string;
premium: boolean;

constructor(response: any) {
super(response);
this.userId = this.getResponseProperty("UserId");
this.premium = this.getResponseProperty("Premium");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { AutomaticUserConfirmationService } from "@bitwarden/auto-confirm";
import { InternalNewPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/new-policy.service.abstraction";
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { AuthRequestAnsweringService } from "@bitwarden/common/auth/abstractions/auth-request-answering/auth-request-answering.service.abstraction";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";

import { mockAccountInfoWith } from "../../../../spec";
import { AccountService } from "../../../auth/abstractions/account.service";
Expand Down Expand Up @@ -40,6 +41,7 @@ describe("DefaultServerNotificationsService (multi-user)", () => {
let policyService: MockProxy<InternalPolicyService>;
let newPolicyService: MockProxy<InternalNewPolicyService>;
let autoConfirmService: MockProxy<AutomaticUserConfirmationService>;
let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>;

let activeUserAccount$: BehaviorSubject<ObservedValueOf<AccountService["activeAccount$"]>>;
let userAccounts$: BehaviorSubject<ObservedValueOf<AccountService["accounts$"]>>;
Expand Down Expand Up @@ -136,6 +138,7 @@ describe("DefaultServerNotificationsService (multi-user)", () => {
policyService = mock<InternalPolicyService>();
newPolicyService = mock<InternalNewPolicyService>();
autoConfirmService = mock<AutomaticUserConfirmationService>();
billingAccountProfileStateService = mock<BillingAccountProfileStateService>();

defaultServerNotificationsService = new DefaultServerNotificationsService(
mock<LogService>(),
Expand All @@ -153,6 +156,7 @@ describe("DefaultServerNotificationsService (multi-user)", () => {
policyService,
newPolicyService,
autoConfirmService,
billingAccountProfileStateService,
);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { InternalNewPolicyService } from "@bitwarden/common/admin-console/abstra
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { AuthRequestAnsweringService } from "@bitwarden/common/auth/abstractions/auth-request-answering/auth-request-answering.service.abstraction";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";

import { awaitAsync, mockAccountInfoWith } from "../../../../spec";
import { Matrix } from "../../../../spec/matrix";
Expand Down Expand Up @@ -49,6 +50,7 @@ describe("NotificationsService", () => {
let policyService: MockProxy<InternalPolicyService>;
let newPolicyService: MockProxy<InternalNewPolicyService>;
let autoConfirmService: MockProxy<AutomaticUserConfirmationService>;
let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>;

let activeAccount: BehaviorSubject<ObservedValueOf<AccountService["activeAccount$"]>>;
let accounts: BehaviorSubject<ObservedValueOf<AccountService["accounts$"]>>;
Expand Down Expand Up @@ -81,6 +83,7 @@ describe("NotificationsService", () => {
policyService = mock<InternalPolicyService>();
newPolicyService = mock<InternalNewPolicyService>();
autoConfirmService = mock<AutomaticUserConfirmationService>();
billingAccountProfileStateService = mock<BillingAccountProfileStateService>();

// For these tests, use the active-user implementation (feature flag disabled)
configService.getFeatureFlag$.mockReturnValue(of(true));
Expand Down Expand Up @@ -136,6 +139,7 @@ describe("NotificationsService", () => {
policyService,
newPolicyService,
autoConfirmService,
billingAccountProfileStateService,
);
});

Expand Down Expand Up @@ -541,5 +545,74 @@ describe("NotificationsService", () => {
);
});
});

describe("NotificationType.PremiumStatusChanged", () => {
beforeEach(() => {
billingAccountProfileStateService.hasPremiumFromAnyOrganization$.mockReturnValue(of(false));
billingAccountProfileStateService.setHasPremium.mockResolvedValue();
});

it("should call setHasPremium with premium=true when notification payload is true", async () => {
const notification = new NotificationResponse({
type: NotificationType.PremiumStatusChanged,
payload: { UserId: mockUser1, Premium: true },
contextId: "different-app-id",
});

await sut["processNotification"](notification, mockUser1);

expect(billingAccountProfileStateService.setHasPremium).toHaveBeenCalledWith(
true,
false,
mockUser1,
);
});

it("should call setHasPremium with premium=false when notification payload is false", async () => {
const notification = new NotificationResponse({
type: NotificationType.PremiumStatusChanged,
payload: { UserId: mockUser1, Premium: false },
contextId: "different-app-id",
});

await sut["processNotification"](notification, mockUser1);

expect(billingAccountProfileStateService.setHasPremium).toHaveBeenCalledWith(
false,
false,
mockUser1,
);
});

it("should preserve existing hasPremiumFromAnyOrganization value", async () => {
billingAccountProfileStateService.hasPremiumFromAnyOrganization$.mockReturnValue(of(true));

const notification = new NotificationResponse({
type: NotificationType.PremiumStatusChanged,
payload: { UserId: mockUser1, Premium: true },
contextId: "different-app-id",
});

await sut["processNotification"](notification, mockUser1);

expect(billingAccountProfileStateService.setHasPremium).toHaveBeenCalledWith(
true,
true,
mockUser1,
);
});

it("should not trigger a full sync", async () => {
const notification = new NotificationResponse({
type: NotificationType.PremiumStatusChanged,
payload: { UserId: mockUser1, Premium: true },
contextId: "different-app-id",
});

await sut["processNotification"](notification, mockUser1);

expect(syncService.fullSync).not.toHaveBeenCalled();
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,12 @@ import { trackedMerge } from "@bitwarden/common/platform/misc";
import { AccountInfo, AccountService } from "../../../auth/abstractions/account.service";
import { AuthService } from "../../../auth/abstractions/auth.service";
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
import { BillingAccountProfileStateService } from "../../../billing/abstractions/account/billing-account-profile-state.service";
import { NotificationType, PushNotificationLogOutReasonType } from "../../../enums";
import {
LogOutNotification,
NotificationResponse,
PremiumStatusChangedNotification,
SyncCipherNotification,
SyncFolderNotification,
SyncSendNotification,
Expand Down Expand Up @@ -75,6 +77,7 @@ export class DefaultServerNotificationsService implements ServerNotificationsSer
private readonly policyService: InternalPolicyService,
private readonly newPolicyService: InternalNewPolicyService,
private autoConfirmService: AutomaticUserConfirmationService,
private readonly billingAccountProfileStateService: BillingAccountProfileStateService,
) {
this.notifications$ = this.accountService.accounts$.pipe(
map((accounts: Record<UserId, AccountInfo>): Set<UserId> => {
Expand Down Expand Up @@ -308,6 +311,20 @@ export class DefaultServerNotificationsService implements ServerNotificationsSer
notification.payload.organizationId,
);
break;
case NotificationType.PremiumStatusChanged: {
const premiumPayload = notification.payload as PremiumStatusChangedNotification;
const hasPremiumFromAnyOrganization = await firstValueFrom(
this.billingAccountProfileStateService.hasPremiumFromAnyOrganization$(
premiumPayload.userId as UserId,
),
);
await this.billingAccountProfileStateService.setHasPremium(
premiumPayload.premium,
hasPremiumFromAnyOrganization,
premiumPayload.userId as UserId,
);
break;
}
default:
break;
}
Expand Down
Loading