-
{{ "sendProtectedPassword" | i18n }}
-
{{ "sendProtectedPasswordDontKnow" | i18n }}
-
-
-
-
-
-
-
-
-
+
+ {{ "viewSendHiddenEmailWarning" | i18n }}
+ {{ "learnMore" | i18n }}.
+
+
+
+
+
+ {{ "sendAccessUnavailable" | i18n }}
+
+
+ {{ "unexpectedErrorSend" | i18n }}
+
+
+
{{ send.name }}
- {{
- "sendHiddenByDefault" | i18n
- }}
-
-
-
-
-
+
- {{ send.file.fileName }}
-
-
+
-
+
Expires: {{ expirationDate | date : "medium" }}
-
+
+
+
+
+ {{ "loading" | i18n }}
+
+
-
- {{ "sendAccessTaglineProductDesc" | i18n }}
+
+
+ {{ "sendAccessTaglineProductDesc" | i18n }}
{{ "sendAccessTaglineLearnMore" | i18n }}
- Bitwarden Send
{{ "sendAccessTaglineOr" | i18n }}
- {{
- "sendAccessTaglineSignUp" | i18n
- }}
+ {{ "sendAccessTaglineSignUp" | i18n }}
{{ "sendAccessTaglineTryToday" | i18n }}
diff --git a/apps/web/src/app/tools/send/access.component.ts b/apps/web/src/app/tools/send/access.component.ts
index 180acfd69f7..d01bc9c52ff 100644
--- a/apps/web/src/app/tools/send/access.component.ts
+++ b/apps/web/src/app/tools/send/access.component.ts
@@ -1,16 +1,14 @@
import { Component, OnInit } from "@angular/core";
+import { FormBuilder } from "@angular/forms";
import { ActivatedRoute } from "@angular/router";
-import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { SEND_KDF_ITERATIONS } from "@bitwarden/common/enums";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
-import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
-import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
import { SendAccess } from "@bitwarden/common/tools/send/models/domain/send-access";
@@ -18,56 +16,65 @@ import { SendAccessRequest } from "@bitwarden/common/tools/send/models/request/s
import { SendAccessResponse } from "@bitwarden/common/tools/send/models/response/send-access.response";
import { SendAccessView } from "@bitwarden/common/tools/send/models/view/send-access.view";
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
+import { NoItemsModule } from "@bitwarden/components";
+
+import { SharedModule } from "../../shared";
+
+import { ExpiredSend } from "./icons/expired-send.icon";
+import { SendAccessFileComponent } from "./send-access-file.component";
+import { SendAccessPasswordComponent } from "./send-access-password.component";
+import { SendAccessTextComponent } from "./send-access-text.component";
@Component({
selector: "app-send-access",
templateUrl: "access.component.html",
+ standalone: true,
+ imports: [
+ SendAccessFileComponent,
+ SendAccessTextComponent,
+ SendAccessPasswordComponent,
+ SharedModule,
+ NoItemsModule,
+ ],
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class AccessComponent implements OnInit {
- send: SendAccessView;
- sendType = SendType;
- downloading = false;
- loading = true;
- passwordRequired = false;
- formPromise: Promise
;
- password: string;
- showText = false;
- unavailable = false;
- error = false;
- hideEmail = false;
+ protected send: SendAccessView;
+ protected sendType = SendType;
+ protected loading = true;
+ protected passwordRequired = false;
+ protected formPromise: Promise;
+ protected password: string;
+ protected unavailable = false;
+ protected error = false;
+ protected hideEmail = false;
+ protected decKey: SymmetricCryptoKey;
+ protected accessRequest: SendAccessRequest;
+ protected expiredSendIcon = ExpiredSend;
+
+ protected formGroup = this.formBuilder.group({});
private id: string;
private key: string;
- private decKey: SymmetricCryptoKey;
- private accessRequest: SendAccessRequest;
constructor(
- private i18nService: I18nService,
private cryptoFunctionService: CryptoFunctionService,
- private apiService: ApiService,
- private platformUtilsService: PlatformUtilsService,
private route: ActivatedRoute,
private cryptoService: CryptoService,
- private fileDownloadService: FileDownloadService,
- private sendApiService: SendApiService
+ private sendApiService: SendApiService,
+ private platformUtilsService: PlatformUtilsService,
+ private i18nService: I18nService,
+ protected formBuilder: FormBuilder
) {}
- get sendText() {
- if (this.send == null || this.send.text == null) {
- return null;
- }
- return this.showText ? this.send.text.text : this.send.text.maskedText;
- }
-
- get expirationDate() {
+ protected get expirationDate() {
if (this.send == null || this.send.expirationDate == null) {
return null;
}
return this.send.expirationDate;
}
- get creatorIdentifier() {
+ protected get creatorIdentifier() {
if (this.send == null || this.send.creatorIdentifier == null) {
return null;
}
@@ -86,77 +93,22 @@ export class AccessComponent implements OnInit {
});
}
- async download() {
- if (this.send == null || this.decKey == null) {
- return;
- }
-
- if (this.downloading) {
- return;
- }
-
- const downloadData = await this.sendApiService.getSendFileDownloadData(
- this.send,
- this.accessRequest
- );
-
- if (Utils.isNullOrWhitespace(downloadData.url)) {
- this.platformUtilsService.showToast("error", null, this.i18nService.t("missingSendFile"));
- return;
- }
-
- this.downloading = true;
- const response = await fetch(new Request(downloadData.url, { cache: "no-store" }));
- if (response.status !== 200) {
- this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred"));
- this.downloading = false;
- return;
- }
-
- try {
- const encBuf = await EncArrayBuffer.fromResponse(response);
- const decBuf = await this.cryptoService.decryptFromBytes(encBuf, this.decKey);
- this.fileDownloadService.download({
- fileName: this.send.file.fileName,
- blobData: decBuf,
- downloadMethod: "save",
- });
- } catch (e) {
- this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred"));
- }
-
- this.downloading = false;
- }
-
- copyText() {
- this.platformUtilsService.copyToClipboard(this.send.text.text);
- this.platformUtilsService.showToast(
- "success",
- null,
- this.i18nService.t("valueCopied", this.i18nService.t("sendTypeText"))
- );
- }
-
- toggleText() {
- this.showText = !this.showText;
- }
-
- async load() {
+ protected load = async () => {
this.unavailable = false;
this.error = false;
this.hideEmail = false;
- const keyArray = Utils.fromUrlB64ToArray(this.key);
- this.accessRequest = new SendAccessRequest();
- if (this.password != null) {
- const passwordHash = await this.cryptoFunctionService.pbkdf2(
- this.password,
- keyArray,
- "sha256",
- SEND_KDF_ITERATIONS
- );
- this.accessRequest.password = Utils.fromBufferToB64(passwordHash);
- }
try {
+ const keyArray = Utils.fromUrlB64ToArray(this.key);
+ this.accessRequest = new SendAccessRequest();
+ if (this.password != null) {
+ const passwordHash = await this.cryptoFunctionService.pbkdf2(
+ this.password,
+ keyArray,
+ "sha256",
+ SEND_KDF_ITERATIONS
+ );
+ this.accessRequest.password = Utils.fromBufferToB64(passwordHash);
+ }
let sendResponse: SendAccessResponse = null;
if (this.loading) {
sendResponse = await this.sendApiService.postSendAccess(this.id, this.accessRequest);
@@ -168,16 +120,23 @@ export class AccessComponent implements OnInit {
const sendAccess = new SendAccess(sendResponse);
this.decKey = await this.cryptoService.makeSendKey(keyArray);
this.send = await sendAccess.decrypt(this.decKey);
- this.showText = this.send.text != null ? !this.send.text.hidden : true;
} catch (e) {
if (e instanceof ErrorResponse) {
if (e.statusCode === 401) {
this.passwordRequired = true;
} else if (e.statusCode === 404) {
this.unavailable = true;
+ } else if (e.statusCode === 400) {
+ this.platformUtilsService.showToast(
+ "error",
+ this.i18nService.t("errorOccurred"),
+ e.message
+ );
} else {
this.error = true;
}
+ } else {
+ this.error = true;
}
}
this.loading = false;
@@ -186,5 +145,9 @@ export class AccessComponent implements OnInit {
!this.passwordRequired &&
!this.loading &&
!this.unavailable;
+ };
+
+ protected setPassword(password: string) {
+ this.password = password;
}
}
diff --git a/apps/web/src/app/tools/send/icons/expired-send.icon.ts b/apps/web/src/app/tools/send/icons/expired-send.icon.ts
new file mode 100644
index 00000000000..b39cdca797d
--- /dev/null
+++ b/apps/web/src/app/tools/send/icons/expired-send.icon.ts
@@ -0,0 +1,11 @@
+import { svgIcon } from "@bitwarden/components";
+
+export const ExpiredSend = svgIcon`
+
+`;
diff --git a/apps/web/src/app/tools/send/send-access-file.component.html b/apps/web/src/app/tools/send/send-access-file.component.html
new file mode 100644
index 00000000000..82880407809
--- /dev/null
+++ b/apps/web/src/app/tools/send/send-access-file.component.html
@@ -0,0 +1,5 @@
+{{ send.file.fileName }}
+
diff --git a/apps/web/src/app/tools/send/send-access-file.component.ts b/apps/web/src/app/tools/send/send-access-file.component.ts
new file mode 100644
index 00000000000..c4e71a51629
--- /dev/null
+++ b/apps/web/src/app/tools/send/send-access-file.component.ts
@@ -0,0 +1,67 @@
+import { Component, Input } from "@angular/core";
+
+import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
+import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
+import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
+import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
+import { Utils } from "@bitwarden/common/platform/misc/utils";
+import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer";
+import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
+import { SendAccessRequest } from "@bitwarden/common/tools/send/models/request/send-access.request";
+import { SendAccessView } from "@bitwarden/common/tools/send/models/view/send-access.view";
+import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
+
+import { SharedModule } from "../../shared";
+
+@Component({
+ selector: "app-send-access-file",
+ templateUrl: "send-access-file.component.html",
+ imports: [SharedModule],
+ standalone: true,
+})
+export class SendAccessFileComponent {
+ @Input() send: SendAccessView;
+ @Input() decKey: SymmetricCryptoKey;
+ @Input() accessRequest: SendAccessRequest;
+ constructor(
+ private i18nService: I18nService,
+ private platformUtilsService: PlatformUtilsService,
+ private cryptoService: CryptoService,
+ private fileDownloadService: FileDownloadService,
+ private sendApiService: SendApiService
+ ) {}
+
+ protected download = async () => {
+ if (this.send == null || this.decKey == null) {
+ return;
+ }
+
+ const downloadData = await this.sendApiService.getSendFileDownloadData(
+ this.send,
+ this.accessRequest
+ );
+
+ if (Utils.isNullOrWhitespace(downloadData.url)) {
+ this.platformUtilsService.showToast("error", null, this.i18nService.t("missingSendFile"));
+ return;
+ }
+
+ const response = await fetch(new Request(downloadData.url, { cache: "no-store" }));
+ if (response.status !== 200) {
+ this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred"));
+ return;
+ }
+
+ try {
+ const encBuf = await EncArrayBuffer.fromResponse(response);
+ const decBuf = await this.cryptoService.decryptFromBytes(encBuf, this.decKey);
+ this.fileDownloadService.download({
+ fileName: this.send.file.fileName,
+ blobData: decBuf,
+ downloadMethod: "save",
+ });
+ } catch (e) {
+ this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred"));
+ }
+ };
+}
diff --git a/apps/web/src/app/tools/send/send-access-password.component.html b/apps/web/src/app/tools/send/send-access-password.component.html
new file mode 100644
index 00000000000..8bb2c306010
--- /dev/null
+++ b/apps/web/src/app/tools/send/send-access-password.component.html
@@ -0,0 +1,28 @@
+{{ "sendProtectedPassword" | i18n }}
+{{ "sendProtectedPasswordDontKnow" | i18n }}
+
+
+ {{ "password" | i18n }}
+
+
+
+
+
+
+
diff --git a/apps/web/src/app/tools/send/send-access-password.component.ts b/apps/web/src/app/tools/send/send-access-password.component.ts
new file mode 100644
index 00000000000..07a08fda7cd
--- /dev/null
+++ b/apps/web/src/app/tools/send/send-access-password.component.ts
@@ -0,0 +1,36 @@
+import { Component, EventEmitter, Input, Output } from "@angular/core";
+import { FormBuilder, Validators } from "@angular/forms";
+import { Subject, takeUntil } from "rxjs";
+
+import { SharedModule } from "../../shared";
+
+@Component({
+ selector: "app-send-access-password",
+ templateUrl: "send-access-password.component.html",
+ imports: [SharedModule],
+ standalone: true,
+})
+export class SendAccessPasswordComponent {
+ private destroy$ = new Subject();
+ protected formGroup = this.formBuilder.group({
+ password: ["", [Validators.required]],
+ });
+
+ @Input() loading: boolean;
+ @Output() setPasswordEvent = new EventEmitter();
+
+ constructor(private formBuilder: FormBuilder) {}
+
+ async ngOnInit() {
+ this.formGroup.controls.password.valueChanges
+ .pipe(takeUntil(this.destroy$))
+ .subscribe((val) => {
+ this.setPasswordEvent.emit(val);
+ });
+ }
+
+ ngOnDestroy() {
+ this.destroy$.next();
+ this.destroy$.complete();
+ }
+}
diff --git a/apps/web/src/app/tools/send/send-access-text.component.html b/apps/web/src/app/tools/send/send-access-text.component.html
new file mode 100644
index 00000000000..ca772251146
--- /dev/null
+++ b/apps/web/src/app/tools/send/send-access-text.component.html
@@ -0,0 +1,26 @@
+{{ "sendHiddenByDefault" | i18n }}
+
+
+
+
+
+
+
+
+
diff --git a/apps/web/src/app/tools/send/send-access-text.component.ts b/apps/web/src/app/tools/send/send-access-text.component.ts
new file mode 100644
index 00000000000..4c3c9c89675
--- /dev/null
+++ b/apps/web/src/app/tools/send/send-access-text.component.ts
@@ -0,0 +1,59 @@
+import { Component, Input } from "@angular/core";
+import { FormBuilder } from "@angular/forms";
+
+import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
+import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
+import { SendAccessView } from "@bitwarden/common/tools/send/models/view/send-access.view";
+
+import { SharedModule } from "../../shared";
+
+@Component({
+ selector: "app-send-access-text",
+ templateUrl: "send-access-text.component.html",
+ imports: [SharedModule],
+ standalone: true,
+})
+export class SendAccessTextComponent {
+ private _send: SendAccessView = null;
+ protected showText = false;
+
+ protected formGroup = this.formBuilder.group({
+ sendText: [""],
+ });
+
+ constructor(
+ private i18nService: I18nService,
+ private platformUtilsService: PlatformUtilsService,
+ private formBuilder: FormBuilder
+ ) {}
+
+ get send(): SendAccessView {
+ return this._send;
+ }
+
+ @Input() set send(value: SendAccessView) {
+ this._send = value;
+ this.showText = this.send.text != null ? !this.send.text.hidden : true;
+
+ if (this.send == null || this.send.text == null) {
+ return;
+ }
+
+ this.formGroup.controls.sendText.patchValue(
+ this.showText ? this.send.text.text : this.send.text.maskedText
+ );
+ }
+
+ protected copyText() {
+ this.platformUtilsService.copyToClipboard(this.send.text.text);
+ this.platformUtilsService.showToast(
+ "success",
+ null,
+ this.i18nService.t("valueCopied", this.i18nService.t("sendTypeText"))
+ );
+ }
+
+ protected toggleText() {
+ this.showText = !this.showText;
+ }
+}
diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json
index 546cd7209cd..7330e5053cd 100644
--- a/apps/web/src/locales/en/messages.json
+++ b/apps/web/src/locales/en/messages.json
@@ -4218,8 +4218,8 @@
"message": "This Send is hidden by default. You can toggle its visibility using the button below.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
- "downloadFile": {
- "message": "Download file"
+ "downloadAttachments": {
+ "message": "Download attachments"
},
"sendAccessUnavailable": {
"message": "The Send you are trying to access does not exist or is no longer available.",
@@ -4609,8 +4609,8 @@
"message": "to try it today.",
"description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'"
},
- "sendCreatorIdentifier": {
- "message": "Bitwarden user $USER_IDENTIFIER$ shared the following with you",
+ "sendAccessCreatorIdentifier": {
+ "message": "Bitwarden member $USER_IDENTIFIER$ shared the following with you",
"placeholders": {
"user_identifier": {
"content": "$1",
@@ -7395,5 +7395,8 @@
},
"projectAccessUpdated": {
"message": "Project access updated"
+ },
+ "unexpectedErrorSend": {
+ "message": "An unexpected error has occurred while loading this Send. Try again later."
}
}