diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 4de26cb269d..1df0bf96616 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -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?" }, diff --git a/apps/browser/src/images/app-store.png b/apps/browser/src/images/app-store.png new file mode 100644 index 00000000000..7b3c9759ef9 Binary files /dev/null and b/apps/browser/src/images/app-store.png differ diff --git a/apps/browser/src/images/download-qr.png b/apps/browser/src/images/download-qr.png new file mode 100644 index 00000000000..4362c1616f4 Binary files /dev/null and b/apps/browser/src/images/download-qr.png differ diff --git a/apps/browser/src/images/google-play.png b/apps/browser/src/images/google-play.png new file mode 100644 index 00000000000..3ff87a25d5c Binary files /dev/null and b/apps/browser/src/images/google-play.png differ diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index 3b271fb1296..3dde9f15fdb 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -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, diff --git a/apps/browser/src/tools/popup/settings/about-page/about-page-v2.component.html b/apps/browser/src/tools/popup/settings/about-page/about-page-v2.component.html index bcc0f12d0d7..9bba3994357 100644 --- a/apps/browser/src/tools/popup/settings/about-page/about-page-v2.component.html +++ b/apps/browser/src/tools/popup/settings/about-page/about-page-v2.component.html @@ -23,12 +23,6 @@ - - - {{ "moreFromBitwarden" | i18n }} - - - {{ "rateExtension" | i18n }} diff --git a/apps/browser/src/tools/popup/settings/settings-v2.component.html b/apps/browser/src/tools/popup/settings/settings-v2.component.html index b6f98b649fe..6189221942c 100644 --- a/apps/browser/src/tools/popup/settings/settings-v2.component.html +++ b/apps/browser/src/tools/popup/settings/settings-v2.component.html @@ -66,5 +66,27 @@ + + + + + {{ "downloadBitwardenOnAllDevices" | i18n }} + 1 + + + + + + + + + {{ "moreFromBitwarden" | i18n }} + + + diff --git a/apps/browser/src/tools/popup/settings/settings-v2.component.ts b/apps/browser/src/tools/popup/settings/settings-v2.component.ts index 737d79ea4ca..61b346a69bc 100644 --- a/apps/browser/src/tools/popup/settings/settings-v2.component.ts +++ b/apps/browser/src/tools/popup/settings/settings-v2.component.ts @@ -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 = new Observable(); - activeUserId: UserId | null = null; + + private authenticatedAccount$: Observable = this.accountService.activeAccount$.pipe( + filter((account): account is Account => account !== null), + shareReplay({ bufferSize: 1, refCount: true }), + ); + + downloadBitwardenNudgeStatus$: Observable = this.authenticatedAccount$.pipe( + switchMap((account) => + this.vaultNudgesService.showNudge$(VaultNudgeType.DownloadBitwarden, account.id), + ), + ); + + showVaultBadge$: Observable = 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); } } } diff --git a/apps/browser/src/vault/popup/settings/download-bitwarden.component.html b/apps/browser/src/vault/popup/settings/download-bitwarden.component.html new file mode 100644 index 00000000000..ad063691e76 --- /dev/null +++ b/apps/browser/src/vault/popup/settings/download-bitwarden.component.html @@ -0,0 +1,50 @@ + + + + + + + + + {{ "getTheMobileApp" | i18n }} + + + + {{ "getTheMobileAppDesc" | i18n }} + + + + + + + + + + + + + + + + + + + + {{ "getTheDesktopApp" | i18n }} + + + {{ "getTheDesktopAppDesc" | i18n }} + + {{ "downloadFromBitwardenNow" | i18n }} + + + + diff --git a/apps/browser/src/vault/popup/settings/download-bitwarden.component.ts b/apps/browser/src/vault/popup/settings/download-bitwarden.component.ts new file mode 100644 index 00000000000..b51619b86d1 --- /dev/null +++ b/apps/browser/src/vault/popup/settings/download-bitwarden.component.ts @@ -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); + } +} diff --git a/apps/browser/src/tools/popup/settings/about-page/more-from-bitwarden-page-v2.component.html b/apps/browser/src/vault/popup/settings/more-from-bitwarden-page-v2.component.html similarity index 100% rename from apps/browser/src/tools/popup/settings/about-page/more-from-bitwarden-page-v2.component.html rename to apps/browser/src/vault/popup/settings/more-from-bitwarden-page-v2.component.html diff --git a/apps/browser/src/tools/popup/settings/about-page/more-from-bitwarden-page-v2.component.ts b/apps/browser/src/vault/popup/settings/more-from-bitwarden-page-v2.component.ts similarity index 90% rename from apps/browser/src/tools/popup/settings/about-page/more-from-bitwarden-page-v2.component.ts rename to apps/browser/src/vault/popup/settings/more-from-bitwarden-page-v2.component.ts index a3d1c553977..b1269963f70 100644 --- a/apps/browser/src/tools/popup/settings/about-page/more-from-bitwarden-page-v2.component.ts +++ b/apps/browser/src/vault/popup/settings/more-from-bitwarden-page-v2.component.ts @@ -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", diff --git a/libs/vault/src/services/custom-nudges-services/download-bitwarden-nudge.service.ts b/libs/vault/src/services/custom-nudges-services/download-bitwarden-nudge.service.ts new file mode 100644 index 00000000000..ff5533edc89 --- /dev/null +++ b/libs/vault/src/services/custom-nudges-services/download-bitwarden-nudge.service.ts @@ -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 { + 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, + }; + }), + ); + } +} diff --git a/libs/vault/src/services/custom-nudges-services/index.ts b/libs/vault/src/services/custom-nudges-services/index.ts index 131db023175..d725553a62d 100644 --- a/libs/vault/src/services/custom-nudges-services/index.ts +++ b/libs/vault/src/services/custom-nudges-services/index.ts @@ -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"; diff --git a/libs/vault/src/services/vault-nudges.service.spec.ts b/libs/vault/src/services/vault-nudges.service.spec.ts index 69ddf1cdaa0..c579a711f38 100644 --- a/libs/vault/src/services/vault-nudges.service.spec.ts +++ b/libs/vault/src/services/vault-nudges.service.spec.ts @@ -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(), }, + { + provide: ApiService, + useValue: mock(), + }, { provide: CipherService, useValue: mock() }, + { + provide: AccountService, + useValue: mock(), + }, + { + provide: LogService, + useValue: mock(), + }, ], }); }); diff --git a/libs/vault/src/services/vault-nudges.service.ts b/libs/vault/src/services/vault-nudges.service.ts index 98f28af9954..fdca28808cd 100644 --- a/libs/vault/src/services/vault-nudges.service.ts +++ b/libs/vault/src/services/vault-nudges.service.ts @@ -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> = { [VaultNudgeType.HasVaultItems]: inject(HasItemsNudgeService), [VaultNudgeType.EmptyVaultNudge]: inject(EmptyVaultNudgeService), + [VaultNudgeType.DownloadBitwarden]: inject(DownloadBitwardenNudgeService), [VaultNudgeType.newLoginItemStatus]: this.newItemNudgeService, [VaultNudgeType.newCardItemStatus]: this.newItemNudgeService, [VaultNudgeType.newIdentityItemStatus]: this.newItemNudgeService,
{{ "downloadBitwardenOnAllDevices" | i18n }}