1
0
mirror of https://github.com/bitwarden/browser synced 2026-01-21 11:53:34 +00:00

Pm 28182 add success page (#17814)

* PM-28182-implemented send confirmation drawer

* PM-28182 resolved lint issue

* PM-28182 resolved pr comment

* PM-28182 put behind feature flag

* Fix feature flag checks in send component

* Fix feature flag checks in send dropdown component

* Add SendUIRefresh feature flag

* PM-28182 resolved lint issues

* PM-28182 resolved N bug in drawer message

* PM28182 resolved expirationDate replaced with delettionDate

* PM-28182 resolved build issue

* PM-28182 resolved failling tests

* PM-28182 resolved pr comment to consolidate expression

* chore: rerun web build

* PM-28182 removed unneeded export
This commit is contained in:
bmbitwarden
2026-01-09 13:39:26 -05:00
committed by GitHub
parent 1b76ce5b7c
commit 92190d734c
8 changed files with 197 additions and 13 deletions

View File

@@ -72,6 +72,7 @@ describe("NewSendDropdownComponent", () => {
const openSpy = jest.spyOn(SendAddEditDialogComponent, "open");
const openDrawerSpy = jest.spyOn(SendAddEditDialogComponent, "openDrawer");
mockConfigService.getFeatureFlag.mockResolvedValue(false);
openSpy.mockReturnValue({ closed: of({}) } as any);
await component.createSend(SendType.Text);
@@ -85,6 +86,8 @@ describe("NewSendDropdownComponent", () => {
mockConfigService.getFeatureFlag.mockImplementation(async (key) =>
key === FeatureFlag.SendUIRefresh ? true : false,
);
const mockRef = { closed: of({}) };
openDrawerSpy.mockReturnValue(mockRef as any);
await component.createSend(SendType.Text);

View File

@@ -1,6 +1,6 @@
import { CommonModule } from "@angular/common";
import { Component, Input } from "@angular/core";
import { firstValueFrom, Observable, of, switchMap } from "rxjs";
import { firstValueFrom, Observable, of, switchMap, lastValueFrom } from "rxjs";
import { PremiumBadgeComponent } from "@bitwarden/angular/billing/components/premium-badge";
import { JslibModule } from "@bitwarden/angular/jslib.module";
@@ -10,7 +10,13 @@ import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
import { ButtonModule, DialogService, MenuModule } from "@bitwarden/components";
import { DefaultSendFormConfigService, SendAddEditDialogComponent } from "@bitwarden/send-ui";
import {
DefaultSendFormConfigService,
SendAddEditDialogComponent,
SendItemDialogResult,
} from "@bitwarden/send-ui";
import { SendSuccessDrawerDialogComponent } from "../shared";
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
@@ -60,12 +66,19 @@ export class NewSendDropdownComponent {
if (!(await firstValueFrom(this.canAccessPremium$)) && type === SendType.File) {
return;
}
const formConfig = await this.addEditFormConfigService.buildConfig("add", undefined, type);
const useRefresh = await this.configService.getFeatureFlag(FeatureFlag.SendUIRefresh);
if (useRefresh) {
SendAddEditDialogComponent.openDrawer(this.dialogService, { formConfig });
const dialogRef = SendAddEditDialogComponent.openDrawer(this.dialogService, { formConfig });
if (dialogRef) {
const result = await lastValueFrom(dialogRef.closed);
if (result?.result === SendItemDialogResult.Saved && result?.send) {
this.dialogService.openDrawer(SendSuccessDrawerDialogComponent, {
data: result.send,
});
}
}
} else {
SendAddEditDialogComponent.open(this.dialogService, { formConfig });
}

View File

@@ -39,6 +39,7 @@ import { HeaderModule } from "../../layouts/header/header.module";
import { SharedModule } from "../../shared";
import { NewSendDropdownComponent } from "./new-send/new-send-dropdown.component";
import { SendSuccessDrawerDialogComponent } from "./shared";
const BroadcasterSubscriptionId = "SendComponent";
@@ -172,12 +173,25 @@ export class SendComponent extends BaseSendComponent implements OnInit, OnDestro
});
}
const result = await lastValueFrom(this.sendItemDialogRef.closed);
const result: SendItemDialogResult = await lastValueFrom(this.sendItemDialogRef.closed);
this.sendItemDialogRef = undefined;
// If the dialog was closed by deleting the cipher, refresh the vault.
if (result === SendItemDialogResult.Deleted || result === SendItemDialogResult.Saved) {
if (
result?.result === SendItemDialogResult.Deleted ||
result?.result === SendItemDialogResult.Saved
) {
await this.load();
}
if (
result?.result === SendItemDialogResult.Saved &&
result?.send &&
(await this.configService.getFeatureFlag(FeatureFlag.SendUIRefresh))
) {
this.dialogService.openDrawer(SendSuccessDrawerDialogComponent, {
data: result.send,
});
}
}
}

View File

@@ -0,0 +1 @@
export { SendSuccessDrawerDialogComponent } from "./send-success-drawer-dialog.component";

View File

@@ -0,0 +1,45 @@
<bit-dialog dialogSize="large" disablePadding="false" background="alt">
<ng-container bitDialogTitle>
<span>{{ dialogTitle() | i18n }}</span>
</ng-container>
<ng-container bitDialogContent>
<div
class="tw-flex tw-flex-col tw-items-center tw-justify-center tw-text-center tw-h-full tw-px-4 tw-pt-20"
>
<div class="tw-mb-6 tw-mt-8">
<div class="tw-size-[95px] tw-content-center">
<bit-icon [icon]="activeSendIcon"></bit-icon>
</div>
</div>
<h3 bitTypography="h3" class="tw-mb-2 tw-font-bold">
{{ "sendCreatedSuccessfully" | i18n }}
</h3>
<p bitTypography="body1" class="tw-mb-6 tw-max-w-sm">
{{ "sendCreatedDescription" | i18n: formattedExpirationTime }}
</p>
<bit-form-field class="tw-w-full tw-max-w-sm tw-mb-4">
<bit-label>{{ "sendLink" | i18n }}</bit-label>
<input bitInput disabled type="text" [value]="sendLink()" />
<button
type="button"
bitIconButton="bwi-clone"
bitSuffix
[label]="'copyLink' | i18n"
(click)="copyLink()"
></button>
</bit-form-field>
</div>
</ng-container>
<ng-container bitDialogFooter>
<button type="button" bitButton buttonType="primary" (click)="copyLink()">
{{ "copyLink" | i18n }}
</button>
<button type="button" bitButton buttonType="secondary" bitDialogClose>
{{ "close" | i18n }}
</button>
</ng-container>
</bit-dialog>

View File

@@ -0,0 +1,75 @@
import { Component, ChangeDetectionStrategy, Inject, signal, computed } from "@angular/core";
import { firstValueFrom } from "rxjs";
import { ActiveSendIcon } from "@bitwarden/assets/svg";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
import { DIALOG_DATA, DialogModule, ToastService, TypographyModule } from "@bitwarden/components";
import { SharedModule } from "@bitwarden/web-vault/app/shared";
@Component({
imports: [SharedModule, DialogModule, TypographyModule],
templateUrl: "./send-success-drawer-dialog.component.html",
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SendSuccessDrawerDialogComponent {
readonly sendLink = signal<string>("");
activeSendIcon = ActiveSendIcon;
// Computed property to get the dialog title based on send type
readonly dialogTitle = computed(() => {
return this.send.type === SendType.Text ? "newTextSend" : "newFileSend";
});
constructor(
@Inject(DIALOG_DATA) public send: SendView,
private environmentService: EnvironmentService,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private toastService: ToastService,
) {
void this.initLink();
}
async initLink() {
const env = await firstValueFrom(this.environmentService.environment$);
this.sendLink.set(env.getSendUrl() + this.send.accessId + "/" + this.send.urlB64Key);
}
get formattedExpirationTime(): string {
if (!this.send.deletionDate) {
return "";
}
const hoursAvailable = this.getHoursAvailable(this.send);
if (hoursAvailable < 24) {
return hoursAvailable === 1
? this.i18nService.t("oneHour").toLowerCase()
: this.i18nService.t("durationTimeHours", String(hoursAvailable)).toLowerCase();
}
const daysAvailable = Math.ceil(hoursAvailable / 24);
return daysAvailable === 1
? this.i18nService.t("oneDay").toLowerCase()
: this.i18nService.t("days", String(daysAvailable)).toLowerCase();
}
private getHoursAvailable(send: SendView): number {
const now = new Date().getTime();
const deletionDate = new Date(send.deletionDate).getTime();
return Math.max(0, Math.ceil((deletionDate - now) / (1000 * 60 * 60)));
}
copyLink() {
const link = this.sendLink();
if (!link) {
return;
}
this.platformUtilsService.copyToClipboard(link);
this.toastService.showToast({
variant: "success",
message: this.i18nService.t("valueCopied", this.i18nService.t("sendLink")),
});
}
}

View File

@@ -5616,6 +5616,37 @@
"message": "Send saved",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendCreatedSuccessfully": {
"message": "Send created successfully!",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendCreatedDescription": {
"message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.",
"placeholders": {
"time": {
"content": "$1",
"example": "7 days"
}
}
},
"durationTimeHours": {
"message": "$HOURS$ hours",
"placeholders": {
"hours": {
"content": "$1",
"example": "5"
}
}
},
"newTextSend": {
"message": "New Text Send",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"newFileSend": {
"message": "New File Send",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"editedSend": {
"message": "Send saved",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
@@ -12581,4 +12612,4 @@
"storageFullDescription": {
"message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage."
}
}
}

View File

@@ -44,8 +44,10 @@ export const SendItemDialogResult = Object.freeze({
} as const);
/** A result of the Send add/edit dialog. */
export type SendItemDialogResult = (typeof SendItemDialogResult)[keyof typeof SendItemDialogResult];
export type SendItemDialogResult = {
result: (typeof SendItemDialogResult)[keyof typeof SendItemDialogResult];
send?: SendView;
};
/**
* Component for adding or editing a send item.
*/
@@ -93,7 +95,7 @@ export class SendAddEditDialogComponent {
*/
async onSendCreated(send: SendView) {
// FIXME Add dialogService.open send-created dialog
this.dialogRef.close(SendItemDialogResult.Saved);
this.dialogRef.close({ result: SendItemDialogResult.Saved, send });
return;
}
@@ -101,14 +103,14 @@ export class SendAddEditDialogComponent {
* Handles the event when the send is updated.
*/
async onSendUpdated(send: SendView) {
this.dialogRef.close(SendItemDialogResult.Saved);
this.dialogRef.close({ result: SendItemDialogResult.Saved });
}
/**
* Handles the event when the send is deleted.
*/
async onSendDeleted() {
this.dialogRef.close(SendItemDialogResult.Deleted);
this.dialogRef.close({ result: SendItemDialogResult.Deleted });
this.toastService.showToast({
variant: "success",