diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 7b30e5aa33..e52f78583d 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -2498,7 +2498,25 @@ "message": "Send created successfully!", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendAvailability": { + "sendExpiresInHoursSingle": { + "message": "The Send will be available to anyone with the link for the next 1 hour.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresInHours": { + "message": "The Send will be available to anyone with the link for the next $HOURS$ hours.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendExpiresInDaysSingle": { + "message": "The Send will be available to anyone with the link for the next 1 day.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresInDays": { "message": "The Send will be available to anyone with the link for the next $DAYS$ days.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", "placeholders": { diff --git a/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.html b/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.html index c97d3da139..7c65cbeb17 100644 --- a/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.html +++ b/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.html @@ -16,7 +16,9 @@ >

{{ "createdSendSuccessfully" | i18n }}

-

{{ "sendAvailability" | i18n: daysAvailable }}

+

+ {{ formatExpirationDate() }} +

diff --git a/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.spec.ts b/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.spec.ts index bcc4d2e2cc..24186ad427 100644 --- a/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.spec.ts +++ b/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.spec.ts @@ -3,7 +3,7 @@ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { ActivatedRoute, Router, RouterLink } from "@angular/router"; import { RouterTestingModule } from "@angular/router/testing"; import { MockProxy, mock } from "jest-mock-extended"; -import { of } from "rxjs"; +import { BehaviorSubject, of } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; @@ -13,7 +13,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { SelfHostedEnvironment } from "@bitwarden/common/platform/services/default-environment.service"; import { SendView } from "@bitwarden/common/tools/send/models/view/send.view"; import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; -import { ButtonModule, IconModule, ToastService } from "@bitwarden/components"; +import { ButtonModule, I18nMockService, IconModule, ToastService } from "@bitwarden/components"; import { PopOutComponent } from "../../../../platform/popup/components/pop-out.component"; import { PopupFooterComponent } from "../../../../platform/popup/layout/popup-footer.component"; @@ -26,7 +26,6 @@ import { SendCreatedComponent } from "./send-created.component"; describe("SendCreatedComponent", () => { let component: SendCreatedComponent; let fixture: ComponentFixture; - let i18nService: MockProxy; let platformUtilsService: MockProxy; let sendService: MockProxy; let toastService: MockProxy; @@ -36,17 +35,10 @@ describe("SendCreatedComponent", () => { let router: MockProxy; const sendId = "test-send-id"; - const deletionDate = new Date(); - deletionDate.setDate(deletionDate.getDate() + 7); - const sendView: SendView = { - id: sendId, - deletionDate, - accessId: "abc", - urlB64Key: "123", - } as SendView; + let sendView: SendView; + let sendViewsSubject: BehaviorSubject; beforeEach(async () => { - i18nService = mock(); platformUtilsService = mock(); sendService = mock(); toastService = mock(); @@ -54,6 +46,17 @@ describe("SendCreatedComponent", () => { activatedRoute = mock(); environmentService = mock(); router = mock(); + + sendView = { + id: sendId, + deletionDate: new Date(), + accessId: "abc", + urlB64Key: "123", + } as SendView; + + sendViewsSubject = new BehaviorSubject([sendView]); + sendService.sendViews$ = sendViewsSubject.asObservable(); + Object.defineProperty(environmentService, "environment$", { configurable: true, get: () => of(new SelfHostedEnvironment({ webVault: "https://example.com" })), @@ -65,8 +68,6 @@ describe("SendCreatedComponent", () => { }, } as any; - sendService.sendViews$ = of([sendView]); - await TestBed.configureTestingModule({ imports: [ CommonModule, @@ -82,7 +83,25 @@ describe("SendCreatedComponent", () => { SendCreatedComponent, ], providers: [ - { provide: I18nService, useValue: i18nService }, + { + provide: I18nService, + useFactory: () => { + return new I18nMockService({ + back: "back", + loading: "loading", + copyLink: "copyLink", + close: "close", + createdSend: "createdSend", + createdSendSuccessfully: "createdSendSuccessfully", + popOutNewWindow: "popOutNewWindow", + sendExpiresInHours: (hours) => `sendExpiresInHours ${hours}`, + sendExpiresInHoursSingle: "sendExpiresInHoursSingle", + sendExpiresInDays: (days) => `sendExpiresInDays ${days}`, + sendExpiresInDaysSingle: "sendExpiresInDaysSingle", + sendLinkCopied: "sendLinkCopied", + }); + }, + }, { provide: PlatformUtilsService, useValue: platformUtilsService }, { provide: SendService, useValue: sendService }, { provide: ToastService, useValue: toastService }, @@ -94,40 +113,73 @@ describe("SendCreatedComponent", () => { { provide: Router, useValue: router }, ], }).compileComponents(); - }); - beforeEach(() => { fixture = TestBed.createComponent(SendCreatedComponent); component = fixture.componentInstance; + fixture.detectChanges(); }); it("should create", () => { - fixture.detectChanges(); expect(component).toBeTruthy(); }); - it("should initialize send and daysAvailable", () => { - fixture.detectChanges(); + it("should initialize send, daysAvailable, and hoursAvailable", () => { expect(component["send"]).toBe(sendView); - expect(component["daysAvailable"]).toBe(7); + expect(component["daysAvailable"]).toBe(0); + expect(component["hoursAvailable"]).toBe(0); }); it("should navigate back to send list on close", async () => { - fixture.detectChanges(); await component.close(); expect(router.navigate).toHaveBeenCalledWith(["/tabs/send"]); }); - describe("getDaysAvailable", () => { - it("returns the correct number of days", () => { + describe("getHoursAvailable", () => { + it("returns the correct number of hours", () => { + sendView.deletionDate.setDate(sendView.deletionDate.getDate() + 7); + sendViewsSubject.next([sendView]); fixture.detectChanges(); - expect(component.getDaysAvailable(sendView)).toBe(7); + + expect(component.getHoursAvailable(sendView)).toBeCloseTo(168, 0); + }); + }); + + describe("formatExpirationDate", () => { + it("returns days plural if expiry is more than 24 hours", () => { + sendView.deletionDate.setDate(sendView.deletionDate.getDate() + 7); + sendViewsSubject.next([sendView]); + fixture.detectChanges(); + + expect(component.formatExpirationDate()).toBe("sendExpiresInDays 7"); + }); + + it("returns days singular if expiry is 24 hours", () => { + sendView.deletionDate.setDate(sendView.deletionDate.getDate() + 1); + sendViewsSubject.next([sendView]); + fixture.detectChanges(); + + expect(component.formatExpirationDate()).toBe("sendExpiresInDaysSingle"); + }); + + it("returns hours plural if expiry is more than 1 hour but less than 24", () => { + sendView.deletionDate.setHours(sendView.deletionDate.getHours() + 2); + sendViewsSubject.next([sendView]); + fixture.detectChanges(); + + expect(component.formatExpirationDate()).toBe("sendExpiresInHours 2"); + }); + + it("returns hours singular if expiry is in 1 hour", () => { + sendView.deletionDate.setHours(sendView.deletionDate.getHours() + 1); + sendViewsSubject.next([sendView]); + fixture.detectChanges(); + + expect(component.formatExpirationDate()).toBe("sendExpiresInHoursSingle"); }); }); describe("copyLink", () => { it("should copy link and show toast", async () => { - fixture.detectChanges(); const link = "https://example.com/#/send/abc/123"; await component.copyLink(); @@ -136,7 +188,7 @@ describe("SendCreatedComponent", () => { expect(toastService.showToast).toHaveBeenCalledWith({ variant: "success", title: null, - message: i18nService.t("sendLinkCopied"), + message: "sendLinkCopied", }); }); }); diff --git a/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.ts b/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.ts index 4ed4da2f81..ae66d14d3f 100644 --- a/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.ts +++ b/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.ts @@ -39,6 +39,7 @@ export class SendCreatedComponent { protected sendCreatedIcon = SendCreatedIcon; protected send: SendView; protected daysAvailable = 0; + protected hoursAvailable = 0; constructor( private i18nService: I18nService, @@ -54,14 +55,26 @@ export class SendCreatedComponent { this.sendService.sendViews$.pipe(takeUntilDestroyed()).subscribe((sendViews) => { this.send = sendViews.find((s) => s.id === sendId); if (this.send) { - this.daysAvailable = this.getDaysAvailable(this.send); + this.hoursAvailable = this.getHoursAvailable(this.send); + this.daysAvailable = Math.ceil(this.hoursAvailable / 24); } }); } - getDaysAvailable(send: SendView): number { + formatExpirationDate(): string { + if (this.hoursAvailable < 24) { + return this.hoursAvailable === 1 + ? this.i18nService.t("sendExpiresInHoursSingle") + : this.i18nService.t("sendExpiresInHours", this.hoursAvailable); + } + return this.daysAvailable === 1 + ? this.i18nService.t("sendExpiresInDaysSingle") + : this.i18nService.t("sendExpiresInDays", this.daysAvailable); + } + + getHoursAvailable(send: SendView): number { const now = new Date().getTime(); - return Math.max(0, Math.ceil((send.deletionDate.getTime() - now) / (1000 * 60 * 60 * 24))); + return Math.max(0, Math.ceil((send.deletionDate.getTime() - now) / (1000 * 60 * 60))); } async close() {