diff --git a/apps/browser/src/vault/popup/components/at-risk-callout/at-risk-password-callout.component.html b/apps/browser/src/vault/popup/components/at-risk-callout/at-risk-password-callout.component.html index bfd24d99587..6426a8958f7 100644 --- a/apps/browser/src/vault/popup/components/at-risk-callout/at-risk-password-callout.component.html +++ b/apps/browser/src/vault/popup/components/at-risk-callout/at-risk-password-callout.component.html @@ -1,5 +1,5 @@ -@if (currentPendingTasks()) { - +@if (currentPendingTasks().length > 0) { + {{ diff --git a/apps/browser/src/vault/popup/components/at-risk-callout/at-risk-password-callout.component.ts b/apps/browser/src/vault/popup/components/at-risk-callout/at-risk-password-callout.component.ts index 610ed99c6f2..873271f6342 100644 --- a/apps/browser/src/vault/popup/components/at-risk-callout/at-risk-password-callout.component.ts +++ b/apps/browser/src/vault/popup/components/at-risk-callout/at-risk-password-callout.component.ts @@ -2,35 +2,17 @@ import { CommonModule } from "@angular/common"; import { Component, computed, inject, effect } from "@angular/core"; import { toSignal } from "@angular/core/rxjs-interop"; import { RouterModule } from "@angular/router"; -import { combineLatest, map, switchMap } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; -import { - StateProvider, - UserKeyDefinition, - VAULT_AT_RISK_PASSWORDS_DISK, -} from "@bitwarden/common/platform/state"; -import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { SecurityTaskType, TaskService } from "@bitwarden/common/vault/tasks"; +import { StateProvider } from "@bitwarden/common/platform/state"; import { AnchorLinkDirective, CalloutModule, BannerModule } from "@bitwarden/components"; import { I18nPipe } from "@bitwarden/ui-common"; -import { UserId } from "@bitwarden/user-core"; - -// Move this state provider code and methods into a new at risk password callout service -// a show/hide boolean for the congrats banner -// a dismissed boolean for the banner dismissal -// a hadPendingTasks boolean to track if the user had pending tasks previously - -const AT_RISK_ITEMS = new UserKeyDefinition( - VAULT_AT_RISK_PASSWORDS_DISK, - "atRiskPasswords", - { - deserializer: (atRiskItems) => atRiskItems, - clearOn: ["logout", "lock"], - }, -); +import { + AtRiskPasswordCalloutData, + AtRiskPasswordCalloutService, +} from "@bitwarden/web-vault/app/vault/services/at-risk-password-callout.service"; @Component({ selector: "vault-at-risk-password-callout", @@ -43,39 +25,24 @@ const AT_RISK_ITEMS = new UserKeyDefinition( BannerModule, JslibModule, ], + providers: [AtRiskPasswordCalloutService], templateUrl: "./at-risk-password-callout.component.html", }) export class AtRiskPasswordCalloutComponent { - private taskService = inject(TaskService); - private cipherService = inject(CipherService); private activeAccount$ = inject(AccountService).activeAccount$.pipe(getUserId); + private atRiskPasswordCalloutService = inject(AtRiskPasswordCalloutService); private userIdSignal = toSignal(this.activeAccount$, { initialValue: null }); - protected pendingTasks$ = this.activeAccount$.pipe( - switchMap((userId) => - combineLatest([ - this.taskService.pendingTasks$(userId), - this.cipherService.cipherViews$(userId), - ]), - ), - map(([tasks, ciphers]) => - tasks.filter((t) => { - const associatedCipher = ciphers.find((c) => c.id === t.cipherId); - - return ( - t.type === SecurityTaskType.UpdateAtRiskCredential && - associatedCipher && - !associatedCipher.isDeleted - ); - }), - ), - ); - private atRiskPasswordStateSignal = toSignal( - this.atRiskPasswordState(this.userIdSignal()!).state$, + this.atRiskPasswordCalloutService.atRiskPasswordState(this.userIdSignal()!).state$, ); - currentPendingTasks = toSignal(this.pendingTasks$, { initialValue: [] }); + currentPendingTasks = toSignal( + this.atRiskPasswordCalloutService.pendingTasks$(this.userIdSignal()!), + { + initialValue: [], + }, + ); showTasksResolved = computed(() => { if (this.atRiskPasswordStateSignal() && this.currentPendingTasks().length === 0) { @@ -86,16 +53,16 @@ export class AtRiskPasswordCalloutComponent { constructor(private stateProvider: StateProvider) { effect(() => { if (this.currentPendingTasks().length > 0) { - this.updateAtRiskPasswordState(this.userIdSignal()!, true); + const updateObject: AtRiskPasswordCalloutData = { + hadPendingTasks: true, + showTasksCompleteBanner: false, + tasksBannerDismissed: false, + }; + this.atRiskPasswordCalloutService.updateAtRiskPasswordState( + this.userIdSignal()!, + updateObject, + ); } }); } - - private atRiskPasswordState(userId: UserId) { - return this.stateProvider.getUser(userId, AT_RISK_ITEMS); - } - - private updateAtRiskPasswordState(userId: UserId, hasAtRiskPassword: boolean) { - void this.atRiskPasswordState(userId).update(() => hasAtRiskPassword); - } } diff --git a/apps/web/src/app/vault/services/at-risk-password-callout.service.spec.ts b/apps/web/src/app/vault/services/at-risk-password-callout.service.spec.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/apps/web/src/app/vault/services/at-risk-password-callout.service.ts b/apps/web/src/app/vault/services/at-risk-password-callout.service.ts new file mode 100644 index 00000000000..3af5433adf7 --- /dev/null +++ b/apps/web/src/app/vault/services/at-risk-password-callout.service.ts @@ -0,0 +1,62 @@ +import { Injectable } from "@angular/core"; +import { combineLatest, map, Observable } from "rxjs"; + +import { + StateProvider, + UserKeyDefinition, + VAULT_AT_RISK_PASSWORDS_DISK, +} from "@bitwarden/common/platform/state"; +import { UserId } from "@bitwarden/common/types/guid"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { SecurityTask, SecurityTaskType, TaskService } from "@bitwarden/common/vault/tasks"; + +export type AtRiskPasswordCalloutData = { + hadPendingTasks: boolean; + showTasksCompleteBanner: boolean; + tasksBannerDismissed: boolean; +}; + +const AT_RISK_PASSWORD_CALLOUT_KEY = new UserKeyDefinition( + VAULT_AT_RISK_PASSWORDS_DISK, + "atRiskPasswords", + { + deserializer: (jsonData) => jsonData, + clearOn: ["logout", "lock"], + }, +); + +@Injectable() +export class AtRiskPasswordCalloutService { + constructor( + private taskService: TaskService, + private cipherService: CipherService, + private stateProvider: StateProvider, + ) {} + + pendingTasks$(userId: UserId): Observable { + return combineLatest([ + this.taskService.pendingTasks$(userId), + this.cipherService.cipherViews$(userId), + ]).pipe( + map(([tasks, ciphers]) => + tasks.filter((t: SecurityTask) => { + const associatedCipher = ciphers.find((c) => c.id === t.cipherId); + + return ( + t.type === SecurityTaskType.UpdateAtRiskCredential && + associatedCipher && + !associatedCipher.isDeleted + ); + }), + ), + ); + } + + atRiskPasswordState(userId: UserId) { + return this.stateProvider.getUser(userId, AT_RISK_PASSWORD_CALLOUT_KEY); + } + + updateAtRiskPasswordState(userId: UserId, updatedState: AtRiskPasswordCalloutData): void { + void this.atRiskPasswordState(userId).update(() => updatedState); + } +}