1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-10 13:23:34 +00:00

[PM-18799] - Settings Tab Badge Updates (#14405)

* download bitwarden page

* add has download bitwarden nudge service

* download bitwarden component and nudge

* fix test

* fix potential badge flash. prefer use of getUserId

* catch profileCreation error. clean up settings observables

* add profile date as observable

* fix failing tests

* remove debugging code and IntroCarouselDismissal

* fix observable name
This commit is contained in:
Jordan Aasen
2025-05-06 13:08:33 -07:00
committed by GitHub
parent 46df5279a3
commit 1486cee8b9
16 changed files with 233 additions and 28 deletions

View File

@@ -4512,6 +4512,27 @@
} }
} }
}, },
"downloadBitwarden": {
"message": "Download Bitwarden"
},
"downloadBitwardenOnAllDevices": {
"message": "Download Bitwarden on all devices"
},
"getTheMobileApp": {
"message": "Get the mobile app"
},
"getTheMobileAppDesc": {
"message": "Access your passwords on the go with the Bitwarden mobile app."
},
"getTheDesktopApp": {
"message": "Get the desktop app"
},
"getTheDesktopAppDesc": {
"message": "Access your vault without a browser, then set up unlock with biometrics to expedite unlocking in both the desktop app and browser extension."
},
"downloadFromBitwardenNow": {
"message": "Download from bitwarden.com now"
},
"permanentlyDeleteAttachmentConfirmation": { "permanentlyDeleteAttachmentConfirmation": {
"message": "Are you sure you want to permanently delete this attachment?" "message": "Are you sure you want to permanently delete this attachment?"
}, },

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -67,7 +67,6 @@ import { SendAddEditComponent as SendAddEditV2Component } from "../tools/popup/s
import { SendCreatedComponent } from "../tools/popup/send-v2/send-created/send-created.component"; import { SendCreatedComponent } from "../tools/popup/send-v2/send-created/send-created.component";
import { SendV2Component } from "../tools/popup/send-v2/send-v2.component"; import { SendV2Component } from "../tools/popup/send-v2/send-v2.component";
import { AboutPageV2Component } from "../tools/popup/settings/about-page/about-page-v2.component"; import { AboutPageV2Component } from "../tools/popup/settings/about-page/about-page-v2.component";
import { MoreFromBitwardenPageV2Component } from "../tools/popup/settings/about-page/more-from-bitwarden-page-v2.component";
import { ExportBrowserV2Component } from "../tools/popup/settings/export/export-browser-v2.component"; import { ExportBrowserV2Component } from "../tools/popup/settings/export/export-browser-v2.component";
import { ImportBrowserV2Component } from "../tools/popup/settings/import/import-browser-v2.component"; import { ImportBrowserV2Component } from "../tools/popup/settings/import/import-browser-v2.component";
import { SettingsV2Component } from "../tools/popup/settings/settings-v2.component"; import { SettingsV2Component } from "../tools/popup/settings/settings-v2.component";
@@ -83,7 +82,9 @@ import { PasswordHistoryV2Component } from "../vault/popup/components/vault-v2/v
import { VaultV2Component } from "../vault/popup/components/vault-v2/vault-v2.component"; import { VaultV2Component } from "../vault/popup/components/vault-v2/vault-v2.component";
import { ViewV2Component } from "../vault/popup/components/vault-v2/view-v2/view-v2.component"; import { ViewV2Component } from "../vault/popup/components/vault-v2/view-v2/view-v2.component";
import { AppearanceV2Component } from "../vault/popup/settings/appearance-v2.component"; import { AppearanceV2Component } from "../vault/popup/settings/appearance-v2.component";
import { DownloadBitwardenComponent } from "../vault/popup/settings/download-bitwarden.component";
import { FoldersV2Component } from "../vault/popup/settings/folders-v2.component"; import { FoldersV2Component } from "../vault/popup/settings/folders-v2.component";
import { MoreFromBitwardenPageV2Component } from "../vault/popup/settings/more-from-bitwarden-page-v2.component";
import { TrashComponent } from "../vault/popup/settings/trash.component"; import { TrashComponent } from "../vault/popup/settings/trash.component";
import { VaultSettingsV2Component } from "../vault/popup/settings/vault-settings-v2.component"; import { VaultSettingsV2Component } from "../vault/popup/settings/vault-settings-v2.component";
@@ -583,6 +584,12 @@ const routes: Routes = [
canActivate: [authGuard], canActivate: [authGuard],
data: { elevation: 2 } satisfies RouteDataProperties, data: { elevation: 2 } satisfies RouteDataProperties,
}, },
{
path: "download-bitwarden",
component: DownloadBitwardenComponent,
canActivate: [authGuard],
data: { elevation: 2 } satisfies RouteDataProperties,
},
{ {
path: "intro-carousel", path: "intro-carousel",
component: ExtensionAnonLayoutWrapperComponent, component: ExtensionAnonLayoutWrapperComponent,

View File

@@ -23,12 +23,6 @@
<i slot="end" class="bwi bwi-external-link" aria-hidden="true"></i> <i slot="end" class="bwi bwi-external-link" aria-hidden="true"></i>
</button> </button>
</bit-item> </bit-item>
<bit-item>
<a bit-item-content routerLink="/more-from-bitwarden">
{{ "moreFromBitwarden" | i18n }}
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
</a>
</bit-item>
<bit-item> <bit-item>
<button type="button" bit-item-content (click)="rate()"> <button type="button" bit-item-content (click)="rate()">
{{ "rateExtension" | i18n }} {{ "rateExtension" | i18n }}

View File

@@ -66,5 +66,27 @@
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i> <i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
</a> </a>
</bit-item> </bit-item>
<bit-item>
<a bit-item-content routerLink="/download-bitwarden">
<i slot="start" class="bwi bwi-mobile" aria-hidden="true"></i>
<div class="tw-flex tw-items-center tw-justify-center">
<p class="tw-pr-2">{{ "downloadBitwardenOnAllDevices" | i18n }}</p>
<span
*ngIf="(downloadBitwardenNudgeStatus$ | async)?.hasBadgeDismissed === false"
bitBadge
variant="notification"
>1
</span>
</div>
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
</a>
</bit-item>
<bit-item>
<a bit-item-content routerLink="/more-from-bitwarden">
<i slot="start" class="bwi bwi-filter" aria-hidden="true"></i>
{{ "moreFromBitwarden" | i18n }}
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
</a>
</bit-item>
</bit-item-group> </bit-item-group>
</popup-page> </popup-page>

View File

@@ -1,11 +1,10 @@
import { CommonModule } from "@angular/common"; import { CommonModule } from "@angular/common";
import { Component, OnInit } from "@angular/core"; import { Component } from "@angular/core";
import { RouterModule } from "@angular/router"; import { RouterModule } from "@angular/router";
import { firstValueFrom, Observable } from "rxjs"; import { filter, firstValueFrom, Observable, shareReplay, switchMap } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module"; import { JslibModule } from "@bitwarden/angular/jslib.module";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { UserId } from "@bitwarden/common/types/guid"; import { UserId } from "@bitwarden/common/types/guid";
import { BadgeComponent, ItemModule } from "@bitwarden/components"; import { BadgeComponent, ItemModule } from "@bitwarden/components";
import { NudgeStatus, VaultNudgesService, VaultNudgeType } from "@bitwarden/vault"; import { NudgeStatus, VaultNudgesService, VaultNudgeType } from "@bitwarden/vault";
@@ -30,26 +29,35 @@ import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.co
BadgeComponent, BadgeComponent,
], ],
}) })
export class SettingsV2Component implements OnInit { export class SettingsV2Component {
VaultNudgeType = VaultNudgeType; VaultNudgeType = VaultNudgeType;
showVaultBadge$: Observable<NudgeStatus> = new Observable();
activeUserId: UserId | null = null; private authenticatedAccount$: Observable<Account> = this.accountService.activeAccount$.pipe(
filter((account): account is Account => account !== null),
shareReplay({ bufferSize: 1, refCount: true }),
);
downloadBitwardenNudgeStatus$: Observable<NudgeStatus> = this.authenticatedAccount$.pipe(
switchMap((account) =>
this.vaultNudgesService.showNudge$(VaultNudgeType.DownloadBitwarden, account.id),
),
);
showVaultBadge$: Observable<NudgeStatus> = this.authenticatedAccount$.pipe(
switchMap((account) =>
this.vaultNudgesService.showNudge$(VaultNudgeType.EmptyVaultNudge, account.id),
),
);
constructor( constructor(
private readonly vaultNudgesService: VaultNudgesService, private readonly vaultNudgesService: VaultNudgesService,
private readonly accountService: AccountService, private readonly accountService: AccountService,
) {} ) {}
async ngOnInit() {
this.activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
this.showVaultBadge$ = this.vaultNudgesService.showNudge$(
VaultNudgeType.EmptyVaultNudge,
this.activeUserId,
);
}
async dismissBadge(type: VaultNudgeType) { async dismissBadge(type: VaultNudgeType) {
if (!(await firstValueFrom(this.showVaultBadge$)).hasBadgeDismissed) { if (!(await firstValueFrom(this.showVaultBadge$)).hasBadgeDismissed) {
await this.vaultNudgesService.dismissNudge(type, this.activeUserId as UserId, true); const account = await firstValueFrom(this.authenticatedAccount$);
await this.vaultNudgesService.dismissNudge(type, account.id as UserId, true);
} }
} }
} }

