[PM-32016] - make at-risk banner dismissable#20505
[PM-32016] - make at-risk banner dismissable#20505jaasen-livefront wants to merge 1 commit intomainfrom
Conversation
|
|
Howdy @jaasen-livefront 👋🏼 I am field testing the new multi-agent code review, I gave it a local run against the PR. The findings are below for your analysis. Please incorporate the findings that are true signals. We are also looking closely into the difference in the way that Claude Code runs the review given different models for pros, cons, etc. We are going to continue to refine the context delivered to Claude during this reviews which may also include information from the Jira ticket itself to help ground Claude in reality. Many Thanks! Sonnet Code Review: [PM-32016] - make at-risk banner dismissable (#20505)Date: 2026-05-05 | Reviewed by: Claude Code (sonnet) Summary
The change introduces a per-client dismiss capability for the at-risk password callout backed by a new Findings
|
| Severity | Count |
|---|---|
| 🛑 Blocker | 0 |
| 1 | |
| ♻️ Refactor | 2 |
The feature logic is sound — keying dismissal state by cipher ID and revision date is correct, and the per-client storage strategy is appropriate. However, there is one meaningful functional bug: VaultItemDialogComponent is shared with the desktop app, so the unconditional [allowDismissAtRiskCallout]="true" binding will make the dismiss button appear on desktop despite the PR explicitly stating that desktop is intentionally excluded. Two refactor findings cover template duplication and a state-key definition that diverges from the established sibling pattern.
Findings
⚠️ Important
Desktop is NOT excluded — VaultItemDialog is used by desktop, contradicting PR description
libs/vault/src/vault-item-dialog/vault-item-dialog.component.html:16
Caught by: Architecture agent
Details
The PR description states: "This change adds a dismiss button (X) to the at-risk password callout in the browser extension and web app only. Desktop is intentionally excluded."
However, the diff sets [allowDismissAtRiskCallout]="true" unconditionally on <app-cipher-view> inside VaultItemDialogComponent. VaultItemDialogComponent is consumed by the desktop app at:
apps/desktop/src/vault/app/vault-v3/vault.component.ts:891
this.activeDrawerRef = await VaultItemDialogComponent.openDrawer(this.dialogService, { ... });As written, every desktop user who views an at-risk cipher will see the dismiss button — the stated exclusion is not enforced.
Suggested fix (either approach):
Option A — Make allowDismissAtRiskCallout an input on VaultItemDialogComponent itself (default false), and set it to true only from web callers (apps/web/src/app/...). The desktop openDrawer call inherits the safe false default.
Option B — Keep the binding in the template but resolve the value inside VaultItemDialogComponent based on a runtime platform check, so desktop continues to receive false.
♻️ Refactor
Two near-identical bit-callout blocks introduced; bodies duplicated rather than parameterized
libs/vault/src/cipher-view/cipher-view.component.html:14
Caught by: Architecture agent
Details
The diff replaces one <bit-callout> with two blocks whose entire inner content — the @if (changePasswordLink()) link/text branch — is duplicated verbatim. The blocks differ only in their *ngIf condition and whether (dismiss) is bound. Future edits to the callout body (i18n key, link attributes, icon, click handler) must be made in both places, and a divergence will silently produce inconsistent UX across the non-dismissable and dismissable variants.
Suggested fix: Use a single callout:
<bit-callout
*ngIf="showAtRiskCallout()"
type="warning"
[title]="''"
(dismiss)="allowDismissAtRiskCallout() ? dismissAtRiskCallout() : null"
>
@if (changePasswordLink()) {
<a bitLink ...>{{ "changeAtRiskPassword" | i18n }}</a>
} @else {
{{ "changeAtRiskPassword" | i18n }}
}
</bit-callout>showAtRiskCallout() already encodes showChangePasswordLink() && !atRiskBannerDismissed(), which correctly collapses to "always shown" when allowDismissAtRiskCallout is false (because atRiskBannerDismissed only flips after dismissAtRiskCallout is called, which is only wired in the dismissable variant).
DISMISSED_AT_RISK_CIPHERS_KEY defined in component file diverges from established sibling pattern
libs/vault/src/cipher-view/cipher-view.component.ts:65
Caught by: Architecture agent
Details
The new UserKeyDefinition DISMISSED_AT_RISK_CIPHERS_KEY (and its DismissedAtRiskCipherRecord type) is declared inline in the component file. The established sibling in the same library — AT_RISK_PASSWORD_CALLOUT_KEY — lives in a dedicated service module at libs/vault/src/services/at-risk-password-callout.service.ts, alongside its data type and a service that owns reads/writes via StateProvider.
Mixing state-key declarations into UI components creates two concrete maintenance costs:
- State keys become discoverable only by reading the component file, fragmenting where reviewers look for the at-risk feature's persistent state.
- The component now owns both rendering and persistence logic (
atRiskBannerDismissedsignal +dismissAtRiskCalloutwriter), where the existing pattern factors persistence behind a service — making the new code harder to reuse from any future view surface without copying state plumbing.
Suggested fix: Extract DISMISSED_AT_RISK_CIPHERS_KEY, DismissedAtRiskCipherRecord, and the state read/write helpers into a service in libs/vault/src/services/ (e.g. at-risk-callout-dismissal.service.ts), matching the shape of AtRiskPasswordCalloutService.
Reviewed and Dismissed
🔍 1 initial finding dismissed after validation
Two near-identical bit-callout blocks differ only in dismiss handler
libs/vault/src/cipher-view/cipher-view.component.html:14
Caught by: Code quality agent
Original severity: ♻️ Refactor
Original confidence: 80/100
Dismissed at: Step 4 validation
Dismissed because: Duplicate of arch-3 — both flag the same template duplication of the two near-identical <bit-callout> blocks at the same file and line.



🎟️ Tracking
https://bitwarden.atlassian.net/browse/PM-32016
📔 Objective
The at-risk password warning callout on the View Login page was persistent — it reappeared every time a user opened the item, with no way to dismiss it. For premium users this is noisy, since they may be aware of the risk and not ready to change the password immediately.
This change adds a dismiss button (X) to the at-risk password callout in the browser extension and web app only. Desktop is intentionally excluded.
Behavior:
web: disk-local) and does not roam between clients.Implementation:
VAULT_AT_RISK_VIEW_DISK_LOCALstate definition (disk,web: disk-local) andDISMISSED_AT_RISK_CIPHERS_KEYuser key definition to persist dismissed cipher IDs alongside the revision date at time of dismissal.allowDismissAtRiskCalloutinput toCipherViewComponent(defaultfalse). Browser extension and web app passtrue.showAtRiskCalloutcomputed combines the existingshowChangePasswordLink()condition with!atRiskBannerDismissed().bit-callout's built-in dismiss button is activated by binding(dismiss)on the dismissable variant only.📸 Screenshots
Screen.Recording.2026-05-04.at.4.23.47.PM.mov