From a8c6e3ffe24a39ff43f1ed3b1c5e84559b405742 Mon Sep 17 00:00:00 2001 From: jng Date: Thu, 17 Jul 2025 18:16:07 -0400 Subject: [PATCH] saved WIP --- apps/browser/src/_locales/en/messages.json | 5 +- .../at-risk-password-callout.component.html | 30 ++++++--- .../at-risk-password-callout.component.ts | 65 ++++++++++++++++++- .../src/platform/state/state-definitions.ts | 1 + 4 files changed, 88 insertions(+), 13 deletions(-) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index ee8cd412625..d0478304362 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -4264,7 +4264,7 @@ }, "uriMatchDefaultStrategyHint": { "message": "URI match detection is how Bitwarden identifies autofill suggestions.", - "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." }, "regExAdvancedOptionWarning": { "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", @@ -5457,5 +5457,8 @@ "wasmNotSupported": { "message": "WebAssembly is not supported on your browser or is not enabled. WebAssembly is required to use the Bitwarden app.", "description": "'WebAssembly' is a technical term and should not be translated." + }, + "atRiskLoginsSecured": { + "message": "Great job securing your at-risk logins!" } } 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 16d9b6a322a..bfd24d99587 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,9 +1,21 @@ - - - - {{ - (taskCount === 1 ? "reviewAndChangeAtRiskPassword" : "reviewAndChangeAtRiskPasswordsPlural") - | i18n: taskCount.toString() - }} - - +@if (currentPendingTasks()) { + + + + {{ + (taskCount === 1 ? "reviewAndChangeAtRiskPassword" : "reviewAndChangeAtRiskPasswordsPlural") + | i18n: taskCount.toString() + }} + + +} + +@if (showTasksResolved()) { + + {{ "atRiskLoginsSecured" | i18n }} + +} 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 3c3270e557c..610ed99c6f2 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 @@ -1,24 +1,55 @@ import { CommonModule } from "@angular/common"; -import { Component, inject } from "@angular/core"; +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 { AnchorLinkDirective, CalloutModule } from "@bitwarden/components"; +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"], + }, +); @Component({ selector: "vault-at-risk-password-callout", - imports: [CommonModule, AnchorLinkDirective, RouterModule, CalloutModule, I18nPipe], + imports: [ + CommonModule, + AnchorLinkDirective, + RouterModule, + CalloutModule, + I18nPipe, + BannerModule, + JslibModule, + ], 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 userIdSignal = toSignal(this.activeAccount$, { initialValue: null }); protected pendingTasks$ = this.activeAccount$.pipe( switchMap((userId) => @@ -39,4 +70,32 @@ export class AtRiskPasswordCalloutComponent { }), ), ); + + private atRiskPasswordStateSignal = toSignal( + this.atRiskPasswordState(this.userIdSignal()!).state$, + ); + + currentPendingTasks = toSignal(this.pendingTasks$, { initialValue: [] }); + + showTasksResolved = computed(() => { + if (this.atRiskPasswordStateSignal() && this.currentPendingTasks().length === 0) { + return true; + } + }); + + constructor(private stateProvider: StateProvider) { + effect(() => { + if (this.currentPendingTasks().length > 0) { + this.updateAtRiskPasswordState(this.userIdSignal()!, true); + } + }); + } + + 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/libs/common/src/platform/state/state-definitions.ts b/libs/common/src/platform/state/state-definitions.ts index 93c489a343e..30bc4f734b2 100644 --- a/libs/common/src/platform/state/state-definitions.ts +++ b/libs/common/src/platform/state/state-definitions.ts @@ -206,3 +206,4 @@ export const VAULT_BROWSER_INTRO_CAROUSEL = new StateDefinition( "vaultBrowserIntroCarousel", "disk", ); +export const VAULT_AT_RISK_PASSWORDS_DISK = new StateDefinition("vaultAtRiskPasswords", "disk");