View File

@@ -0,0 +1,50 @@
<popup-page>
<popup-header slot="header" pageTitle="{{ 'downloadBitwarden' | i18n }}" showBackButton>
<ng-container slot="end">
<app-pop-out></app-pop-out>
<app-current-account></app-current-account>
</ng-container>
</popup-header>
<h2 bitTypography="h6">
{{ "getTheMobileApp" | i18n }}
</h2>
<bit-card>
<span bitTypography="body2">
{{ "getTheMobileAppDesc" | i18n }}
</span>
<div class="tw-flex tw-items-center tw-justify-center tw-my-4">
<img
src="../../../images/download-qr.png"
alt=""
class="tw-w-[43%] tw-border-solid tw-border tw-border-secondary-300 tw-rounded-lg"
/>
</div>
<div class="tw-flex tw-justify-center tw-gap-4">
<div class="tw-w-[43%]">
<a href="https://apps.apple.com/app/bitwarden-password-manager/id1137397744">
<img class="tw-w-full" src="../../../images/app-store.png" alt="" />
</a>
</div>
<div class="tw-w-[43%]">
<a href="https://play.google.com/store/apps/details?id=com.x8bit.bitwarden">
<img class="tw-w-full" src="../../../images/google-play.png" alt="" />
</a>
</div>
</div>
</bit-card>
<h2 class="tw-mt-6" bitTypography="h6">
{{ "getTheDesktopApp" | i18n }}
</h2>
<bit-card>
<span bitTypography="body2">{{ "getTheDesktopAppDesc" | i18n }}</span>
<a
class="tw-text-primary-600 tw-mt-4 tw-flex tw-no-underline tw-gap-2 tw-items-center"
href="https://bitwarden.com/download/#downloads-desktop"
target="_blank"
>
{{ "downloadFromBitwardenNow" | i18n }}
<i slot="end" class="bwi bwi-external-link" aria-hidden="true"></i>
</a>
</bit-card>
</popup-page>

