diff --git a/apps/remix/app/components/dialogs/document-duplicate-dialog.tsx b/apps/remix/app/components/dialogs/document-duplicate-dialog.tsx index 81ec5bdf4..21e788ffa 100644 --- a/apps/remix/app/components/dialogs/document-duplicate-dialog.tsx +++ b/apps/remix/app/components/dialogs/document-duplicate-dialog.tsx @@ -19,13 +19,15 @@ import { useToast } from '@documenso/ui/primitives/use-toast'; import { useCurrentTeam } from '~/providers/team'; type DocumentDuplicateDialogProps = { - id: number; + id: string; + token?: string; open: boolean; onOpenChange: (_open: boolean) => void; }; export const DocumentDuplicateDialog = ({ id, + token, open, onOpenChange, }: DocumentDuplicateDialogProps) => { @@ -36,27 +38,23 @@ export const DocumentDuplicateDialog = ({ const team = useCurrentTeam(); - const { data: document, isLoading } = trpcReact.document.get.useQuery( - { - documentId: id, - }, - { - queryHash: `document-duplicate-dialog-${id}`, - enabled: open === true, - }, - ); + const { data: envelopeItemsPayload, isLoading: isLoadingEnvelopeItems } = + trpcReact.envelope.item.getManyByToken.useQuery( + { + envelopeId: id, + access: token ? { type: 'recipient', token } : { type: 'user' }, + }, + { + enabled: open, + }, + ); - const documentData = document?.documentData - ? { - ...document.documentData, - data: document.documentData.initialData, - } - : undefined; + const envelopeItems = envelopeItemsPayload?.data || []; const documentsPath = formatDocumentsPath(team.url); - const { mutateAsync: duplicateDocument, isPending: isDuplicateLoading } = - trpcReact.document.duplicate.useMutation({ + const { mutateAsync: duplicateEnvelope, isPending: isDuplicating } = + trpcReact.envelope.duplicate.useMutation({ onSuccess: async ({ id }) => { toast({ title: _(msg`Document Duplicated`), @@ -71,7 +69,7 @@ export const DocumentDuplicateDialog = ({ const onDuplicate = async () => { try { - await duplicateDocument({ documentId: id }); + await duplicateEnvelope({ envelopeId: id }); } catch { toast({ title: _(msg`Something went wrong`), @@ -83,14 +81,14 @@ export const DocumentDuplicateDialog = ({ }; return ( - !isLoading && onOpenChange(value)}> + !isDuplicating && onOpenChange(value)}> Duplicate - {!documentData || isLoading ? ( + {isLoadingEnvelopeItems || !envelopeItems || envelopeItems.length === 0 ? (

Loading Document... @@ -98,7 +96,12 @@ export const DocumentDuplicateDialog = ({

) : (
- +
)} @@ -115,8 +118,8 @@ export const DocumentDuplicateDialog = ({ {envelopeStatus === DocumentStatus.COMPLETED && ( @@ -206,7 +190,7 @@ export const EnvelopeDownloadDialog = ({ {!isDownloadingState[generateDownloadKey(item.id, 'signed')] && ( )} - Signed + Signed )} diff --git a/apps/remix/app/components/dialogs/envelope-duplicate-dialog.tsx b/apps/remix/app/components/dialogs/envelope-duplicate-dialog.tsx index 92d55b71b..b327a1549 100644 --- a/apps/remix/app/components/dialogs/envelope-duplicate-dialog.tsx +++ b/apps/remix/app/components/dialogs/envelope-duplicate-dialog.tsx @@ -43,7 +43,7 @@ export const EnvelopeDuplicateDialog = ({ const { mutateAsync: duplicateEnvelope, isPending: isDuplicating } = trpc.envelope.duplicate.useMutation({ - onSuccess: async ({ duplicatedEnvelopeId }) => { + onSuccess: async ({ id }) => { toast({ title: t`Envelope Duplicated`, description: t`Your envelope has been successfully duplicated.`, @@ -55,7 +55,7 @@ export const EnvelopeDuplicateDialog = ({ ? formatDocumentsPath(team.url) : formatTemplatesPath(team.url); - await navigate(`${path}/${duplicatedEnvelopeId}/edit`); + await navigate(`${path}/${id}/edit`); setOpen(false); }, }); diff --git a/apps/remix/app/components/dialogs/organisation-member-invite-dialog.tsx b/apps/remix/app/components/dialogs/organisation-member-invite-dialog.tsx index adcb65ab8..acc52ca08 100644 --- a/apps/remix/app/components/dialogs/organisation-member-invite-dialog.tsx +++ b/apps/remix/app/components/dialogs/organisation-member-invite-dialog.tsx @@ -185,6 +185,10 @@ export const OrganisationMemberInviteDialog = ({ return 'form'; } + if (fullOrganisation.members.length < fullOrganisation.organisationClaim.memberCount) { + return 'form'; + } + // This is probably going to screw us over in the future. if (fullOrganisation.organisationClaim.originalSubscriptionClaimId !== INTERNAL_CLAIM_ID.TEAM) { return 'alert'; diff --git a/apps/remix/app/components/dialogs/passkey-create-dialog.tsx b/apps/remix/app/components/dialogs/passkey-create-dialog.tsx index 8bfcbe3e5..0fa2ccbba 100644 --- a/apps/remix/app/components/dialogs/passkey-create-dialog.tsx +++ b/apps/remix/app/components/dialogs/passkey-create-dialog.tsx @@ -3,7 +3,7 @@ import { useEffect, useState } from 'react'; import { zodResolver } from '@hookform/resolvers/zod'; import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react'; -import { Trans } from '@lingui/react/macro'; +import { Plural, Trans } from '@lingui/react/macro'; import type * as DialogPrimitive from '@radix-ui/react-dialog'; import { startRegistration } from '@simplewebauthn/browser'; import { KeyRoundIcon } from 'lucide-react'; @@ -209,7 +209,11 @@ export const PasskeyCreateDialog = ({ trigger, onSuccess, ...props }: PasskeyCre )) .with('TOO_MANY_PASSKEYS', () => ( - You cannot have more than {MAXIMUM_PASSKEYS} passkeys. + )) .with('InvalidStateError', () => ( diff --git a/apps/remix/app/components/dialogs/sign-field-number-dialog.tsx b/apps/remix/app/components/dialogs/sign-field-number-dialog.tsx index 184c3e563..c1bed70f3 100644 --- a/apps/remix/app/components/dialogs/sign-field-number-dialog.tsx +++ b/apps/remix/app/components/dialogs/sign-field-number-dialog.tsx @@ -1,5 +1,4 @@ import { zodResolver } from '@hookform/resolvers/zod'; -import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react/macro'; import { Trans } from '@lingui/react/macro'; import { createCallable } from 'react-call'; @@ -28,49 +27,71 @@ import { } from '@documenso/ui/primitives/form/form'; import { Input } from '@documenso/ui/primitives/input'; -const createNumberFieldSchema = (fieldMeta: TNumberFieldMeta) => { - let schema = z.coerce.number({ - invalid_type_error: msg`Please enter a valid number`.id, - }); - - const { numberFormat, minValue, maxValue } = fieldMeta; - - if (typeof minValue === 'number') { - schema = schema.min(minValue); - } - - if (typeof maxValue === 'number') { - schema = schema.max(maxValue); - } - - if (numberFormat) { - const foundRegex = numberFormatValues.find((item) => item.value === numberFormat)?.regex; - - if (!foundRegex) { - return schema; - } - - return schema.refine( - (value) => { - return foundRegex.test(value.toString()); - }, - { - message: msg`Number needs to be formatted as ${numberFormat}`.id, - }, - ); - } - - return schema; -}; - export type SignFieldNumberDialogProps = { fieldMeta: TNumberFieldMeta; }; -export const SignFieldNumberDialog = createCallable( +export const SignFieldNumberDialog = createCallable( ({ call, fieldMeta }) => { const { t } = useLingui(); + // Needs to be inside dialog for translation purposes. + const createNumberFieldSchema = (fieldMeta: TNumberFieldMeta) => { + const { numberFormat, minValue, maxValue } = fieldMeta; + + if (numberFormat) { + const foundRegex = numberFormatValues.find((item) => item.value === numberFormat)?.regex; + + if (foundRegex) { + return z.string().refine( + (value) => { + return foundRegex.test(value.toString()); + }, + { + message: t`Number needs to be formatted as ${numberFormat}`, + }, + ); + } + } + + // Not gong to work with min/max numbers + number format + // Since currently doesn't work in V1 going to ignore for now. + return z.string().superRefine((value, ctx) => { + const isValidNumber = /^[0-9,.]+$/.test(value.toString()); + + if (!isValidNumber) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: t`Please enter a valid number`, + }); + + return; + } + + if (typeof minValue === 'number' && parseFloat(value) < minValue) { + ctx.addIssue({ + code: z.ZodIssueCode.too_small, + minimum: minValue, + inclusive: true, + type: 'number', + }); + + return; + } + + if (typeof maxValue === 'number' && parseFloat(value) > maxValue) { + ctx.addIssue({ + code: z.ZodIssueCode.too_big, + maximum: maxValue, + inclusive: true, + type: 'number', + }); + + return; + } + }); + }; + const ZSignFieldNumberFormSchema = z.object({ number: createNumberFieldSchema(fieldMeta), }); diff --git a/apps/remix/app/components/dialogs/template-create-dialog.tsx b/apps/remix/app/components/dialogs/template-create-dialog.tsx index 76d4e0c8a..676537e4c 100644 --- a/apps/remix/app/components/dialogs/template-create-dialog.tsx +++ b/apps/remix/app/components/dialogs/template-create-dialog.tsx @@ -7,9 +7,9 @@ import { FilePlus, Loader } from 'lucide-react'; import { useNavigate } from 'react-router'; import { useSession } from '@documenso/lib/client-only/providers/session'; -import { putPdfFile } from '@documenso/lib/universal/upload/put-file'; import { formatTemplatesPath } from '@documenso/lib/utils/teams'; import { trpc } from '@documenso/trpc/react'; +import type { TCreateTemplatePayloadSchema } from '@documenso/trpc/server/template-router/schema'; import { Button } from '@documenso/ui/primitives/button'; import { Dialog, @@ -54,13 +54,17 @@ export const TemplateCreateDialog = ({ folderId }: TemplateCreateDialogProps) => setIsUploadingFile(true); try { - const response = await putPdfFile(file); - - const { legacyTemplateId: id } = await createTemplate({ + const payload = { title: file.name, - templateDocumentDataId: response.id, folderId: folderId, - }); + } satisfies TCreateTemplatePayloadSchema; + + const formData = new FormData(); + + formData.append('payload', JSON.stringify(payload)); + formData.append('file', file); + + const { envelopeId: id } = await createTemplate(formData); toast({ title: _(msg`Template document uploaded`), @@ -92,7 +96,7 @@ export const TemplateCreateDialog = ({ folderId }: TemplateCreateDialogProps) => diff --git a/apps/remix/app/components/dialogs/template-direct-link-dialog.tsx b/apps/remix/app/components/dialogs/template-direct-link-dialog.tsx index f69d310be..4e267b0e5 100644 --- a/apps/remix/app/components/dialogs/template-direct-link-dialog.tsx +++ b/apps/remix/app/components/dialogs/template-direct-link-dialog.tsx @@ -265,7 +265,7 @@ export const TemplateDirectLinkDialog = ({ {remaining.directTemplates !== 0 && ( )} diff --git a/apps/remix/app/components/dialogs/template-use-dialog.tsx b/apps/remix/app/components/dialogs/template-use-dialog.tsx index e5a438b3f..129132dd4 100644 --- a/apps/remix/app/components/dialogs/template-use-dialog.tsx +++ b/apps/remix/app/components/dialogs/template-use-dialog.tsx @@ -143,7 +143,7 @@ export function TemplateUseDialog({ }, ); - const envelopeItems = response?.envelopeItems ?? []; + const envelopeItems = response?.data ?? []; const { mutateAsync: createDocumentFromTemplate } = trpc.template.createDocumentFromTemplate.useMutation(); diff --git a/apps/remix/app/components/embed/authoring/configure-fields-view.tsx b/apps/remix/app/components/embed/authoring/configure-fields-view.tsx index 704109a90..aac5f6fc9 100644 --- a/apps/remix/app/components/embed/authoring/configure-fields-view.tsx +++ b/apps/remix/app/components/embed/authoring/configure-fields-view.tsx @@ -4,6 +4,7 @@ import { useLingui } from '@lingui/react'; import { Trans } from '@lingui/react/macro'; import type { DocumentData, FieldType } from '@prisma/client'; import { ReadStatus, type Recipient, SendStatus, SigningStatus } from '@prisma/client'; +import { base64 } from '@scure/base'; import { ChevronsUpDown } from 'lucide-react'; import { useFieldArray, useForm } from 'react-hook-form'; import { useHotkeys } from 'react-hotkeys-hook'; @@ -12,7 +13,6 @@ import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-c import { useDocumentElement } from '@documenso/lib/client-only/hooks/use-document-element'; import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer'; import { type TFieldMetaSchema, ZFieldMetaSchema } from '@documenso/lib/types/field-meta'; -import { base64 } from '@documenso/lib/universal/base64'; import { nanoid } from '@documenso/lib/universal/id'; import { ADVANCED_FIELD_TYPES_WITH_OPTIONAL_SETTING } from '@documenso/lib/utils/advanced-fields-helpers'; import { useRecipientColors } from '@documenso/ui/lib/recipient-colors'; @@ -83,21 +83,14 @@ export const ConfigureFieldsView = ({ const normalizedDocumentData = useMemo(() => { if (documentData) { - return documentData; + return documentData.data; } if (!configData.documentData) { return null; } - const data = base64.encode(configData.documentData?.data); - - return { - id: 'preview', - type: 'BYTES_64', - data, - initialData: data, - } satisfies DocumentData; + return base64.encode(configData.documentData.data); }, [configData.documentData]); const recipients = useMemo(() => { @@ -541,7 +534,15 @@ export const ConfigureFieldsView = ({
{normalizedDocumentData && (
- + { @@ -37,6 +39,7 @@ export const EmbedAuthenticationRequired = ({ []; recipient: Recipient; fields: Field[]; metadata?: DocumentMeta | null; @@ -59,7 +59,7 @@ export const EmbedDirectTemplateClientPage = ({ token, envelopeId, updatedAt, - documentData, + envelopeItems, recipient, fields, metadata, @@ -335,7 +335,9 @@ export const EmbedDirectTemplateClientPage = ({ {/* Viewer */}
setHasDocumentLoaded(true)} />
diff --git a/apps/remix/app/components/embed/embed-document-signing-page.tsx b/apps/remix/app/components/embed/embed-document-signing-page-v1.tsx similarity index 97% rename from apps/remix/app/components/embed/embed-document-signing-page.tsx rename to apps/remix/app/components/embed/embed-document-signing-page-v1.tsx index 329a38446..59bd442ec 100644 --- a/apps/remix/app/components/embed/embed-document-signing-page.tsx +++ b/apps/remix/app/components/embed/embed-document-signing-page-v1.tsx @@ -3,14 +3,8 @@ import { useEffect, useId, useLayoutEffect, useMemo, useState } from 'react'; import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react'; import { Trans } from '@lingui/react/macro'; -import type { DocumentMeta } from '@prisma/client'; -import { - type DocumentData, - type Field, - FieldType, - RecipientRole, - SigningStatus, -} from '@prisma/client'; +import type { DocumentMeta, EnvelopeItem } from '@prisma/client'; +import { type Field, FieldType, RecipientRole, SigningStatus } from '@prisma/client'; import { LucideChevronDown, LucideChevronUp } from 'lucide-react'; import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn'; @@ -46,11 +40,11 @@ import { EmbedDocumentCompleted } from './embed-document-completed'; import { EmbedDocumentFields } from './embed-document-fields'; import { EmbedDocumentRejected } from './embed-document-rejected'; -export type EmbedSignDocumentClientPageProps = { +export type EmbedSignDocumentV1ClientPageProps = { token: string; documentId: number; envelopeId: string; - documentData: DocumentData; + envelopeItems: Pick[]; recipient: RecipientWithFields; fields: Field[]; completedFields: DocumentField[]; @@ -61,11 +55,11 @@ export type EmbedSignDocumentClientPageProps = { allRecipients?: RecipientWithFields[]; }; -export const EmbedSignDocumentClientPage = ({ +export const EmbedSignDocumentV1ClientPage = ({ token, documentId, envelopeId, - documentData, + envelopeItems, recipient, fields, completedFields, @@ -74,7 +68,7 @@ export const EmbedSignDocumentClientPage = ({ hidePoweredBy = false, allowWhitelabelling = false, allRecipients = [], -}: EmbedSignDocumentClientPageProps) => { +}: EmbedSignDocumentV1ClientPageProps) => { const { _ } = useLingui(); const { toast } = useToast(); @@ -293,7 +287,9 @@ export const EmbedSignDocumentClientPage = ({ {/* Viewer */}
setHasDocumentLoaded(true)} />
diff --git a/apps/remix/app/components/embed/embed-document-signing-page-v2.tsx b/apps/remix/app/components/embed/embed-document-signing-page-v2.tsx new file mode 100644 index 000000000..a6069e286 --- /dev/null +++ b/apps/remix/app/components/embed/embed-document-signing-page-v2.tsx @@ -0,0 +1,232 @@ +import { useEffect, useLayoutEffect, useState } from 'react'; + +import { useLingui } from '@lingui/react'; + +import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope'; + +import { ZSignDocumentEmbedDataSchema } from '~/types/embed-document-sign-schema'; +import { injectCss } from '~/utils/css-vars'; + +import { DocumentSigningPageViewV2 } from '../general/document-signing/document-signing-page-view-v2'; +import { useRequiredEnvelopeSigningContext } from '../general/document-signing/envelope-signing-provider'; +import { EmbedClientLoading } from './embed-client-loading'; +import { EmbedDocumentCompleted } from './embed-document-completed'; +import { EmbedDocumentRejected } from './embed-document-rejected'; +import { EmbedSigningProvider } from './embed-signing-context'; + +export type EmbedSignDocumentV2ClientPageProps = { + hidePoweredBy?: boolean; + allowWhitelabelling?: boolean; +}; + +export const EmbedSignDocumentV2ClientPage = ({ + hidePoweredBy = false, + allowWhitelabelling = false, +}: EmbedSignDocumentV2ClientPageProps) => { + const { _ } = useLingui(); + + const { envelope, recipient, envelopeData, setFullName, fullName } = + useRequiredEnvelopeSigningContext(); + + const { isCompleted, isRejected, recipientSignature } = envelopeData; + + // !: Not used at the moment, may be removed in the future. + // const [hasDocumentLoaded, setHasDocumentLoaded] = useState(false); + const [hasFinishedInit, setHasFinishedInit] = useState(false); + const [allowDocumentRejection, setAllowDocumentRejection] = useState(false); + const [isNameLocked, setIsNameLocked] = useState(false); + + const onDocumentCompleted = (data: { + token: string; + documentId: number; + envelopeId: string; + recipientId: number; + }) => { + if (window.parent) { + window.parent.postMessage( + { + action: 'document-completed', + data, + }, + '*', + ); + } + }; + + const onDocumentError = () => { + if (window.parent) { + window.parent.postMessage( + { + action: 'document-error', + data: null, + }, + '*', + ); + } + }; + + const onDocumentReady = () => { + if (window.parent) { + window.parent.postMessage( + { + action: 'document-ready', + data: null, + }, + '*', + ); + } + }; + + const onFieldSigned = (data: { fieldId?: number; value?: string; isBase64?: boolean }) => { + if (window.parent) { + window.parent.postMessage( + { + action: 'field-signed', + data, + }, + '*', + ); + } + }; + + const onFieldUnsigned = (data: { fieldId?: number }) => { + if (window.parent) { + window.parent.postMessage( + { + action: 'field-unsigned', + data, + }, + '*', + ); + } + }; + + const onDocumentRejected = (data: { + token: string; + documentId: number; + envelopeId: string; + recipientId: number; + reason?: string; + }) => { + if (window.parent) { + window.parent.postMessage( + { + action: 'document-rejected', + data, + }, + '*', + ); + } + }; + + useLayoutEffect(() => { + const hash = window.location.hash.slice(1); + + try { + const data = ZSignDocumentEmbedDataSchema.parse(JSON.parse(decodeURIComponent(atob(hash)))); + + if (!isCompleted && data.name) { + setFullName(data.name); + } + + // Since a recipient can be provided a name we can lock it without requiring + // a to be provided by the parent application, unlike direct templates. + setIsNameLocked(!!data.lockName); + setAllowDocumentRejection(!!data.allowDocumentRejection); + + if (data.darkModeDisabled) { + document.documentElement.classList.add('dark-mode-disabled'); + } + + if (allowWhitelabelling) { + injectCss({ + css: data.css, + cssVars: data.cssVars, + }); + } + } catch (err) { + console.error(err); + } + + setHasFinishedInit(true); + + // !: While the setters are stable we still want to ensure we're avoiding + // !: re-renders. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [allowWhitelabelling]); + + useEffect(() => { + if (hasFinishedInit) { + onDocumentReady(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [hasFinishedInit]); + + // Listen for document completion events from the envelope signing context + useEffect(() => { + if (isCompleted) { + onDocumentCompleted({ + token: recipient.token, + envelopeId: envelope.id, + documentId: mapSecondaryIdToDocumentId(envelope.secondaryId), + recipientId: recipient.id, + }); + } + }, [isCompleted, envelope.id, recipient.id, recipient.token]); + + // Listen for document rejection events + useEffect(() => { + if (isRejected) { + onDocumentRejected({ + token: recipient.token, + envelopeId: envelope.id, + documentId: mapSecondaryIdToDocumentId(envelope.secondaryId), + recipientId: recipient.id, + }); + } + }, [isRejected, envelope.id, recipient.id, recipient.token]); + + if (isRejected) { + return ; + } + + if (isCompleted) { + return ( + + ); + } + + return ( + +
+ {!hasFinishedInit && } + + +
+
+ ); +}; diff --git a/apps/remix/app/components/embed/embed-signing-context.tsx b/apps/remix/app/components/embed/embed-signing-context.tsx new file mode 100644 index 000000000..408bdff49 --- /dev/null +++ b/apps/remix/app/components/embed/embed-signing-context.tsx @@ -0,0 +1,101 @@ +import { createContext, useContext } from 'react'; + +export type EmbedSigningContextValue = { + isEmbed: true; + allowDocumentRejection: boolean; + isNameLocked: boolean; + isEmailLocked: boolean; + hidePoweredBy: boolean; + onDocumentCompleted: (data: { + token: string; + documentId: number; + envelopeId: string; + recipientId: number; + }) => void; + onDocumentError: () => void; + onDocumentRejected: (data: { + token: string; + documentId: number; + envelopeId: string; + recipientId: number; + reason?: string; + }) => void; + onDocumentReady: () => void; + onFieldSigned: (data: { fieldId?: number; value?: string; isBase64?: boolean }) => void; + onFieldUnsigned: (data: { fieldId?: number }) => void; +}; + +const EmbedSigningContext = createContext(null); + +export const useEmbedSigningContext = () => { + return useContext(EmbedSigningContext); +}; + +export const useRequiredEmbedSigningContext = () => { + const context = useEmbedSigningContext(); + + if (!context) { + throw new Error('useRequiredEmbedSigningContext must be used within EmbedSigningProvider'); + } + + return context; +}; + +export type EmbedSigningProviderProps = { + allowDocumentRejection?: boolean; + isNameLocked?: boolean; + isEmailLocked?: boolean; + hidePoweredBy?: boolean; + onDocumentCompleted: (data: { + token: string; + documentId: number; + envelopeId: string; + recipientId: number; + }) => void; + onDocumentError: () => void; + onDocumentRejected: (data: { + token: string; + documentId: number; + envelopeId: string; + recipientId: number; + reason?: string; + }) => void; + onDocumentReady: () => void; + onFieldSigned: (data: { fieldId?: number; value?: string; isBase64?: boolean }) => void; + onFieldUnsigned: (data: { fieldId?: number }) => void; + children: React.ReactNode; +}; + +export const EmbedSigningProvider = ({ + allowDocumentRejection = false, + isNameLocked = false, + isEmailLocked = true, + hidePoweredBy = false, + onDocumentCompleted, + onDocumentError, + onDocumentRejected, + onDocumentReady, + onFieldSigned, + onFieldUnsigned, + children, +}: EmbedSigningProviderProps) => { + return ( + + {children} + + ); +}; diff --git a/apps/remix/app/components/embed/multisign/multi-sign-document-signing-view.tsx b/apps/remix/app/components/embed/multisign/multi-sign-document-signing-view.tsx index d74b88f2d..14c906704 100644 --- a/apps/remix/app/components/embed/multisign/multi-sign-document-signing-view.tsx +++ b/apps/remix/app/components/embed/multisign/multi-sign-document-signing-view.tsx @@ -226,7 +226,9 @@ export const MultiSignDocumentSigningView = ({ })} > { setHasDocumentLoaded(true); onDocumentReady?.(); diff --git a/apps/remix/app/components/filters/date-range-filter.tsx b/apps/remix/app/components/filters/date-range-filter.tsx new file mode 100644 index 000000000..aa79774bd --- /dev/null +++ b/apps/remix/app/components/filters/date-range-filter.tsx @@ -0,0 +1,49 @@ +import { useTransition } from 'react'; + +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; + +import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params'; +import type { DateRange } from '@documenso/lib/types/search-params'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@documenso/ui/primitives/select'; + +type DateRangeFilterProps = { + currentRange: DateRange; +}; + +export const DateRangeFilter = ({ currentRange }: DateRangeFilterProps) => { + const { _ } = useLingui(); + const [isPending, startTransition] = useTransition(); + const updateSearchParams = useUpdateSearchParams(); + + const handleRangeChange = (value: string) => { + startTransition(() => { + updateSearchParams({ + dateRange: value as DateRange, + page: 1, + }); + }); + }; + + return ( +
+ +
+ ); +}; diff --git a/apps/remix/app/components/forms/branding-preferences-form.tsx b/apps/remix/app/components/forms/branding-preferences-form.tsx index 85355a7b1..9c8bc05ce 100644 --- a/apps/remix/app/components/forms/branding-preferences-form.tsx +++ b/apps/remix/app/components/forms/branding-preferences-form.tsx @@ -1,14 +1,14 @@ import { useEffect, useState } from 'react'; import { zodResolver } from '@hookform/resolvers/zod'; -import { useLingui } from '@lingui/react/macro'; -import { Trans } from '@lingui/react/macro'; +import { Trans, useLingui } from '@lingui/react/macro'; import type { TeamGlobalSettings } from '@prisma/client'; import { Loader } from 'lucide-react'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; -import { getFile } from '@documenso/lib/universal/upload/get-file'; +import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation'; +import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; import { cn } from '@documenso/ui/lib/utils'; import { Button } from '@documenso/ui/primitives/button'; import { @@ -29,6 +29,8 @@ import { } from '@documenso/ui/primitives/select'; import { Textarea } from '@documenso/ui/primitives/textarea'; +import { useOptionalCurrentTeam } from '~/providers/team'; + const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB const ACCEPTED_FILE_TYPES = ['image/jpeg', 'image/png', 'image/webp']; @@ -68,6 +70,9 @@ export function BrandingPreferencesForm({ }: BrandingPreferencesFormProps) { const { t } = useLingui(); + const team = useOptionalCurrentTeam(); + const organisation = useCurrentOrganisation(); + const [previewUrl, setPreviewUrl] = useState(''); const [hasLoadedPreview, setHasLoadedPreview] = useState(false); @@ -88,14 +93,13 @@ export function BrandingPreferencesForm({ const file = JSON.parse(settings.brandingLogo); if ('type' in file && 'data' in file) { - void getFile(file).then((binaryData) => { - const objectUrl = URL.createObjectURL(new Blob([binaryData])); + const logoUrl = + context === 'Team' + ? `${NEXT_PUBLIC_WEBAPP_URL()}/api/branding/logo/team/${team?.id}` + : `${NEXT_PUBLIC_WEBAPP_URL()}/api/branding/logo/organisation/${organisation?.id}`; - setPreviewUrl(objectUrl); - setHasLoadedPreview(true); - }); - - return; + setPreviewUrl(logoUrl); + setHasLoadedPreview(true); } } diff --git a/apps/remix/app/components/forms/editor/editor-field-date-form.tsx b/apps/remix/app/components/forms/editor/editor-field-date-form.tsx index b313a261d..8abe1aec8 100644 --- a/apps/remix/app/components/forms/editor/editor-field-date-form.tsx +++ b/apps/remix/app/components/forms/editor/editor-field-date-form.tsx @@ -7,6 +7,7 @@ import type { z } from 'zod'; import { DEFAULT_FIELD_FONT_SIZE, type TDateFieldMeta as DateFieldMeta, + FIELD_DEFAULT_GENERIC_ALIGN, ZDateFieldMeta, } from '@documenso/lib/types/field-meta'; import { Form } from '@documenso/ui/primitives/form/form'; @@ -39,7 +40,7 @@ export const EditorFieldDateForm = ({ mode: 'onChange', defaultValues: { fontSize: value.fontSize || DEFAULT_FIELD_FONT_SIZE, - textAlign: value.textAlign || 'left', + textAlign: value.textAlign ?? FIELD_DEFAULT_GENERIC_ALIGN, }, }); diff --git a/apps/remix/app/components/forms/editor/editor-field-email-form.tsx b/apps/remix/app/components/forms/editor/editor-field-email-form.tsx index 5da10652d..c51f3c74f 100644 --- a/apps/remix/app/components/forms/editor/editor-field-email-form.tsx +++ b/apps/remix/app/components/forms/editor/editor-field-email-form.tsx @@ -7,6 +7,7 @@ import type { z } from 'zod'; import { DEFAULT_FIELD_FONT_SIZE, type TEmailFieldMeta as EmailFieldMeta, + FIELD_DEFAULT_GENERIC_ALIGN, ZEmailFieldMeta, } from '@documenso/lib/types/field-meta'; import { Form } from '@documenso/ui/primitives/form/form'; @@ -39,7 +40,7 @@ export const EditorFieldEmailForm = ({ mode: 'onChange', defaultValues: { fontSize: value.fontSize || DEFAULT_FIELD_FONT_SIZE, - textAlign: value.textAlign || 'left', + textAlign: value.textAlign ?? FIELD_DEFAULT_GENERIC_ALIGN, }, }); diff --git a/apps/remix/app/components/forms/editor/editor-field-generic-field-forms.tsx b/apps/remix/app/components/forms/editor/editor-field-generic-field-forms.tsx index 9e8fd0b7c..3ba8c434a 100644 --- a/apps/remix/app/components/forms/editor/editor-field-generic-field-forms.tsx +++ b/apps/remix/app/components/forms/editor/editor-field-generic-field-forms.tsx @@ -3,6 +3,10 @@ import { useEffect } from 'react'; import { Trans, useLingui } from '@lingui/react/macro'; import { type Control, useFormContext } from 'react-hook-form'; +import { FIELD_MIN_LINE_HEIGHT } from '@documenso/lib/types/field-meta'; +import { FIELD_MAX_LINE_HEIGHT } from '@documenso/lib/types/field-meta'; +import { FIELD_MIN_LETTER_SPACING } from '@documenso/lib/types/field-meta'; +import { FIELD_MAX_LETTER_SPACING } from '@documenso/lib/types/field-meta'; import { cn } from '@documenso/ui/lib/utils'; import { Checkbox } from '@documenso/ui/primitives/checkbox'; import { @@ -107,6 +111,119 @@ export const EditorGenericTextAlignField = ({ ); }; +export const EditorGenericVerticalAlignField = ({ + formControl, + className, +}: { + formControl: FormControlType; + className?: string; +}) => { + const { t } = useLingui(); + + return ( + ( + + + Vertical Align + + + + + + + )} + /> + ); +}; + +export const EditorGenericLineHeightField = ({ + formControl, + className, +}: { + formControl: FormControlType; + className?: string; +}) => { + const { t } = useLingui(); + + return ( + ( + + + Line Height + + + + + + + )} + /> + ); +}; + +export const EditorGenericLetterSpacingField = ({ + formControl, + className, +}: { + formControl: FormControlType; + className?: string; +}) => { + const { t } = useLingui(); + + return ( + ( + + + Letter Spacing + + + + + + + )} + /> + ); +}; + export const EditorGenericRequiredField = ({ formControl, className, diff --git a/apps/remix/app/components/forms/editor/editor-field-initials-form.tsx b/apps/remix/app/components/forms/editor/editor-field-initials-form.tsx index 2f54c9cd4..ebc478faf 100644 --- a/apps/remix/app/components/forms/editor/editor-field-initials-form.tsx +++ b/apps/remix/app/components/forms/editor/editor-field-initials-form.tsx @@ -6,6 +6,7 @@ import type { z } from 'zod'; import { DEFAULT_FIELD_FONT_SIZE, + FIELD_DEFAULT_GENERIC_ALIGN, type TInitialsFieldMeta as InitialsFieldMeta, ZInitialsFieldMeta, } from '@documenso/lib/types/field-meta'; @@ -39,7 +40,7 @@ export const EditorFieldInitialsForm = ({ mode: 'onChange', defaultValues: { fontSize: value.fontSize || DEFAULT_FIELD_FONT_SIZE, - textAlign: value.textAlign || 'left', + textAlign: value.textAlign ?? FIELD_DEFAULT_GENERIC_ALIGN, }, }); diff --git a/apps/remix/app/components/forms/editor/editor-field-name-form.tsx b/apps/remix/app/components/forms/editor/editor-field-name-form.tsx index 4c57ed917..9e5849dd8 100644 --- a/apps/remix/app/components/forms/editor/editor-field-name-form.tsx +++ b/apps/remix/app/components/forms/editor/editor-field-name-form.tsx @@ -6,6 +6,7 @@ import type { z } from 'zod'; import { DEFAULT_FIELD_FONT_SIZE, + FIELD_DEFAULT_GENERIC_ALIGN, type TNameFieldMeta as NameFieldMeta, ZNameFieldMeta, } from '@documenso/lib/types/field-meta'; @@ -39,7 +40,7 @@ export const EditorFieldNameForm = ({ mode: 'onChange', defaultValues: { fontSize: value.fontSize || DEFAULT_FIELD_FONT_SIZE, - textAlign: value.textAlign || 'left', + textAlign: value.textAlign ?? FIELD_DEFAULT_GENERIC_ALIGN, }, }); diff --git a/apps/remix/app/components/forms/editor/editor-field-number-form.tsx b/apps/remix/app/components/forms/editor/editor-field-number-form.tsx index 0871d8e53..bc6e7ae6a 100644 --- a/apps/remix/app/components/forms/editor/editor-field-number-form.tsx +++ b/apps/remix/app/components/forms/editor/editor-field-number-form.tsx @@ -6,6 +6,11 @@ import { useForm, useWatch } from 'react-hook-form'; import type { z } from 'zod'; import { + DEFAULT_FIELD_FONT_SIZE, + FIELD_DEFAULT_GENERIC_ALIGN, + FIELD_DEFAULT_GENERIC_VERTICAL_ALIGN, + FIELD_DEFAULT_LETTER_SPACING, + FIELD_DEFAULT_LINE_HEIGHT, type TNumberFieldMeta as NumberFieldMeta, ZNumberFieldMeta, } from '@documenso/lib/types/field-meta'; @@ -31,9 +36,12 @@ import { Separator } from '@documenso/ui/primitives/separator'; import { EditorGenericFontSizeField, EditorGenericLabelField, + EditorGenericLetterSpacingField, + EditorGenericLineHeightField, EditorGenericReadOnlyField, EditorGenericRequiredField, EditorGenericTextAlignField, + EditorGenericVerticalAlignField, } from './editor-field-generic-field-forms'; const ZNumberFieldFormSchema = ZNumberFieldMeta.pick({ @@ -43,6 +51,9 @@ const ZNumberFieldFormSchema = ZNumberFieldMeta.pick({ numberFormat: true, fontSize: true, textAlign: true, + lineHeight: true, + letterSpacing: true, + verticalAlign: true, required: true, readOnly: true, minValue: true, @@ -99,8 +110,11 @@ export const EditorFieldNumberForm = ({ placeholder: value.placeholder || '', value: value.value || '', numberFormat: value.numberFormat || null, - fontSize: value.fontSize || 14, - textAlign: value.textAlign || 'left', + fontSize: value.fontSize || DEFAULT_FIELD_FONT_SIZE, + textAlign: value.textAlign ?? FIELD_DEFAULT_GENERIC_ALIGN, + lineHeight: value.lineHeight ?? FIELD_DEFAULT_LINE_HEIGHT, + letterSpacing: value.letterSpacing ?? FIELD_DEFAULT_LETTER_SPACING, + verticalAlign: value.verticalAlign ?? FIELD_DEFAULT_GENERIC_VERTICAL_ALIGN, required: value.required || false, readOnly: value.readOnly || false, minValue: value.minValue, @@ -118,6 +132,10 @@ export const EditorFieldNumberForm = ({ useEffect(() => { const validatedFormValues = ZNumberFieldFormSchema.safeParse(formValues); + if (formValues.readOnly && !formValues.value) { + void form.trigger('value'); + } + if (validatedFormValues.success) { onValueChange({ type: 'number', @@ -130,10 +148,12 @@ export const EditorFieldNumberForm = ({
-
- + +
+ +
@@ -204,6 +224,12 @@ export const EditorFieldNumberForm = ({ )} /> +
+ + + +
+
diff --git a/apps/remix/app/components/forms/editor/editor-field-signature-form.tsx b/apps/remix/app/components/forms/editor/editor-field-signature-form.tsx index bf5d6ac26..2a1064751 100644 --- a/apps/remix/app/components/forms/editor/editor-field-signature-form.tsx +++ b/apps/remix/app/components/forms/editor/editor-field-signature-form.tsx @@ -5,11 +5,8 @@ import { Trans } from '@lingui/react/macro'; import { useForm, useWatch } from 'react-hook-form'; import type { z } from 'zod'; -import { - DEFAULT_FIELD_FONT_SIZE, - type TSignatureFieldMeta, - ZSignatureFieldMeta, -} from '@documenso/lib/types/field-meta'; +import { DEFAULT_SIGNATURE_TEXT_FONT_SIZE } from '@documenso/lib/constants/pdf'; +import { type TSignatureFieldMeta, ZSignatureFieldMeta } from '@documenso/lib/types/field-meta'; import { Form } from '@documenso/ui/primitives/form/form'; import { EditorGenericFontSizeField } from './editor-field-generic-field-forms'; @@ -35,7 +32,7 @@ export const EditorFieldSignatureForm = ({ resolver: zodResolver(ZSignatureFieldFormSchema), mode: 'onChange', defaultValues: { - fontSize: value.fontSize || DEFAULT_FIELD_FONT_SIZE, + fontSize: value.fontSize || DEFAULT_SIGNATURE_TEXT_FONT_SIZE, }, }); diff --git a/apps/remix/app/components/forms/editor/editor-field-text-form.tsx b/apps/remix/app/components/forms/editor/editor-field-text-form.tsx index c634d3a9c..7f23de986 100644 --- a/apps/remix/app/components/forms/editor/editor-field-text-form.tsx +++ b/apps/remix/app/components/forms/editor/editor-field-text-form.tsx @@ -3,11 +3,16 @@ import { useEffect } from 'react'; import { zodResolver } from '@hookform/resolvers/zod'; import { Trans, useLingui } from '@lingui/react/macro'; import { useForm, useWatch } from 'react-hook-form'; -import { z } from 'zod'; +import type { z } from 'zod'; import { DEFAULT_FIELD_FONT_SIZE, + FIELD_DEFAULT_GENERIC_ALIGN, + FIELD_DEFAULT_GENERIC_VERTICAL_ALIGN, + FIELD_DEFAULT_LETTER_SPACING, + FIELD_DEFAULT_LINE_HEIGHT, type TTextFieldMeta as TextFieldMeta, + ZTextFieldMeta, } from '@documenso/lib/types/field-meta'; import { Form, @@ -22,32 +27,36 @@ import { Textarea } from '@documenso/ui/primitives/textarea'; import { EditorGenericFontSizeField, + EditorGenericLetterSpacingField, + EditorGenericLineHeightField, EditorGenericReadOnlyField, EditorGenericRequiredField, EditorGenericTextAlignField, + EditorGenericVerticalAlignField, } from './editor-field-generic-field-forms'; -const ZTextFieldFormSchema = z - .object({ - label: z.string().optional(), - placeholder: z.string().optional(), - text: z.string().optional(), - characterLimit: z.coerce.number().min(0).optional(), - fontSize: z.coerce.number().min(8).max(96).optional(), - textAlign: z.enum(['left', 'center', 'right']).optional(), - required: z.boolean().optional(), - readOnly: z.boolean().optional(), - }) - .refine( - (data) => { - // A read-only field must have text - return !data.readOnly || (data.text && data.text.length > 0); - }, - { - message: 'A read-only field must have text', - path: ['text'], - }, - ); +const ZTextFieldFormSchema = ZTextFieldMeta.pick({ + label: true, + placeholder: true, + text: true, + characterLimit: true, + fontSize: true, + textAlign: true, + lineHeight: true, + letterSpacing: true, + verticalAlign: true, + required: true, + readOnly: true, +}).refine( + (data) => { + // A read-only field must have text + return !data.readOnly || (data.text && data.text.length > 0); + }, + { + message: 'A read-only field must have text', + path: ['text'], + }, +); type TTextFieldFormSchema = z.infer; @@ -73,7 +82,10 @@ export const EditorFieldTextForm = ({ text: value.text || '', characterLimit: value.characterLimit || 0, fontSize: value.fontSize || DEFAULT_FIELD_FONT_SIZE, - textAlign: value.textAlign || 'left', + textAlign: value.textAlign ?? FIELD_DEFAULT_GENERIC_ALIGN, + lineHeight: value.lineHeight ?? FIELD_DEFAULT_LINE_HEIGHT, + letterSpacing: value.letterSpacing ?? FIELD_DEFAULT_LETTER_SPACING, + verticalAlign: value.verticalAlign ?? FIELD_DEFAULT_GENERIC_VERTICAL_ALIGN, required: value.required || false, readOnly: value.readOnly || false, }, @@ -89,6 +101,10 @@ export const EditorFieldTextForm = ({ useEffect(() => { const validatedFormValues = ZTextFieldFormSchema.safeParse(formValues); + if (formValues.readOnly && !formValues.text) { + void form.trigger('text'); + } + if (validatedFormValues.success) { onValueChange({ type: 'text', @@ -101,10 +117,12 @@ export const EditorFieldTextForm = ({
-
- + +
+ +
{ + const values = form.getValues(); + const characterLimit = values.characterLimit || 0; + let textValue = e.target.value; + + if (characterLimit > 0 && textValue.length > characterLimit) { + textValue = textValue.slice(0, characterLimit); + } + + e.target.value = textValue; + field.onChange(e); + }} rows={1} /> @@ -170,11 +200,22 @@ export const EditorFieldTextForm = ({ { + const values = form.getValues(); + const characterLimit = parseInt(e.target.value, 10) || 0; + + field.onChange(characterLimit || ''); + + const textValue = values.text || ''; + + if (characterLimit > 0 && textValue.length > characterLimit) { + form.setValue('text', textValue.slice(0, characterLimit)); + } + }} /> @@ -182,6 +223,12 @@ export const EditorFieldTextForm = ({ )} /> +
+ + + +
+
diff --git a/apps/remix/app/components/forms/signin.tsx b/apps/remix/app/components/forms/signin.tsx index 3ce7a935b..5ea14ee44 100644 --- a/apps/remix/app/components/forms/signin.tsx +++ b/apps/remix/app/components/forms/signin.tsx @@ -92,6 +92,7 @@ export const SignInForm = ({ const [isTwoFactorAuthenticationDialogOpen, setIsTwoFactorAuthenticationDialogOpen] = useState(false); + const [isEmbeddedRedirect, setIsEmbeddedRedirect] = useState(false); const [twoFactorAuthenticationMethod, setTwoFactorAuthenticationMethod] = useState< 'totp' | 'backup' @@ -317,6 +318,8 @@ export const SignInForm = ({ if (email) { form.setValue('email', email); } + + setIsEmbeddedRedirect(params.get('embedded') === 'true'); }, [form]); return ( @@ -383,56 +386,64 @@ export const SignInForm = ({ {isSubmitting ? Signing in... : Sign In} - {hasSocialAuthEnabled && ( -
-
- - Or continue with - -
-
- )} + {!isEmbeddedRedirect && ( + <> + {hasSocialAuthEnabled && ( +
+
+ + Or continue with + +
+
+ )} - {isGoogleSSOEnabled && ( - - )} + {isGoogleSSOEnabled && ( + + )} - {isMicrosoftSSOEnabled && ( - - )} + {isMicrosoftSSOEnabled && ( + + )} - {isOIDCSSOEnabled && ( - + {isOIDCSSOEnabled && ( + + )} + )}
diff --git a/apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx b/apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx index b2877f2e6..3d991d5ee 100644 --- a/apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx +++ b/apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx @@ -22,7 +22,7 @@ export const DocumentSigningAuthAccount = ({ actionVerb = 'sign', onOpenChange, }: DocumentSigningAuthAccountProps) => { - const { recipient } = useRequiredDocumentSigningAuthContext(); + const { recipient, isDirectTemplate } = useRequiredDocumentSigningAuthContext(); const { t } = useLingui(); @@ -34,8 +34,10 @@ export const DocumentSigningAuthAccount = ({ try { setIsSigningOut(true); + const currentPath = `${window.location.pathname}${window.location.search}${window.location.hash}`; + await authClient.signOut({ - redirectPath: `/signin#email=${email}`, + redirectPath: `/signin?returnTo=${encodeURIComponent(currentPath)}#embedded=true&email=${isDirectTemplate ? '' : email}`, }); } catch { setIsSigningOut(false); @@ -55,16 +57,28 @@ export const DocumentSigningAuthAccount = ({ {actionTarget === 'DOCUMENT' && recipient.role === RecipientRole.VIEWER ? ( - - To mark this document as viewed, you need to be logged in as{' '} - {recipient.email} - + {isDirectTemplate ? ( + To mark this document as viewed, you need to be logged in. + ) : ( + + To mark this document as viewed, you need to be logged in as{' '} + {recipient.email} + + )} ) : ( - {/* Todo: Translate */} - To {actionVerb.toLowerCase()} this {actionTarget.toLowerCase()}, you need to be logged - in as {recipient.email} + {isDirectTemplate ? ( + + To {actionVerb.toLowerCase()} this {actionTarget.toLowerCase()}, you need to be + logged in. + + ) : ( + + To {actionVerb.toLowerCase()} this {actionTarget.toLowerCase()}, you need to be + logged in as {recipient.email} + + )} )} diff --git a/apps/remix/app/components/general/document-signing/document-signing-auth-dialog.tsx b/apps/remix/app/components/general/document-signing/document-signing-auth-dialog.tsx index 706daf686..9225b3bc6 100644 --- a/apps/remix/app/components/general/document-signing/document-signing-auth-dialog.tsx +++ b/apps/remix/app/components/general/document-signing/document-signing-auth-dialog.tsx @@ -47,7 +47,8 @@ export const DocumentSigningAuthDialog = ({ onOpenChange, onReauthFormSubmit, }: DocumentSigningAuthDialogProps) => { - const { recipient, user, isCurrentlyAuthenticating } = useRequiredDocumentSigningAuthContext(); + const { recipient, user, isCurrentlyAuthenticating, isDirectTemplate } = + useRequiredDocumentSigningAuthContext(); // Filter out EXPLICIT_NONE from available auth types for the chooser const validAuthTypes = availableAuthTypes.filter( @@ -168,7 +169,11 @@ export const DocumentSigningAuthDialog = ({ match({ documentAuthType: selectedAuthType, user }) .with( { documentAuthType: DocumentAuth.ACCOUNT }, - { user: P.when((user) => !user || user.email !== recipient.email) }, // Assume all current auth methods requires them to be logged in. + { + user: P.when( + (user) => !user || (user.email !== recipient.email && !isDirectTemplate), + ), + }, // Assume all current auth methods requires them to be logged in. () => , ) .with({ documentAuthType: DocumentAuth.PASSKEY }, () => ( diff --git a/apps/remix/app/components/general/document-signing/document-signing-auth-page.tsx b/apps/remix/app/components/general/document-signing/document-signing-auth-page.tsx index 21b1be6ca..123baafa5 100644 --- a/apps/remix/app/components/general/document-signing/document-signing-auth-page.tsx +++ b/apps/remix/app/components/general/document-signing/document-signing-auth-page.tsx @@ -9,7 +9,7 @@ import { Button } from '@documenso/ui/primitives/button'; import { useToast } from '@documenso/ui/primitives/use-toast'; export type DocumentSigningAuthPageViewProps = { - email: string; + email?: string; emailHasAccount?: boolean; }; @@ -22,12 +22,18 @@ export const DocumentSigningAuthPageView = ({ const [isSigningOut, setIsSigningOut] = useState(false); - const handleChangeAccount = async (email: string) => { + const handleChangeAccount = async (email?: string) => { try { setIsSigningOut(true); + let redirectPath = '/signin'; + + if (email) { + redirectPath = emailHasAccount ? `/signin#email=${email}` : `/signup#email=${email}`; + } + await authClient.signOut({ - redirectPath: emailHasAccount ? `/signin#email=${email}` : `/signup#email=${email}`, + redirectPath, }); } catch { toast({ @@ -49,9 +55,13 @@ export const DocumentSigningAuthPageView = ({

- - You need to be logged in as {email} to view this page. - + {email ? ( + + You need to be logged in as {email} to view this page. + + ) : ( + You need to be logged in to view this page. + )}

)} - {/* Footer of left sidebar. */} -
- +
+ {/* Footer of left sidebar. */} + {!isEmbed && ( +
+ +
+ )}
-
+
{/* Horizontal envelope item selector */} {envelopeItems.length > 1 && ( @@ -202,11 +226,11 @@ export const DocumentSigningPageViewV2 = () => { )} {/* Document View */} -
+
{currentEnvelopeItem ? ( ) : ( @@ -218,9 +242,20 @@ export const DocumentSigningPageViewV2 = () => { )} {/* Mobile widget - Additional padding to allow users to scroll */} -
+
+ + {!hidePoweredBy && ( + + Powered by + + + )}
diff --git a/apps/remix/app/components/general/document-signing/envelope-signing-provider.tsx b/apps/remix/app/components/general/document-signing/envelope-signing-provider.tsx index b7d1a1d08..ddaf3c355 100644 --- a/apps/remix/app/components/general/document-signing/envelope-signing-provider.tsx +++ b/apps/remix/app/components/general/document-signing/envelope-signing-provider.tsx @@ -13,6 +13,7 @@ import { prop, sortBy } from 'remeda'; import { isBase64Image } from '@documenso/lib/constants/signatures'; import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc'; import type { EnvelopeForSigningResponse } from '@documenso/lib/server-only/envelope/get-envelope-for-recipient-signing'; +import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth'; import { isFieldUnsignedAndRequired, isRequiredField, @@ -51,7 +52,11 @@ export type EnvelopeSigningContextValue = { setSelectedAssistantRecipientId: (_value: number | null) => void; selectedAssistantRecipient: EnvelopeForSigningResponse['envelope']['recipients'][number] | null; - signField: (_fieldId: number, _value: TSignEnvelopeFieldValue) => Promise; + signField: ( + _fieldId: number, + _value: TSignEnvelopeFieldValue, + authOptions?: TRecipientActionAuth, + ) => Promise>; }; const EnvelopeSigningContext = createContext(null); @@ -284,21 +289,26 @@ export const EnvelopeSigningProvider = ({ : null; }, [envelope.documentMeta?.signingOrder, envelope.recipients, recipient.id]); - const signField = async (fieldId: number, fieldValue: TSignEnvelopeFieldValue) => { - console.log('insertField', fieldId, fieldValue); - + const signField = async ( + fieldId: number, + fieldValue: TSignEnvelopeFieldValue, + authOptions?: TRecipientActionAuth, + ) => { // Set the field locally for direct templates. if (isDirectTemplate) { - handleDirectTemplateFieldInsertion(fieldId, fieldValue); - return; + const signedField = handleDirectTemplateFieldInsertion(fieldId, fieldValue); + + return signedField; } - await signEnvelopeField({ + const { signedField } = await signEnvelopeField({ token: envelopeData.recipient.token, fieldId, fieldValue, - authOptions: undefined, + authOptions, }); + + return signedField; }; const handleDirectTemplateFieldInsertion = ( @@ -356,6 +366,8 @@ export const EnvelopeSigningProvider = ({ fields: prev.recipient.fields.map((field) => (field.id === fieldId ? updatedField : field)), }, })); + + return updatedField; }; return ( diff --git a/apps/remix/app/components/general/document/document-certificate-qr-view.tsx b/apps/remix/app/components/general/document/document-certificate-qr-view.tsx index b4360d6bb..1f0c7e89e 100644 --- a/apps/remix/app/components/general/document/document-certificate-qr-view.tsx +++ b/apps/remix/app/components/general/document/document-certificate-qr-view.tsx @@ -1,7 +1,8 @@ import { useEffect, useState } from 'react'; import { Trans } from '@lingui/react/macro'; -import type { DocumentData, EnvelopeItem } from '@prisma/client'; +import { type DocumentData, DocumentStatus, type EnvelopeItem, EnvelopeType } from '@prisma/client'; +import { DownloadIcon } from 'lucide-react'; import { DateTime } from 'luxon'; import { @@ -22,9 +23,10 @@ import { } from '@documenso/ui/primitives/dialog'; import { PDFViewer } from '@documenso/ui/primitives/pdf-viewer'; +import { EnvelopeDownloadDialog } from '~/components/dialogs/envelope-download-dialog'; + import { EnvelopeRendererFileSelector } from '../envelope-editor/envelope-file-selector'; import EnvelopeGenericPageRenderer from '../envelope-editor/envelope-generic-page-renderer'; -import { ShareDocumentDownloadButton } from '../share-document-download-button'; export type DocumentCertificateQRViewProps = { documentId: number; @@ -34,6 +36,7 @@ export type DocumentCertificateQRViewProps = { documentTeamUrl: string; recipientCount?: number; completedDate?: Date; + token: string; }; export const DocumentCertificateQRView = ({ @@ -44,6 +47,7 @@ export const DocumentCertificateQRView = ({ documentTeamUrl, recipientCount = 0, completedDate, + token, }: DocumentCertificateQRViewProps) => { const { data: documentViaUser } = trpc.document.get.useQuery({ documentId, @@ -96,11 +100,19 @@ export const DocumentCertificateQRView = ({ )} {internalVersion === 2 ? ( - + ) : ( @@ -119,14 +131,27 @@ export const DocumentCertificateQRView = ({
- + + Download + + } />
- +
)} @@ -138,14 +163,16 @@ type DocumentCertificateQrV2Props = { title: string; recipientCount: number; formattedDate: string; + token: string; }; const DocumentCertificateQrV2 = ({ title, recipientCount, formattedDate, + token, }: DocumentCertificateQrV2Props) => { - const { currentEnvelopeItem } = useCurrentEnvelopeRender(); + const { currentEnvelopeItem, envelopeItems } = useCurrentEnvelopeRender(); return (
@@ -163,18 +190,24 @@ const DocumentCertificateQrV2 = ({
- {currentEnvelopeItem && ( - - )} + + + Download + + } + />
- +
); diff --git a/apps/remix/app/components/general/document/document-edit-form.tsx b/apps/remix/app/components/general/document/document-edit-form.tsx index 0b731fa35..e31224f70 100644 --- a/apps/remix/app/components/general/document/document-edit-form.tsx +++ b/apps/remix/app/components/general/document/document-edit-form.tsx @@ -441,9 +441,10 @@ export const DocumentEditForm = ({ > setIsDocumentPdfLoaded(true)} /> diff --git a/apps/remix/app/components/general/document/document-page-view-button.tsx b/apps/remix/app/components/general/document/document-page-view-button.tsx index 361109580..e3e7e0afc 100644 --- a/apps/remix/app/components/general/document/document-page-view-button.tsx +++ b/apps/remix/app/components/general/document/document-page-view-button.tsx @@ -1,18 +1,14 @@ -import { msg } from '@lingui/core/macro'; -import { useLingui } from '@lingui/react'; import { Trans } from '@lingui/react/macro'; import { DocumentStatus, RecipientRole, SigningStatus } from '@prisma/client'; import { CheckCircle, Download, EyeIcon, Pencil } from 'lucide-react'; import { Link } from 'react-router'; import { match } from 'ts-pattern'; -import { downloadPDF } from '@documenso/lib/client-only/download-pdf'; import { useSession } from '@documenso/lib/client-only/providers/session'; import type { TEnvelope } from '@documenso/lib/types/envelope'; import { isDocumentCompleted } from '@documenso/lib/utils/document'; import { formatDocumentsPath } from '@documenso/lib/utils/teams'; import { Button } from '@documenso/ui/primitives/button'; -import { useToast } from '@documenso/ui/primitives/use-toast'; import { EnvelopeDownloadDialog } from '~/components/dialogs/envelope-download-dialog'; @@ -23,9 +19,6 @@ export type DocumentPageViewButtonProps = { export const DocumentPageViewButton = ({ envelope }: DocumentPageViewButtonProps) => { const { user } = useSession(); - const { toast } = useToast(); - const { _ } = useLingui(); - const recipient = envelope.recipients.find((recipient) => recipient.email === user.email); const isRecipient = !!recipient; @@ -37,25 +30,6 @@ export const DocumentPageViewButton = ({ envelope }: DocumentPageViewButtonProps const documentsPath = formatDocumentsPath(envelope.team.url); const formatPath = `${documentsPath}/${envelope.id}/edit`; - const onDownloadClick = async () => { - try { - // Todo; Envelopes - Support multiple items - const envelopeItem = envelope.envelopeItems[0]; - - if (!envelopeItem.documentData) { - throw new Error('No document available'); - } - - await downloadPDF({ documentData: envelopeItem.documentData, fileName: envelopeItem.title }); - } catch (err) { - toast({ - title: _(msg`Something went wrong`), - description: _(msg`An error occurred while downloading your document.`), - variant: 'destructive', - }); - } - }; - return match({ isRecipient, isPending, @@ -95,7 +69,7 @@ export const DocumentPageViewButton = ({ envelope }: DocumentPageViewButtonProps )) - .with({ isComplete: true, internalVersion: 2 }, () => ( + .with({ isComplete: true }, () => ( )) - .with({ isComplete: true }, () => ( - - )) .otherwise(() => null); }; diff --git a/apps/remix/app/components/general/document/document-page-view-dropdown.tsx b/apps/remix/app/components/general/document/document-page-view-dropdown.tsx index c5b6c9371..282740369 100644 --- a/apps/remix/app/components/general/document/document-page-view-dropdown.tsx +++ b/apps/remix/app/components/general/document/document-page-view-dropdown.tsx @@ -1,6 +1,5 @@ import { useState } from 'react'; -import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react'; import { Trans } from '@lingui/react/macro'; import { DocumentStatus } from '@prisma/client'; @@ -16,13 +15,11 @@ import { } from 'lucide-react'; import { Link, useNavigate } from 'react-router'; -import { downloadPDF } from '@documenso/lib/client-only/download-pdf'; import { useSession } from '@documenso/lib/client-only/providers/session'; import type { TEnvelope } from '@documenso/lib/types/envelope'; import { isDocumentCompleted } from '@documenso/lib/utils/document'; import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope'; import { formatDocumentsPath } from '@documenso/lib/utils/teams'; -import { trpc as trpcClient } from '@documenso/trpc/client'; import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button'; import { DropdownMenu, @@ -67,64 +64,6 @@ export const DocumentPageViewDropdown = ({ envelope }: DocumentPageViewDropdownP const documentsPath = formatDocumentsPath(team.url); - const onDownloadClick = async () => { - try { - const documentWithData = await trpcClient.document.get.query( - { - documentId: mapSecondaryIdToDocumentId(envelope.secondaryId), - }, - { - context: { - teamId: team?.id?.toString(), - }, - }, - ); - - const documentData = documentWithData?.documentData; - - if (!documentData) { - return; - } - - await downloadPDF({ documentData, fileName: envelope.title }); - } catch (err) { - toast({ - title: _(msg`Something went wrong`), - description: _(msg`An error occurred while downloading your document.`), - variant: 'destructive', - }); - } - }; - - const onDownloadOriginalClick = async () => { - try { - const documentWithData = await trpcClient.document.get.query( - { - documentId: mapSecondaryIdToDocumentId(envelope.secondaryId), - }, - { - context: { - teamId: team?.id?.toString(), - }, - }, - ); - - const documentData = documentWithData?.documentData; - - if (!documentData) { - return; - } - - await downloadPDF({ documentData, fileName: envelope.title, version: 'original' }); - } catch (err) { - toast({ - title: _(msg`Something went wrong`), - description: _(msg`An error occurred while downloading your document.`), - variant: 'destructive', - }); - } - }; - const nonSignedRecipients = envelope.recipients.filter((item) => item.signingStatus !== 'SIGNED'); return ( @@ -147,36 +86,20 @@ export const DocumentPageViewDropdown = ({ envelope }: DocumentPageViewDropdownP )} - {envelope.internalVersion === 2 ? ( - e.preventDefault()}> -
- - Download -
- - } - /> - ) : ( - <> - {isComplete && ( - + e.preventDefault()}> +
Download - - )} - - - - Download Original +
- - )} + } + /> @@ -250,7 +173,8 @@ export const DocumentPageViewDropdown = ({ envelope }: DocumentPageViewDropdownP {isDuplicateDialogOpen && ( diff --git a/apps/remix/app/components/general/document/document-page-view-information.tsx b/apps/remix/app/components/general/document/document-page-view-information.tsx index d4607ca51..91dee53d1 100644 --- a/apps/remix/app/components/general/document/document-page-view-information.tsx +++ b/apps/remix/app/components/general/document/document-page-view-information.tsx @@ -7,6 +7,7 @@ import { DateTime } from 'luxon'; import { useIsMounted } from '@documenso/lib/client-only/hooks/use-is-mounted'; import type { TEnvelope } from '@documenso/lib/types/envelope'; +import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope'; export type DocumentPageViewInformationProps = { userId: number; @@ -40,6 +41,10 @@ export const DocumentPageViewInformation = ({ .setLocale(i18n.locales?.[0] || i18n.locale) .toRelative(), }, + { + description: msg`Document ID (Legacy)`, + value: mapSecondaryIdToDocumentId(envelope.secondaryId), + }, ]; // eslint-disable-next-line react-hooks/exhaustive-deps }, [isMounted, envelope, userId]); diff --git a/apps/remix/app/components/general/document/document-upload-button.tsx b/apps/remix/app/components/general/document/document-upload-button-legacy.tsx similarity index 84% rename from apps/remix/app/components/general/document/document-upload-button.tsx rename to apps/remix/app/components/general/document/document-upload-button-legacy.tsx index 7e092363e..f4f13aaf0 100644 --- a/apps/remix/app/components/general/document/document-upload-button.tsx +++ b/apps/remix/app/components/general/document/document-upload-button-legacy.tsx @@ -3,6 +3,7 @@ import { useMemo, useState } from 'react'; import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react'; import { Trans } from '@lingui/react/macro'; +import { EnvelopeType } from '@prisma/client'; import { useNavigate, useParams } from 'react-router'; import { match } from 'ts-pattern'; @@ -13,11 +14,11 @@ import { useSession } from '@documenso/lib/client-only/providers/session'; import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app'; import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; -import { putPdfFile } from '@documenso/lib/universal/upload/put-file'; import { formatDocumentsPath } from '@documenso/lib/utils/teams'; import { trpc } from '@documenso/trpc/react'; +import type { TCreateDocumentPayloadSchema } from '@documenso/trpc/server/document-router/create-document.types'; import { cn } from '@documenso/ui/lib/utils'; -import { DocumentDropzone } from '@documenso/ui/primitives/document-upload'; +import { DocumentUploadButton as DocumentUploadButtonPrimitive } from '@documenso/ui/primitives/document-upload-button'; import { Tooltip, TooltipContent, @@ -28,11 +29,11 @@ import { useToast } from '@documenso/ui/primitives/use-toast'; import { useCurrentTeam } from '~/providers/team'; -export type DocumentUploadButtonProps = { +export type DocumentUploadButtonLegacyProps = { className?: string; }; -export const DocumentUploadButton = ({ className }: DocumentUploadButtonProps) => { +export const DocumentUploadButtonLegacy = ({ className }: DocumentUploadButtonLegacyProps) => { const { _ } = useLingui(); const { toast } = useToast(); const { user } = useSession(); @@ -73,14 +74,20 @@ export const DocumentUploadButton = ({ className }: DocumentUploadButtonProps) = try { setIsLoading(true); - const response = await putPdfFile(file); - - const { legacyDocumentId: id } = await createDocument({ + const payload = { title: file.name, - documentDataId: response.id, - timezone: userTimezone, folderId: folderId ?? undefined, - }); + meta: { + timezone: userTimezone, + }, + } satisfies TCreateDocumentPayloadSchema; + + const formData = new FormData(); + + formData.append('payload', JSON.stringify(payload)); + formData.append('file', file); + + const { envelopeId: id } = await createDocument(formData); void refreshLimits(); @@ -140,12 +147,14 @@ export const DocumentUploadButton = ({ className }: DocumentUploadButtonProps) =
- onFileDrop(files[0])} onDropRejected={onFileDropRejected} + type={EnvelopeType.DOCUMENT} + internalVersion="1" />
diff --git a/apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page-renderer.tsx b/apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page-renderer.tsx index f0af450d8..bfdebc0c4 100644 --- a/apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page-renderer.tsx +++ b/apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page-renderer.tsx @@ -30,7 +30,7 @@ import { fieldButtonList } from './envelope-editor-fields-drag-drop'; export default function EnvelopeEditorFieldsPageRenderer() { const { t, i18n } = useLingui(); const { envelope, editorFields, getRecipientColorKey } = useCurrentEnvelopeEditor(); - const { currentEnvelopeItem } = useCurrentEnvelopeRender(); + const { currentEnvelopeItem, setRenderError } = useCurrentEnvelopeRender(); const interactiveTransformer = useRef(null); @@ -116,7 +116,6 @@ export default function EnvelopeEditorFieldsPageRenderer() { fieldUpdates.height = fieldPageHeight; } - // Todo: envelopes Use id editorFields.updateFieldByFormId(fieldFormId, fieldUpdates); // Select the field if it is not already selected. @@ -127,7 +126,7 @@ export default function EnvelopeEditorFieldsPageRenderer() { pageLayer.current?.batchDraw(); }; - const renderFieldOnLayer = (field: TLocalField) => { + const unsafeRenderFieldOnLayer = (field: TLocalField) => { if (!pageLayer.current) { return; } @@ -173,6 +172,15 @@ export default function EnvelopeEditorFieldsPageRenderer() { fieldGroup.on('dragend', handleResizeOrMove); }; + const renderFieldOnLayer = (field: TLocalField) => { + try { + unsafeRenderFieldOnLayer(field); + } catch (err) { + console.error(err); + setRenderError(true); + } + }; + /** * Initialize the Konva page canvas and all fields and interactions. */ @@ -630,13 +638,14 @@ export default function EnvelopeEditorFieldsPageRenderer() { transform: 'translateX(-50%)', zIndex: 50, }} - className="text-muted-foreground grid w-max grid-cols-5 gap-x-1 gap-y-0.5 rounded-md border bg-white p-1 shadow-sm" + // Don't use darkmode for this component, it should look the same for both light/dark modes. + className="grid w-max grid-cols-5 gap-x-1 gap-y-0.5 rounded-md border border-gray-300 bg-white p-1 text-gray-500 shadow-sm" > {fieldButtonList.map((field) => ( diff --git a/apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx b/apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx index dbd5faa4e..fcae5d9b1 100644 --- a/apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx +++ b/apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx @@ -30,7 +30,7 @@ import { FIELD_META_DEFAULT_VALUES } from '@documenso/lib/types/field-meta'; import { canRecipientFieldsBeModified } from '@documenso/lib/utils/recipients'; import { AnimateGenericFadeInOut } from '@documenso/ui/components/animate/animate-generic-fade-in-out'; import PDFViewerKonvaLazy from '@documenso/ui/components/pdf-viewer/pdf-viewer-konva-lazy'; -import { Alert, AlertDescription } from '@documenso/ui/primitives/alert'; +import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert'; import { Button } from '@documenso/ui/primitives/button'; import { RecipientSelector } from '@documenso/ui/primitives/recipient-selector'; import { Separator } from '@documenso/ui/primitives/separator'; @@ -263,9 +263,34 @@ export const EnvelopeEditorFieldsPage = () => { {/* Document View */} -
+
+ {envelope.recipients.length === 0 && ( + +
+ + Missing Recipients + + + You need at least one recipient to add fields + +
+ + +
+ )} + {currentEnvelopeItem !== null ? ( - + ) : (
@@ -281,7 +306,7 @@ export const EnvelopeEditorFieldsPage = () => {
{/* Right Section - Form Fields Panel */} - {currentEnvelopeItem && ( + {currentEnvelopeItem && envelope.recipients.length > 0 && (
{/* Recipient selector section. */}
@@ -289,29 +314,15 @@ export const EnvelopeEditorFieldsPage = () => { Selected Recipient - {envelope.recipients.length === 0 ? ( - - - You need at least one recipient to add fields - - -

- Click here to add a recipient -

- -
-
- ) : ( - - editorFields.setSelectedRecipient(recipient.id) - } - recipients={envelope.recipients} - className="w-full" - align="end" - /> - )} + + editorFields.setSelectedRecipient(recipient.id) + } + recipients={envelope.recipients} + className="w-full" + align="end" + /> {editorFields.selectedRecipient && !canRecipientFieldsBeModified(editorFields.selectedRecipient, envelope.fields) && ( diff --git a/apps/remix/app/components/general/envelope-editor/envelope-editor-preview-page.tsx b/apps/remix/app/components/general/envelope-editor/envelope-editor-preview-page.tsx index e57740cd1..c884fdba9 100644 --- a/apps/remix/app/components/general/envelope-editor/envelope-editor-preview-page.tsx +++ b/apps/remix/app/components/general/envelope-editor/envelope-editor-preview-page.tsx @@ -1,10 +1,20 @@ -import { lazy, useEffect, useState } from 'react'; +import { lazy, useEffect, useMemo, useState } from 'react'; +import { faker } from '@faker-js/faker/locale/en'; import { Trans } from '@lingui/react/macro'; -import { ConstructionIcon, FileTextIcon } from 'lucide-react'; +import { FieldType, SigningStatus } from '@prisma/client'; +import { FileTextIcon } from 'lucide-react'; +import { match } from 'ts-pattern'; import { useCurrentEnvelopeEditor } from '@documenso/lib/client-only/providers/envelope-editor-provider'; -import { useCurrentEnvelopeRender } from '@documenso/lib/client-only/providers/envelope-render-provider'; +import { + EnvelopeRenderProvider, + useCurrentEnvelopeRender, +} from '@documenso/lib/client-only/providers/envelope-render-provider'; +import { ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta'; +import { extractFieldInsertionValues } from '@documenso/lib/utils/envelope-signing'; +import { toCheckboxCustomText } from '@documenso/lib/utils/fields'; +import { extractInitials } from '@documenso/lib/utils/recipient-formatter'; import { AnimateGenericFadeInOut } from '@documenso/ui/components/animate/animate-generic-fade-in-out'; import PDFViewerKonvaLazy from '@documenso/ui/components/pdf-viewer/pdf-viewer-konva-lazy'; import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert'; @@ -15,15 +25,169 @@ import { EnvelopeRendererFileSelector } from './envelope-file-selector'; const EnvelopeGenericPageRenderer = lazy(async () => import('./envelope-generic-page-renderer')); +// Todo: Envelopes - Dynamically import faker export const EnvelopeEditorPreviewPage = () => { const { envelope, editorFields } = useCurrentEnvelopeEditor(); - const { currentEnvelopeItem } = useCurrentEnvelopeRender(); + const { currentEnvelopeItem, fields } = useCurrentEnvelopeRender(); const [selectedPreviewMode, setSelectedPreviewMode] = useState<'recipient' | 'signed'>( 'recipient', ); + const fieldsWithPlaceholders = useMemo(() => { + return fields.map((field) => { + const fieldMeta = ZFieldAndMetaSchema.parse(field); + + const recipient = envelope.recipients.find((recipient) => recipient.id === field.recipientId); + + if (!recipient) { + throw new Error('Recipient not found'); + } + + faker.seed(recipient.id); + + const recipientName = recipient.name || faker.person.fullName(); + const recipientEmail = recipient.email || faker.internet.email(); + + faker.seed(recipient.id + field.id); + + return { + ...field, + inserted: true, + ...match(fieldMeta) + .with({ type: FieldType.TEXT }, ({ fieldMeta }) => { + let text = fieldMeta?.text || faker.lorem.words(5); + + if (fieldMeta?.characterLimit) { + text = text.slice(0, fieldMeta?.characterLimit); + } + + return { + customText: text, + }; + }) + .with({ type: FieldType.NUMBER }, ({ fieldMeta }) => { + let number = fieldMeta?.value ?? ''; + + if (number === '') { + number = faker.number + .int({ + min: fieldMeta?.minValue ?? 0, + max: fieldMeta?.maxValue ?? 1000, + }) + .toString(); + } + + return { + customText: number, + }; + }) + .with({ type: FieldType.DATE }, () => { + const date = extractFieldInsertionValues({ + fieldValue: { + type: FieldType.DATE, + value: true, + }, + field, + documentMeta: envelope.documentMeta, + }); + + return { + customText: date.customText, + }; + }) + .with({ type: FieldType.EMAIL }, () => { + return { + customText: recipientEmail, + }; + }) + .with({ type: FieldType.NAME }, () => { + return { + customText: recipientName, + }; + }) + .with({ type: FieldType.INITIALS }, () => { + return { + customText: extractInitials(recipientName), + }; + }) + .with({ type: FieldType.RADIO }, ({ fieldMeta }) => { + const values = fieldMeta?.values ?? []; + + if (values.length === 0) { + return ''; + } + + let customText = ''; + + const preselectedValue = values.findIndex((value) => value.checked); + + if (preselectedValue !== -1) { + customText = preselectedValue.toString(); + } else { + const randomIndex = faker.number.int({ min: 0, max: values.length - 1 }); + customText = randomIndex.toString(); + } + + return { + customText, + }; + }) + .with({ type: FieldType.CHECKBOX }, ({ fieldMeta }) => { + let checkedValues: number[] = []; + + const values = fieldMeta?.values ?? []; + + values.forEach((value, index) => { + if (value.checked) { + checkedValues.push(index); + } + }); + + if (checkedValues.length === 0 && values.length > 0) { + const numberOfValues = fieldMeta?.validationLength || 1; + + checkedValues = Array.from({ length: numberOfValues }, (_, index) => index); + } + + return { + customText: toCheckboxCustomText(checkedValues), + }; + }) + .with({ type: FieldType.DROPDOWN }, ({ fieldMeta }) => { + const values = fieldMeta?.values ?? []; + + let customText = fieldMeta?.defaultValue || ''; + + if (!customText && values.length > 0) { + const randomIndex = faker.number.int({ min: 0, max: values.length - 1 }); + customText = values[randomIndex].value; + } + + return { + customText, + }; + }) + .with({ type: FieldType.SIGNATURE }, () => { + return { + customText: '', + signature: { + signatureImageAsBase64: '', + typedSignature: recipientName, + }, + }; + }) + .with({ type: FieldType.FREE_SIGNATURE }, () => { + return { + customText: '', + }; + }) + .exhaustive(), + }; + }); + }, [fields, envelope, envelope.recipients, envelope.documentMeta]); + /** * Set the selected recipient to the first recipient in the envelope. */ @@ -31,40 +195,41 @@ export const EnvelopeEditorPreviewPage = () => { editorFields.setSelectedRecipient(envelope.recipients[0]?.id ?? null); }, []); + // Override the parent renderer provider so we can inject custom fields. return ( -
-
- {/* Horizontal envelope item selector */} - + ({ + ...recipient, + signingStatus: SigningStatus.SIGNED, + }))} + overrideSettings={{ + mode: 'export', + }} + > +
+
+ {/* Horizontal envelope item selector */} + - {/* Document View */} -
- - - Preview Mode - - - Preview what the signed document will look like with placeholder data - - + {/* Document View */} +
+ + + Preview Mode + + + Preview what the signed document will look like with placeholder data + + - {/* Coming soon section */} -
-
- -

- Coming soon -

-

- This feature is coming soon -

-
-
- - {/* Todo: Envelopes - Remove div after preview mode is implemented */} -
{currentEnvelopeItem !== null ? ( - + ) : (
@@ -78,27 +243,28 @@ export const EnvelopeEditorPreviewPage = () => { )}
-
- {/* Right Section - Form Fields Panel */} - {currentEnvelopeItem && false && ( -
- {/* Add fields section. */} -
- {/*

+ {/* Right Section - Form Fields Panel */} + {currentEnvelopeItem && false && ( +
+ {/* Add fields section. */} +
+ {/*

Preivew Mode

*/} - - - Preview Mode - - - Preview what the signed document will look like with placeholder data - - + + + Preview Mode + + + + Preview what the signed document will look like with placeholder data + + + - {/* + {/* {
Preview what a recipient will see
Preview the signed document
*/} -
+

- {false && ( - - {selectedPreviewMode === 'recipient' && ( - <> - + {false && ( + + {selectedPreviewMode === 'recipient' && ( + <> + - {/* Recipient selector section. */} -
-

- Selected Recipient -

+ {/* Recipient selector section. */} +
+

+ Selected Recipient +

- - editorFields.setSelectedRecipient(recipient.id) - } - recipients={envelope.recipients} - className="w-full" - align="end" - /> -
- - )} - - )} -
- )} -
+ + editorFields.setSelectedRecipient(recipient.id) + } + recipients={envelope.recipients} + className="w-full" + align="end" + /> +
+ + )} + + )} +
+ )} +
+ ); }; diff --git a/apps/remix/app/components/general/envelope-editor/envelope-editor-recipient-form.tsx b/apps/remix/app/components/general/envelope-editor/envelope-editor-recipient-form.tsx index ef8a1288c..18e0d077e 100644 --- a/apps/remix/app/components/general/envelope-editor/envelope-editor-recipient-form.tsx +++ b/apps/remix/app/components/general/envelope-editor/envelope-editor-recipient-form.tsx @@ -212,7 +212,7 @@ export const EnvelopeEditorRecipientForm = () => { ); const hasDocumentBeenSent = recipients.some( - (recipient) => recipient.sendStatus === SendStatus.SENT, + (recipient) => recipient.role !== RecipientRole.CC && recipient.sendStatus === SendStatus.SENT, ); const canRecipientBeModified = (recipientId?: number) => { @@ -482,30 +482,46 @@ export const EnvelopeEditorRecipientForm = () => { const { data } = validatedFormValues; + // Weird edge case where the whole envelope is created via API + // with no signing order. If they come to this page it will show an error + // since they aren't equal and the recipient is no longer editable. + const envelopeRecipients = data.signers.map((recipient) => { + if (!canRecipientBeModified(recipient.id)) { + return { + ...recipient, + signingOrder: recipient.signingOrder, + }; + } + return recipient; + }); + const hasSigningOrderChanged = envelope.documentMeta.signingOrder !== data.signingOrder; const hasAllowDictateNextSignerChanged = envelope.documentMeta.allowDictateNextSigner !== data.allowDictateNextSigner; const hasSignersChanged = - data.signers.length !== recipients.length || - data.signers.some((signer) => { + envelopeRecipients.length !== recipients.length || + envelopeRecipients.some((signer) => { const recipient = recipients.find((recipient) => recipient.id === signer.id); if (!recipient) { return true; } + const signerActionAuth = signer.actionAuth; + const recipientActionAuth = recipient.authOptions?.actionAuth || []; + return ( signer.email !== recipient.email || signer.name !== recipient.name || signer.role !== recipient.role || signer.signingOrder !== recipient.signingOrder || - !isDeepEqual(signer.actionAuth, recipient.authOptions?.actionAuth) + !isDeepEqual(signerActionAuth, recipientActionAuth) ); }); if (hasSignersChanged) { - setRecipientsDebounced(validatedFormValues.data.signers); + setRecipientsDebounced(envelopeRecipients); } if (hasSigningOrderChanged || hasAllowDictateNextSignerChanged) { diff --git a/apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx b/apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx index 0c505d55c..6593609f5 100644 --- a/apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx +++ b/apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx @@ -174,7 +174,7 @@ export const EnvelopeEditorSettingsDialog = ({ const { t, i18n } = useLingui(); const { toast } = useToast(); - const { envelope } = useCurrentEnvelopeEditor(); + const { envelope, updateEnvelopeAsync } = useCurrentEnvelopeEditor(); const team = useCurrentTeam(); const organisation = useCurrentOrganisation(); @@ -186,14 +186,12 @@ export const EnvelopeEditorSettingsDialog = ({ documentAuth: envelope.authOptions, }); - const form = useForm({ - resolver: zodResolver(ZAddSettingsFormSchema), - defaultValues: { - externalId: envelope.externalId || '', // Todo: String or undefined? + const createDefaultValues = () => { + return { + externalId: envelope.externalId || '', visibility: envelope.visibility || '', globalAccessAuth: documentAuthOption?.globalAccessAuth || [], globalActionAuth: documentAuthOption?.globalActionAuth || [], - meta: { subject: envelope.documentMeta.subject ?? '', message: envelope.documentMeta.message ?? '', @@ -210,10 +208,13 @@ export const EnvelopeEditorSettingsDialog = ({ emailSettings: ZDocumentEmailSettingsSchema.parse(envelope.documentMeta.emailSettings), signatureTypes: extractTeamSignatureSettings(envelope.documentMeta), }, - }, - }); + }; + }; - const { mutateAsync: updateEnvelope } = trpc.envelope.update.useMutation(); + const form = useForm({ + resolver: zodResolver(ZAddSettingsFormSchema), + defaultValues: createDefaultValues(), + }); const envelopeHasBeenSent = envelope.type === EnvelopeType.DOCUMENT && @@ -229,7 +230,6 @@ export const EnvelopeEditorSettingsDialog = ({ const emails = emailData?.data || []; - // Todo: Envelopes this doesn't make sense (look at previous) const canUpdateVisibility = canAccessTeamDocument(team.currentTeamRole, envelope.visibility); const onFormSubmit = async (data: TAddSettingsFormSchema) => { @@ -240,9 +240,7 @@ export const EnvelopeEditorSettingsDialog = ({ .safeParse(data.globalAccessAuth); try { - await updateEnvelope({ - envelopeId: envelope.id, - envelopeType: envelope.type, + await updateEnvelopeAsync({ data: { externalId: data.externalId || null, visibility: data.visibility, @@ -297,7 +295,7 @@ export const EnvelopeEditorSettingsDialog = ({ ]); useEffect(() => { - form.reset(); + form.reset(createDefaultValues()); setActiveTab('general'); }, [open, form]); @@ -323,7 +321,7 @@ export const EnvelopeEditorSettingsDialog = ({ {/* Sidebar. */} -
+
Document Settings diff --git a/apps/remix/app/components/general/envelope-editor/envelope-editor-upload-page.tsx b/apps/remix/app/components/general/envelope-editor/envelope-editor-upload-page.tsx index caeea13e7..c606d267e 100644 --- a/apps/remix/app/components/general/envelope-editor/envelope-editor-upload-page.tsx +++ b/apps/remix/app/components/general/envelope-editor/envelope-editor-upload-page.tsx @@ -17,9 +17,9 @@ import { import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation'; import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app'; import { nanoid } from '@documenso/lib/universal/id'; -import { putPdfFile } from '@documenso/lib/universal/upload/put-file'; import { canEnvelopeItemsBeModified } from '@documenso/lib/utils/envelope'; import { trpc } from '@documenso/trpc/react'; +import type { TCreateEnvelopeItemsPayload } from '@documenso/trpc/server/envelope-router/create-envelope-items.types'; import { Button } from '@documenso/ui/primitives/button'; import { Card, @@ -48,7 +48,7 @@ export const EnvelopeEditorUploadPage = () => { const organisation = useCurrentOrganisation(); const { t } = useLingui(); - const { envelope, setLocalEnvelope, relativePath } = useCurrentEnvelopeEditor(); + const { envelope, setLocalEnvelope, relativePath, editorFields } = useCurrentEnvelopeEditor(); const { maximumEnvelopeItemCount, remaining } = useLimits(); const { toast } = useToast(); @@ -66,8 +66,8 @@ export const EnvelopeEditorUploadPage = () => { const { mutateAsync: createEnvelopeItems, isPending: isCreatingEnvelopeItems } = trpc.envelope.item.createMany.useMutation({ - onSuccess: (data) => { - const createdEnvelopes = data.createdEnvelopeItems.filter( + onSuccess: ({ data }) => { + const createdEnvelopes = data.filter( (item) => !envelope.envelopeItems.find((envelopeItem) => envelopeItem.id === item.id), ); @@ -78,10 +78,10 @@ export const EnvelopeEditorUploadPage = () => { }); const { mutateAsync: updateEnvelopeItems } = trpc.envelope.item.updateMany.useMutation({ - onSuccess: (data) => { + onSuccess: ({ data }) => { setLocalEnvelope({ envelopeItems: envelope.envelopeItems.map((originalItem) => { - const updatedItem = data.updatedEnvelopeItems.find((item) => item.id === originalItem.id); + const updatedItem = data.find((item) => item.id === originalItem.id); if (updatedItem) { return { @@ -113,36 +113,19 @@ export const EnvelopeEditorUploadPage = () => { setLocalFiles((prev) => [...prev, ...newUploadingFiles]); - const result = await Promise.all( - files.map(async (file, index) => { - try { - const response = await putPdfFile(file); - - // Mark as uploaded (remove from uploading state) - return { - title: file.name, - documentDataId: response.id, - }; - } catch (_error) { - setLocalFiles((prev) => - prev.map((uploadingFile) => - uploadingFile.id === newUploadingFiles[index].id - ? { ...uploadingFile, isError: true, isUploading: false } - : uploadingFile, - ), - ); - } - }), - ); - - const envelopeItemsToCreate = result.filter( - (item): item is { title: string; documentDataId: string } => item !== undefined, - ); - - const { createdEnvelopeItems } = await createEnvelopeItems({ + const payload = { envelopeId: envelope.id, - items: envelopeItemsToCreate, - }).catch((error) => { + } satisfies TCreateEnvelopeItemsPayload; + + const formData = new FormData(); + + formData.append('payload', JSON.stringify(payload)); + + for (const file of files) { + formData.append('files', file); + } + + const { data } = await createEnvelopeItems(formData).catch((error) => { console.error(error); // Set error state on files in batch upload. @@ -164,7 +147,7 @@ export const EnvelopeEditorUploadPage = () => { ); return filteredFiles.concat( - createdEnvelopeItems.map((item) => ({ + data.map((item) => ({ id: item.id, envelopeItemId: item.id, title: item.title, @@ -181,9 +164,17 @@ export const EnvelopeEditorUploadPage = () => { const onFileDelete = (envelopeItemId: string) => { setLocalFiles((prev) => prev.filter((uploadingFile) => uploadingFile.id !== envelopeItemId)); + const fieldsWithoutDeletedItem = envelope.fields.filter( + (field) => field.envelopeItemId !== envelopeItemId, + ); + setLocalEnvelope({ envelopeItems: envelope.envelopeItems.filter((item) => item.id !== envelopeItemId), + fields: envelope.fields.filter((field) => field.envelopeItemId !== envelopeItemId), }); + + // Reset editor fields. + editorFields.resetForm(fieldsWithoutDeletedItem); }; /** @@ -202,7 +193,6 @@ export const EnvelopeEditorUploadPage = () => { debouncedUpdateEnvelopeItems(items); }; - // Todo: Envelopes - Sync into envelopes data const debouncedUpdateEnvelopeItems = useDebounceFunction((files: LocalFile[]) => { void updateEnvelopeItems({ envelopeId: envelope.id, diff --git a/apps/remix/app/components/general/envelope-editor/envelope-file-selector.tsx b/apps/remix/app/components/general/envelope-editor/envelope-file-selector.tsx index 1d570bed7..144a2bc41 100644 --- a/apps/remix/app/components/general/envelope-editor/envelope-file-selector.tsx +++ b/apps/remix/app/components/general/envelope-editor/envelope-file-selector.tsx @@ -29,7 +29,7 @@ export const EnvelopeItemSelector = ({ {...buttonProps} >
diff --git a/apps/remix/app/components/general/envelope-editor/envelope-generic-page-renderer.tsx b/apps/remix/app/components/general/envelope-editor/envelope-generic-page-renderer.tsx index f6ae5c5d7..370d35240 100644 --- a/apps/remix/app/components/general/envelope-editor/envelope-generic-page-renderer.tsx +++ b/apps/remix/app/components/general/envelope-editor/envelope-generic-page-renderer.tsx @@ -1,6 +1,7 @@ import { useEffect, useMemo } from 'react'; import { useLingui } from '@lingui/react/macro'; +import { DocumentStatus, type Recipient, SigningStatus } from '@prisma/client'; import type Konva from 'konva'; import { usePageRenderer } from '@documenso/lib/client-only/hooks/use-page-renderer'; @@ -8,11 +9,24 @@ import { useCurrentEnvelopeRender } from '@documenso/lib/client-only/providers/e import type { TEnvelope } from '@documenso/lib/types/envelope'; import { renderField } from '@documenso/lib/universal/field-renderer/render-field'; import { getClientSideFieldTranslations } from '@documenso/lib/utils/fields'; +import { EnvelopeRecipientFieldTooltip } from '@documenso/ui/components/document/envelope-recipient-field-tooltip'; + +type GenericLocalField = TEnvelope['fields'][number] & { + recipient: Pick; +}; export default function EnvelopeGenericPageRenderer() { const { i18n } = useLingui(); - const { currentEnvelopeItem, fields, getRecipientColorKey } = useCurrentEnvelopeRender(); + const { + envelopeStatus, + currentEnvelopeItem, + fields, + recipients, + getRecipientColorKey, + setRenderError, + overrideSettings, + } = useCurrentEnvelopeRender(); const { stage, @@ -28,44 +42,81 @@ export default function EnvelopeGenericPageRenderer() { const { _className, scale } = pageContext; - const localPageFields = useMemo( - () => - fields.filter( + const localPageFields = useMemo((): GenericLocalField[] => { + if (envelopeStatus === DocumentStatus.COMPLETED) { + return []; + } + + return fields + .filter( (field) => field.page === pageContext.pageNumber && field.envelopeItemId === currentEnvelopeItem?.id, - ), - [fields, pageContext.pageNumber], - ); + ) + .map((field) => { + const recipient = recipients.find((recipient) => recipient.id === field.recipientId); - const renderFieldOnLayer = (field: TEnvelope['fields'][number]) => { + if (!recipient) { + throw new Error(`Recipient not found for field ${field.id}`); + } + + const isInserted = recipient.signingStatus === SigningStatus.SIGNED && field.inserted; + + return { + ...field, + inserted: isInserted, + customText: isInserted ? field.customText : '', + recipient, + }; + }) + .filter( + ({ inserted, fieldMeta, recipient }) => + (recipient.signingStatus === SigningStatus.SIGNED ? inserted : true) || + fieldMeta?.readOnly, + ); + }, [fields, pageContext.pageNumber, currentEnvelopeItem?.id, recipients]); + + const unsafeRenderFieldOnLayer = (field: GenericLocalField) => { if (!pageLayer.current) { console.error('Layer not loaded yet'); return; } + const fieldTranslations = getClientSideFieldTranslations(i18n); + renderField({ scale, pageLayer: pageLayer.current, field: { renderId: field.id.toString(), ...field, - customText: '', width: Number(field.width), height: Number(field.height), positionX: Number(field.positionX), positionY: Number(field.positionY), - inserted: false, fieldMeta: field.fieldMeta, + signature: { + signatureImageAsBase64: '', + typedSignature: fieldTranslations.SIGNATURE, + }, }, - translations: getClientSideFieldTranslations(i18n), + translations: fieldTranslations, pageWidth: unscaledViewport.width, pageHeight: unscaledViewport.height, color: getRecipientColorKey(field.recipientId), editable: false, - mode: 'sign', + mode: overrideSettings?.mode ?? 'edit', }); }; + const renderFieldOnLayer = (field: GenericLocalField) => { + try { + unsafeRenderFieldOnLayer(field); + } catch (err) { + console.error(err); + setRenderError(true); + } + }; + /** * Initialize the Konva page canvas and all fields and interactions. */ @@ -113,6 +164,16 @@ export default function EnvelopeGenericPageRenderer() { className="relative w-full" key={`${currentEnvelopeItem.id}-renderer-${pageContext.pageNumber}`} > + {overrideSettings?.showRecipientTooltip && + localPageFields.map((field) => ( + + ))} + {/* The element Konva will inject it's canvas into. */}
diff --git a/apps/remix/app/components/general/envelope-signing/envelope-signer-form.tsx b/apps/remix/app/components/general/envelope-signing/envelope-signer-form.tsx index 98a9173ee..e010e5821 100644 --- a/apps/remix/app/components/general/envelope-signing/envelope-signer-form.tsx +++ b/apps/remix/app/components/general/envelope-signing/envelope-signer-form.tsx @@ -8,6 +8,8 @@ import { Label } from '@documenso/ui/primitives/label'; import { RadioGroup, RadioGroupItem } from '@documenso/ui/primitives/radio-group'; import { SignaturePadDialog } from '@documenso/ui/primitives/signature-pad/signature-pad-dialog'; +import { useEmbedSigningContext } from '~/components/embed/embed-signing-context'; + import { useRequiredEnvelopeSigningContext } from '../document-signing/envelope-signing-provider'; export default function EnvelopeSignerForm() { @@ -25,6 +27,8 @@ export default function EnvelopeSignerForm() { setSelectedAssistantRecipientId, } = useRequiredEnvelopeSigningContext(); + const { isNameLocked, isEmailLocked } = useEmbedSigningContext() || {}; + const hasSignatureField = useMemo(() => { return recipientFields.some((field) => field.type === FieldType.SIGNATURE); }, [recipientFields]); @@ -37,7 +41,7 @@ export default function EnvelopeSignerForm() { if (recipient.role === RecipientRole.ASSISTANT) { return ( -
+
setFullName(e.target.value.trimStart())} + disabled={isNameLocked} + onChange={(e) => !isNameLocked && setFullName(e.target.value.trimStart())} />
diff --git a/apps/remix/app/components/general/envelope-signing/envelope-signer-header.tsx b/apps/remix/app/components/general/envelope-signing/envelope-signer-header.tsx index b48ee0c55..bf7b2a971 100644 --- a/apps/remix/app/components/general/envelope-signing/envelope-signer-header.tsx +++ b/apps/remix/app/components/general/envelope-signing/envelope-signer-header.tsx @@ -16,6 +16,7 @@ import { import { Separator } from '@documenso/ui/primitives/separator'; import { EnvelopeDownloadDialog } from '~/components/dialogs/envelope-download-dialog'; +import { useEmbedSigningContext } from '~/components/embed/embed-signing-context'; import { BrandingLogo } from '~/components/general/branding-logo'; import { BrandingLogoIcon } from '../branding-logo-icon'; @@ -28,7 +29,7 @@ export const EnvelopeSignerHeader = () => { useRequiredEnvelopeSigningContext(); return ( -