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:
@@ -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": {
|
||||
"message": "Are you sure you want to permanently delete this attachment?"
|
||||
},
|
||||
|
||||
BIN
apps/browser/src/images/app-store.png
Normal file
BIN
apps/browser/src/images/app-store.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
BIN
apps/browser/src/images/download-qr.png
Normal file
BIN
apps/browser/src/images/download-qr.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
BIN
apps/browser/src/images/google-play.png
Normal file
BIN
apps/browser/src/images/google-play.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
@@ -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 { SendV2Component } from "../tools/popup/send-v2/send-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 { ImportBrowserV2Component } from "../tools/popup/settings/import/import-browser-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 { ViewV2Component } from "../vault/popup/components/vault-v2/view-v2/view-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 { MoreFromBitwardenPageV2Component } from "../vault/popup/settings/more-from-bitwarden-page-v2.component";
|
||||
import { TrashComponent } from "../vault/popup/settings/trash.component";
|
||||
import { VaultSettingsV2Component } from "../vault/popup/settings/vault-settings-v2.component";
|
||||
|
||||
@@ -583,6 +584,12 @@ const routes: Routes = [
|
||||
canActivate: [authGuard],
|
||||
data: { elevation: 2 } satisfies RouteDataProperties,
|
||||
},
|
||||
{
|
||||
path: "download-bitwarden",
|
||||
component: DownloadBitwardenComponent,
|
||||
canActivate: [authGuard],
|
||||
data: { elevation: 2 } satisfies RouteDataProperties,
|
||||
},
|
||||
{
|
||||
path: "intro-carousel",
|
||||
component: ExtensionAnonLayoutWrapperComponent,
|
||||
|
||||
@@ -23,12 +23,6 @@
|
||||
<i slot="end" class="bwi bwi-external-link" aria-hidden="true"></i>
|
||||
</button>
|
||||
</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>
|
||||
<button type="button" bit-item-content (click)="rate()">
|
||||
{{ "rateExtension" | i18n }}
|
||||
|
||||
@@ -66,5 +66,27 @@
|
||||
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
||||
</a>
|
||||
</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>
|
||||
</popup-page>
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { Component } from "@angular/core";
|
||||
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 { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { BadgeComponent, ItemModule } from "@bitwarden/components";
|
||||
import { NudgeStatus, VaultNudgesService, VaultNudgeType } from "@bitwarden/vault";
|
||||
@@ -30,26 +29,35 @@ import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.co
|
||||
BadgeComponent,
|
||||
],
|
||||
})
|
||||
export class SettingsV2Component implements OnInit {
|
||||
export class SettingsV2Component {
|
||||
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(
|
||||
private readonly vaultNudgesService: VaultNudgesService,
|
||||
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) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -11,11 +11,11 @@ import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abs
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { DialogService, ItemModule } from "@bitwarden/components";
|
||||
|
||||
import { FamiliesPolicyService } from "../../../../billing/services/families-policy.service";
|
||||
import { BrowserApi } from "../../../../platform/browser/browser-api";
|
||||
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";
|
||||
import { FamiliesPolicyService } from "../../../billing/services/families-policy.service";
|
||||
import { BrowserApi } from "../../../platform/browser/browser-api";
|
||||
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: "more-from-bitwarden-page-v2.component.html",
|
||||
@@ -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,
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from "./has-items-nudge.service";
|
||||
export * from "./download-bitwarden-nudge.service";
|
||||
export * from "./empty-vault-nudge.service";
|
||||
export * from "./has-nudge.service";
|
||||
export * from "./new-item-nudge.service";
|
||||
|
||||
@@ -2,7 +2,10 @@ import { TestBed } from "@angular/core/testing";
|
||||
import { mock } from "jest-mock-extended";
|
||||
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 { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
@@ -47,7 +50,19 @@ describe("Vault Nudges Service", () => {
|
||||
provide: EmptyVaultNudgeService,
|
||||
useValue: mock<EmptyVaultNudgeService>(),
|
||||
},
|
||||
{
|
||||
provide: ApiService,
|
||||
useValue: mock<ApiService>(),
|
||||
},
|
||||
{ provide: CipherService, useValue: mock<CipherService>() },
|
||||
{
|
||||
provide: AccountService,
|
||||
useValue: mock<AccountService>(),
|
||||
},
|
||||
{
|
||||
provide: LogService,
|
||||
useValue: mock<LogService>(),
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ import { UserId } from "@bitwarden/common/types/guid";
|
||||
import {
|
||||
HasItemsNudgeService,
|
||||
EmptyVaultNudgeService,
|
||||
DownloadBitwardenNudgeService,
|
||||
NewItemNudgeService,
|
||||
} from "./custom-nudges-services";
|
||||
import { DefaultSingleNudgeService, SingleNudgeService } from "./default-single-nudge.service";
|
||||
@@ -27,6 +28,7 @@ export enum VaultNudgeType {
|
||||
*/
|
||||
EmptyVaultNudge = "empty-vault-nudge",
|
||||
HasVaultItems = "has-vault-items",
|
||||
DownloadBitwarden = "download-bitwarden",
|
||||
newLoginItemStatus = "new-login-item-status",
|
||||
newCardItemStatus = "new-card-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
|
||||
* @private
|
||||
*/
|
||||
private customNudgeServices: any = {
|
||||
private customNudgeServices: Partial<Record<VaultNudgeType, SingleNudgeService>> = {
|
||||
[VaultNudgeType.HasVaultItems]: inject(HasItemsNudgeService),
|
||||
[VaultNudgeType.EmptyVaultNudge]: inject(EmptyVaultNudgeService),
|
||||
[VaultNudgeType.DownloadBitwarden]: inject(DownloadBitwardenNudgeService),
|
||||
[VaultNudgeType.newLoginItemStatus]: this.newItemNudgeService,
|
||||
[VaultNudgeType.newCardItemStatus]: this.newItemNudgeService,
|
||||
[VaultNudgeType.newIdentityItemStatus]: this.newItemNudgeService,
|
||||
|
||||
Reference in New Issue
Block a user