View File

@@ -0,0 +1,42 @@
import { CommonModule } from "@angular/common";
import { Component, OnInit } from "@angular/core";
import { RouterModule } from "@angular/router";
import { firstValueFrom } 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 { CardComponent, TypographyModule } from "@bitwarden/components";
import { VaultNudgesService, VaultNudgeType } from "@bitwarden/vault";
import { CurrentAccountComponent } from "../../../auth/popup/account-switching/current-account.component";
import { PopOutComponent } from "../../../platform/popup/components/pop-out.component";
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
@Component({
templateUrl: "download-bitwarden.component.html",
standalone: true,
imports: [
CommonModule,
JslibModule,
RouterModule,
PopupPageComponent,
PopupHeaderComponent,
PopOutComponent,
CardComponent,
TypographyModule,
CurrentAccountComponent,
],
})
export class DownloadBitwardenComponent implements OnInit {
constructor(
private vaultNudgeService: VaultNudgesService,
private accountService: AccountService,
) {}
async ngOnInit() {
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
await this.vaultNudgeService.dismissNudge(VaultNudgeType.DownloadBitwarden, userId);
}
}

View File

@@ -11,11 +11,11 @@ import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abs
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { DialogService, ItemModule } from "@bitwarden/components"; import { DialogService, ItemModule } from "@bitwarden/components";
import { FamiliesPolicyService } from "../../../../billing/services/families-policy.service"; import { FamiliesPolicyService } from "../../../billing/services/families-policy.service";
import { BrowserApi } from "../../../../platform/browser/browser-api"; import { BrowserApi } from "../../../platform/browser/browser-api";
import { PopOutComponent } from "../../../../platform/popup/components/pop-out.component"; import { PopOutComponent } from "../../../platform/popup/components/pop-out.component";
import { PopupHeaderComponent } from "../../../../platform/popup/layout/popup-header.component"; import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page.component"; import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
@Component({ @Component({
templateUrl: "more-from-bitwarden-page-v2.component.html", templateUrl: "more-from-bitwarden-page-v2.component.html",

View File

@@ -0,0 +1,42 @@
import { Injectable, inject } from "@angular/core";
import { Observable, combineLatest, from, of } from "rxjs";
import { catchError, map } from "rxjs/operators";
import { VaultProfileService } from "@bitwarden/angular/vault/services/vault-profile.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { UserId } from "@bitwarden/common/types/guid";
import { DefaultSingleNudgeService } from "../default-single-nudge.service";
import { NudgeStatus, VaultNudgeType } from "../vault-nudges.service";
const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;
@Injectable({ providedIn: "root" })
export class DownloadBitwardenNudgeService extends DefaultSingleNudgeService {
private vaultProfileService = inject(VaultProfileService);
private logService = inject(LogService);
nudgeStatus$(nudgeType: VaultNudgeType, userId: UserId): Observable<NudgeStatus> {
const profileDate$ = from(this.vaultProfileService.getProfileCreationDate(userId)).pipe(
catchError(() => {
this.logService.error("Failed to load profile date:");
// Default to today to ensure the nudge is shown
return of(new Date());
}),
);
return combineLatest([
profileDate$,
this.getNudgeStatus$(nudgeType, userId),
of(Date.now() - THIRTY_DAYS_MS),
]).pipe(
map(([profileCreationDate, status, profileCutoff]) => {
const profileOlderThanCutoff = profileCreationDate.getTime() < profileCutoff;
return {
hasBadgeDismissed: status.hasBadgeDismissed || profileOlderThanCutoff,
hasSpotlightDismissed: status.hasSpotlightDismissed || profileOlderThanCutoff,
};
}),
);
}
}

View File

@@ -1,4 +1,5 @@
export * from "./has-items-nudge.service"; export * from "./has-items-nudge.service";
export * from "./download-bitwarden-nudge.service";
export * from "./empty-vault-nudge.service"; export * from "./empty-vault-nudge.service";
export * from "./has-nudge.service"; export * from "./has-nudge.service";
export * from "./new-item-nudge.service"; export * from "./new-item-nudge.service";

View File

@@ -2,7 +2,10 @@ import { TestBed } from "@angular/core/testing";
import { mock } from "jest-mock-extended"; import { mock } from "jest-mock-extended";
import { firstValueFrom, of } from "rxjs"; import { firstValueFrom, of } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { StateProvider } from "@bitwarden/common/platform/state"; import { StateProvider } from "@bitwarden/common/platform/state";
import { UserId } from "@bitwarden/common/types/guid"; import { UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
@@ -47,7 +50,19 @@ describe("Vault Nudges Service", () => {
provide: EmptyVaultNudgeService, provide: EmptyVaultNudgeService,
useValue: mock<EmptyVaultNudgeService>(), useValue: mock<EmptyVaultNudgeService>(),
}, },
{
provide: ApiService,
useValue: mock<ApiService>(),
},
{ provide: CipherService, useValue: mock<CipherService>() }, { provide: CipherService, useValue: mock<CipherService>() },
{
provide: AccountService,
useValue: mock<AccountService>(),
},
{
provide: LogService,
useValue: mock<LogService>(),
},
], ],
}); });
}); });

