[PM-2806] Migrate send access to Component Library (#6139)
* Remove unneeded ApiService * Extract SendAccess for sends of type text * Migrate form and card-body * Migrate callout * Extract SendAccess for sends of type file * Converted SendAccess component to standalone * Migrated bottom message to CL * Added Send Access Password Component * Added No item component, password component and changed bootstrap classes * Updated send texts and added layout for unexpected error * Changed SendAccessTextComponent to standalone * Moved AccessComponent to oss.module.ts and removed unnecessary components from app.module * Properly set access modifiers * Using async action on download button * Updated links * Using tailwind classes * Using ng-template and ng-container * Added validation to check if status code is from a wrong password * Using Component Library Forms * using subscriber to update password on send access * Using reactive forms to show the text on send access * Updated message.json keys for changed values * Removed unnecessary components and changed classes to tailwind ones * added margin bottom on send-access-password to keep consistent with other send-access layouts * removed duplicated message key * Added error toast message on wrong password --------- Co-authored-by: Daniel James Smith <djsmith@web.de>
This commit is contained in:
parent
5b1717fd41
commit
3952af058c
|
@ -5,6 +5,7 @@ import { AuthModule } from "./auth";
|
||||||
import { LoginModule } from "./auth/login/login.module";
|
import { LoginModule } from "./auth/login/login.module";
|
||||||
import { TrialInitiationModule } from "./auth/trial-initiation/trial-initiation.module";
|
import { TrialInitiationModule } from "./auth/trial-initiation/trial-initiation.module";
|
||||||
import { LooseComponentsModule, SharedModule } from "./shared";
|
import { LooseComponentsModule, SharedModule } from "./shared";
|
||||||
|
import { AccessComponent } from "./tools/send/access.component";
|
||||||
import { OrganizationBadgeModule } from "./vault/individual-vault/organization-badge/organization-badge.module";
|
import { OrganizationBadgeModule } from "./vault/individual-vault/organization-badge/organization-badge.module";
|
||||||
import { VaultFilterModule } from "./vault/individual-vault/vault-filter/vault-filter.module";
|
import { VaultFilterModule } from "./vault/individual-vault/vault-filter/vault-filter.module";
|
||||||
|
|
||||||
|
@ -18,6 +19,7 @@ import { VaultFilterModule } from "./vault/individual-vault/vault-filter/vault-f
|
||||||
OrganizationUserModule,
|
OrganizationUserModule,
|
||||||
LoginModule,
|
LoginModule,
|
||||||
AuthModule,
|
AuthModule,
|
||||||
|
AccessComponent,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
SharedModule,
|
SharedModule,
|
||||||
|
@ -26,6 +28,7 @@ import { VaultFilterModule } from "./vault/individual-vault/vault-filter/vault-f
|
||||||
VaultFilterModule,
|
VaultFilterModule,
|
||||||
OrganizationBadgeModule,
|
OrganizationBadgeModule,
|
||||||
LoginModule,
|
LoginModule,
|
||||||
|
AccessComponent,
|
||||||
],
|
],
|
||||||
bootstrap: [],
|
bootstrap: [],
|
||||||
})
|
})
|
||||||
|
|
|
@ -73,7 +73,6 @@ import { SettingsComponent } from "../settings/settings.component";
|
||||||
import { VaultTimeoutInputComponent } from "../settings/vault-timeout-input.component";
|
import { VaultTimeoutInputComponent } from "../settings/vault-timeout-input.component";
|
||||||
import { GeneratorComponent } from "../tools/generator.component";
|
import { GeneratorComponent } from "../tools/generator.component";
|
||||||
import { PasswordGeneratorHistoryComponent } from "../tools/password-generator-history.component";
|
import { PasswordGeneratorHistoryComponent } from "../tools/password-generator-history.component";
|
||||||
import { AccessComponent } from "../tools/send/access.component";
|
|
||||||
import { AddEditComponent as SendAddEditComponent } from "../tools/send/add-edit.component";
|
import { AddEditComponent as SendAddEditComponent } from "../tools/send/add-edit.component";
|
||||||
import { ToolsComponent } from "../tools/tools.component";
|
import { ToolsComponent } from "../tools/tools.component";
|
||||||
import { PremiumBadgeComponent } from "../vault/components/premium-badge.component";
|
import { PremiumBadgeComponent } from "../vault/components/premium-badge.component";
|
||||||
|
@ -108,7 +107,6 @@ import { SharedModule } from "./shared.module";
|
||||||
declarations: [
|
declarations: [
|
||||||
AcceptFamilySponsorshipComponent,
|
AcceptFamilySponsorshipComponent,
|
||||||
AcceptOrganizationComponent,
|
AcceptOrganizationComponent,
|
||||||
AccessComponent,
|
|
||||||
AccountComponent,
|
AccountComponent,
|
||||||
AddEditComponent,
|
AddEditComponent,
|
||||||
AddEditCustomFieldsComponent,
|
AddEditCustomFieldsComponent,
|
||||||
|
@ -191,7 +189,6 @@ import { SharedModule } from "./shared.module";
|
||||||
UserVerificationModule,
|
UserVerificationModule,
|
||||||
PremiumBadgeComponent,
|
PremiumBadgeComponent,
|
||||||
AcceptOrganizationComponent,
|
AcceptOrganizationComponent,
|
||||||
AccessComponent,
|
|
||||||
AccountComponent,
|
AccountComponent,
|
||||||
AddEditComponent,
|
AddEditComponent,
|
||||||
AddEditCustomFieldsComponent,
|
AddEditCustomFieldsComponent,
|
||||||
|
|
|
@ -1,150 +1,84 @@
|
||||||
<form #form (ngSubmit)="load()" [appApiAction]="formPromise" class="container" ngNativeValidate>
|
<form [formGroup]="formGroup" [bitSubmit]="load">
|
||||||
<div class="row justify-content-center mt-5">
|
<div
|
||||||
<div class="col-12">
|
class="tw-mx-auto tw-mt-5 tw-flex tw-max-w-xl tw-flex-col tw-items-center tw-justify-center tw-p-8"
|
||||||
<h1 class="lead text-center mb-4">Bitwarden Send</h1>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 text-center" *ngIf="creatorIdentifier != null">
|
|
||||||
<p>{{ "sendCreatorIdentifier" | i18n : creatorIdentifier }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="col-8" *ngIf="hideEmail">
|
|
||||||
<app-callout type="warning" title="{{ 'warning' | i18n }}">
|
|
||||||
{{ "viewSendHiddenEmailWarning" | i18n }}
|
|
||||||
<a href="https://bitwarden.com/help/receive-send/" target="_blank">{{
|
|
||||||
"learnMore" | i18n
|
|
||||||
}}</a
|
|
||||||
>.
|
|
||||||
</app-callout>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row justify-content-center">
|
|
||||||
<div class="col-5">
|
|
||||||
<div class="card d-block">
|
|
||||||
<div class="card-body" *ngIf="loading" class="text-center">
|
|
||||||
<i
|
|
||||||
class="bwi bwi-spinner bwi-spin bwi-2x text-muted"
|
|
||||||
title="{{ 'loading' | i18n }}"
|
|
||||||
aria-hidden="true"
|
|
||||||
></i>
|
|
||||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="card-body" *ngIf="!loading && passwordRequired">
|
|
||||||
<p>{{ "sendProtectedPassword" | i18n }}</p>
|
|
||||||
<p>{{ "sendProtectedPasswordDontKnow" | i18n }}</p>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="password">{{ "password" | i18n }}</label>
|
|
||||||
<input
|
|
||||||
id="password"
|
|
||||||
type="password"
|
|
||||||
name="Password"
|
|
||||||
class="text-monospace form-control"
|
|
||||||
[(ngModel)]="password"
|
|
||||||
required
|
|
||||||
appInputVerbatim
|
|
||||||
appAutofocus
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex">
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="btn btn-primary btn-block btn-submit"
|
|
||||||
[disabled]="form.loading"
|
|
||||||
>
|
>
|
||||||
<span>
|
<img class="logo logo-themed" alt="Bitwarden" />
|
||||||
<i class="bwi bwi-sign-in" aria-hidden="true"></i> {{ "continue" | i18n }}
|
<div class="tw-mt-5 tw-w-full">
|
||||||
</span>
|
<h2 bitTypography="h2" class="tw-mb-4 tw-text-center">View Send</h2>
|
||||||
<i
|
|
||||||
class="bwi bwi-spinner bwi-spin"
|
|
||||||
title="{{ 'loading' | i18n }}"
|
|
||||||
aria-hidden="true"
|
|
||||||
></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="tw-w-full tw-text-center" *ngIf="creatorIdentifier != null">
|
||||||
|
<p>{{ "sendAccessCreatorIdentifier" | i18n : creatorIdentifier }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body" *ngIf="!loading && unavailable">
|
<bit-callout *ngIf="hideEmail" type="warning" title="{{ 'warning' | i18n }}">
|
||||||
{{ "sendAccessUnavailable" | i18n }}
|
{{ "viewSendHiddenEmailWarning" | i18n }}
|
||||||
</div>
|
<a
|
||||||
<div class="card-body" *ngIf="!loading && error">
|
bitLink
|
||||||
{{ "unexpectedError" | i18n }}
|
href="https://bitwarden.com/help/receive-send/"
|
||||||
</div>
|
target="_blank"
|
||||||
<div class="card-body" *ngIf="!loading && !passwordRequired && send">
|
rel="noopener noreferrer"
|
||||||
<p class="text-center">
|
>{{ "learnMore" | i18n }}</a
|
||||||
|
>.
|
||||||
|
</bit-callout>
|
||||||
|
<div
|
||||||
|
class="tw-mt-3 tw-w-10/12 tw-rounded-md tw-border tw-border-solid tw-border-secondary-300 tw-bg-background tw-p-6"
|
||||||
|
>
|
||||||
|
<ng-container *ngIf="!loading; else spinner">
|
||||||
|
<app-send-access-password
|
||||||
|
(setPasswordEvent)="setPassword($event)"
|
||||||
|
*ngIf="passwordRequired && !error"
|
||||||
|
></app-send-access-password>
|
||||||
|
<bit-no-items [icon]="expiredSendIcon" class="tw-text-main" *ngIf="unavailable">
|
||||||
|
<ng-container slot="description">{{ "sendAccessUnavailable" | i18n }}</ng-container>
|
||||||
|
</bit-no-items>
|
||||||
|
<bit-no-items [icon]="expiredSendIcon" class="tw-text-main" *ngIf="error">
|
||||||
|
<ng-container slot="description">{{ "unexpectedErrorSend" | i18n }}</ng-container>
|
||||||
|
</bit-no-items>
|
||||||
|
<div *ngIf="!passwordRequired && send && !error && !unavailable">
|
||||||
|
<p class="tw-text-center">
|
||||||
<b>{{ send.name }}</b>
|
<b>{{ send.name }}</b>
|
||||||
</p>
|
</p>
|
||||||
<hr />
|
<hr />
|
||||||
<!-- Text -->
|
<!-- Text -->
|
||||||
<ng-container *ngIf="send.type === sendType.Text">
|
<ng-container *ngIf="send.type === sendType.Text">
|
||||||
<app-callout *ngIf="send.text.hidden" type="tip">{{
|
<app-send-access-text [send]="send"></app-send-access-text>
|
||||||
"sendHiddenByDefault" | i18n
|
|
||||||
}}</app-callout>
|
|
||||||
<div class="form-group">
|
|
||||||
<textarea
|
|
||||||
id="text"
|
|
||||||
rows="8"
|
|
||||||
name="Text"
|
|
||||||
[ngModel]="sendText"
|
|
||||||
class="form-control"
|
|
||||||
readonly
|
|
||||||
></textarea>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
class="btn btn-block btn-link"
|
|
||||||
type="button"
|
|
||||||
(click)="toggleText()"
|
|
||||||
*ngIf="send.text.hidden"
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
class="bwi bwi-lg"
|
|
||||||
aria-hidden="true"
|
|
||||||
[ngClass]="{ 'bwi-eye': !showText, 'bwi-eye-slash': showText }"
|
|
||||||
></i>
|
|
||||||
{{ "toggleVisibility" | i18n }}
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-block btn-link" type="button" (click)="copyText()">
|
|
||||||
<i class="bwi bwi-copy" aria-hidden="true"></i> {{ "copyValue" | i18n }}
|
|
||||||
</button>
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<!-- File -->
|
<!-- File -->
|
||||||
<ng-container *ngIf="send.type === sendType.File">
|
<ng-container *ngIf="send.type === sendType.File">
|
||||||
<p>{{ send.file.fileName }}</p>
|
<app-send-access-file
|
||||||
<button
|
[send]="send"
|
||||||
class="btn btn-primary btn-block"
|
[decKey]="decKey"
|
||||||
type="button"
|
[accessRequest]="accessRequest"
|
||||||
(click)="download()"
|
></app-send-access-file>
|
||||||
*ngIf="!downloading"
|
|
||||||
>
|
|
||||||
<i class="bwi bwi-download" aria-hidden="true"></i>
|
|
||||||
{{ "downloadFile" | i18n }} ({{ send.file.sizeName }})
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="btn btn-primary btn-block"
|
|
||||||
type="button"
|
|
||||||
*ngIf="downloading"
|
|
||||||
disabled="true"
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
class="bwi bwi-spinner bwi-spin"
|
|
||||||
title="{{ 'loading' | i18n }}"
|
|
||||||
aria-hidden="true"
|
|
||||||
></i>
|
|
||||||
</button>
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<p *ngIf="expirationDate" class="text-center text-muted">
|
<p *ngIf="expirationDate" class="tw-text-center tw-text-muted">
|
||||||
Expires: {{ expirationDate | date : "medium" }}
|
Expires: {{ expirationDate | date : "medium" }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
<ng-template #spinner>
|
||||||
|
<div class="tw-text-center">
|
||||||
|
<i
|
||||||
|
class="bwi bwi-spinner bwi-spin bwi-2x tw-text-muted"
|
||||||
|
title="{{ 'loading' | i18n }}"
|
||||||
|
aria-hidden="true"
|
||||||
|
></i>
|
||||||
|
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
</ng-template>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 text-center mt-5 text-muted">
|
<div class="tw-mt-5 tw-w-10/12 tw-text-center tw-text-muted">
|
||||||
<p class="mb-0">
|
<p bitTypography="body2" class="tw-mb-0">
|
||||||
{{ "sendAccessTaglineProductDesc" | i18n }}<br />
|
{{ "sendAccessTaglineProductDesc" | i18n }}
|
||||||
{{ "sendAccessTaglineLearnMore" | i18n }}
|
{{ "sendAccessTaglineLearnMore" | i18n }}
|
||||||
<a href="https://www.bitwarden.com/products/send?source=web-vault" target="_blank"
|
<a
|
||||||
|
bitLink
|
||||||
|
href="https://www.bitwarden.com/products/send?source=web-vault"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
>Bitwarden Send</a
|
>Bitwarden Send</a
|
||||||
>
|
>
|
||||||
{{ "sendAccessTaglineOr" | i18n }}
|
{{ "sendAccessTaglineOr" | i18n }}
|
||||||
<a href="https://vault.bitwarden.com/#/register" target="_blank">{{
|
<a bitLink routerLink="/register" target="_blank">{{ "sendAccessTaglineSignUp" | i18n }}</a>
|
||||||
"sendAccessTaglineSignUp" | i18n
|
|
||||||
}}</a>
|
|
||||||
{{ "sendAccessTaglineTryToday" | i18n }}
|
{{ "sendAccessTaglineTryToday" | i18n }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,16 +1,14 @@
|
||||||
import { Component, OnInit } from "@angular/core";
|
import { Component, OnInit } from "@angular/core";
|
||||||
|
import { FormBuilder } from "@angular/forms";
|
||||||
import { ActivatedRoute } from "@angular/router";
|
import { ActivatedRoute } from "@angular/router";
|
||||||
|
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
|
||||||
import { SEND_KDF_ITERATIONS } from "@bitwarden/common/enums";
|
import { SEND_KDF_ITERATIONS } from "@bitwarden/common/enums";
|
||||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
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 { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||||
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
|
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
|
||||||
import { SendAccess } from "@bitwarden/common/tools/send/models/domain/send-access";
|
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 { SendAccessResponse } from "@bitwarden/common/tools/send/models/response/send-access.response";
|
||||||
import { SendAccessView } from "@bitwarden/common/tools/send/models/view/send-access.view";
|
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 { 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({
|
@Component({
|
||||||
selector: "app-send-access",
|
selector: "app-send-access",
|
||||||
templateUrl: "access.component.html",
|
templateUrl: "access.component.html",
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
SendAccessFileComponent,
|
||||||
|
SendAccessTextComponent,
|
||||||
|
SendAccessPasswordComponent,
|
||||||
|
SharedModule,
|
||||||
|
NoItemsModule,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||||
export class AccessComponent implements OnInit {
|
export class AccessComponent implements OnInit {
|
||||||
send: SendAccessView;
|
protected send: SendAccessView;
|
||||||
sendType = SendType;
|
protected sendType = SendType;
|
||||||
downloading = false;
|
protected loading = true;
|
||||||
loading = true;
|
protected passwordRequired = false;
|
||||||
passwordRequired = false;
|
protected formPromise: Promise<SendAccessResponse>;
|
||||||
formPromise: Promise<SendAccessResponse>;
|
protected password: string;
|
||||||
password: string;
|
protected unavailable = false;
|
||||||
showText = false;
|
protected error = false;
|
||||||
unavailable = false;
|
protected hideEmail = false;
|
||||||
error = false;
|
protected decKey: SymmetricCryptoKey;
|
||||||
hideEmail = false;
|
protected accessRequest: SendAccessRequest;
|
||||||
|
protected expiredSendIcon = ExpiredSend;
|
||||||
|
|
||||||
|
protected formGroup = this.formBuilder.group({});
|
||||||
|
|
||||||
private id: string;
|
private id: string;
|
||||||
private key: string;
|
private key: string;
|
||||||
private decKey: SymmetricCryptoKey;
|
|
||||||
private accessRequest: SendAccessRequest;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private i18nService: I18nService,
|
|
||||||
private cryptoFunctionService: CryptoFunctionService,
|
private cryptoFunctionService: CryptoFunctionService,
|
||||||
private apiService: ApiService,
|
|
||||||
private platformUtilsService: PlatformUtilsService,
|
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private cryptoService: CryptoService,
|
private cryptoService: CryptoService,
|
||||||
private fileDownloadService: FileDownloadService,
|
private sendApiService: SendApiService,
|
||||||
private sendApiService: SendApiService
|
private platformUtilsService: PlatformUtilsService,
|
||||||
|
private i18nService: I18nService,
|
||||||
|
protected formBuilder: FormBuilder
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
get sendText() {
|
protected get expirationDate() {
|
||||||
if (this.send == null || this.send.text == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return this.showText ? this.send.text.text : this.send.text.maskedText;
|
|
||||||
}
|
|
||||||
|
|
||||||
get expirationDate() {
|
|
||||||
if (this.send == null || this.send.expirationDate == null) {
|
if (this.send == null || this.send.expirationDate == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return this.send.expirationDate;
|
return this.send.expirationDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
get creatorIdentifier() {
|
protected get creatorIdentifier() {
|
||||||
if (this.send == null || this.send.creatorIdentifier == null) {
|
if (this.send == null || this.send.creatorIdentifier == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -86,65 +93,11 @@ export class AccessComponent implements OnInit {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async download() {
|
protected load = async () => {
|
||||||
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() {
|
|
||||||
this.unavailable = false;
|
this.unavailable = false;
|
||||||
this.error = false;
|
this.error = false;
|
||||||
this.hideEmail = false;
|
this.hideEmail = false;
|
||||||
|
try {
|
||||||
const keyArray = Utils.fromUrlB64ToArray(this.key);
|
const keyArray = Utils.fromUrlB64ToArray(this.key);
|
||||||
this.accessRequest = new SendAccessRequest();
|
this.accessRequest = new SendAccessRequest();
|
||||||
if (this.password != null) {
|
if (this.password != null) {
|
||||||
|
@ -156,7 +109,6 @@ export class AccessComponent implements OnInit {
|
||||||
);
|
);
|
||||||
this.accessRequest.password = Utils.fromBufferToB64(passwordHash);
|
this.accessRequest.password = Utils.fromBufferToB64(passwordHash);
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
let sendResponse: SendAccessResponse = null;
|
let sendResponse: SendAccessResponse = null;
|
||||||
if (this.loading) {
|
if (this.loading) {
|
||||||
sendResponse = await this.sendApiService.postSendAccess(this.id, this.accessRequest);
|
sendResponse = await this.sendApiService.postSendAccess(this.id, this.accessRequest);
|
||||||
|
@ -168,16 +120,23 @@ export class AccessComponent implements OnInit {
|
||||||
const sendAccess = new SendAccess(sendResponse);
|
const sendAccess = new SendAccess(sendResponse);
|
||||||
this.decKey = await this.cryptoService.makeSendKey(keyArray);
|
this.decKey = await this.cryptoService.makeSendKey(keyArray);
|
||||||
this.send = await sendAccess.decrypt(this.decKey);
|
this.send = await sendAccess.decrypt(this.decKey);
|
||||||
this.showText = this.send.text != null ? !this.send.text.hidden : true;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof ErrorResponse) {
|
if (e instanceof ErrorResponse) {
|
||||||
if (e.statusCode === 401) {
|
if (e.statusCode === 401) {
|
||||||
this.passwordRequired = true;
|
this.passwordRequired = true;
|
||||||
} else if (e.statusCode === 404) {
|
} else if (e.statusCode === 404) {
|
||||||
this.unavailable = true;
|
this.unavailable = true;
|
||||||
|
} else if (e.statusCode === 400) {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
this.i18nService.t("errorOccurred"),
|
||||||
|
e.message
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
this.error = true;
|
this.error = true;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
this.error = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
|
@ -186,5 +145,9 @@ export class AccessComponent implements OnInit {
|
||||||
!this.passwordRequired &&
|
!this.passwordRequired &&
|
||||||
!this.loading &&
|
!this.loading &&
|
||||||
!this.unavailable;
|
!this.unavailable;
|
||||||
|
};
|
||||||
|
|
||||||
|
protected setPassword(password: string) {
|
||||||
|
this.password = password;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { svgIcon } from "@bitwarden/components";
|
||||||
|
|
||||||
|
export const ExpiredSend = svgIcon`
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="130" height="130" fill="none">
|
||||||
|
<path class="tw-fill-secondary-500" fill-rule="evenodd" d="M22.75 29.695c0-4.991 4.074-9.037 9.1-9.037h14.3v2.582h-14.3c-3.59 0-6.5 2.89-6.5 6.455v68.428h-2.6V29.696Zm75.4 76.175V68.428h2.6v37.442c0 4.991-4.074 9.038-9.1 9.038h-53.3v-2.582h53.3c3.59 0 6.5-2.891 6.5-6.456Z" clip-rule="evenodd"/>
|
||||||
|
<path class="tw-fill-secondary-500" fill-rule="evenodd" d="M43.55 37.441c0-17.113 13.969-30.986 31.2-30.986s31.2 13.873 31.2 30.986c0 17.114-13.969 30.987-31.2 30.987s-31.2-13.873-31.2-30.986Zm31.2-33.568c-18.667 0-33.8 15.03-33.8 33.569S56.083 71.01 74.75 71.01c18.668 0 33.8-15.03 33.8-33.569S93.418 3.873 74.75 3.873Z" clip-rule="evenodd"/>
|
||||||
|
<path class="tw-fill-secondary-500" fill-rule="evenodd" d="M73.972 65.2c0 .357.291.646.65.646 15.968 0 28.925-12.71 28.925-28.404a.648.648 0 0 0-.65-.646.648.648 0 0 0-.65.646c0 14.967-12.36 27.113-27.625 27.113a.648.648 0 0 0-.65.645ZM46.347 38.087c.36 0 .65-.289.65-.645 0-14.968 12.361-27.113 27.625-27.113.36 0 .65-.29.65-.646a.648.648 0 0 0-.65-.646c-15.968 0-28.925 12.71-28.925 28.405 0 .356.291.645.65.645Z" clip-rule="evenodd"/>
|
||||||
|
<path class="tw-fill-secondary-500" fill-rule="evenodd" d="M123.729 81.869a1.926 1.926 0 0 1 0 2.739l-1.439 1.43a1.96 1.96 0 0 1-2.758 0L95.577 62.245a1.306 1.306 0 0 0-1.839 0 1.285 1.285 0 0 0 0 1.826l23.956 23.791a4.571 4.571 0 0 0 6.434 0l1.44-1.43a4.497 4.497 0 0 0 0-6.39l-23.956-23.791a1.306 1.306 0 0 0-1.838 0 1.285 1.285 0 0 0 0 1.825l23.955 23.792ZM34.45 36.797c0-.714.582-1.292 1.3-1.292h5.85c.718 0 1.3.578 1.3 1.291 0 .714-.582 1.292-1.3 1.292h-5.85c-.718 0-1.3-.578-1.3-1.291Zm0 10.973c0-.713.582-1.29 1.3-1.29h7.8c.718 0 1.3.578 1.3 1.29 0 .714-.582 1.292-1.3 1.292h-7.8c-.718 0-1.3-.578-1.3-1.291Zm0 10.975c0-.713.582-1.291 1.3-1.291H49.4c.718 0 1.3.578 1.3 1.29 0 .714-.582 1.292-1.3 1.292H35.75c-.718 0-1.3-.578-1.3-1.291Zm0 10.975c0-.714.582-1.292 1.3-1.292H72.8c.718 0 1.3.578 1.3 1.291s-.582 1.291-1.3 1.291H35.75c-.718 0-1.3-.578-1.3-1.29Zm0 10.973c0-.713.582-1.29 1.3-1.29h27.3c.718 0 1.3.577 1.3 1.29 0 .713-.582 1.291-1.3 1.291h-27.3c-.718 0-1.3-.578-1.3-1.29Zm6.5 10.975c0-.713.582-1.291 1.3-1.291H88.4c.718 0 1.3.578 1.3 1.291s-.582 1.291-1.3 1.291H42.25c-.718 0-1.3-.578-1.3-1.291Zm0 10.974c0-.713.582-1.291 1.3-1.291H88.4c.718 0 1.3.578 1.3 1.291s-.582 1.291-1.3 1.291H42.25c-.718 0-1.3-.578-1.3-1.291Z" clip-rule="evenodd"/>
|
||||||
|
<path class="tw-fill-secondary-500" fill-rule="evenodd" d="M43.664 86.742c.412.292.617.794.524 1.289l-6.366 33.964a1.305 1.305 0 0 1-1.745.968l-9.692-3.707-4.914 5.689c-.355.41-.928.557-1.438.37a1.292 1.292 0 0 1-.849-1.211v-8.444c0-.305.108-.599.306-.832l14.73-17.357a1.306 1.306 0 0 1 1.831-.156c.549.46.619 1.275.156 1.82L21.784 116.13v4.485l3.225-3.733c.358-.414.94-.56 1.454-.364l9.089 3.476 5.567-29.698-32.42 18.385 6.813 3.082c.653.296.941 1.061.643 1.71a1.303 1.303 0 0 1-1.722.64l-9.122-4.128a1.289 1.289 0 0 1-.106-2.296l37.06-21.017c.44-.249.986-.222 1.399.07Z" clip-rule="evenodd"/>
|
||||||
|
</svg>
|
||||||
|
`;
|
|
@ -0,0 +1,5 @@
|
||||||
|
<p>{{ send.file.fileName }}</p>
|
||||||
|
<button bitButton type="button" buttonType="primary" [bitAction]="download" [block]="true">
|
||||||
|
<i class="bwi bwi-download" aria-hidden="true"></i>
|
||||||
|
{{ "downloadAttachments" | i18n }} ({{ send.file.sizeName }})
|
||||||
|
</button>
|
|
@ -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"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
<p bitTypography="body1">{{ "sendProtectedPassword" | i18n }}</p>
|
||||||
|
<p bitTypography="body1">{{ "sendProtectedPasswordDontKnow" | i18n }}</p>
|
||||||
|
<div class="tw-mb-3" [formGroup]="formGroup">
|
||||||
|
<bit-form-field>
|
||||||
|
<bit-label>{{ "password" | i18n }}</bit-label>
|
||||||
|
<input
|
||||||
|
bitInput
|
||||||
|
type="password"
|
||||||
|
formControlName="password"
|
||||||
|
required
|
||||||
|
appInputVerbatim
|
||||||
|
appAutofocus
|
||||||
|
/>
|
||||||
|
<button type="button" bitIconButton bitSuffix bitPasswordInputToggle></button>
|
||||||
|
</bit-form-field>
|
||||||
|
<div class="tw-flex">
|
||||||
|
<button
|
||||||
|
bitButton
|
||||||
|
bitFormButton
|
||||||
|
type="submit"
|
||||||
|
buttonType="primary"
|
||||||
|
[loading]="loading"
|
||||||
|
[block]="true"
|
||||||
|
>
|
||||||
|
<span> <i class="bwi bwi-sign-in" aria-hidden="true"></i> {{ "continue" | i18n }} </span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -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<void>();
|
||||||
|
protected formGroup = this.formBuilder.group({
|
||||||
|
password: ["", [Validators.required]],
|
||||||
|
});
|
||||||
|
|
||||||
|
@Input() loading: boolean;
|
||||||
|
@Output() setPasswordEvent = new EventEmitter<string>();
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
<bit-callout *ngIf="send.text.hidden" type="info">{{ "sendHiddenByDefault" | i18n }}</bit-callout>
|
||||||
|
<bit-form-field [formGroup]="formGroup">
|
||||||
|
<textarea id="text" bitInput rows="8" name="Text" formControlName="sendText" readonly></textarea>
|
||||||
|
</bit-form-field>
|
||||||
|
<div class="tw-mb-3">
|
||||||
|
<button
|
||||||
|
bitButton
|
||||||
|
type="button"
|
||||||
|
buttonType="secondary"
|
||||||
|
[block]="true"
|
||||||
|
(click)="toggleText()"
|
||||||
|
*ngIf="send.text.hidden"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="bwi bwi-lg"
|
||||||
|
aria-hidden="true"
|
||||||
|
[ngClass]="{ 'bwi-eye': !showText, 'bwi-eye-slash': showText }"
|
||||||
|
></i>
|
||||||
|
{{ "toggleVisibility" | i18n }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="tw-mb-3">
|
||||||
|
<button bitButton type="button" buttonType="primary" [block]="true" (click)="copyText()">
|
||||||
|
<i class="bwi bwi-clone" aria-hidden="true"></i> {{ "copyValue" | i18n }}
|
||||||
|
</button>
|
||||||
|
</div>
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -4218,8 +4218,8 @@
|
||||||
"message": "This Send is hidden by default. You can toggle its visibility using the button below.",
|
"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."
|
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||||
},
|
},
|
||||||
"downloadFile": {
|
"downloadAttachments": {
|
||||||
"message": "Download file"
|
"message": "Download attachments"
|
||||||
},
|
},
|
||||||
"sendAccessUnavailable": {
|
"sendAccessUnavailable": {
|
||||||
"message": "The Send you are trying to access does not exist or is no longer available.",
|
"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.",
|
"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.**'"
|
"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": {
|
"sendAccessCreatorIdentifier": {
|
||||||
"message": "Bitwarden user $USER_IDENTIFIER$ shared the following with you",
|
"message": "Bitwarden member $USER_IDENTIFIER$ shared the following with you",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"user_identifier": {
|
"user_identifier": {
|
||||||
"content": "$1",
|
"content": "$1",
|
||||||
|
@ -7395,5 +7395,8 @@
|
||||||
},
|
},
|
||||||
"projectAccessUpdated": {
|
"projectAccessUpdated": {
|
||||||
"message": "Project access updated"
|
"message": "Project access updated"
|
||||||
|
},
|
||||||
|
"unexpectedErrorSend": {
|
||||||
|
"message": "An unexpected error has occurred while loading this Send. Try again later."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue