1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-19 10:54:00 +00:00

move state update logic from component to at risk password callout service

This commit is contained in:
jng
2025-09-16 19:33:00 -04:00
parent 75a46567b0
commit 92db4e9645
3 changed files with 204 additions and 49 deletions

View File

@@ -109,4 +109,144 @@ describe("AtRiskPasswordCalloutService", () => {
expect(updater({})).toEqual(updateObj);
});
});
describe("shouldShowCompletionBanner$", () => {
beforeEach(() => {
jest.spyOn(mockTaskService, "pendingTasks$").mockReturnValue(of([]));
jest.spyOn(mockCipherService, "cipherViews$").mockReturnValue(of([]));
});
it("should return false when user has pending tasks", async () => {
const tasks = [{ id: "t1", cipherId: "c1", type: SecurityTaskType.UpdateAtRiskCredential }];
const ciphers = [new MockCipherView("c1", false)];
const state: AtRiskPasswordCalloutData = {
hadPendingTasks: true,
showTasksCompleteBanner: false,
tasksBannerDismissed: false,
};
jest.spyOn(mockTaskService, "pendingTasks$").mockReturnValue(of(tasks));
jest.spyOn(mockCipherService, "cipherViews$").mockReturnValue(of(ciphers));
mockStateProvider.getUser.mockReturnValue({ state$: of(state) });
const result = await firstValueFrom(service.shouldShowCompletionBanner$(userId));
expect(result).toBe(false);
});
it("should return true when no pending tasks and showTasksCompleteBanner is true", async () => {
const state: AtRiskPasswordCalloutData = {
hadPendingTasks: false,
showTasksCompleteBanner: true,
tasksBannerDismissed: false,
};
mockStateProvider.getUser.mockReturnValue({ state$: of(state) });
const result = await firstValueFrom(service.shouldShowCompletionBanner$(userId));
expect(result).toBe(true);
});
it("should return true when no pending tasks and hadPendingTasks is true", async () => {
const state: AtRiskPasswordCalloutData = {
hadPendingTasks: true,
showTasksCompleteBanner: false,
tasksBannerDismissed: false,
};
mockStateProvider.getUser.mockReturnValue({ state$: of(state) });
const result = await firstValueFrom(service.shouldShowCompletionBanner$(userId));
expect(result).toBe(true);
});
it("should return false when banner has been dismissed", async () => {
const state: AtRiskPasswordCalloutData = {
hadPendingTasks: false,
showTasksCompleteBanner: false,
tasksBannerDismissed: true,
};
mockStateProvider.getUser.mockReturnValue({ state$: of(state) });
const result = await firstValueFrom(service.shouldShowCompletionBanner$(userId));
expect(result).toBe(false);
});
it("should return false when state is null", async () => {
mockStateProvider.getUser.mockReturnValue({ state$: of(null) });
const result = await firstValueFrom(service.shouldShowCompletionBanner$(userId));
expect(result).toBe(false);
});
});
describe("updatePendingTasksState", () => {
it("should set showTasksCompleteBanner to true when user had tasks and resolved all of them", () => {
const currentState: AtRiskPasswordCalloutData = {
hadPendingTasks: true,
showTasksCompleteBanner: false,
tasksBannerDismissed: false,
};
const returnedState = {
state$: of(currentState),
update: jest.fn().mockResolvedValue(undefined),
};
mockStateProvider.getUser.mockReturnValue(returnedState);
service.updatePendingTasksState(userId, 0);
expect(returnedState.update).toHaveBeenCalledWith(expect.any(Function));
const updater = (returnedState.update as jest.Mock).mock.calls[0][0];
expect(updater()).toEqual({
hadPendingTasks: false,
showTasksCompleteBanner: true,
tasksBannerDismissed: false,
});
});
it("should set hadPendingTasks to true when user has tasks", () => {
const currentState: AtRiskPasswordCalloutData = {
hadPendingTasks: false,
showTasksCompleteBanner: false,
tasksBannerDismissed: false,
};
const returnedState = {
state$: of(currentState),
update: jest.fn().mockResolvedValue(undefined),
};
mockStateProvider.getUser.mockReturnValue(returnedState);
service.updatePendingTasksState(userId, 2);
expect(returnedState.update).toHaveBeenCalledWith(expect.any(Function));
const updater = (returnedState.update as jest.Mock).mock.calls[0][0];
expect(updater()).toEqual({
hadPendingTasks: true,
showTasksCompleteBanner: false,
tasksBannerDismissed: false,
});
});
it("should not update state when no changes made", () => {
const currentState: AtRiskPasswordCalloutData = {
hadPendingTasks: false,
showTasksCompleteBanner: false,
tasksBannerDismissed: false,
};
const returnedState = {
state$: of(currentState),
update: jest.fn().mockResolvedValue(undefined),
};
mockStateProvider.getUser.mockReturnValue(returnedState);
service.updatePendingTasksState(userId, 0);
expect(returnedState.update).not.toHaveBeenCalled();
});
});
});

View File

@@ -1,5 +1,5 @@
import { Injectable } from "@angular/core";
import { combineLatest, map, Observable } from "rxjs";
import { combineLatest, map, Observable, take, tap } from "rxjs";
import {
SingleUserState,
@@ -53,6 +53,30 @@ export class AtRiskPasswordCalloutService {
);
}
shouldShowCompletionBanner$(userId: UserId): Observable<boolean> {
return combineLatest([
this.pendingTasks$(userId),
this.atRiskPasswordState(userId).state$,
]).pipe(
map(([pendingTasks, state]) => {
const hasPendingTasks = pendingTasks.length > 0;
const {
showTasksCompleteBanner = false,
hadPendingTasks = false,
tasksBannerDismissed = false,
} = state ?? {};
// Show banner if
// user has resolved all pending tasks
// user had showTasksCompleteBanner set to true and hasn't received new tasks
// user had showTasksCompleteBanner set to true and hasn't dismissed
return (
!hasPendingTasks && (showTasksCompleteBanner || hadPendingTasks) && !tasksBannerDismissed
);
}),
);
}
atRiskPasswordState(userId: UserId): SingleUserState<AtRiskPasswordCalloutData> {
return this.stateProvider.getUser(userId, AT_RISK_PASSWORD_CALLOUT_KEY);
}
@@ -60,4 +84,35 @@ export class AtRiskPasswordCalloutService {
updateAtRiskPasswordState(userId: UserId, updatedState: AtRiskPasswordCalloutData): void {
void this.atRiskPasswordState(userId).update(() => updatedState);
}
updatePendingTasksState(userId: UserId, currentTaskCount: number): void {
this.atRiskPasswordState(userId)
.state$.pipe(
take(1),
tap((currentState) => {
let updateObject: AtRiskPasswordCalloutData | null = null;
// If user had pending tasks and resolved all, show banner
if (currentState?.hadPendingTasks && currentTaskCount === 0) {
updateObject = {
hadPendingTasks: false,
showTasksCompleteBanner: true,
tasksBannerDismissed: false,
};
// If user has pending tasks, set hadPendingTasks to true
} else if (currentTaskCount > 0) {
updateObject = {
hadPendingTasks: true,
showTasksCompleteBanner: false,
tasksBannerDismissed: false,
};
}
if (updateObject) {
this.updateAtRiskPasswordState(userId, updateObject);
}
}),
)
.subscribe();
}
}