View File

@@ -9,6 +9,7 @@ import { UserId } from "@bitwarden/common/types/guid";
import { import {
HasItemsNudgeService, HasItemsNudgeService,
EmptyVaultNudgeService, EmptyVaultNudgeService,
DownloadBitwardenNudgeService,
NewItemNudgeService, NewItemNudgeService,
} from "./custom-nudges-services"; } from "./custom-nudges-services";
import { DefaultSingleNudgeService, SingleNudgeService } from "./default-single-nudge.service"; import { DefaultSingleNudgeService, SingleNudgeService } from "./default-single-nudge.service";
@@ -27,6 +28,7 @@ export enum VaultNudgeType {
*/ */
EmptyVaultNudge = "empty-vault-nudge", EmptyVaultNudge = "empty-vault-nudge",
HasVaultItems = "has-vault-items", HasVaultItems = "has-vault-items",
DownloadBitwarden = "download-bitwarden",
newLoginItemStatus = "new-login-item-status", newLoginItemStatus = "new-login-item-status",
newCardItemStatus = "new-card-item-status", newCardItemStatus = "new-card-item-status",
newIdentityItemStatus = "new-identity-item-status", newIdentityItemStatus = "new-identity-item-status",
@@ -52,9 +54,10 @@ export class VaultNudgesService {
* Each nudge type can have its own service to determine when to show the nudge * Each nudge type can have its own service to determine when to show the nudge
* @private * @private
*/ */
private customNudgeServices: any = { private customNudgeServices: Partial<Record<VaultNudgeType, SingleNudgeService>> = {
[VaultNudgeType.HasVaultItems]: inject(HasItemsNudgeService), [VaultNudgeType.HasVaultItems]: inject(HasItemsNudgeService),
[VaultNudgeType.EmptyVaultNudge]: inject(EmptyVaultNudgeService), [VaultNudgeType.EmptyVaultNudge]: inject(EmptyVaultNudgeService),
[VaultNudgeType.DownloadBitwarden]: inject(DownloadBitwardenNudgeService),
[VaultNudgeType.newLoginItemStatus]: this.newItemNudgeService, [VaultNudgeType.newLoginItemStatus]: this.newItemNudgeService,
[VaultNudgeType.newCardItemStatus]: this.newItemNudgeService, [VaultNudgeType.newCardItemStatus]: this.newItemNudgeService,
[VaultNudgeType.newIdentityItemStatus]: this.newItemNudgeService, [VaultNudgeType.newIdentityItemStatus]: this.newItemNudgeService,