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");