mirror of
https://github.com/documenso/documenso.git
synced 2026-06-22 04:12:06 +10:00
fix: update new quota and rates UX (#2954)
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||
import { getRecipientType } from '@documenso/lib/client-only/recipient-type';
|
||||
import { AppError } from '@documenso/lib/errors/app-error';
|
||||
import type { TRecipientLite } from '@documenso/lib/types/recipient';
|
||||
import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter';
|
||||
import type { Document } from '@documenso/prisma/types/document-legacy-schema';
|
||||
@@ -30,6 +31,7 @@ import { useForm, useWatch } from 'react-hook-form';
|
||||
import * as z from 'zod';
|
||||
|
||||
import { useCurrentTeam } from '~/providers/team';
|
||||
import { getDistributeErrorMessage } from '~/utils/toast-error-messages';
|
||||
|
||||
import { StackAvatar } from '../general/stack-avatar';
|
||||
|
||||
@@ -99,9 +101,12 @@ export const DocumentResendDialog = ({ document, recipients }: DocumentResendDia
|
||||
|
||||
setIsOpen(false);
|
||||
} catch (err) {
|
||||
const error = AppError.parseError(err);
|
||||
const errorMessage = getDistributeErrorMessage(error.code);
|
||||
|
||||
toast({
|
||||
title: _(msg`Something went wrong`),
|
||||
description: _(msg`This document could not be re-sent at this time. Please try again.`),
|
||||
title: _(errorMessage.title),
|
||||
description: _(errorMessage.description),
|
||||
variant: 'destructive',
|
||||
duration: 7500,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useCurrentEnvelopeEditor } from '@documenso/lib/client-only/providers/envelope-editor-provider';
|
||||
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
|
||||
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
|
||||
import { AppError } from '@documenso/lib/errors/app-error';
|
||||
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
||||
import { getRecipientsWithMissingFields } from '@documenso/lib/utils/recipients';
|
||||
import { zEmail } from '@documenso/lib/utils/zod';
|
||||
@@ -37,6 +38,7 @@ import { useForm } from 'react-hook-form';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { match } from 'ts-pattern';
|
||||
import * as z from 'zod';
|
||||
import { getDistributeErrorMessage } from '~/utils/toast-error-messages';
|
||||
|
||||
export type EnvelopeDistributeDialogProps = {
|
||||
onDistribute?: () => Promise<void>;
|
||||
@@ -66,7 +68,7 @@ export const EnvelopeDistributeDialog = ({
|
||||
const { envelope, syncEnvelope, isAutosaving, autosaveError } = useCurrentEnvelopeEditor();
|
||||
|
||||
const { toast } = useToast();
|
||||
const { t } = useLingui();
|
||||
const { t, i18n } = useLingui();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
@@ -174,9 +176,13 @@ export const EnvelopeDistributeDialog = ({
|
||||
|
||||
setIsOpen(false);
|
||||
} catch (err) {
|
||||
const error = AppError.parseError(err);
|
||||
|
||||
const errorMessage = getDistributeErrorMessage(error.code);
|
||||
|
||||
toast({
|
||||
title: t`Something went wrong`,
|
||||
description: t`This envelope could not be distributed at this time. Please try again.`,
|
||||
title: i18n._(errorMessage.title),
|
||||
description: i18n._(errorMessage.description),
|
||||
variant: 'destructive',
|
||||
duration: 7500,
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { getRecipientType } from '@documenso/lib/client-only/recipient-type';
|
||||
import { AppError } from '@documenso/lib/errors/app-error';
|
||||
import type { TEnvelope } from '@documenso/lib/types/envelope';
|
||||
import type { TEnvelopeRecipientLite } from '@documenso/lib/types/recipient';
|
||||
import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter';
|
||||
@@ -25,7 +26,7 @@ import { DocumentStatus, EnvelopeType, SigningStatus } from '@prisma/client';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import * as z from 'zod';
|
||||
|
||||
import { getDistributeErrorMessage } from '~/utils/toast-error-messages';
|
||||
import { StackAvatar } from '../general/stack-avatar';
|
||||
|
||||
export type EnvelopeRedistributeDialogProps = {
|
||||
@@ -47,7 +48,7 @@ export const EnvelopeRedistributeDialog = ({ envelope, trigger }: EnvelopeRedist
|
||||
const recipients = envelope.recipients;
|
||||
|
||||
const { toast } = useToast();
|
||||
const { t } = useLingui();
|
||||
const { t, i18n } = useLingui();
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
@@ -77,9 +78,12 @@ export const EnvelopeRedistributeDialog = ({ envelope, trigger }: EnvelopeRedist
|
||||
|
||||
setIsOpen(false);
|
||||
} catch (err) {
|
||||
const error = AppError.parseError(err);
|
||||
const errorMessage = getDistributeErrorMessage(error.code);
|
||||
|
||||
toast({
|
||||
title: t`Something went wrong`,
|
||||
description: t`This envelope could not be resent at this time. Please try again.`,
|
||||
title: i18n._(errorMessage.title),
|
||||
description: i18n._(errorMessage.description),
|
||||
variant: 'destructive',
|
||||
duration: 7500,
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
TEMPLATE_RECIPIENT_NAME_PLACEHOLDER_REGEX,
|
||||
} from '@documenso/lib/constants/template';
|
||||
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION, SKIP_QUERY_BATCH_META } from '@documenso/lib/constants/trpc';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { AppError } from '@documenso/lib/errors/app-error';
|
||||
import { type TRecipientLite, ZRecipientEmailSchema } from '@documenso/lib/types/recipient';
|
||||
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
@@ -35,8 +35,8 @@ import { FileTextIcon, InfoIcon, Plus, UploadCloudIcon, X } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useFieldArray, useForm } from 'react-hook-form';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { match } from 'ts-pattern';
|
||||
import * as z from 'zod';
|
||||
import { getTemplateUseErrorMessage } from '~/utils/toast-error-messages';
|
||||
|
||||
const ZAddRecipientsForNewDocumentSchema = z.object({
|
||||
distributeDocument: z.boolean(),
|
||||
@@ -180,22 +180,11 @@ export function TemplateUseDialog({
|
||||
await navigate(documentPath);
|
||||
} catch (err) {
|
||||
const error = AppError.parseError(err);
|
||||
|
||||
const errorMessage = match(error.code)
|
||||
.with('DOCUMENT_SEND_FAILED', () => msg`The document was created but could not be sent to recipients.`)
|
||||
.with(
|
||||
AppErrorCode.INVALID_BODY,
|
||||
AppErrorCode.INVALID_REQUEST,
|
||||
() =>
|
||||
msg`The document could not be created because of missing or invalid information. Please review the template's recipients and fields.`,
|
||||
)
|
||||
.with(AppErrorCode.NOT_FOUND, () => msg`The template or one of its recipients could not be found.`)
|
||||
.with(AppErrorCode.LIMIT_EXCEEDED, () => msg`You have reached your document limit for this plan.`)
|
||||
.otherwise(() => msg`An error occurred while creating document from template.`);
|
||||
const errorMessage = getTemplateUseErrorMessage(error.code);
|
||||
|
||||
toast({
|
||||
title: _(msg`Error`),
|
||||
description: _(errorMessage),
|
||||
title: _(errorMessage.title),
|
||||
description: _(errorMessage.description),
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-form
|
||||
import { APP_I18N_OPTIONS } from '@documenso/lib/constants/i18n';
|
||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||
import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones';
|
||||
import { AppError } from '@documenso/lib/errors/app-error';
|
||||
import { ZDirectTemplateEmbedDataSchema } from '@documenso/lib/types/embed-direct-template-schema';
|
||||
import { isFieldUnsignedAndRequired, isRequiredField } from '@documenso/lib/utils/advanced-fields-helpers';
|
||||
import { getDocumentDataUrlForPdfViewer } from '@documenso/lib/utils/envelope-download';
|
||||
@@ -42,6 +43,7 @@ import { useSearchParams } from 'react-router';
|
||||
import { BrandingLogo } from '~/components/general/branding-logo';
|
||||
import PDFViewerLazy from '~/components/general/pdf-viewer/pdf-viewer-lazy';
|
||||
import { injectCss } from '~/utils/css-vars';
|
||||
import { getDirectTemplateErrorMessage } from '~/utils/toast-error-messages';
|
||||
|
||||
import type { DirectTemplateLocalField } from '../general/direct-template/direct-template-signing-form';
|
||||
import { DocumentSigningAttachmentsPopover } from '../general/document-signing/document-signing-attachments-popover';
|
||||
@@ -259,9 +261,12 @@ export const EmbedDirectTemplateClientPage = ({
|
||||
);
|
||||
}
|
||||
|
||||
const error = AppError.parseError(err);
|
||||
const errorMessage = getDirectTemplateErrorMessage(error.code);
|
||||
|
||||
toast({
|
||||
title: _(msg`Something went wrong`),
|
||||
description: _(msg`We were unable to submit this document at this time. Please try again later.`),
|
||||
title: _(errorMessage.title),
|
||||
description: _(errorMessage.description),
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
||||
import { AppError } from '@documenso/lib/errors/app-error';
|
||||
import type { TTemplate } from '@documenso/lib/types/template';
|
||||
import { isRequiredField } from '@documenso/lib/utils/advanced-fields-helpers';
|
||||
import { getDocumentDataUrlForPdfViewer } from '@documenso/lib/utils/envelope-download';
|
||||
@@ -17,6 +18,7 @@ import { useNavigate, useSearchParams } from 'react-router';
|
||||
import { useRequiredDocumentSigningAuthContext } from '~/components/general/document-signing/document-signing-auth-provider';
|
||||
import { useRequiredDocumentSigningContext } from '~/components/general/document-signing/document-signing-provider';
|
||||
import PDFViewerLazy from '~/components/general/pdf-viewer/pdf-viewer-lazy';
|
||||
import { getDirectTemplateErrorMessage } from '~/utils/toast-error-messages';
|
||||
|
||||
import { DirectTemplateConfigureForm, type TDirectTemplateConfigureFormSchema } from './direct-template-configure-form';
|
||||
import { type DirectTemplateLocalField, DirectTemplateSigningForm } from './direct-template-signing-form';
|
||||
@@ -120,9 +122,12 @@ export const DirectTemplatePageView = ({
|
||||
await navigate(`/sign/${token}/complete`);
|
||||
}
|
||||
} catch (err) {
|
||||
const error = AppError.parseError(err);
|
||||
const errorMessage = getDirectTemplateErrorMessage(error.code);
|
||||
|
||||
toast({
|
||||
title: _(msg`Something went wrong`),
|
||||
description: _(msg`We were unable to submit this document at this time. Please try again later.`),
|
||||
title: _(errorMessage.title),
|
||||
description: _(errorMessage.description),
|
||||
variant: 'destructive',
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { DocumentSignatureType } from '@documenso/lib/constants/document';
|
||||
import { isValidLanguageCode } from '@documenso/lib/constants/i18n';
|
||||
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION, SKIP_QUERY_BATCH_META } from '@documenso/lib/constants/trpc';
|
||||
import { AppError } from '@documenso/lib/errors/app-error';
|
||||
import type { TDocument } from '@documenso/lib/types/document';
|
||||
import { ZDocumentAccessAuthTypesSchema } from '@documenso/lib/types/document-auth';
|
||||
import { getDocumentDataUrlForPdfViewer } from '@documenso/lib/utils/envelope-download';
|
||||
@@ -25,9 +26,9 @@ import { DocumentDistributionMethod, DocumentStatus } from '@prisma/client';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useNavigate, useSearchParams } from 'react-router';
|
||||
import { z } from 'zod';
|
||||
|
||||
import PDFViewerLazy from '~/components/general/pdf-viewer/pdf-viewer-lazy';
|
||||
import { useCurrentTeam } from '~/providers/team';
|
||||
import { getDistributeErrorMessage } from '~/utils/toast-error-messages';
|
||||
|
||||
export type DocumentEditFormProps = {
|
||||
className?: string;
|
||||
@@ -387,9 +388,12 @@ export const DocumentEditForm = ({ className, initialDocument, documentRootPath
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
const error = AppError.parseError(err);
|
||||
const errorMessage = getDistributeErrorMessage(error.code);
|
||||
|
||||
toast({
|
||||
title: _(msg`Error`),
|
||||
description: _(msg`An error occurred while sending the document.`),
|
||||
title: _(errorMessage.title),
|
||||
description: _(errorMessage.description),
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
|
||||
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
|
||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { AppError } from '@documenso/lib/errors/app-error';
|
||||
import { formatDocumentsPath, formatTemplatesPath } from '@documenso/lib/utils/teams';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import type { TCreateDocumentPayloadSchema } from '@documenso/trpc/server/document-router/create-document.types';
|
||||
@@ -20,9 +20,9 @@ import { EnvelopeType } from '@prisma/client';
|
||||
import { useMemo, useState } from 'react';
|
||||
import type { FileRejection } from 'react-dropzone';
|
||||
import { useNavigate, useParams } from 'react-router';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { useCurrentTeam } from '~/providers/team';
|
||||
import { getUploadErrorMessage } from '~/utils/toast-error-messages';
|
||||
|
||||
export type DocumentUploadButtonLegacyProps = {
|
||||
className?: string;
|
||||
@@ -130,30 +130,11 @@ export const DocumentUploadButtonLegacy = ({ className, type }: DocumentUploadBu
|
||||
|
||||
console.error(err);
|
||||
|
||||
const errorMessage = match(error.code)
|
||||
.with('INVALID_DOCUMENT_FILE', () => msg`You cannot upload encrypted PDFs.`)
|
||||
.with(
|
||||
AppErrorCode.LIMIT_EXCEEDED,
|
||||
() => msg`You have reached your document limit for this month. Please upgrade your plan.`,
|
||||
)
|
||||
.with(
|
||||
'ENVELOPE_ITEM_LIMIT_EXCEEDED',
|
||||
() => msg`You have reached the limit of the number of files per envelope.`,
|
||||
)
|
||||
.with('UNSUPPORTED_FILE_TYPE', () => msg`This file type isn't supported. Please upload a PDF or Word document.`)
|
||||
.with(
|
||||
'CONVERSION_SERVICE_UNAVAILABLE',
|
||||
() => msg`Document conversion is temporarily unavailable. Please try again shortly or upload a PDF.`,
|
||||
)
|
||||
.with(
|
||||
'CONVERSION_FAILED',
|
||||
() => msg`We couldn't convert this file. Please check it's a valid Word document or upload a PDF instead.`,
|
||||
)
|
||||
.otherwise(() => msg`An error occurred while uploading your document.`);
|
||||
const errorMessage = getUploadErrorMessage(error.code);
|
||||
|
||||
toast({
|
||||
title: _(msg`Error`),
|
||||
description: _(errorMessage),
|
||||
title: _(errorMessage.title),
|
||||
description: _(errorMessage.description),
|
||||
variant: 'destructive',
|
||||
duration: 7500,
|
||||
});
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
} from '@documenso/ui/components/recipient/recipient-autocomplete-input';
|
||||
import { RecipientRoleSelect } from '@documenso/ui/components/recipient/recipient-role-select';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@documenso/ui/primitives/card';
|
||||
import { Checkbox } from '@documenso/ui/primitives/checkbox';
|
||||
@@ -563,6 +564,9 @@ export const EnvelopeEditorRecipientForm = () => {
|
||||
}
|
||||
}, [formValues]);
|
||||
|
||||
const recipientCountLimit = organisation.organisationClaim.recipientCount;
|
||||
const isOverRecipientLimit = recipientCountLimit > 0 && signers.length > recipientCountLimit;
|
||||
|
||||
return (
|
||||
<Card backdropBlur={false} className="border">
|
||||
<CardHeader className="flex flex-row justify-between">
|
||||
@@ -627,6 +631,17 @@ export const EnvelopeEditorRecipientForm = () => {
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
{isOverRecipientLimit && (
|
||||
<Alert variant="destructive" className="mb-4">
|
||||
<AlertDescription>
|
||||
<Trans>
|
||||
This envelope cannot have more than {recipientCountLimit} recipients. Please contact support if you need
|
||||
more.
|
||||
</Trans>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Form {...form}>
|
||||
<div
|
||||
className={cn('-mt-2 mb-2 space-y-4 rounded-md bg-accent/50 p-4', {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT, IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
||||
import { getAllowedUploadMimeTypes } from '@documenso/lib/constants/document-conversion';
|
||||
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { AppError } from '@documenso/lib/errors/app-error';
|
||||
import { megabytesToBytes } from '@documenso/lib/universal/unit-convertions';
|
||||
import { formatDocumentsPath, formatTemplatesPath } from '@documenso/lib/utils/teams';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
@@ -20,9 +20,9 @@ import { Loader } from 'lucide-react';
|
||||
import { type ReactNode, useState } from 'react';
|
||||
import { ErrorCode as DropzoneErrorCode, type FileRejection, useDropzone } from 'react-dropzone';
|
||||
import { Link, useNavigate, useParams } from 'react-router';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { useCurrentTeam } from '~/providers/team';
|
||||
import { getUploadErrorMessage } from '~/utils/toast-error-messages';
|
||||
|
||||
export interface EnvelopeDropZoneWrapperProps {
|
||||
children: ReactNode;
|
||||
@@ -109,27 +109,11 @@ export const EnvelopeDropZoneWrapper = ({ children, type, className }: EnvelopeD
|
||||
} catch (err) {
|
||||
const error = AppError.parseError(err);
|
||||
|
||||
const errorMessage = match(error.code)
|
||||
.with('INVALID_DOCUMENT_FILE', () => t`You cannot upload encrypted PDFs.`)
|
||||
.with(
|
||||
AppErrorCode.LIMIT_EXCEEDED,
|
||||
() => t`You have reached your document limit for this month. Please upgrade your plan.`,
|
||||
)
|
||||
.with('ENVELOPE_ITEM_LIMIT_EXCEEDED', () => t`You have reached the limit of the number of files per envelope.`)
|
||||
.with('UNSUPPORTED_FILE_TYPE', () => t`This file type isn't supported. Please upload a PDF or Word document.`)
|
||||
.with(
|
||||
'CONVERSION_SERVICE_UNAVAILABLE',
|
||||
() => t`Document conversion is temporarily unavailable. Please try again shortly or upload a PDF.`,
|
||||
)
|
||||
.with(
|
||||
'CONVERSION_FAILED',
|
||||
() => t`We couldn't convert this file. Please check it's a valid Word document or upload a PDF instead.`,
|
||||
)
|
||||
.otherwise(() => t`An error occurred during upload.`);
|
||||
const errorMessage = getUploadErrorMessage(error.code);
|
||||
|
||||
toast({
|
||||
title: t`Error`,
|
||||
description: errorMessage,
|
||||
title: i18n._(errorMessage.title),
|
||||
description: i18n._(errorMessage.description),
|
||||
variant: 'destructive',
|
||||
duration: 7500,
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
|
||||
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
|
||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||
import { TIME_ZONES } from '@documenso/lib/constants/time-zones';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { AppError } from '@documenso/lib/errors/app-error';
|
||||
import { formatDocumentsPath, formatTemplatesPath } from '@documenso/lib/utils/teams';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import type { TCreateEnvelopePayload } from '@documenso/trpc/server/envelope-router/create-envelope.types';
|
||||
@@ -17,9 +17,9 @@ import { EnvelopeType } from '@prisma/client';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { ErrorCode as DropzoneErrorCode, type FileRejection } from 'react-dropzone';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { useCurrentTeam } from '~/providers/team';
|
||||
import { getUploadErrorMessage } from '~/utils/toast-error-messages';
|
||||
|
||||
export type EnvelopeUploadButtonProps = {
|
||||
className?: string;
|
||||
@@ -112,27 +112,11 @@ export const EnvelopeUploadButton = ({ className, type, folderId }: EnvelopeUplo
|
||||
|
||||
console.error(err);
|
||||
|
||||
const errorMessage = match(error.code)
|
||||
.with('INVALID_DOCUMENT_FILE', () => t`You cannot upload encrypted PDFs.`)
|
||||
.with(
|
||||
AppErrorCode.LIMIT_EXCEEDED,
|
||||
() => t`You have reached your document limit for this month. Please upgrade your plan.`,
|
||||
)
|
||||
.with('ENVELOPE_ITEM_LIMIT_EXCEEDED', () => t`You have reached the limit of the number of files per envelope.`)
|
||||
.with('UNSUPPORTED_FILE_TYPE', () => t`This file type isn't supported. Please upload a PDF or Word document.`)
|
||||
.with(
|
||||
'CONVERSION_SERVICE_UNAVAILABLE',
|
||||
() => t`Document conversion is temporarily unavailable. Please try again shortly or upload a PDF.`,
|
||||
)
|
||||
.with(
|
||||
'CONVERSION_FAILED',
|
||||
() => t`We couldn't convert this file. Please check it's a valid Word document or upload a PDF instead.`,
|
||||
)
|
||||
.otherwise(() => t`An error occurred while uploading your document.`);
|
||||
const errorMessage = getUploadErrorMessage(error.code);
|
||||
|
||||
toast({
|
||||
title: t`Error`,
|
||||
description: errorMessage,
|
||||
title: i18n._(errorMessage.title),
|
||||
description: i18n._(errorMessage.description),
|
||||
variant: 'destructive',
|
||||
duration: 7500,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
import { useOptionalCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
|
||||
import { SUPPORT_EMAIL } from '@documenso/lib/constants/app';
|
||||
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION, SKIP_QUERY_BATCH_META } from '@documenso/lib/constants/trpc';
|
||||
import { INTERNAL_CLAIM_ID } from '@documenso/lib/types/subscription';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@documenso/ui/primitives/dialog';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { AlertTriangle } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
|
||||
export const OrganisationQuotaBanner = () => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const organisation = useOptionalCurrentOrganisation();
|
||||
|
||||
const { data: quotaFlags } = trpc.organisation.getQuotaFlags.useQuery(
|
||||
{ organisationId: organisation?.id ?? '' },
|
||||
{
|
||||
enabled: Boolean(organisation),
|
||||
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||
...SKIP_QUERY_BATCH_META,
|
||||
refetchInterval: 1000 * 60,
|
||||
refetchIntervalInBackground: false,
|
||||
},
|
||||
);
|
||||
|
||||
const isAnyQuotaExceeded = Boolean(
|
||||
quotaFlags?.isDocumentQuotaExceeded || quotaFlags?.isEmailQuotaExceeded || quotaFlags?.isApiQuotaExceeded,
|
||||
);
|
||||
|
||||
// Every member of the organisation sees the banner when a quota is exhausted.
|
||||
// Note: Skipping free plan banner for now because their quota can incorrectly show as exceeded.
|
||||
if (
|
||||
!organisation ||
|
||||
!quotaFlags ||
|
||||
!isAnyQuotaExceeded ||
|
||||
organisation.organisationClaim.originalSubscriptionClaimId === INTERNAL_CLAIM_ID.FREE
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="bg-yellow-200 text-yellow-900 dark:bg-yellow-400">
|
||||
<div className="mx-auto flex max-w-screen-xl items-center justify-center gap-x-4 px-4 py-2 font-medium text-sm">
|
||||
<div className="flex items-center">
|
||||
<AlertTriangle className="mr-2.5 h-5 w-5" />
|
||||
|
||||
<Trans>Your organisation has exceeded a fair use limit</Trans>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
className="text-yellow-900 hover:bg-yellow-100 dark:hover:bg-yellow-500"
|
||||
onClick={() => setIsOpen(true)}
|
||||
size="sm"
|
||||
>
|
||||
<Trans>Learn more</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<Trans>Fair use limit exceeded</Trans>
|
||||
</DialogTitle>
|
||||
|
||||
<DialogDescription>
|
||||
<Trans>
|
||||
Your organisation has exceeded a fair use limit. Please contact{' '}
|
||||
<a className="text-primary" href={`mailto:${SUPPORT_EMAIL}`}>
|
||||
support
|
||||
</a>{' '}
|
||||
to review your plan's limits.
|
||||
</Trans>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<Alert variant="neutral">
|
||||
<AlertDescription>
|
||||
<ul className="list-inside list-disc text-sm">
|
||||
{quotaFlags.isDocumentQuotaExceeded && (
|
||||
<li className="list-disc">
|
||||
<Trans>Document creation has been temporarily paused</Trans>
|
||||
</li>
|
||||
)}
|
||||
{quotaFlags.isEmailQuotaExceeded && (
|
||||
<li className="list-disc">
|
||||
<Trans>Email sending has been temporarily paused</Trans>
|
||||
</li>
|
||||
)}
|
||||
{quotaFlags.isApiQuotaExceeded && (
|
||||
<li className="list-disc">
|
||||
<Trans>API requests have been temporarily paused</Trans>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button variant="secondary">
|
||||
<Trans>Close</Trans>
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -13,6 +13,7 @@ import { AppBanner } from '~/components/general/app-banner';
|
||||
import { Header } from '~/components/general/app-header';
|
||||
import { GenericErrorLayout } from '~/components/general/generic-error-layout';
|
||||
import { OrganisationBillingBanner } from '~/components/general/organisations/organisation-billing-banner';
|
||||
import { OrganisationQuotaBanner } from '~/components/general/organisations/organisation-quota-banner';
|
||||
import { VerifyEmailBanner } from '~/components/general/verify-email-banner';
|
||||
import { TeamProvider } from '~/providers/team';
|
||||
|
||||
@@ -109,6 +110,8 @@ export default function Layout({ loaderData, params, matches }: Route.ComponentP
|
||||
<TeamProvider team={currentTeam || null}>
|
||||
<OrganisationBillingBanner />
|
||||
|
||||
<OrganisationQuotaBanner />
|
||||
|
||||
{!user.emailVerified && <VerifyEmailBanner email={user.email} />}
|
||||
|
||||
{banner && !hideHeader && <AppBanner banner={banner} />}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import { AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import type { MessageDescriptor } from '@lingui/core';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
export type ToastMessageDescriptor = {
|
||||
title: MessageDescriptor;
|
||||
description: MessageDescriptor;
|
||||
};
|
||||
|
||||
export const RECIPIENT_LIMIT_EXCEEDED_ERROR_MESSAGE = {
|
||||
title: msg`Too many recipients`,
|
||||
description: msg`This document has too many recipients. Please remove some recipients or contact support if you need more.`,
|
||||
};
|
||||
|
||||
export const FAIR_USE_LIMIT_EXCEEDED_ERROR_MESSAGE = {
|
||||
title: msg`Fair use limit exceeded`,
|
||||
description: msg`Your organisation has reached its plan's fair use limit. Please contact your organisation administrator or support to continue.`,
|
||||
};
|
||||
|
||||
export const getDistributeErrorMessage = (code: string): ToastMessageDescriptor => {
|
||||
return match(code)
|
||||
.with('RECIPIENT_LIMIT_EXCEEDED', () => RECIPIENT_LIMIT_EXCEEDED_ERROR_MESSAGE)
|
||||
.with(AppErrorCode.TOO_MANY_REQUESTS, () => FAIR_USE_LIMIT_EXCEEDED_ERROR_MESSAGE)
|
||||
.otherwise(() => ({
|
||||
title: msg`Something went wrong`,
|
||||
description: msg`An error occurred while distributing the document.`,
|
||||
}));
|
||||
};
|
||||
|
||||
export const getDirectTemplateErrorMessage = (code: string): ToastMessageDescriptor => {
|
||||
return match(code)
|
||||
.with('RECIPIENT_LIMIT_EXCEEDED', () => RECIPIENT_LIMIT_EXCEEDED_ERROR_MESSAGE)
|
||||
.with(AppErrorCode.TOO_MANY_REQUESTS, () => FAIR_USE_LIMIT_EXCEEDED_ERROR_MESSAGE)
|
||||
.otherwise(() => ({
|
||||
title: msg`Something went wrong`,
|
||||
description: msg`We were unable to submit this document at this time. Please try again later.`,
|
||||
}));
|
||||
};
|
||||
|
||||
export const getUploadErrorMessage = (code: string): ToastMessageDescriptor => {
|
||||
return match(code)
|
||||
.with(AppErrorCode.TOO_MANY_REQUESTS, () => FAIR_USE_LIMIT_EXCEEDED_ERROR_MESSAGE)
|
||||
.with('INVALID_DOCUMENT_FILE', () => ({
|
||||
title: msg`Error`,
|
||||
description: msg`You cannot upload encrypted PDFs.`,
|
||||
}))
|
||||
.with(AppErrorCode.LIMIT_EXCEEDED, () => ({
|
||||
title: msg`Error`,
|
||||
description: msg`You have reached your document limit for this month. Please upgrade your plan.`,
|
||||
}))
|
||||
.with('ENVELOPE_ITEM_LIMIT_EXCEEDED', () => ({
|
||||
title: msg`Error`,
|
||||
description: msg`You have reached the limit of the number of files per envelope.`,
|
||||
}))
|
||||
.with('UNSUPPORTED_FILE_TYPE', () => ({
|
||||
title: msg`Error`,
|
||||
description: msg`This file type isn't supported. Please upload a PDF or Word document.`,
|
||||
}))
|
||||
.with('CONVERSION_SERVICE_UNAVAILABLE', () => ({
|
||||
title: msg`Error`,
|
||||
description: msg`Document conversion is temporarily unavailable. Please try again shortly or upload a PDF.`,
|
||||
}))
|
||||
.with('CONVERSION_FAILED', () => ({
|
||||
title: msg`Error`,
|
||||
description: msg`We couldn't convert this file. Please check it's a valid Word document or upload a PDF instead.`,
|
||||
}))
|
||||
.otherwise(() => ({
|
||||
title: msg`Error`,
|
||||
description: msg`An error occurred while uploading your document.`,
|
||||
}));
|
||||
};
|
||||
|
||||
export const getTemplateUseErrorMessage = (code: string): ToastMessageDescriptor => {
|
||||
return match(code)
|
||||
.with('DOCUMENT_SEND_FAILED', () => ({
|
||||
title: msg`Error`,
|
||||
description: msg`The document was created but could not be sent to recipients.`,
|
||||
}))
|
||||
.with(AppErrorCode.INVALID_BODY, AppErrorCode.INVALID_REQUEST, () => ({
|
||||
title: msg`Error`,
|
||||
description: msg`The document could not be created because of missing or invalid information. Please review the template's recipients and fields.`,
|
||||
}))
|
||||
.with(AppErrorCode.NOT_FOUND, () => ({
|
||||
title: msg`Error`,
|
||||
description: msg`The template or one of its recipients could not be found.`,
|
||||
}))
|
||||
.with(AppErrorCode.LIMIT_EXCEEDED, () => ({
|
||||
title: msg`Error`,
|
||||
description: msg`You have reached your document limit for this plan. Please upgrade your plan.`,
|
||||
}))
|
||||
.with(AppErrorCode.TOO_MANY_REQUESTS, () => FAIR_USE_LIMIT_EXCEEDED_ERROR_MESSAGE)
|
||||
.otherwise(() => ({
|
||||
title: msg`Error`,
|
||||
description: msg`An error occurred while creating document from template.`,
|
||||
}));
|
||||
};
|
||||
Reference in New Issue
Block a user