From b96b33cb55f77eaaa177a6af9f58f363fe26b48e Mon Sep 17 00:00:00 2001 From: Daniel James Smith Date: Wed, 12 Mar 2025 10:54:25 +0100 Subject: [PATCH] Create desktop specific NewSendDropdown component --- .../new-send/new-send-dropdown.component.html | 29 +++++++ .../new-send-dropdown.component.spec.ts | 86 +++++++++++++++++++ .../new-send/new-send-dropdown.component.ts | 61 +++++++++++++ apps/desktop/src/locales/en/messages.json | 6 ++ 4 files changed, 182 insertions(+) create mode 100644 apps/desktop/src/app/tools/send/new-send/new-send-dropdown.component.html create mode 100644 apps/desktop/src/app/tools/send/new-send/new-send-dropdown.component.spec.ts create mode 100644 apps/desktop/src/app/tools/send/new-send/new-send-dropdown.component.ts diff --git a/apps/desktop/src/app/tools/send/new-send/new-send-dropdown.component.html b/apps/desktop/src/app/tools/send/new-send/new-send-dropdown.component.html new file mode 100644 index 00000000000..5007f900e70 --- /dev/null +++ b/apps/desktop/src/app/tools/send/new-send/new-send-dropdown.component.html @@ -0,0 +1,29 @@ + + + + + {{ "sendTypeText" | i18n }} + + + + {{ "sendTypeFile" | i18n }} + + + diff --git a/apps/desktop/src/app/tools/send/new-send/new-send-dropdown.component.spec.ts b/apps/desktop/src/app/tools/send/new-send/new-send-dropdown.component.spec.ts new file mode 100644 index 00000000000..d264882bc88 --- /dev/null +++ b/apps/desktop/src/app/tools/send/new-send/new-send-dropdown.component.spec.ts @@ -0,0 +1,86 @@ +import { CommonModule } from "@angular/common"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { of } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; +import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; +import { ButtonModule, MenuModule, BadgeModule } from "@bitwarden/components"; + +import { NewSendDropdownComponent } from "./new-send-dropdown.component"; + +describe("NewSendDropdownComponent", () => { + let component: NewSendDropdownComponent; + let fixture: ComponentFixture; + let accountServiceMock: any; + let billingAccountProfileStateServiceMock: any; + let messagingServiceMock: any; + + beforeEach(async () => { + accountServiceMock = { + activeAccount$: of({ id: "test-account-id" }), + }; + + billingAccountProfileStateServiceMock = { + hasPremiumFromAnySource$: jest.fn().mockReturnValue(of(true)), + }; + + messagingServiceMock = { + send: jest.fn(), + }; + + await TestBed.configureTestingModule({ + imports: [CommonModule, JslibModule, ButtonModule, MenuModule, BadgeModule], + providers: [ + { provide: I18nService, useValue: { t: (key: string) => key } }, + { provide: AccountService, useValue: accountServiceMock }, + { + provide: BillingAccountProfileStateService, + useValue: billingAccountProfileStateServiceMock, + }, + { provide: MessagingService, useValue: messagingServiceMock }, + ], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(NewSendDropdownComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); + + it("should emit onCreateSendOfType when createSend is called with a valid type", async () => { + const sendType = SendType.Text; + jest.spyOn(component.onCreateSendOfType, "emit"); + + await component.createSend(sendType); + + expect(component.onCreateSendOfType.emit).toHaveBeenCalledWith(sendType); + }); + + it("should call messagingService.send when createSend is called with SendType.File and no premium access", async () => { + billingAccountProfileStateServiceMock.hasPremiumFromAnySource$.mockReturnValue(of(false)); + + await component.createSend(SendType.File); + + expect(messagingServiceMock.send).toHaveBeenCalledWith("openPremium"); + }); + + it("should not call messagingService.send when createSend is called with SendType.File and has premium access", async () => { + const sendType = SendType.File; + jest.spyOn(component.onCreateSendOfType, "emit"); + billingAccountProfileStateServiceMock.hasPremiumFromAnySource$.mockReturnValue(of(true)); + + await component.createSend(sendType); + + expect(messagingServiceMock.send).not.toHaveBeenCalled(); + expect(component.onCreateSendOfType.emit).toHaveBeenCalledWith(sendType); + }); +}); diff --git a/apps/desktop/src/app/tools/send/new-send/new-send-dropdown.component.ts b/apps/desktop/src/app/tools/send/new-send/new-send-dropdown.component.ts new file mode 100644 index 00000000000..86fd49d8957 --- /dev/null +++ b/apps/desktop/src/app/tools/send/new-send/new-send-dropdown.component.ts @@ -0,0 +1,61 @@ +import { CommonModule } from "@angular/common"; +import { Component, EventEmitter, Input, Output } from "@angular/core"; +import { firstValueFrom, Observable, of, switchMap } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; +import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; +import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; +import { BadgeModule, ButtonModule, MenuModule } from "@bitwarden/components"; + +@Component({ + selector: "tools-new-send-dropdown", + templateUrl: "new-send-dropdown.component.html", + standalone: true, + imports: [JslibModule, CommonModule, ButtonModule, MenuModule, BadgeModule], +}) +/** + * A dropdown component that allows the user to create a new Send of a specific type. + */ +export class NewSendDropdownComponent { + /** If true, the plus icon will be hidden */ + @Input() hideIcon: boolean = false; + + /** SendType provided for the markup to pass back the selected type of Send */ + protected sendType = SendType; + + /** Indicates whether the user can access premium features. */ + protected canAccessPremium$: Observable; + + /** Emitted when an allowed SendType has been selected. */ + @Output() onCreateSendOfType = new EventEmitter(); + + constructor( + private billingAccountProfileStateService: BillingAccountProfileStateService, + private accountService: AccountService, + private messagingService: MessagingService, + ) { + this.canAccessPremium$ = this.accountService.activeAccount$.pipe( + switchMap((account) => + account + ? this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id) + : of(false), + ), + ); + } + + /** + * Emits an event with the user selected SendType, for the hosting control to launch the Add new Send page with the provided SendType. + * If has user does not have premium access and the type is File, the user will be redirected to the premium settings page. + * @param type The type of Send to create. + */ + async createSend(type: SendType) { + if (!(await firstValueFrom(this.canAccessPremium$)) && type === SendType.File) { + this.messagingService.send("openPremium"); + return; + } + + this.onCreateSendOfType.emit(type); + } +} diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 7739ab84577..790ee411747 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -3439,6 +3439,12 @@ } } }, + "new": { + "message": "New" + }, + "premium": { + "message": "Premium" + }, "backTo": { "message": "Back to $NAME$", "description": "Navigate back to a previous folder or collection",