Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 46d712f4cc | |||
| 62c609f105 | |||
| 4babe9b192 | |||
| 7e422bc3fd |
@@ -1,168 +0,0 @@
|
||||
---
|
||||
date: 2026-02-11
|
||||
title: Cert Page Width Mismatch
|
||||
---
|
||||
|
||||
## Problem
|
||||
|
||||
Certificate and audit log pages are generated with hardcoded A4 dimensions (`PDF_SIZE_A4_72PPI`: 595×842) regardless of the actual document page sizes. When the source document uses a different page size (e.g., Letter, Legal, or custom dimensions), the certificate/audit log pages end up with a different width than the document pages. This causes problems with courts that expect uniform page dimensions throughout a PDF.
|
||||
|
||||
**Both width and height must match** the last page of the document so the entire PDF prints uniformly.
|
||||
|
||||
**Root cause**: In `seal-document.handler.ts` (lines 186-187), the certificate payload always uses:
|
||||
|
||||
```ts
|
||||
pageWidth: PDF_SIZE_A4_72PPI.width, // 595
|
||||
pageHeight: PDF_SIZE_A4_72PPI.height, // 842
|
||||
```
|
||||
|
||||
These hardcoded values flow into `generateCertificatePdf`, `generateAuditLogPdf`, `renderCertificate`, and `renderAuditLogs` — all of which use `pageWidth`/`pageHeight` to set Konva stage dimensions and layout content.
|
||||
|
||||
## Key Files
|
||||
|
||||
| File | Role |
|
||||
| ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
|
||||
| `packages/lib/jobs/definitions/internal/seal-document.handler.ts` | Orchestrates sealing; passes page dimensions to cert/audit generators |
|
||||
| `packages/lib/constants/pdf.ts` | Defines `PDF_SIZE_A4_72PPI` (595×842) |
|
||||
| `packages/lib/server-only/pdf/generate-certificate-pdf.ts` | Generates certificate PDF; accepts `pageWidth`/`pageHeight` |
|
||||
| `packages/lib/server-only/pdf/generate-audit-log-pdf.ts` | Generates audit log PDF; accepts `pageWidth`/`pageHeight` |
|
||||
| `packages/lib/server-only/pdf/render-certificate.ts` | Renders certificate pages via Konva; uses `pageWidth`/`pageHeight` for stage + layout |
|
||||
| `packages/lib/server-only/pdf/render-audit-logs.ts` | Renders audit log pages via Konva; uses `pageWidth`/`pageHeight` for stage + layout |
|
||||
| `packages/lib/server-only/pdf/get-page-size.ts` | Existing utility — extend with `@libpdf/core` version |
|
||||
| `packages/trpc/server/document-router/download-document-certificate.ts` | Standalone certificate download (also hardcodes A4) |
|
||||
| `packages/trpc/server/document-router/download-document-audit-logs.ts` | Standalone audit log download (also hardcodes A4) |
|
||||
|
||||
## Architecture
|
||||
|
||||
### Current Flow
|
||||
|
||||
1. **One cert PDF + one audit log PDF** generated per envelope with hardcoded A4 dims
|
||||
2. Both appended to **every** envelope item (document) via `decorateAndSignPdf` → `pdfDoc.copyPagesFrom()`
|
||||
3. The audit log is envelope-level (all recipients, all events across all docs) — one per envelope, not per document
|
||||
|
||||
### Multi-Document Envelopes
|
||||
|
||||
- V1 envelopes: single document only
|
||||
- V2 envelopes: support multiple documents (envelope items)
|
||||
- Each envelope item gets both cert + audit log pages appended to it
|
||||
- If documents have different page sizes → need size-matched cert/audit for each
|
||||
|
||||
### Reading Page Dimensions (`@libpdf/core` only)
|
||||
|
||||
Use `@libpdf/core`'s `PDF` class — NOT `@cantoo/pdf-lib`:
|
||||
|
||||
```ts
|
||||
const pdfDoc = await PDF.load(pdfData);
|
||||
const lastPage = pdfDoc.getPage(pdfDoc.getPageCount() - 1);
|
||||
const { width, height } = lastPage; // e.g. 612, 792 for Letter
|
||||
```
|
||||
|
||||
Already used this way in `seal-document.handler.ts` lines 403-410 for V2 field insertion.
|
||||
"Last page" = last page of the original document, before cert/audit pages are appended.
|
||||
|
||||
### Content Layout Adaptation
|
||||
|
||||
Both renderers already handle variable dimensions gracefully:
|
||||
|
||||
- **Width**: `render-certificate.ts:713` / `render-audit-logs.ts:588` — `Math.min(pageWidth - minimumMargin * 2, contentMaxWidth)` with `contentMaxWidth = 768`. Wider pages get more margin, narrower pages tighter margins.
|
||||
- **Height**: Both renderers paginate content into pages using `groupRowsIntoPages()` which respects `pageHeight` via `maxTableHeight = pageHeight - pageTopMargin - pageBottomMargin`. Shorter pages just mean more pages; taller pages fit more rows per page.
|
||||
|
||||
### Playwright PDF Path — Out of Scope
|
||||
|
||||
The `NEXT_PRIVATE_USE_PLAYWRIGHT_PDF` toggle enables a deprecated Playwright-based PDF generation path (`get-certificate-pdf.ts`, `get-audit-logs-pdf.ts`) that also hardcodes `format: 'A4'` in `page.pdf()`. This path is **not being updated** as part of this fix:
|
||||
|
||||
- Both files are marked `@deprecated`
|
||||
- The Konva-based path is the default and recommended path
|
||||
- The Playwright path is behind a feature flag and will be removed
|
||||
|
||||
No changes needed. Add a code comment noting the A4 limitation if the Playwright path is ever re-enabled.
|
||||
|
||||
## Plan
|
||||
|
||||
### 1. Extend `get-page-size.ts` with `@libpdf/core` utility
|
||||
|
||||
Add a `getLastPageDimensions` function to the existing `packages/lib/server-only/pdf/get-page-size.ts` file. This consolidates page-size logic in one place (the file already has the legacy `@cantoo/pdf-lib` version).
|
||||
|
||||
```ts
|
||||
export const getLastPageDimensions = (pdfDoc: PDF): { width: number; height: number } => {
|
||||
const lastPage = pdfDoc.getPage(pdfDoc.getPageCount() - 1);
|
||||
const width = Math.round(lastPage.width);
|
||||
const height = Math.round(lastPage.height);
|
||||
|
||||
if (width < MIN_CERT_PAGE_WIDTH || height < MIN_CERT_PAGE_HEIGHT) {
|
||||
return { width: PDF_SIZE_A4_72PPI.width, height: PDF_SIZE_A4_72PPI.height };
|
||||
}
|
||||
|
||||
return { width, height };
|
||||
};
|
||||
```
|
||||
|
||||
**Dimension rounding**: `Math.round()` both width and height. PDF points at 72ppi are typically whole numbers; rounding avoids spurious float-precision mismatches (e.g., 612.0 vs 612.00001) that would cause unnecessary duplicate cert/audit PDF generation.
|
||||
|
||||
**Minimum page dimensions**: Enforce a minimum threshold (e.g., 300pt for both width and height). If either dimension falls below the minimum, fall back to A4 (595×842). The certificate and audit log renderers have headers, table rows, margins, and QR codes that require a minimum viable area.
|
||||
|
||||
### 2. Read last page dimensions from each envelope item's PDF
|
||||
|
||||
In `seal-document.handler.ts`, before generating cert/audit PDFs:
|
||||
|
||||
- For each `envelopeItem`, load the PDF and read the **last page's width and height** using `getLastPageDimensions`
|
||||
- Use `PDF.load()` then pass the loaded doc to the utility
|
||||
|
||||
**Resealing consideration**: When `isResealing` is true, envelope items are remapped to use `initialData` (lines 152-158) before this point. Page-size extraction must operate on the same data source that `decorateAndSignPdf` will use. Since the `envelopeItems` array is already remapped by the time we read dimensions, reading from `envelopeItem.documentData` will naturally give the correct (initial) data. No special handling needed beyond ensuring the dimension read happens **after** the resealing remap.
|
||||
|
||||
### 3. Generate cert/audit PDFs per unique page size
|
||||
|
||||
Current flow generates one cert + one audit log doc per envelope. Change to:
|
||||
|
||||
1. Collect `{ width, height }` of the last page for each envelope item
|
||||
2. Deduplicate by `"${width}x${height}"` key (using the already-rounded integers)
|
||||
3. For each unique size, generate cert PDF and audit log PDF with those dimensions
|
||||
4. Store in a `Map<string, { certificateDoc, auditLogDoc }>` keyed by `"${width}x${height}"`
|
||||
|
||||
For the common single-document case, this is one generation — same perf as today.
|
||||
|
||||
### 4. Thread the correct docs into `decorateAndSignPdf`
|
||||
|
||||
In the envelope item loop, look up the item's last-page dimensions in the map and pass the matching cert/audit docs. Signature of `decorateAndSignPdf` doesn't change — it still receives a single `certificateDoc` and `auditLogDoc`, just the right ones per item.
|
||||
|
||||
### 5. Update standalone download routes
|
||||
|
||||
`download-document-certificate.ts` and `download-document-audit-logs.ts` also hardcode A4:
|
||||
|
||||
- Both routes have `documentId` which maps to a specific envelope item
|
||||
- Fetch **that specific document's** PDF data, load it, read last page width + height via `getLastPageDimensions`
|
||||
- Pass `{ pageWidth, pageHeight }` to the generator
|
||||
- This ensures the standalone download matches the dimensions the user would see in the sealed PDF for that document
|
||||
|
||||
### 6. Edge cases
|
||||
|
||||
| Scenario | Behavior |
|
||||
| --------------------------------------- | ------------------------------------------------------------------------------------------- |
|
||||
| Mixed page sizes within one PDF | Use last page's dimensions (per spec) |
|
||||
| Page dimensions below minimum threshold | Fall back to A4 (595×842) |
|
||||
| Landscape pages | width/height just swap roles; renderers adapt via `Math.min()` capping. No special handling |
|
||||
| Fallback if page dims unreadable | Default to A4 (595×842) |
|
||||
| Resealing | Dimensions read after `initialData` remap — correct source automatically |
|
||||
| Playwright PDF path enabled | Remains A4 — out of scope, deprecated |
|
||||
| Single-doc envelope (most common) | One generation, same perf as today |
|
||||
| Multi-doc envelope, same page sizes | Dedup key matches → one generation |
|
||||
| Multi-doc envelope, different sizes | One generation per unique size |
|
||||
|
||||
### 7. Tests
|
||||
|
||||
- Add assertion-based E2E test (no visual regression / reference images needed)
|
||||
- Seal a Letter-size (612×792) PDF through the full flow
|
||||
- Load the sealed output and assert all pages (document + cert + audit) have matching width/height
|
||||
- Can be added to `envelope-alignment.spec.ts` or as a new focused test
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. **Extend `get-page-size.ts`** — add `getLastPageDimensions(pdfDoc: PDF): { width: number; height: number }` using `@libpdf/core`, with `Math.round()` and minimum dimension enforcement
|
||||
2. **In `seal-document.handler.ts`**:
|
||||
a. After the resealing remap (line ~159), load each envelope item's PDF via `PDF.load()` and collect last page `{ width, height }` using `getLastPageDimensions`
|
||||
b. Deduplicate by `"${width}x${height}"` key
|
||||
c. Generate cert/audit PDFs per unique size (parallel via `Promise.all`)
|
||||
d. In envelope item loop, look up matching cert/audit doc by size key
|
||||
3. **Fix `download-document-certificate.ts`** — load the specific document's PDF, read last page dims via `getLastPageDimensions`, pass to generator
|
||||
4. **Fix `download-document-audit-logs.ts`** — same as above, using the specific `documentId`'s PDF
|
||||
5. **Add E2E test** — assertion-based test with a Letter-size document verifying all page dimensions match after sealing
|
||||
@@ -116,11 +116,9 @@ export function AssistantConfirmationDialog({
|
||||
{!isEditingNextSigner && (
|
||||
<div>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
<Trans>
|
||||
The next recipient to sign this document will be{' '}
|
||||
<span className="font-semibold">{form.watch('name')}</span> (
|
||||
<span className="font-semibold">{form.watch('email')}</span>).
|
||||
</Trans>
|
||||
The next recipient to sign this document will be{' '}
|
||||
<span className="font-semibold">{form.watch('name')}</span> (
|
||||
<span className="font-semibold">{form.watch('email')}</span>).
|
||||
</p>
|
||||
|
||||
<Button
|
||||
|
||||
@@ -258,10 +258,10 @@ export const EnvelopeDistributeDialog = ({
|
||||
>
|
||||
<TabsList className="w-full">
|
||||
<TabsTrigger className="w-full" value={DocumentDistributionMethod.EMAIL}>
|
||||
<Trans>Email</Trans>
|
||||
Email
|
||||
</TabsTrigger>
|
||||
<TabsTrigger className="w-full" value={DocumentDistributionMethod.NONE}>
|
||||
<Trans>None</Trans>
|
||||
None
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
|
||||
@@ -127,11 +127,7 @@ export const TeamCreateDialog = ({ trigger, onCreated, ...props }: TeamCreateDia
|
||||
};
|
||||
|
||||
const mapTextToUrl = (text: string) => {
|
||||
return text
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, '-');
|
||||
return text.toLowerCase().replace(/\s+/g, '-');
|
||||
};
|
||||
|
||||
const dialogState = useMemo(() => {
|
||||
@@ -264,7 +260,7 @@ export const TeamCreateDialog = ({ trigger, onCreated, ...props }: TeamCreateDia
|
||||
<Input className="bg-background" {...field} />
|
||||
</FormControl>
|
||||
{!form.formState.errors.teamUrl && (
|
||||
<span className="text-xs font-normal text-foreground/50">
|
||||
<span className="text-foreground/50 text-xs font-normal">
|
||||
{field.value ? (
|
||||
`${NEXT_PUBLIC_WEBAPP_URL()}/t/${field.value}`
|
||||
) : (
|
||||
@@ -292,7 +288,7 @@ export const TeamCreateDialog = ({ trigger, onCreated, ...props }: TeamCreateDia
|
||||
/>
|
||||
|
||||
<label
|
||||
className="ml-2 text-sm text-muted-foreground"
|
||||
className="text-muted-foreground ml-2 text-sm"
|
||||
htmlFor="inherit-members"
|
||||
>
|
||||
<Trans>Allow all organisation members to access this team</Trans>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { Loader } from 'lucide-react';
|
||||
|
||||
export const EmbedClientLoading = () => {
|
||||
@@ -6,9 +5,7 @@ export const EmbedClientLoading = () => {
|
||||
<div className="bg-background fixed left-0 top-0 z-[9999] flex h-full w-full items-center justify-center">
|
||||
<Loader className="mr-2 h-4 w-4 animate-spin" />
|
||||
|
||||
<span>
|
||||
<Trans>Loading...</Trans>
|
||||
</span>
|
||||
<span>Loading...</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -499,9 +499,7 @@ export const EmbedDirectTemplateClientPage = ({
|
||||
|
||||
{!hidePoweredBy && (
|
||||
<div className="fixed bottom-0 left-0 z-40 rounded-tr bg-primary px-2 py-1 text-xs font-medium text-primary-foreground opacity-60 hover:opacity-100">
|
||||
<span>
|
||||
<Trans>Powered by</Trans>
|
||||
</span>
|
||||
<span>Powered by</span>
|
||||
<BrandingLogo className="ml-2 inline-block h-[14px]" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -510,9 +510,7 @@ export const EmbedSignDocumentV1ClientPage = ({
|
||||
|
||||
{!hidePoweredBy && (
|
||||
<div className="fixed bottom-0 left-0 z-40 rounded-tr bg-primary px-2 py-1 text-xs font-medium text-primary-foreground opacity-60 hover:opacity-100">
|
||||
<span>
|
||||
<Trans>Powered by</Trans>
|
||||
</span>
|
||||
<span>Powered by</span>
|
||||
<BrandingLogo className="ml-2 inline-block h-[14px]" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useEffect, useLayoutEffect, useState } from 'react';
|
||||
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
|
||||
|
||||
@@ -26,7 +25,7 @@ export const EmbedSignDocumentV2ClientPage = ({
|
||||
}: EmbedSignDocumentV2ClientPageProps) => {
|
||||
const { _ } = useLingui();
|
||||
|
||||
const { envelope, recipient, envelopeData, setFullName, setEmail, fullName } =
|
||||
const { envelope, recipient, envelopeData, setFullName, fullName } =
|
||||
useRequiredEnvelopeSigningContext();
|
||||
|
||||
const { isCompleted, isRejected, recipientSignature } = envelopeData;
|
||||
@@ -36,7 +35,6 @@ export const EmbedSignDocumentV2ClientPage = ({
|
||||
const [hasFinishedInit, setHasFinishedInit] = useState(false);
|
||||
const [allowDocumentRejection, setAllowDocumentRejection] = useState(false);
|
||||
const [isNameLocked, setIsNameLocked] = useState(false);
|
||||
const [isEmailLocked, setIsEmailLocked] = useState(envelope.type === EnvelopeType.DOCUMENT);
|
||||
|
||||
const onDocumentCompleted = (data: {
|
||||
token: string;
|
||||
@@ -134,17 +132,6 @@ export const EmbedSignDocumentV2ClientPage = ({
|
||||
// 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);
|
||||
|
||||
if (envelope.type === EnvelopeType.TEMPLATE) {
|
||||
if (!isCompleted && data.email) {
|
||||
setEmail(data.email);
|
||||
}
|
||||
|
||||
if (data.email) {
|
||||
setIsEmailLocked(!!data.lockEmail);
|
||||
}
|
||||
}
|
||||
|
||||
setAllowDocumentRejection(!!data.allowDocumentRejection);
|
||||
|
||||
if (data.darkModeDisabled) {
|
||||
@@ -226,7 +213,6 @@ export const EmbedSignDocumentV2ClientPage = ({
|
||||
return (
|
||||
<EmbedSigningProvider
|
||||
isNameLocked={isNameLocked}
|
||||
isEmailLocked={isEmailLocked}
|
||||
hidePoweredBy={hidePoweredBy}
|
||||
allowDocumentRejection={allowDocumentRejection}
|
||||
onDocumentCompleted={onDocumentCompleted}
|
||||
|
||||
@@ -124,9 +124,7 @@ export const AppNavMobile = ({ isMenuOpen, onMenuOpenChange }: AppNavMobileProps
|
||||
</div>
|
||||
|
||||
<p className="text-muted-foreground text-sm">
|
||||
© {new Date().getFullYear()} Documenso, Inc.
|
||||
<br />
|
||||
<Trans>All rights reserved.</Trans>
|
||||
© {new Date().getFullYear()} Documenso, Inc. <br /> All rights reserved.
|
||||
</p>
|
||||
</div>
|
||||
</SheetContent>
|
||||
|
||||
@@ -118,9 +118,7 @@ export const BillingPlans = ({ plans }: BillingPlansProps) => {
|
||||
|
||||
{price.product.features && price.product.features.length > 0 && (
|
||||
<div className="mt-4 text-muted-foreground">
|
||||
<div className="text-sm font-medium">
|
||||
<Trans>Includes:</Trans>
|
||||
</div>
|
||||
<div className="text-sm font-medium">Includes:</div>
|
||||
|
||||
<ul className="mt-1 divide-y text-sm">
|
||||
{price.product.features.map((feature, index) => (
|
||||
|
||||
@@ -4,7 +4,6 @@ import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { RecipientRole } from '@prisma/client';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { match } from 'ts-pattern';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { AppError } from '@documenso/lib/errors/app-error';
|
||||
@@ -28,6 +27,7 @@ import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-p
|
||||
|
||||
export type DocumentSigningAuth2FAProps = {
|
||||
actionTarget?: 'FIELD' | 'DOCUMENT';
|
||||
actionVerb?: string;
|
||||
open: boolean;
|
||||
onOpenChange: (value: boolean) => void;
|
||||
onReauthFormSubmit: (values?: TRecipientActionAuth) => Promise<void> | void;
|
||||
@@ -44,6 +44,7 @@ type T2FAAuthFormSchema = z.infer<typeof Z2FAAuthFormSchema>;
|
||||
|
||||
export const DocumentSigningAuth2FA = ({
|
||||
actionTarget = 'FIELD',
|
||||
actionVerb = 'sign',
|
||||
onReauthFormSubmit,
|
||||
open,
|
||||
onOpenChange,
|
||||
@@ -100,39 +101,14 @@ export const DocumentSigningAuth2FA = ({
|
||||
<Alert variant="warning">
|
||||
<AlertDescription>
|
||||
<p>
|
||||
{match({ role: recipient.role, actionTarget })
|
||||
.with({ role: RecipientRole.SIGNER, actionTarget: 'FIELD' }, () => (
|
||||
<Trans>You need to setup 2FA to sign this field.</Trans>
|
||||
))
|
||||
.with({ role: RecipientRole.SIGNER, actionTarget: 'DOCUMENT' }, () => (
|
||||
<Trans>You need to setup 2FA to sign this document.</Trans>
|
||||
))
|
||||
.with({ role: RecipientRole.APPROVER, actionTarget: 'FIELD' }, () => (
|
||||
<Trans>You need to setup 2FA to approve this field.</Trans>
|
||||
))
|
||||
.with({ role: RecipientRole.APPROVER, actionTarget: 'DOCUMENT' }, () => (
|
||||
<Trans>You need to setup 2FA to approve this document.</Trans>
|
||||
))
|
||||
.with({ role: RecipientRole.VIEWER, actionTarget: 'FIELD' }, () => (
|
||||
<Trans>You need to setup 2FA to view this field.</Trans>
|
||||
))
|
||||
.with({ role: RecipientRole.VIEWER, actionTarget: 'DOCUMENT' }, () => (
|
||||
<Trans>You need to setup 2FA to mark this document as viewed.</Trans>
|
||||
))
|
||||
.with({ role: RecipientRole.CC, actionTarget: 'FIELD' }, () => (
|
||||
<Trans>You need to setup 2FA to view this field.</Trans>
|
||||
))
|
||||
.with({ role: RecipientRole.CC, actionTarget: 'DOCUMENT' }, () => (
|
||||
<Trans>You need to setup 2FA to view this document.</Trans>
|
||||
))
|
||||
.with({ role: RecipientRole.ASSISTANT, actionTarget: 'FIELD' }, () => (
|
||||
<Trans>You need to setup 2FA to assist with this field.</Trans>
|
||||
))
|
||||
.with({ role: RecipientRole.ASSISTANT, actionTarget: 'DOCUMENT' }, () => (
|
||||
<Trans>You need to setup 2FA to assist with this document.</Trans>
|
||||
))
|
||||
.exhaustive()}
|
||||
{recipient.role === RecipientRole.VIEWER && actionTarget === 'DOCUMENT' ? (
|
||||
<Trans>You need to setup 2FA to mark this document as viewed.</Trans>
|
||||
) : (
|
||||
// Todo: Translate
|
||||
`You need to setup 2FA to ${actionVerb.toLowerCase()} this ${actionTarget.toLowerCase()}.`
|
||||
)}
|
||||
</p>
|
||||
|
||||
<p className="mt-2">
|
||||
<Trans>
|
||||
By enabling 2FA, you will be required to enter a code from your authenticator app
|
||||
@@ -162,9 +138,7 @@ export const DocumentSigningAuth2FA = ({
|
||||
name="token"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel required>
|
||||
<Trans>2FA token</Trans>
|
||||
</FormLabel>
|
||||
<FormLabel required>2FA token</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<PinInput {...field} value={field.value ?? ''} maxLength={6}>
|
||||
|
||||
@@ -2,7 +2,6 @@ import { useState } from 'react';
|
||||
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import { RecipientRole } from '@prisma/client';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { authClient } from '@documenso/auth/client';
|
||||
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
|
||||
@@ -14,11 +13,13 @@ import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-p
|
||||
|
||||
export type DocumentSigningAuthAccountProps = {
|
||||
actionTarget?: 'FIELD' | 'DOCUMENT';
|
||||
actionVerb?: string;
|
||||
onOpenChange: (value: boolean) => void;
|
||||
};
|
||||
|
||||
export const DocumentSigningAuthAccount = ({
|
||||
actionTarget = 'FIELD',
|
||||
actionVerb = 'sign',
|
||||
onOpenChange,
|
||||
}: DocumentSigningAuthAccountProps) => {
|
||||
const { recipient, isDirectTemplate } = useRequiredDocumentSigningAuthContext();
|
||||
@@ -54,110 +55,32 @@ export const DocumentSigningAuthAccount = ({
|
||||
<fieldset disabled={isSigningOut} className="space-y-4">
|
||||
<Alert variant="warning">
|
||||
<AlertDescription>
|
||||
<span>
|
||||
{match({ role: recipient.role, actionTarget })
|
||||
.with({ role: RecipientRole.SIGNER, actionTarget: 'FIELD' }, () =>
|
||||
isDirectTemplate ? (
|
||||
<Trans>To sign this field, you need to be logged in.</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
To sign this field, you need to be logged in as{' '}
|
||||
<strong>{recipient.email}</strong>
|
||||
</Trans>
|
||||
),
|
||||
)
|
||||
.with({ role: RecipientRole.SIGNER, actionTarget: 'DOCUMENT' }, () =>
|
||||
isDirectTemplate ? (
|
||||
<Trans>To sign this document, you need to be logged in.</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
To sign this document, you need to be logged in as{' '}
|
||||
<strong>{recipient.email}</strong>
|
||||
</Trans>
|
||||
),
|
||||
)
|
||||
.with({ role: RecipientRole.APPROVER, actionTarget: 'FIELD' }, () =>
|
||||
isDirectTemplate ? (
|
||||
<Trans>To approve this field, you need to be logged in.</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
To approve this field, you need to be logged in as{' '}
|
||||
<strong>{recipient.email}</strong>
|
||||
</Trans>
|
||||
),
|
||||
)
|
||||
.with({ role: RecipientRole.APPROVER, actionTarget: 'DOCUMENT' }, () =>
|
||||
isDirectTemplate ? (
|
||||
<Trans>To approve this document, you need to be logged in.</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
To approve this document, you need to be logged in as{' '}
|
||||
<strong>{recipient.email}</strong>
|
||||
</Trans>
|
||||
),
|
||||
)
|
||||
.with({ role: RecipientRole.VIEWER, actionTarget: 'FIELD' }, () =>
|
||||
isDirectTemplate ? (
|
||||
<Trans>To view this field, you need to be logged in.</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
To view this field, you need to be logged in as{' '}
|
||||
<strong>{recipient.email}</strong>
|
||||
</Trans>
|
||||
),
|
||||
)
|
||||
.with({ role: RecipientRole.VIEWER, actionTarget: 'DOCUMENT' }, () =>
|
||||
isDirectTemplate ? (
|
||||
<Trans>To mark this document as viewed, you need to be logged in.</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
To mark this document as viewed, you need to be logged in as{' '}
|
||||
<strong>{recipient.email}</strong>
|
||||
</Trans>
|
||||
),
|
||||
)
|
||||
.with({ role: RecipientRole.CC, actionTarget: 'FIELD' }, () =>
|
||||
isDirectTemplate ? (
|
||||
<Trans>To view this field, you need to be logged in.</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
To view this field, you need to be logged in as{' '}
|
||||
<strong>{recipient.email}</strong>
|
||||
</Trans>
|
||||
),
|
||||
)
|
||||
.with({ role: RecipientRole.CC, actionTarget: 'DOCUMENT' }, () =>
|
||||
isDirectTemplate ? (
|
||||
<Trans>To view this document, you need to be logged in.</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
To view this document, you need to be logged in as{' '}
|
||||
<strong>{recipient.email}</strong>
|
||||
</Trans>
|
||||
),
|
||||
)
|
||||
.with({ role: RecipientRole.ASSISTANT, actionTarget: 'FIELD' }, () =>
|
||||
isDirectTemplate ? (
|
||||
<Trans>To assist with this field, you need to be logged in.</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
To assist with this field, you need to be logged in as{' '}
|
||||
<strong>{recipient.email}</strong>
|
||||
</Trans>
|
||||
),
|
||||
)
|
||||
.with({ role: RecipientRole.ASSISTANT, actionTarget: 'DOCUMENT' }, () =>
|
||||
isDirectTemplate ? (
|
||||
<Trans>To assist with this document, you need to be logged in.</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
To assist with this document, you need to be logged in as{' '}
|
||||
<strong>{recipient.email}</strong>
|
||||
</Trans>
|
||||
),
|
||||
)
|
||||
.exhaustive()}
|
||||
</span>
|
||||
{actionTarget === 'DOCUMENT' && recipient.role === RecipientRole.VIEWER ? (
|
||||
<span>
|
||||
{isDirectTemplate ? (
|
||||
<Trans>To mark this document as viewed, you need to be logged in.</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
To mark this document as viewed, you need to be logged in as{' '}
|
||||
<strong>{recipient.email}</strong>
|
||||
</Trans>
|
||||
)}
|
||||
</span>
|
||||
) : (
|
||||
<span>
|
||||
{isDirectTemplate ? (
|
||||
<Trans>
|
||||
To {actionVerb.toLowerCase()} this {actionTarget.toLowerCase()}, you need to be
|
||||
logged in.
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
To {actionVerb.toLowerCase()} this {actionTarget.toLowerCase()}, you need to be
|
||||
logged in as <strong>{recipient.email}</strong>
|
||||
</Trans>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import { RecipientRole } from '@prisma/client';
|
||||
import { browserSupportsWebAuthn, startAuthentication } from '@simplewebauthn/browser';
|
||||
import { Loader } from 'lucide-react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { match } from 'ts-pattern';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { AppError } from '@documenso/lib/errors/app-error';
|
||||
@@ -39,6 +38,7 @@ import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-p
|
||||
|
||||
export type DocumentSigningAuthPasskeyProps = {
|
||||
actionTarget?: 'FIELD' | 'DOCUMENT';
|
||||
actionVerb?: string;
|
||||
open: boolean;
|
||||
onOpenChange: (value: boolean) => void;
|
||||
onReauthFormSubmit: (values?: TRecipientActionAuth) => Promise<void> | void;
|
||||
@@ -52,6 +52,7 @@ type TPasskeyAuthFormSchema = z.infer<typeof ZPasskeyAuthFormSchema>;
|
||||
|
||||
export const DocumentSigningAuthPasskey = ({
|
||||
actionTarget = 'FIELD',
|
||||
actionVerb = 'sign',
|
||||
onReauthFormSubmit,
|
||||
open,
|
||||
onOpenChange,
|
||||
@@ -127,62 +128,9 @@ export const DocumentSigningAuthPasskey = ({
|
||||
<div className="space-y-4">
|
||||
<Alert variant="warning">
|
||||
<AlertDescription>
|
||||
{match({ role: recipient.role, actionTarget })
|
||||
.with({ role: RecipientRole.SIGNER, actionTarget: 'FIELD' }, () => (
|
||||
<Trans>
|
||||
Your browser does not support passkeys, which is required to sign this field.
|
||||
</Trans>
|
||||
))
|
||||
.with({ role: RecipientRole.SIGNER, actionTarget: 'DOCUMENT' }, () => (
|
||||
<Trans>
|
||||
Your browser does not support passkeys, which is required to sign this document.
|
||||
</Trans>
|
||||
))
|
||||
.with({ role: RecipientRole.APPROVER, actionTarget: 'FIELD' }, () => (
|
||||
<Trans>
|
||||
Your browser does not support passkeys, which is required to approve this field.
|
||||
</Trans>
|
||||
))
|
||||
.with({ role: RecipientRole.APPROVER, actionTarget: 'DOCUMENT' }, () => (
|
||||
<Trans>
|
||||
Your browser does not support passkeys, which is required to approve this
|
||||
document.
|
||||
</Trans>
|
||||
))
|
||||
.with({ role: RecipientRole.VIEWER, actionTarget: 'FIELD' }, () => (
|
||||
<Trans>
|
||||
Your browser does not support passkeys, which is required to view this field.
|
||||
</Trans>
|
||||
))
|
||||
.with({ role: RecipientRole.VIEWER, actionTarget: 'DOCUMENT' }, () => (
|
||||
<Trans>
|
||||
Your browser does not support passkeys, which is required to mark this document as
|
||||
viewed.
|
||||
</Trans>
|
||||
))
|
||||
.with({ role: RecipientRole.CC, actionTarget: 'FIELD' }, () => (
|
||||
<Trans>
|
||||
Your browser does not support passkeys, which is required to view this field.
|
||||
</Trans>
|
||||
))
|
||||
.with({ role: RecipientRole.CC, actionTarget: 'DOCUMENT' }, () => (
|
||||
<Trans>
|
||||
Your browser does not support passkeys, which is required to view this document.
|
||||
</Trans>
|
||||
))
|
||||
.with({ role: RecipientRole.ASSISTANT, actionTarget: 'FIELD' }, () => (
|
||||
<Trans>
|
||||
Your browser does not support passkeys, which is required to assist with this
|
||||
field.
|
||||
</Trans>
|
||||
))
|
||||
.with({ role: RecipientRole.ASSISTANT, actionTarget: 'DOCUMENT' }, () => (
|
||||
<Trans>
|
||||
Your browser does not support passkeys, which is required to assist with this
|
||||
document.
|
||||
</Trans>
|
||||
))
|
||||
.exhaustive()}
|
||||
{/* Todo: Translate */}
|
||||
Your browser does not support passkeys, which is required to {actionVerb.toLowerCase()}{' '}
|
||||
this {actionTarget.toLowerCase()}.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
@@ -230,38 +178,10 @@ export const DocumentSigningAuthPasskey = ({
|
||||
<div className="space-y-4">
|
||||
<Alert variant="warning">
|
||||
<AlertDescription>
|
||||
{match({ role: recipient.role, actionTarget })
|
||||
.with({ role: RecipientRole.SIGNER, actionTarget: 'FIELD' }, () => (
|
||||
<Trans>You need to setup a passkey to sign this field.</Trans>
|
||||
))
|
||||
.with({ role: RecipientRole.SIGNER, actionTarget: 'DOCUMENT' }, () => (
|
||||
<Trans>You need to setup a passkey to sign this document.</Trans>
|
||||
))
|
||||
.with({ role: RecipientRole.APPROVER, actionTarget: 'FIELD' }, () => (
|
||||
<Trans>You need to setup a passkey to approve this field.</Trans>
|
||||
))
|
||||
.with({ role: RecipientRole.APPROVER, actionTarget: 'DOCUMENT' }, () => (
|
||||
<Trans>You need to setup a passkey to approve this document.</Trans>
|
||||
))
|
||||
.with({ role: RecipientRole.VIEWER, actionTarget: 'FIELD' }, () => (
|
||||
<Trans>You need to setup a passkey to view this field.</Trans>
|
||||
))
|
||||
.with({ role: RecipientRole.VIEWER, actionTarget: 'DOCUMENT' }, () => (
|
||||
<Trans>You need to setup a passkey to mark this document as viewed.</Trans>
|
||||
))
|
||||
.with({ role: RecipientRole.CC, actionTarget: 'FIELD' }, () => (
|
||||
<Trans>You need to setup a passkey to view this field.</Trans>
|
||||
))
|
||||
.with({ role: RecipientRole.CC, actionTarget: 'DOCUMENT' }, () => (
|
||||
<Trans>You need to setup a passkey to view this document.</Trans>
|
||||
))
|
||||
.with({ role: RecipientRole.ASSISTANT, actionTarget: 'FIELD' }, () => (
|
||||
<Trans>You need to setup a passkey to assist with this field.</Trans>
|
||||
))
|
||||
.with({ role: RecipientRole.ASSISTANT, actionTarget: 'DOCUMENT' }, () => (
|
||||
<Trans>You need to setup a passkey to assist with this document.</Trans>
|
||||
))
|
||||
.exhaustive()}
|
||||
{/* Todo: Translate */}
|
||||
{recipient.role === RecipientRole.VIEWER && actionTarget === 'DOCUMENT'
|
||||
? 'You need to setup a passkey to mark this document as viewed.'
|
||||
: `You need to setup a passkey to ${actionVerb.toLowerCase()} this ${actionTarget.toLowerCase()}.`}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
@@ -293,9 +213,7 @@ export const DocumentSigningAuthPasskey = ({
|
||||
name="passkeyId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel required>
|
||||
<Trans>Passkey</Trans>
|
||||
</FormLabel>
|
||||
<FormLabel required>Passkey</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Select {...field} onValueChange={field.onChange}>
|
||||
@@ -323,24 +241,20 @@ export const DocumentSigningAuthPasskey = ({
|
||||
|
||||
{formErrorCode && (
|
||||
<Alert variant="destructive">
|
||||
<AlertTitle>
|
||||
<Trans>Unauthorized</Trans>
|
||||
</AlertTitle>
|
||||
<AlertTitle>Unauthorized</AlertTitle>
|
||||
<AlertDescription>
|
||||
<Trans>
|
||||
We were unable to verify your details. Please try again or contact support
|
||||
</Trans>
|
||||
We were unable to verify your details. Please try again or contact support
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="secondary" onClick={() => onOpenChange(false)}>
|
||||
<Trans>Cancel</Trans>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Button type="submit" loading={isCurrentlyAuthenticating}>
|
||||
<Trans>Sign</Trans>
|
||||
Sign
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</div>
|
||||
|
||||
@@ -23,6 +23,8 @@ import { Input } from '@documenso/ui/primitives/input';
|
||||
import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
|
||||
|
||||
export type DocumentSigningAuthPasswordProps = {
|
||||
actionTarget?: 'FIELD' | 'DOCUMENT';
|
||||
actionVerb?: string;
|
||||
open: boolean;
|
||||
onOpenChange: (value: boolean) => void;
|
||||
onReauthFormSubmit: (values?: TRecipientActionAuth) => Promise<void> | void;
|
||||
@@ -38,6 +40,8 @@ const ZPasswordAuthFormSchema = z.object({
|
||||
type TPasswordAuthFormSchema = z.infer<typeof ZPasswordAuthFormSchema>;
|
||||
|
||||
export const DocumentSigningAuthPassword = ({
|
||||
actionTarget = 'FIELD',
|
||||
actionVerb = 'sign',
|
||||
onReauthFormSubmit,
|
||||
open,
|
||||
onOpenChange,
|
||||
|
||||
@@ -162,9 +162,7 @@ export const DocumentSigningAutoSign = ({ recipient, fields }: DocumentSigningAu
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<Trans>Automatically sign fields</Trans>
|
||||
</DialogTitle>
|
||||
<DialogTitle>Automatically sign fields</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="text-muted-foreground max-w-[50ch]">
|
||||
|
||||
@@ -122,9 +122,7 @@ export const DocumentSigningMobileWidget = () => {
|
||||
|
||||
{!hidePoweredBy && (
|
||||
<div className="mt-2 inline-block rounded bg-primary px-2 py-1 text-xs font-medium text-primary-foreground opacity-60 hover:opacity-100 lg:hidden">
|
||||
<span>
|
||||
<Trans>Powered by</Trans>
|
||||
</span>
|
||||
<span>Powered by</span>
|
||||
<BrandingLogo className="ml-2 inline-block h-[14px]" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -252,9 +252,7 @@ export const DocumentSigningPageViewV2 = () => {
|
||||
target="_blank"
|
||||
className="fixed bottom-0 right-0 z-40 hidden cursor-pointer rounded-tl bg-primary px-2 py-1 text-xs font-medium text-primary-foreground opacity-60 hover:opacity-100 lg:block"
|
||||
>
|
||||
<span>
|
||||
<Trans>Powered by</Trans>
|
||||
</span>
|
||||
<span>Powered by</span>
|
||||
<BrandingLogo className="ml-2 inline-block h-[14px]" />
|
||||
</a>
|
||||
)}
|
||||
|
||||
@@ -434,31 +434,15 @@ export default function EnvelopeEditorFieldsPageRenderer() {
|
||||
renderFieldOnLayer(field);
|
||||
});
|
||||
|
||||
// Reconcile selection state with live field nodes after flush/sync updates.
|
||||
const liveSelectedFieldGroups = selectedKonvaFieldGroups.filter((fieldGroup) => {
|
||||
if (!fieldGroup.getStage() || !fieldGroup.getParent()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return localPageFields.some((field) => field.formId === fieldGroup.id());
|
||||
});
|
||||
|
||||
if (liveSelectedFieldGroups.length !== selectedKonvaFieldGroups.length) {
|
||||
setSelectedFields(liveSelectedFieldGroups);
|
||||
}
|
||||
|
||||
// Rerender the transformer
|
||||
interactiveTransformer.current?.forceUpdate();
|
||||
|
||||
pageLayer.current.batchDraw();
|
||||
}, [localPageFields, selectedKonvaFieldGroups]);
|
||||
}, [localPageFields]);
|
||||
|
||||
const setSelectedFields = (nodes: Konva.Node[]) => {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
const fieldGroups = nodes.filter(
|
||||
(node) =>
|
||||
node.hasName('field-group') && Boolean(node.getStage()) && Boolean(node.getParent()),
|
||||
) as Konva.Group[];
|
||||
const fieldGroups = nodes.filter((node) => node.hasName('field-group')) as Konva.Group[];
|
||||
|
||||
interactiveTransformer.current?.nodes(fieldGroups);
|
||||
setSelectedKonvaFieldGroups(fieldGroups);
|
||||
@@ -690,10 +674,6 @@ const FieldActionButtons = ({
|
||||
selectedFieldFormId.includes(field.formId),
|
||||
);
|
||||
|
||||
if (fields.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const recipient = envelope.recipients.find(
|
||||
(recipient) => recipient.id === fields[0].recipientId,
|
||||
);
|
||||
@@ -709,7 +689,7 @@ const FieldActionButtons = ({
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [editorFields.localFields, envelope.recipients, selectedFieldFormId]);
|
||||
}, [editorFields.localFields]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center" {...props}>
|
||||
|
||||
@@ -296,31 +296,19 @@ export const EnvelopeEditorFieldsPage = () => {
|
||||
|
||||
<div className="space-y-2 rounded-md border border-border bg-muted/50 p-3 text-sm text-foreground">
|
||||
<p>
|
||||
<span className="min-w-12 text-muted-foreground">
|
||||
<Trans>Pos X:</Trans>
|
||||
</span>
|
||||
|
||||
<span className="min-w-12 text-muted-foreground">Pos X: </span>
|
||||
{selectedField.positionX.toFixed(2)}
|
||||
</p>
|
||||
<p>
|
||||
<span className="min-w-12 text-muted-foreground">
|
||||
<Trans>Pos Y:</Trans>
|
||||
</span>
|
||||
|
||||
<span className="min-w-12 text-muted-foreground">Pos Y: </span>
|
||||
{selectedField.positionY.toFixed(2)}
|
||||
</p>
|
||||
<p>
|
||||
<span className="min-w-12 text-muted-foreground">
|
||||
<Trans>Width:</Trans>
|
||||
</span>
|
||||
|
||||
<span className="min-w-12 text-muted-foreground">Width: </span>
|
||||
{selectedField.width.toFixed(2)}
|
||||
</p>
|
||||
<p>
|
||||
<span className="min-w-12 text-muted-foreground">
|
||||
<Trans>Height:</Trans>
|
||||
</span>
|
||||
|
||||
<span className="min-w-12 text-muted-foreground">Height: </span>
|
||||
{selectedField.height.toFixed(2)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -51,7 +51,7 @@ export const UserProfileSkeleton = ({ className, user, rows = 2 }: UserProfileSk
|
||||
<div className="mt-8 w-full">
|
||||
<div className="dark:divide-foreground/30 dark:border-foreground/30 divide-y-2 divide-neutral-200 overflow-hidden rounded-lg border-2 border-neutral-200">
|
||||
<div className="text-muted-foreground dark:bg-foreground/20 bg-neutral-50 p-4 font-medium">
|
||||
<Trans>Documents</Trans>
|
||||
Documents
|
||||
</div>
|
||||
|
||||
{Array(rows)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { WebhookTriggerEvents } from '@prisma/client';
|
||||
|
||||
import { toFriendlyWebhookEventName } from '@documenso/lib/universal/webhook/to-friendly-webhook-event-name';
|
||||
@@ -42,11 +41,7 @@ export const WebhookMultiSelectCombobox = ({
|
||||
placeholder={_(msg`Select triggers`)}
|
||||
hideClearAllButton
|
||||
hidePlaceholderWhenSelected
|
||||
emptyIndicator={
|
||||
<p className="text-center text-sm">
|
||||
<Trans>No triggers available</Trans>
|
||||
</p>
|
||||
}
|
||||
emptyIndicator={<p className="text-center text-sm">No triggers available</p>}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -96,9 +96,7 @@ export const AdminDocumentLogsTable = ({ envelopeId }: AdminDocumentLogsTablePro
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<p>
|
||||
<Trans>N/A</Trans>
|
||||
</p>
|
||||
<p>N/A</p>
|
||||
),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -2,7 +2,6 @@ import { useMemo } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { DateTime } from 'luxon';
|
||||
import type { DateTimeFormatOptions } from 'luxon';
|
||||
import { useSearchParams } from 'react-router';
|
||||
@@ -89,9 +88,7 @@ export const DocumentLogsTable = ({ documentId, userId }: DocumentLogsTableProps
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<p>
|
||||
<Trans>N/A</Trans>
|
||||
</p>
|
||||
<p>N/A</p>
|
||||
),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -107,11 +107,7 @@ export default function OrganisationGroupSettingsPage({
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<Link to={`/admin/users/${row.original.user.id}`}>{row.original.user.name}</Link>
|
||||
{row.original.user.id === organisation?.ownerUserId && (
|
||||
<Badge>
|
||||
<Trans>Owner</Trans>
|
||||
</Badge>
|
||||
)}
|
||||
{row.original.user.id === organisation?.ownerUserId && <Badge>Owner</Badge>}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
@@ -212,9 +208,7 @@ export default function OrganisationGroupSettingsPage({
|
||||
{SUBSCRIPTION_STATUS_MAP[organisation.subscription.status]} subscription found
|
||||
</span>
|
||||
) : (
|
||||
<span>
|
||||
<Trans>No subscription found</Trans>
|
||||
</span>
|
||||
<span>No subscription found</span>
|
||||
)}
|
||||
</AlertDescription>
|
||||
</div>
|
||||
|
||||
@@ -66,9 +66,7 @@ export default function DocumentsFoldersPage() {
|
||||
onClick={() => navigateToFolder(null)}
|
||||
>
|
||||
<HomeIcon className="h-4 w-4" />
|
||||
<span>
|
||||
<Trans>Home</Trans>
|
||||
</span>
|
||||
<span>Home</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -66,9 +66,7 @@ export default function TemplatesFoldersPage() {
|
||||
onClick={() => navigateToFolder(null)}
|
||||
>
|
||||
<HomeIcon className="h-4 w-4" />
|
||||
<span>
|
||||
<Trans>Home</Trans>
|
||||
</span>
|
||||
<span>Home</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { Outlet, isRouteErrorResponse, useRouteError } from 'react-router';
|
||||
|
||||
import {
|
||||
@@ -90,9 +89,5 @@ export function ErrorBoundary({ loaderData }: Route.ErrorBoundaryProps) {
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Trans>Not Found</Trans>
|
||||
</div>
|
||||
);
|
||||
return <div>Not Found</div>;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useLayoutEffect } from 'react';
|
||||
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { Outlet, useLoaderData } from 'react-router';
|
||||
|
||||
import { verifyEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/verify-embedding-presign-token';
|
||||
@@ -76,11 +75,7 @@ export default function AuthoringLayout() {
|
||||
}, []);
|
||||
|
||||
if (!hasValidToken) {
|
||||
return (
|
||||
<div>
|
||||
<Trans>Invalid embedding presign token provided</Trans>
|
||||
</div>
|
||||
);
|
||||
return <div>Invalid embedding presign token provided</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useEffect, useLayoutEffect, useState } from 'react';
|
||||
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { SigningStatus } from '@prisma/client';
|
||||
import { useRevalidator } from 'react-router';
|
||||
|
||||
@@ -284,9 +283,7 @@ export default function MultisignPage() {
|
||||
|
||||
{!hidePoweredBy && (
|
||||
<div className="bg-primary text-primary-foreground fixed bottom-0 left-0 z-40 rounded-tr px-2 py-1 text-xs font-medium opacity-60 hover:opacity-100">
|
||||
<span>
|
||||
<Trans>Powered by</Trans>
|
||||
</span>
|
||||
<span>Powered by</span>
|
||||
<BrandingLogo className="ml-2 inline-block h-[14px]" />
|
||||
</div>
|
||||
)}
|
||||
@@ -301,9 +298,7 @@ export default function MultisignPage() {
|
||||
|
||||
{!hidePoweredBy && (
|
||||
<div className="bg-primary text-primary-foreground fixed bottom-0 left-0 z-40 rounded-tr px-2 py-1 text-xs font-medium opacity-60 hover:opacity-100">
|
||||
<span>
|
||||
<Trans>Powered by</Trans>
|
||||
</span>
|
||||
<span>Powered by</span>
|
||||
<BrandingLogo className="ml-2 inline-block h-[14px]" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -106,5 +106,5 @@
|
||||
"vite-plugin-babel-macros": "^1.0.6",
|
||||
"vite-tsconfig-paths": "^5.1.4"
|
||||
},
|
||||
"version": "2.6.1"
|
||||
"version": "2.6.0"
|
||||
}
|
||||
|
||||
@@ -12,8 +12,6 @@ import { API_V2_BETA_URL, API_V2_URL } from '@documenso/lib/constants/app';
|
||||
import { jobsClient } from '@documenso/lib/jobs/client';
|
||||
import { LicenseClient } from '@documenso/lib/server-only/license/license-client';
|
||||
import { TelemetryClient } from '@documenso/lib/server-only/telemetry/telemetry-client';
|
||||
import { migrateDeletedAccountServiceAccount } from '@documenso/lib/server-only/user/service-accounts/deleted-account';
|
||||
import { migrateLegacyServiceAccount } from '@documenso/lib/server-only/user/service-accounts/legacy-service-account';
|
||||
import { getIpAddress } from '@documenso/lib/universal/get-ip-address';
|
||||
import { env } from '@documenso/lib/utils/env';
|
||||
import { logger } from '@documenso/lib/utils/logger';
|
||||
@@ -146,7 +144,4 @@ if (env('NODE_ENV') !== 'development') {
|
||||
// Start license client to verify license on startup.
|
||||
void LicenseClient.start();
|
||||
|
||||
void migrateDeletedAccountServiceAccount();
|
||||
void migrateLegacyServiceAccount();
|
||||
|
||||
export default app;
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
%PDF-1.7
|
||||
%âãÏÓ
|
||||
1 0 obj
|
||||
<<
|
||||
/Type /Pages
|
||||
/Kids [4 0 R]
|
||||
/Count 1
|
||||
>>
|
||||
endobj
|
||||
2 0 obj
|
||||
<<
|
||||
/Type /Catalog
|
||||
/Pages 1 0 R
|
||||
>>
|
||||
endobj
|
||||
3 0 obj
|
||||
<<
|
||||
/Title (Untitled)
|
||||
/Author (Unknown)
|
||||
/Creator (@libpdf/core)
|
||||
/Producer (@libpdf/core)
|
||||
/CreationDate (D:20260211083727Z)
|
||||
/ModDate (D:20260211083727Z)
|
||||
>>
|
||||
endobj
|
||||
4 0 obj
|
||||
<<
|
||||
/Type /Page
|
||||
/MediaBox [0 0 595 842]
|
||||
/Resources <<
|
||||
>>
|
||||
/Parent 1 0 R
|
||||
>>
|
||||
endobj
|
||||
xref
|
||||
0 5
|
||||
0000000000 65535 f
|
||||
0000000015 00000 n
|
||||
0000000072 00000 n
|
||||
0000000121 00000 n
|
||||
0000000290 00000 n
|
||||
trailer
|
||||
<<
|
||||
/Size 5
|
||||
/Root 2 0 R
|
||||
/Info 3 0 R
|
||||
/ID [<B051F100F1EED01A592FC6119F589603> <B051F100F1EED01A592FC6119F589603>]
|
||||
>>
|
||||
startxref
|
||||
378
|
||||
%%EOF
|
||||
@@ -1,51 +0,0 @@
|
||||
%PDF-1.7
|
||||
%âãÏÓ
|
||||
1 0 obj
|
||||
<<
|
||||
/Type /Pages
|
||||
/Kids [4 0 R]
|
||||
/Count 1
|
||||
>>
|
||||
endobj
|
||||
2 0 obj
|
||||
<<
|
||||
/Type /Catalog
|
||||
/Pages 1 0 R
|
||||
>>
|
||||
endobj
|
||||
3 0 obj
|
||||
<<
|
||||
/Title (Untitled)
|
||||
/Author (Unknown)
|
||||
/Creator (@libpdf/core)
|
||||
/Producer (@libpdf/core)
|
||||
/CreationDate (D:20260211081729Z)
|
||||
/ModDate (D:20260211081729Z)
|
||||
>>
|
||||
endobj
|
||||
4 0 obj
|
||||
<<
|
||||
/Type /Page
|
||||
/MediaBox [0 0 612 792]
|
||||
/Resources <<
|
||||
>>
|
||||
/Parent 1 0 R
|
||||
>>
|
||||
endobj
|
||||
xref
|
||||
0 5
|
||||
0000000000 65535 f
|
||||
0000000015 00000 n
|
||||
0000000072 00000 n
|
||||
0000000121 00000 n
|
||||
0000000290 00000 n
|
||||
trailer
|
||||
<<
|
||||
/Size 5
|
||||
/Root 2 0 R
|
||||
/Info 3 0 R
|
||||
/ID [<94A5FB5DCF5A94AD8C472C493420962C> <94A5FB5DCF5A94AD8C472C493420962C>]
|
||||
>>
|
||||
startxref
|
||||
378
|
||||
%%EOF
|
||||
@@ -1,51 +0,0 @@
|
||||
%PDF-1.7
|
||||
%âãÏÓ
|
||||
1 0 obj
|
||||
<<
|
||||
/Type /Pages
|
||||
/Kids [4 0 R]
|
||||
/Count 1
|
||||
>>
|
||||
endobj
|
||||
2 0 obj
|
||||
<<
|
||||
/Type /Catalog
|
||||
/Pages 1 0 R
|
||||
>>
|
||||
endobj
|
||||
3 0 obj
|
||||
<<
|
||||
/Title (Untitled)
|
||||
/Author (Unknown)
|
||||
/Creator (@libpdf/core)
|
||||
/Producer (@libpdf/core)
|
||||
/CreationDate (D:20260211084535Z)
|
||||
/ModDate (D:20260211084535Z)
|
||||
>>
|
||||
endobj
|
||||
4 0 obj
|
||||
<<
|
||||
/Type /Page
|
||||
/MediaBox [0 0 1224 792]
|
||||
/Resources <<
|
||||
>>
|
||||
/Parent 1 0 R
|
||||
>>
|
||||
endobj
|
||||
xref
|
||||
0 5
|
||||
0000000000 65535 f
|
||||
0000000015 00000 n
|
||||
0000000072 00000 n
|
||||
0000000121 00000 n
|
||||
0000000290 00000 n
|
||||
trailer
|
||||
<<
|
||||
/Size 5
|
||||
/Root 2 0 R
|
||||
/Info 3 0 R
|
||||
/ID [<694452F2208AC8E3DD2D2488544F9F0C> <694452F2208AC8E3DD2D2488544F9F0C>]
|
||||
>>
|
||||
startxref
|
||||
379
|
||||
%%EOF
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@documenso/root",
|
||||
"version": "2.6.1",
|
||||
"version": "2.6.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@documenso/root",
|
||||
"version": "2.6.1",
|
||||
"version": "2.6.0",
|
||||
"hasInstallScript": true,
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
@@ -108,7 +108,7 @@
|
||||
},
|
||||
"apps/remix": {
|
||||
"name": "@documenso/remix",
|
||||
"version": "2.6.1",
|
||||
"version": "2.6.0",
|
||||
"dependencies": {
|
||||
"@cantoo/pdf-lib": "^2.5.3",
|
||||
"@documenso/api": "*",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"apps/*",
|
||||
"packages/*"
|
||||
],
|
||||
"version": "2.6.1",
|
||||
"version": "2.6.0",
|
||||
"scripts": {
|
||||
"postinstall": "patch-package",
|
||||
"build": "turbo run build",
|
||||
|
||||
@@ -106,10 +106,10 @@ test.describe('AutoSave Subject Step', () => {
|
||||
const { user, document, team } = await setupDocumentAndNavigateToSubjectStep(page);
|
||||
|
||||
// Toggle some email settings checkboxes (randomly - some checked, some unchecked)
|
||||
await page.getByText('Email the owner when a recipient signs').click();
|
||||
await page.getByText("Email recipients when they're removed from a pending document").click();
|
||||
await page.getByText('Email recipients when the document is completed', { exact: true }).click();
|
||||
await page.getByText('Email recipients when a pending document is deleted').click();
|
||||
await page.getByText('Send recipient signed email').click();
|
||||
await page.getByText('Send recipient removed email').click();
|
||||
await page.getByText('Send document completed email', { exact: true }).click();
|
||||
await page.getByText('Send document deleted email').click();
|
||||
|
||||
await triggerAutosave(page);
|
||||
|
||||
@@ -126,30 +126,26 @@ test.describe('AutoSave Subject Step', () => {
|
||||
|
||||
const emailSettings = retrievedDocumentData.documentMeta?.emailSettings;
|
||||
|
||||
await expect(page.getByText('Email the owner when a recipient signs')).toBeChecked({
|
||||
await expect(page.getByText('Send recipient signed email')).toBeChecked({
|
||||
checked: emailSettings?.recipientSigned,
|
||||
});
|
||||
await expect(
|
||||
page.getByText("Email recipients when they're removed from a pending document"),
|
||||
).toBeChecked({
|
||||
await expect(page.getByText('Send recipient removed email')).toBeChecked({
|
||||
checked: emailSettings?.recipientRemoved,
|
||||
});
|
||||
await expect(
|
||||
page.getByText('Email recipients when the document is completed', { exact: true }),
|
||||
).toBeChecked({
|
||||
await expect(page.getByText('Send document completed email', { exact: true })).toBeChecked({
|
||||
checked: emailSettings?.documentCompleted,
|
||||
});
|
||||
await expect(page.getByText('Email recipients when a pending document is deleted')).toBeChecked({
|
||||
await expect(page.getByText('Send document deleted email')).toBeChecked({
|
||||
checked: emailSettings?.documentDeleted,
|
||||
});
|
||||
|
||||
await expect(page.getByText('Email recipients with a signing request')).toBeChecked({
|
||||
await expect(page.getByText('Send recipient signing request email')).toBeChecked({
|
||||
checked: emailSettings?.recipientSigningRequest,
|
||||
});
|
||||
await expect(page.getByText('Email the signer if the document is still pending')).toBeChecked({
|
||||
await expect(page.getByText('Send document pending email')).toBeChecked({
|
||||
checked: emailSettings?.documentPending,
|
||||
});
|
||||
await expect(page.getByText('Email the owner when the document is completed')).toBeChecked({
|
||||
await expect(page.getByText('Send document completed email to the owner')).toBeChecked({
|
||||
checked: emailSettings?.ownerDocumentCompleted,
|
||||
});
|
||||
}).toPass();
|
||||
@@ -165,10 +161,10 @@ test.describe('AutoSave Subject Step', () => {
|
||||
await page.getByRole('textbox', { name: 'Subject (Optional)' }).fill(subject);
|
||||
await page.getByRole('textbox', { name: 'Message (Optional)' }).fill(message);
|
||||
|
||||
await page.getByText('Email the owner when a recipient signs').click();
|
||||
await page.getByText("Email recipients when they're removed from a pending document").click();
|
||||
await page.getByText('Email recipients when the document is completed', { exact: true }).click();
|
||||
await page.getByText('Email recipients when a pending document is deleted').click();
|
||||
await page.getByText('Send recipient signed email').click();
|
||||
await page.getByText('Send recipient removed email').click();
|
||||
await page.getByText('Send document completed email', { exact: true }).click();
|
||||
await page.getByText('Send document deleted email').click();
|
||||
|
||||
await triggerAutosave(page);
|
||||
|
||||
@@ -194,30 +190,26 @@ test.describe('AutoSave Subject Step', () => {
|
||||
retrievedDocumentData.documentMeta?.message ?? '',
|
||||
);
|
||||
|
||||
await expect(page.getByText('Email the owner when a recipient signs')).toBeChecked({
|
||||
await expect(page.getByText('Send recipient signed email')).toBeChecked({
|
||||
checked: retrievedDocumentData.documentMeta?.emailSettings?.recipientSigned,
|
||||
});
|
||||
await expect(
|
||||
page.getByText("Email recipients when they're removed from a pending document"),
|
||||
).toBeChecked({
|
||||
await expect(page.getByText('Send recipient removed email')).toBeChecked({
|
||||
checked: retrievedDocumentData.documentMeta?.emailSettings?.recipientRemoved,
|
||||
});
|
||||
await expect(
|
||||
page.getByText('Email recipients when the document is completed', { exact: true }),
|
||||
).toBeChecked({
|
||||
await expect(page.getByText('Send document completed email', { exact: true })).toBeChecked({
|
||||
checked: retrievedDocumentData.documentMeta?.emailSettings?.documentCompleted,
|
||||
});
|
||||
await expect(page.getByText('Email recipients when a pending document is deleted')).toBeChecked({
|
||||
await expect(page.getByText('Send document deleted email')).toBeChecked({
|
||||
checked: retrievedDocumentData.documentMeta?.emailSettings?.documentDeleted,
|
||||
});
|
||||
|
||||
await expect(page.getByText('Email recipients with a signing request')).toBeChecked({
|
||||
await expect(page.getByText('Send recipient signing request email')).toBeChecked({
|
||||
checked: retrievedDocumentData.documentMeta?.emailSettings?.recipientSigningRequest,
|
||||
});
|
||||
await expect(page.getByText('Email the signer if the document is still pending')).toBeChecked({
|
||||
await expect(page.getByText('Send document pending email')).toBeChecked({
|
||||
checked: retrievedDocumentData.documentMeta?.emailSettings?.documentPending,
|
||||
});
|
||||
await expect(page.getByText('Email the owner when the document is completed')).toBeChecked({
|
||||
await expect(page.getByText('Send document completed email to the owner')).toBeChecked({
|
||||
checked: retrievedDocumentData.documentMeta?.emailSettings?.ownerDocumentCompleted,
|
||||
});
|
||||
}).toPass();
|
||||
|
||||
@@ -1,218 +0,0 @@
|
||||
import { type APIRequestContext, type Page, expect, test } from '@playwright/test';
|
||||
import { DocumentStatus, EnvelopeType, FieldType } from '@prisma/client';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf.mjs';
|
||||
|
||||
import { getEnvelopeItemPdfUrl } from '@documenso/lib/utils/envelope-download';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { seedUser } from '@documenso/prisma/seed/users';
|
||||
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../lib/constants/app';
|
||||
import { createApiToken } from '../../../lib/server-only/public-api/create-api-token';
|
||||
import { RecipientRole } from '../../../prisma/generated/types';
|
||||
import type {
|
||||
TCreateEnvelopePayload,
|
||||
TCreateEnvelopeResponse,
|
||||
} from '../../../trpc/server/envelope-router/create-envelope.types';
|
||||
import type { TDistributeEnvelopeRequest } from '../../../trpc/server/envelope-router/distribute-envelope.types';
|
||||
import { apiSignin } from '../fixtures/authentication';
|
||||
|
||||
const WEBAPP_BASE_URL = NEXT_PUBLIC_WEBAPP_URL();
|
||||
const baseUrl = `${WEBAPP_BASE_URL}/api/v2`;
|
||||
|
||||
test.describe.configure({ mode: 'parallel', timeout: 60000 });
|
||||
|
||||
const signAndVerifyPageDimensions = async ({
|
||||
page,
|
||||
request,
|
||||
pdfFile,
|
||||
identifier,
|
||||
title,
|
||||
expectedWidth,
|
||||
expectedHeight,
|
||||
}: {
|
||||
page: Page;
|
||||
request: APIRequestContext;
|
||||
pdfFile: string;
|
||||
identifier: string;
|
||||
title: string;
|
||||
expectedWidth: number;
|
||||
expectedHeight: number;
|
||||
}) => {
|
||||
const { user, team } = await seedUser();
|
||||
|
||||
const { token } = await createApiToken({
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
tokenName: 'test',
|
||||
expiresIn: null,
|
||||
});
|
||||
|
||||
const pdfBuffer = fs.readFileSync(path.join(__dirname, `../../../../assets/${pdfFile}`));
|
||||
|
||||
const formData = new FormData();
|
||||
|
||||
const createEnvelopePayload: TCreateEnvelopePayload = {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
title,
|
||||
recipients: [
|
||||
{
|
||||
email: user.email,
|
||||
name: user.name || '',
|
||||
role: RecipientRole.SIGNER,
|
||||
fields: [
|
||||
{
|
||||
identifier,
|
||||
type: FieldType.SIGNATURE,
|
||||
fieldMeta: { type: 'signature' },
|
||||
page: 1,
|
||||
positionX: 10,
|
||||
positionY: 10,
|
||||
width: 40,
|
||||
height: 10,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
formData.append('payload', JSON.stringify(createEnvelopePayload));
|
||||
formData.append('files', new File([pdfBuffer], identifier, { type: 'application/pdf' }));
|
||||
|
||||
const createResponse = await request.post(`${baseUrl}/envelope/create`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: formData,
|
||||
});
|
||||
|
||||
expect(createResponse.ok()).toBeTruthy();
|
||||
|
||||
const { id: envelopeId }: TCreateEnvelopeResponse = await createResponse.json();
|
||||
|
||||
const envelope = await prisma.envelope.findUniqueOrThrow({
|
||||
where: { id: envelopeId },
|
||||
include: { recipients: true },
|
||||
});
|
||||
|
||||
const distributeResponse = await request.post(`${baseUrl}/envelope/distribute`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
data: { envelopeId: envelope.id } satisfies TDistributeEnvelopeRequest,
|
||||
});
|
||||
|
||||
expect(distributeResponse.ok()).toBeTruthy();
|
||||
|
||||
// Pre-insert all fields via Prisma so we can skip the UI field interaction.
|
||||
const fields = await prisma.field.findMany({
|
||||
where: { envelopeId: envelope.id, inserted: false },
|
||||
});
|
||||
|
||||
for (const field of fields) {
|
||||
await prisma.field.update({
|
||||
where: { id: field.id },
|
||||
data: {
|
||||
inserted: true,
|
||||
signature: {
|
||||
create: {
|
||||
recipientId: envelope.recipients[0].id,
|
||||
typedSignature: 'Test Signature',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const recipientToken = envelope.recipients[0].token;
|
||||
const signUrl = `/sign/${recipientToken}`;
|
||||
|
||||
await apiSignin({
|
||||
page,
|
||||
email: user.email,
|
||||
redirectPath: signUrl,
|
||||
});
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Sign Document' })).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: 'Complete' }).click();
|
||||
await page.getByRole('button', { name: 'Sign' }).click();
|
||||
await page.waitForURL(`${signUrl}/complete`);
|
||||
|
||||
await expect(async () => {
|
||||
const { status } = await prisma.envelope.findFirstOrThrow({
|
||||
where: { id: envelope.id },
|
||||
});
|
||||
|
||||
expect(status).toBe(DocumentStatus.COMPLETED);
|
||||
}).toPass({ timeout: 10000 });
|
||||
|
||||
const completedEnvelope = await prisma.envelope.findFirstOrThrow({
|
||||
where: { id: envelope.id },
|
||||
include: {
|
||||
envelopeItems: {
|
||||
orderBy: { order: 'asc' },
|
||||
include: { documentData: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
for (const item of completedEnvelope.envelopeItems) {
|
||||
const documentUrl = getEnvelopeItemPdfUrl({
|
||||
type: 'download',
|
||||
envelopeItem: item,
|
||||
token: recipientToken,
|
||||
version: 'signed',
|
||||
});
|
||||
|
||||
const pdfData = await fetch(documentUrl).then(async (res) => await res.arrayBuffer());
|
||||
const loadingTask = pdfjsLib.getDocument({ data: new Uint8Array(pdfData) });
|
||||
const pdf = await loadingTask.promise;
|
||||
|
||||
expect(pdf.numPages).toBeGreaterThan(1);
|
||||
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
const pdfPage = await pdf.getPage(i);
|
||||
const viewport = pdfPage.getViewport({ scale: 1 });
|
||||
|
||||
expect(Math.round(viewport.width)).toBe(expectedWidth);
|
||||
expect(Math.round(viewport.height)).toBe(expectedHeight);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
test('cert and audit log pages match letter page dimensions', async ({ page, request }) => {
|
||||
await signAndVerifyPageDimensions({
|
||||
page,
|
||||
request,
|
||||
pdfFile: 'letter-size.pdf',
|
||||
identifier: 'letter-doc',
|
||||
title: 'Letter Size Dimension Test',
|
||||
expectedWidth: 612,
|
||||
expectedHeight: 792,
|
||||
});
|
||||
});
|
||||
|
||||
test('cert and audit log pages match A4 page dimensions', async ({ page, request }) => {
|
||||
await signAndVerifyPageDimensions({
|
||||
page,
|
||||
request,
|
||||
pdfFile: 'a4-size.pdf',
|
||||
identifier: 'a4-doc',
|
||||
title: 'A4 Size Dimension Test',
|
||||
expectedWidth: 595,
|
||||
expectedHeight: 842,
|
||||
});
|
||||
});
|
||||
|
||||
test('cert and audit log pages match tabloid landscape page dimensions', async ({
|
||||
page,
|
||||
request,
|
||||
}) => {
|
||||
await signAndVerifyPageDimensions({
|
||||
page,
|
||||
request,
|
||||
pdfFile: 'tabloid-landscape.pdf',
|
||||
identifier: 'tabloid-doc',
|
||||
title: 'Tabloid Landscape Dimension Test',
|
||||
expectedWidth: 1224,
|
||||
expectedHeight: 792,
|
||||
});
|
||||
});
|
||||
@@ -298,34 +298,18 @@ test('field placement visual regression', async ({ page, request }, testInfo) =>
|
||||
*
|
||||
* DON'T COMMIT THIS WITHOUT THE "SKIP" COMMAND.
|
||||
*/
|
||||
test.skip('download envelope images', async ({ page, request }) => {
|
||||
test.skip('download envelope images', async ({ page }) => {
|
||||
const { user, team } = await seedUser();
|
||||
|
||||
const { token: apiToken } = await createApiToken({
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
tokenName: 'test',
|
||||
expiresIn: null,
|
||||
});
|
||||
|
||||
const envelope = await seedAlignmentTestDocument({
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
recipientName: user.name || '',
|
||||
recipientEmail: user.email,
|
||||
insertFields: true,
|
||||
status: DocumentStatus.DRAFT,
|
||||
status: DocumentStatus.PENDING,
|
||||
});
|
||||
|
||||
const distributeEnvelopeRequest = await request.post(`${baseUrl}/envelope/distribute`, {
|
||||
headers: { Authorization: `Bearer ${apiToken}` },
|
||||
data: {
|
||||
envelopeId: envelope.id,
|
||||
} satisfies TDistributeEnvelopeRequest,
|
||||
});
|
||||
|
||||
expect(distributeEnvelopeRequest.ok()).toBeTruthy();
|
||||
|
||||
const token = envelope.recipients[0].token;
|
||||
|
||||
const signUrl = `/sign/${token}`;
|
||||
|
||||
@@ -205,13 +205,9 @@ test('[ORGANISATIONS]: manage email preferences', async ({ page }) => {
|
||||
await page.getByRole('textbox', { name: 'Reply to email' }).fill('organisation@documenso.com');
|
||||
|
||||
// Update email document settings by enabling/disabling some checkboxes
|
||||
await page.getByRole('checkbox', { name: 'Email the owner when a recipient signs' }).uncheck();
|
||||
await page
|
||||
.getByRole('checkbox', { name: 'Email the signer if the document is still pending' })
|
||||
.uncheck();
|
||||
await page
|
||||
.getByRole('checkbox', { name: 'Email recipients when a pending document is deleted' })
|
||||
.uncheck();
|
||||
await page.getByRole('checkbox', { name: 'Send recipient signed email' }).uncheck();
|
||||
await page.getByRole('checkbox', { name: 'Send document pending email' }).uncheck();
|
||||
await page.getByRole('checkbox', { name: 'Send document deleted email' }).uncheck();
|
||||
|
||||
await page.getByRole('button', { name: 'Update' }).first().click();
|
||||
await expect(page.getByText('Your email preferences have been updated').first()).toBeVisible();
|
||||
@@ -244,14 +240,12 @@ test('[ORGANISATIONS]: manage email preferences', async ({ page }) => {
|
||||
await page.getByRole('option', { name: 'Override organisation settings' }).click();
|
||||
|
||||
// Update some email settings
|
||||
await page.getByRole('checkbox', { name: 'Send recipient signing request email' }).uncheck();
|
||||
await page
|
||||
.getByRole('checkbox', { name: 'Email recipients with a signing request' })
|
||||
.getByRole('checkbox', { name: 'Send document completed email', exact: true })
|
||||
.uncheck();
|
||||
await page
|
||||
.getByRole('checkbox', { name: 'Email recipients when the document is completed', exact: true })
|
||||
.uncheck();
|
||||
await page
|
||||
.getByRole('checkbox', { name: 'Email the owner when the document is completed' })
|
||||
.getByRole('checkbox', { name: 'Send document completed email to the owner' })
|
||||
.uncheck();
|
||||
|
||||
await page.getByRole('button', { name: 'Update' }).first().click();
|
||||
|
||||
|
Before Width: | Height: | Size: 352 KiB After Width: | Height: | Size: 139 KiB |
|
Before Width: | Height: | Size: 266 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 310 KiB After Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 246 KiB After Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 163 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 191 KiB After Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 223 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 202 KiB After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 162 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 224 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 246 KiB After Width: | Height: | Size: 117 KiB |
@@ -5,8 +5,6 @@ import { deleteCookie } from 'hono/cookie';
|
||||
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { onCreateUserHook } from '@documenso/lib/server-only/user/create-user';
|
||||
import { deletedServiceAccountEmail } from '@documenso/lib/server-only/user/service-accounts/deleted-account';
|
||||
import { legacyServiceAccountEmail } from '@documenso/lib/server-only/user/service-accounts/legacy-service-account';
|
||||
import { isValidReturnTo, normalizeReturnTo } from '@documenso/lib/utils/is-valid-return-to';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
@@ -28,13 +26,6 @@ export const handleOAuthCallbackUrl = async (options: HandleOAuthCallbackUrlOpti
|
||||
const { email, name, sub, accessToken, accessTokenExpiresAt, idToken, redirectPath } =
|
||||
await validateOauth({ c, clientOptions });
|
||||
|
||||
if (
|
||||
email.toLowerCase() === legacyServiceAccountEmail() ||
|
||||
email.toLowerCase() === deletedServiceAccountEmail()
|
||||
) {
|
||||
return c.text('FORBIDDEN', 403);
|
||||
}
|
||||
|
||||
// Find the account if possible.
|
||||
const existingAccount = await prisma.account.findFirst({
|
||||
where: {
|
||||
|
||||
@@ -17,10 +17,7 @@ import { viewBackupCodes } from '@documenso/lib/server-only/2fa/view-backup-code
|
||||
import { createUser } from '@documenso/lib/server-only/user/create-user';
|
||||
import { forgotPassword } from '@documenso/lib/server-only/user/forgot-password';
|
||||
import { getMostRecentEmailVerificationToken } from '@documenso/lib/server-only/user/get-most-recent-email-verification-token';
|
||||
import { getUserByResetToken } from '@documenso/lib/server-only/user/get-user-by-reset-token';
|
||||
import { resetPassword } from '@documenso/lib/server-only/user/reset-password';
|
||||
import { deletedServiceAccountEmail } from '@documenso/lib/server-only/user/service-accounts/deleted-account';
|
||||
import { legacyServiceAccountEmail } from '@documenso/lib/server-only/user/service-accounts/legacy-service-account';
|
||||
import { updatePassword } from '@documenso/lib/server-only/user/update-password';
|
||||
import { verifyEmail } from '@documenso/lib/server-only/user/verify-email';
|
||||
import { env } from '@documenso/lib/utils/env';
|
||||
@@ -60,13 +57,6 @@ export const emailPasswordRoute = new Hono<HonoAuthContext>()
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
email.toLowerCase() === legacyServiceAccountEmail() ||
|
||||
email.toLowerCase() === deletedServiceAccountEmail()
|
||||
) {
|
||||
return c.text('FORBIDDEN', 403);
|
||||
}
|
||||
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
email: email.toLowerCase(),
|
||||
@@ -251,13 +241,6 @@ export const emailPasswordRoute = new Hono<HonoAuthContext>()
|
||||
.post('/forgot-password', sValidator('json', ZForgotPasswordSchema), async (c) => {
|
||||
const { email } = c.req.valid('json');
|
||||
|
||||
if (
|
||||
email.toLowerCase() === legacyServiceAccountEmail() ||
|
||||
email.toLowerCase() === deletedServiceAccountEmail()
|
||||
) {
|
||||
return c.text('FORBIDDEN', 403);
|
||||
}
|
||||
|
||||
await forgotPassword({
|
||||
email,
|
||||
});
|
||||
@@ -270,15 +253,6 @@ export const emailPasswordRoute = new Hono<HonoAuthContext>()
|
||||
.post('/reset-password', sValidator('json', ZResetPasswordSchema), async (c) => {
|
||||
const { token, password } = c.req.valid('json');
|
||||
|
||||
const user = await getUserByResetToken({ token });
|
||||
|
||||
if (
|
||||
user.email.toLowerCase() === legacyServiceAccountEmail() ||
|
||||
user.email.toLowerCase() === deletedServiceAccountEmail()
|
||||
) {
|
||||
return c.text('FORBIDDEN', 403);
|
||||
}
|
||||
|
||||
const requestMetadata = c.get('requestMetadata');
|
||||
|
||||
const { userId } = await resetPassword({
|
||||
|
||||
@@ -5,8 +5,6 @@ import { isoBase64URL } from '@simplewebauthn/server/helpers';
|
||||
import { Hono } from 'hono';
|
||||
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { deletedServiceAccountEmail } from '@documenso/lib/server-only/user/service-accounts/deleted-account';
|
||||
import { legacyServiceAccountEmail } from '@documenso/lib/server-only/user/service-accounts/legacy-service-account';
|
||||
import type { TAuthenticationResponseJSONSchema } from '@documenso/lib/types/webauthn';
|
||||
import { ZAuthenticationResponseJSONSchema } from '@documenso/lib/types/webauthn';
|
||||
import { getAuthenticatorOptions } from '@documenso/lib/utils/authenticator';
|
||||
@@ -76,13 +74,6 @@ export const passkeyRoute = new Hono<HonoAuthContext>()
|
||||
|
||||
const user = passkey.user;
|
||||
|
||||
if (
|
||||
user.email.toLowerCase() === legacyServiceAccountEmail() ||
|
||||
user.email.toLowerCase() === deletedServiceAccountEmail()
|
||||
) {
|
||||
return c.text('FORBIDDEN', 403);
|
||||
}
|
||||
|
||||
const { rpId, origin } = getAuthenticatorOptions();
|
||||
|
||||
const verification = await verifyAuthenticationResponse({
|
||||
|
||||
@@ -1,89 +1,19 @@
|
||||
import { SubscriptionStatus } from '@prisma/client';
|
||||
|
||||
import { createOrganisationClaimUpsertData } from '@documenso/lib/server-only/organisation/create-organisation';
|
||||
import type { Stripe } from '@documenso/lib/server-only/stripe';
|
||||
import { INTERNAL_CLAIM_ID, internalClaims } from '@documenso/lib/types/subscription';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { extractStripeClaimId } from './on-subscription-updated';
|
||||
|
||||
export type OnSubscriptionDeletedOptions = {
|
||||
subscription: Stripe.Subscription;
|
||||
};
|
||||
|
||||
export const onSubscriptionDeleted = async ({ subscription }: OnSubscriptionDeletedOptions) => {
|
||||
const existingSubscription = await prisma.subscription.findUnique({
|
||||
where: {
|
||||
planId: subscription.id,
|
||||
},
|
||||
include: {
|
||||
organisation: {
|
||||
include: {
|
||||
organisationClaim: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// If the subscription doesn't exist, we don't need to do anything.
|
||||
if (!existingSubscription) {
|
||||
return;
|
||||
}
|
||||
|
||||
const subscriptionClaimId = await extractClaimIdFromStripeSubscription(subscription);
|
||||
|
||||
// Individuals get their subscription deleted so they can return to the
|
||||
// free plan.
|
||||
if (subscriptionClaimId === INTERNAL_CLAIM_ID.INDIVIDUAL) {
|
||||
await prisma.$transaction(async (tx) => {
|
||||
await tx.subscription.delete({
|
||||
where: {
|
||||
id: existingSubscription.id,
|
||||
},
|
||||
});
|
||||
|
||||
await tx.organisationClaim.update({
|
||||
where: {
|
||||
id: existingSubscription.organisation.organisationClaim.id,
|
||||
},
|
||||
data: {
|
||||
originalSubscriptionClaimId: INTERNAL_CLAIM_ID.FREE,
|
||||
...createOrganisationClaimUpsertData(internalClaims[INTERNAL_CLAIM_ID.FREE]),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// For all other cases, mark the subscription as inactive since
|
||||
// they should still have a "Personal" account.
|
||||
await prisma.subscription.update({
|
||||
where: {
|
||||
id: existingSubscription.id,
|
||||
planId: subscription.id,
|
||||
},
|
||||
data: {
|
||||
status: SubscriptionStatus.INACTIVE,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Extracts the claim ID from the Stripe subscription.
|
||||
*
|
||||
* Returns `null` if no claim ID found.
|
||||
*/
|
||||
const extractClaimIdFromStripeSubscription = async (subscription: Stripe.Subscription) => {
|
||||
const deletedItem = subscription.items.data[0];
|
||||
|
||||
if (!deletedItem) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return await extractStripeClaimId(deletedItem.price);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -8,6 +8,8 @@ export const DOCUMENSO_INTERNAL_EMAIL = {
|
||||
address: FROM_ADDRESS,
|
||||
};
|
||||
|
||||
export const SERVICE_USER_EMAIL = 'serviceaccount@documenso.com';
|
||||
|
||||
export const EMAIL_VERIFICATION_STATE = {
|
||||
NOT_FOUND: 'NOT_FOUND',
|
||||
VERIFIED: 'VERIFIED',
|
||||
|
||||
@@ -15,11 +15,11 @@ import { groupBy } from 'remeda';
|
||||
import { addRejectionStampToPdf } from '@documenso/lib/server-only/pdf/add-rejection-stamp-to-pdf';
|
||||
import { generateAuditLogPdf } from '@documenso/lib/server-only/pdf/generate-audit-log-pdf';
|
||||
import { generateCertificatePdf } from '@documenso/lib/server-only/pdf/generate-certificate-pdf';
|
||||
import { getLastPageDimensions } from '@documenso/lib/server-only/pdf/get-page-size';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { signPdf } from '@documenso/signing';
|
||||
|
||||
import { NEXT_PRIVATE_USE_PLAYWRIGHT_PDF } from '../../../constants/app';
|
||||
import { PDF_SIZE_A4_72PPI } from '../../../constants/pdf';
|
||||
import { AppError, AppErrorCode } from '../../../errors/app-error';
|
||||
import { sendCompletedEmail } from '../../../server-only/document/send-completed-email';
|
||||
import { getAuditLogsPdf } from '../../../server-only/htmltopdf/get-audit-logs-pdf';
|
||||
@@ -29,10 +29,7 @@ import { insertFieldInPDFV2 } from '../../../server-only/pdf/insert-field-in-pdf
|
||||
import { legacy_insertFieldInPDF } from '../../../server-only/pdf/legacy-insert-field-in-pdf';
|
||||
import { getTeamSettings } from '../../../server-only/team/get-team-settings';
|
||||
import { triggerWebhook } from '../../../server-only/webhooks/trigger/trigger-webhook';
|
||||
import {
|
||||
DOCUMENT_AUDIT_LOG_TYPE,
|
||||
type TDocumentAuditLog,
|
||||
} from '../../../types/document-audit-logs';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../../types/document-audit-logs';
|
||||
import {
|
||||
ZWebhookDocumentSchema,
|
||||
mapEnvelopeToWebhookDocumentPayload,
|
||||
@@ -172,7 +169,6 @@ export const run = async ({
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
const envelopeCompletedAuditLog = createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_COMPLETED,
|
||||
envelopeId: envelope.id,
|
||||
@@ -186,24 +182,60 @@ export const run = async ({
|
||||
|
||||
const finalEnvelopeStatus = isRejected ? DocumentStatus.REJECTED : DocumentStatus.COMPLETED;
|
||||
|
||||
// Pre-fetch all PDF data so we can read dimensions and pass it
|
||||
// to decorateAndSignPdf without fetching again.
|
||||
const prefetchedItems = await Promise.all(
|
||||
envelopeItems.map(async (envelopeItem) => {
|
||||
const pdfData = await getFileServerSide(envelopeItem.documentData);
|
||||
let certificateDoc: PDF | null = null;
|
||||
let auditLogDoc: PDF | null = null;
|
||||
|
||||
return { envelopeItem, pdfData };
|
||||
}),
|
||||
);
|
||||
if (settings.includeSigningCertificate || settings.includeAuditLog) {
|
||||
const certificatePayload = {
|
||||
envelope: {
|
||||
...envelope,
|
||||
status: finalEnvelopeStatus,
|
||||
},
|
||||
recipients: envelope.recipients, // Need to use the recipients from envelope which contains ALL recipients.
|
||||
fields,
|
||||
language: envelope.documentMeta.language,
|
||||
envelopeOwner: {
|
||||
email: envelope.user.email,
|
||||
name: envelope.user.name || '',
|
||||
},
|
||||
envelopeItems: envelopeItems.map((item) => item.title),
|
||||
pageWidth: PDF_SIZE_A4_72PPI.width,
|
||||
pageHeight: PDF_SIZE_A4_72PPI.height,
|
||||
envelopeCompletedAuditLog,
|
||||
};
|
||||
|
||||
const usePlaywrightPdf = NEXT_PRIVATE_USE_PLAYWRIGHT_PDF();
|
||||
// Use Playwright-based PDF generation if enabled, otherwise use Konva-based generation.
|
||||
// This is a temporary toggle while we validate the Konva-based approach.
|
||||
const usePlaywrightPdf = NEXT_PRIVATE_USE_PLAYWRIGHT_PDF();
|
||||
|
||||
const needsCertificate = settings.includeSigningCertificate;
|
||||
const needsAuditLog = settings.includeAuditLog;
|
||||
const makeCertificatePdf = async () =>
|
||||
usePlaywrightPdf
|
||||
? getCertificatePdf({
|
||||
documentId,
|
||||
language: envelope.documentMeta.language,
|
||||
}).then(async (buffer) => PDF.load(buffer))
|
||||
: generateCertificatePdf(certificatePayload);
|
||||
|
||||
const makeAuditLogPdf = async () =>
|
||||
usePlaywrightPdf
|
||||
? getAuditLogsPdf({
|
||||
documentId,
|
||||
language: envelope.documentMeta.language,
|
||||
}).then(async (buffer) => PDF.load(buffer))
|
||||
: generateAuditLogPdf(certificatePayload);
|
||||
|
||||
const [createdCertificatePdf, createdAuditLogPdf] = await Promise.all([
|
||||
settings.includeSigningCertificate ? makeCertificatePdf() : null,
|
||||
settings.includeAuditLog ? makeAuditLogPdf() : null,
|
||||
]);
|
||||
|
||||
certificateDoc = createdCertificatePdf;
|
||||
auditLogDoc = createdAuditLogPdf;
|
||||
}
|
||||
|
||||
const newDocumentData: Array<{ oldDocumentDataId: string; newDocumentDataId: string }> = [];
|
||||
|
||||
for (const { envelopeItem, pdfData } of prefetchedItems) {
|
||||
for (const envelopeItem of envelopeItems) {
|
||||
const envelopeItemFields = envelope.envelopeItems.find(
|
||||
(item) => item.id === envelopeItem.id,
|
||||
)?.field;
|
||||
@@ -212,70 +244,12 @@ export const run = async ({
|
||||
throw new Error(`Envelope item fields not found for envelope item ${envelopeItem.id}`);
|
||||
}
|
||||
|
||||
let certificateDoc: PDF | null = null;
|
||||
let auditLogDoc: PDF | null = null;
|
||||
|
||||
if (needsCertificate || needsAuditLog) {
|
||||
const pdfDoc = await PDF.load(pdfData);
|
||||
|
||||
const { width: pageWidth, height: pageHeight } = getLastPageDimensions(pdfDoc);
|
||||
|
||||
const additionalAuditLogs = [
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
{
|
||||
...envelopeCompletedAuditLog,
|
||||
id: '',
|
||||
createdAt: new Date(),
|
||||
} as TDocumentAuditLog,
|
||||
];
|
||||
|
||||
const certificatePayload = {
|
||||
envelope: {
|
||||
...envelope,
|
||||
status: finalEnvelopeStatus,
|
||||
},
|
||||
recipients: envelope.recipients,
|
||||
fields,
|
||||
language: envelope.documentMeta.language,
|
||||
envelopeOwner: {
|
||||
email: envelope.user.email,
|
||||
name: envelope.user.name || '',
|
||||
},
|
||||
envelopeItems: envelopeItems.map((item) => item.title),
|
||||
pageWidth,
|
||||
pageHeight,
|
||||
additionalAuditLogs,
|
||||
};
|
||||
|
||||
const makeCertificatePdf = async () =>
|
||||
usePlaywrightPdf
|
||||
? getCertificatePdf({
|
||||
documentId,
|
||||
language: envelope.documentMeta.language,
|
||||
}).then(async (buffer) => PDF.load(buffer))
|
||||
: generateCertificatePdf(certificatePayload);
|
||||
|
||||
const makeAuditLogPdf = async () =>
|
||||
usePlaywrightPdf
|
||||
? getAuditLogsPdf({
|
||||
documentId,
|
||||
language: envelope.documentMeta.language,
|
||||
}).then(async (buffer) => PDF.load(buffer))
|
||||
: generateAuditLogPdf(certificatePayload);
|
||||
|
||||
[certificateDoc, auditLogDoc] = await Promise.all([
|
||||
needsCertificate ? makeCertificatePdf() : null,
|
||||
needsAuditLog ? makeAuditLogPdf() : null,
|
||||
]);
|
||||
}
|
||||
|
||||
const result = await decorateAndSignPdf({
|
||||
envelope,
|
||||
envelopeItem,
|
||||
envelopeItemFields,
|
||||
isRejected,
|
||||
rejectionReason,
|
||||
pdfData,
|
||||
certificateDoc,
|
||||
auditLogDoc,
|
||||
});
|
||||
@@ -364,13 +338,12 @@ type DecorateAndSignPdfOptions = {
|
||||
envelopeItemFields: Field[];
|
||||
isRejected: boolean;
|
||||
rejectionReason: string;
|
||||
pdfData: Uint8Array;
|
||||
certificateDoc: PDF | null;
|
||||
auditLogDoc: PDF | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Normalize, flatten and insert fields into a PDF document.
|
||||
* Fetch, normalize, flatten and insert fields into a PDF document.
|
||||
*/
|
||||
const decorateAndSignPdf = async ({
|
||||
envelope,
|
||||
@@ -378,10 +351,11 @@ const decorateAndSignPdf = async ({
|
||||
envelopeItemFields,
|
||||
isRejected,
|
||||
rejectionReason,
|
||||
pdfData,
|
||||
certificateDoc,
|
||||
auditLogDoc,
|
||||
}: DecorateAndSignPdfOptions) => {
|
||||
const pdfData = await getFileServerSide(envelopeItem.documentData);
|
||||
|
||||
let pdfDoc = await PDF.load(pdfData);
|
||||
|
||||
// Normalize and flatten layers that could cause issues with the signature
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { PDF, rgb } from '@libpdf/core';
|
||||
import type { FieldType, Recipient } from '@prisma/client';
|
||||
|
||||
import { type TFieldAndMeta, ZEnvelopeFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
|
||||
import { type TFieldAndMeta, ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
|
||||
|
||||
import { parseFieldMetaFromPlaceholder, parseFieldTypeFromPlaceholder } from './helpers';
|
||||
|
||||
@@ -117,7 +117,7 @@ export const extractPlaceholdersFromPDF = async (pdf: Buffer): Promise<Placehold
|
||||
|
||||
const parsedFieldMeta = parseFieldMetaFromPlaceholder(rawFieldMeta, fieldType);
|
||||
|
||||
const fieldAndMeta: TFieldAndMeta = ZEnvelopeFieldAndMetaSchema.parse({
|
||||
const fieldAndMeta: TFieldAndMeta = ZFieldAndMetaSchema.parse({
|
||||
type: fieldType,
|
||||
fieldMeta: parsedFieldMeta,
|
||||
});
|
||||
|
||||
@@ -13,7 +13,6 @@ import { renderAuditLogs } from './render-audit-logs';
|
||||
|
||||
type GenerateAuditLogPdfOptions = GenerateCertificatePdfOptions & {
|
||||
envelopeItems: string[];
|
||||
additionalAuditLogs?: TDocumentAuditLog[];
|
||||
};
|
||||
|
||||
export const generateAuditLogPdf = async (options: GenerateAuditLogPdfOptions) => {
|
||||
@@ -25,7 +24,7 @@ export const generateAuditLogPdf = async (options: GenerateAuditLogPdfOptions) =
|
||||
language,
|
||||
pageWidth,
|
||||
pageHeight,
|
||||
additionalAuditLogs = [],
|
||||
envelopeCompletedAuditLog,
|
||||
} = options;
|
||||
|
||||
const documentLanguage = ZSupportedLanguageCodeSchema.parse(language);
|
||||
@@ -41,7 +40,16 @@ export const generateAuditLogPdf = async (options: GenerateAuditLogPdfOptions) =
|
||||
messages,
|
||||
});
|
||||
|
||||
const auditLogs: TDocumentAuditLog[] = [...additionalAuditLogs, ...partialAuditLogs];
|
||||
const auditLogs: TDocumentAuditLog[] = [...partialAuditLogs];
|
||||
|
||||
if (envelopeCompletedAuditLog) {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
auditLogs.unshift({
|
||||
...envelopeCompletedAuditLog,
|
||||
id: '',
|
||||
createdAt: new Date(),
|
||||
} satisfies Omit<TDocumentAuditLog, 'type'> as TDocumentAuditLog);
|
||||
}
|
||||
|
||||
const auditLogPages = await renderAuditLogs({
|
||||
envelope,
|
||||
|
||||
@@ -7,6 +7,8 @@ import { FieldType } from '@prisma/client';
|
||||
import { prop, sortBy } from 'remeda';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import type { CreateDocumentAuditLogDataResponse } from '@documenso/lib/utils/document-audit-logs';
|
||||
|
||||
import { ZSupportedLanguageCodeSchema } from '../../constants/i18n';
|
||||
import type { TDocumentAuditLogBaseSchema } from '../../types/document-audit-logs';
|
||||
import { extractDocumentAuthMethods } from '../../utils/document-auth';
|
||||
@@ -37,6 +39,7 @@ export type GenerateCertificatePdfOptions = {
|
||||
language?: string;
|
||||
pageWidth: number;
|
||||
pageHeight: number;
|
||||
envelopeCompletedAuditLog?: CreateDocumentAuditLogDataResponse;
|
||||
};
|
||||
|
||||
export const generateCertificatePdf = async (options: GenerateCertificatePdfOptions) => {
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import type { PDFPage } from '@cantoo/pdf-lib';
|
||||
import type { PDF } from '@libpdf/core';
|
||||
|
||||
import { PDF_SIZE_A4_72PPI } from '../../constants/pdf';
|
||||
|
||||
const MIN_CERT_PAGE_WIDTH = 300;
|
||||
const MIN_CERT_PAGE_HEIGHT = 300;
|
||||
|
||||
/**
|
||||
* Gets the effective page size for PDF operations.
|
||||
@@ -22,20 +16,3 @@ export const getPageSize = (page: PDFPage) => {
|
||||
|
||||
return cropBox;
|
||||
};
|
||||
|
||||
export const getLastPageDimensions = (pdfDoc: PDF): { width: number; height: number } => {
|
||||
const lastPage = pdfDoc.getPage(pdfDoc.getPageCount() - 1);
|
||||
|
||||
if (!lastPage) {
|
||||
return PDF_SIZE_A4_72PPI;
|
||||
}
|
||||
|
||||
const width = Math.round(lastPage.width);
|
||||
const height = Math.round(lastPage.height);
|
||||
|
||||
if (width < MIN_CERT_PAGE_WIDTH || height < MIN_CERT_PAGE_HEIGHT) {
|
||||
return PDF_SIZE_A4_72PPI;
|
||||
}
|
||||
|
||||
return { width, height };
|
||||
};
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
|
||||
export interface GetUserByResetTokenOptions {
|
||||
token: string;
|
||||
}
|
||||
|
||||
export const getUserByResetToken = async ({ token }: GetUserByResetTokenOptions) => {
|
||||
const result = await prisma.passwordResetToken.findFirst({
|
||||
where: {
|
||||
token,
|
||||
},
|
||||
include: {
|
||||
user: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!result || !result.user) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND);
|
||||
}
|
||||
|
||||
return result.user;
|
||||
};
|
||||
@@ -1,27 +1,9 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
const LEGACY_DELETED_ACCOUNT_EMAIL = 'deleted-account@documenso.com';
|
||||
|
||||
export const deletedServiceAccountEmail = () => {
|
||||
try {
|
||||
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
||||
if (process.env.NEXT_PRIVATE_DELETED_SERVICE_ACCOUNT_EMAIL) {
|
||||
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
||||
return process.env.NEXT_PRIVATE_DELETED_SERVICE_ACCOUNT_EMAIL;
|
||||
}
|
||||
|
||||
const { hostname } = new URL(process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000');
|
||||
|
||||
return `deleted-account@${hostname}`;
|
||||
} catch (error) {
|
||||
return LEGACY_DELETED_ACCOUNT_EMAIL;
|
||||
}
|
||||
};
|
||||
|
||||
export const deletedAccountServiceAccount = async () => {
|
||||
const serviceAccount = await prisma.user.findFirst({
|
||||
where: {
|
||||
email: deletedServiceAccountEmail(),
|
||||
email: 'deleted-account@documenso.com',
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
@@ -47,20 +29,3 @@ export const deletedAccountServiceAccount = async () => {
|
||||
|
||||
return serviceAccount;
|
||||
};
|
||||
|
||||
export const migrateDeletedAccountServiceAccount = async () => {
|
||||
if (deletedServiceAccountEmail() !== LEGACY_DELETED_ACCOUNT_EMAIL) {
|
||||
console.log(
|
||||
`Migrating deleted account service account to new email: ${deletedServiceAccountEmail()}`,
|
||||
);
|
||||
|
||||
await prisma.user.updateMany({
|
||||
where: {
|
||||
email: LEGACY_DELETED_ACCOUNT_EMAIL,
|
||||
},
|
||||
data: {
|
||||
email: deletedServiceAccountEmail(),
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
const LEGACY_SERVICE_ACCOUNT_EMAIL = 'serviceaccount@documenso.com';
|
||||
|
||||
export const legacyServiceAccountEmail = () => {
|
||||
try {
|
||||
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
||||
if (process.env.NEXT_PRIVATE_LEGACY_SERVICE_ACCOUNT_EMAIL) {
|
||||
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
||||
return process.env.NEXT_PRIVATE_LEGACY_SERVICE_ACCOUNT_EMAIL;
|
||||
}
|
||||
|
||||
const { hostname } = new URL(process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000');
|
||||
|
||||
return `serviceaccount@${hostname}`;
|
||||
} catch (error) {
|
||||
return LEGACY_SERVICE_ACCOUNT_EMAIL;
|
||||
}
|
||||
};
|
||||
|
||||
export const migrateLegacyServiceAccount = async () => {
|
||||
if (legacyServiceAccountEmail() !== LEGACY_SERVICE_ACCOUNT_EMAIL) {
|
||||
console.log(`Migrating legacy service account to new email: ${legacyServiceAccountEmail()}`);
|
||||
|
||||
await prisma.user.updateMany({
|
||||
where: {
|
||||
email: LEGACY_SERVICE_ACCOUNT_EMAIL,
|
||||
},
|
||||
data: {
|
||||
email: legacyServiceAccountEmail(),
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -243,11 +243,6 @@ msgstr "{0} couldn't be uploaded:"
|
||||
msgid "{0} direct signing templates"
|
||||
msgstr "{0} direct signing templates"
|
||||
|
||||
#. placeholder {0}: t(FRIENDLY_FIELD_TYPE[field.type])
|
||||
#: packages/ui/components/document/envelope-recipient-field-tooltip.tsx
|
||||
msgid "{0} field"
|
||||
msgstr "{0} field"
|
||||
|
||||
#. placeholder {0}: team.name
|
||||
#. placeholder {1}: envelope.title
|
||||
#: packages/lib/jobs/definitions/emails/send-signing-email.handler.ts
|
||||
@@ -741,10 +736,6 @@ msgstr "2FA"
|
||||
msgid "2FA Reset"
|
||||
msgstr "2FA Reset"
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
|
||||
msgid "2FA token"
|
||||
msgstr "2FA token"
|
||||
|
||||
#: apps/remix/app/components/forms/token.tsx
|
||||
msgid "3 months"
|
||||
msgstr "3 months"
|
||||
@@ -1421,10 +1412,6 @@ msgstr "All recipients have signed. The document is being processed and you will
|
||||
msgid "All recipients will be notified"
|
||||
msgstr "All recipients will be notified"
|
||||
|
||||
#: apps/remix/app/components/general/app-nav-mobile.tsx
|
||||
msgid "All rights reserved."
|
||||
msgstr "All rights reserved."
|
||||
|
||||
#: packages/email/template-components/template-document-cancel.tsx
|
||||
msgid "All signatures have been voided."
|
||||
msgstr "All signatures have been voided."
|
||||
@@ -2039,10 +2026,6 @@ msgstr "Authentication required"
|
||||
msgid "Authenticator app"
|
||||
msgstr "Authenticator app"
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auto-sign.tsx
|
||||
msgid "Automatically sign fields"
|
||||
msgstr "Automatically sign fields"
|
||||
|
||||
#: apps/remix/app/components/forms/avatar-image.tsx
|
||||
msgid "Avatar"
|
||||
msgstr "Avatar"
|
||||
@@ -2294,7 +2277,6 @@ msgstr "Can't find someone?"
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-password.tsx
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auto-sign.tsx
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
|
||||
@@ -3061,7 +3043,6 @@ msgid "Created"
|
||||
msgstr "Created"
|
||||
|
||||
#: apps/remix/app/components/tables/admin-organisations-table.tsx
|
||||
#: apps/remix/app/components/tables/admin-user-teams-table.tsx
|
||||
#: apps/remix/app/components/tables/user-organisations-table.tsx
|
||||
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/audit-log.tsx
|
||||
#: packages/lib/server-only/pdf/render-audit-logs.ts
|
||||
@@ -4188,7 +4169,6 @@ msgstr "Electronic Signature Disclosure"
|
||||
|
||||
#: apps/remix/app/components/dialogs/ai-field-detection-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/team-email-add-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/team-email-update-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/template-use-dialog.tsx
|
||||
@@ -4226,7 +4206,6 @@ msgstr "Electronic Signature Disclosure"
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx
|
||||
#: packages/ui/primitives/document-flow/add-subject.tsx
|
||||
#: packages/ui/primitives/document-flow/types.ts
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx
|
||||
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx
|
||||
@@ -5147,11 +5126,6 @@ msgstr "Groups"
|
||||
msgid "Having an assistant as the last signer means they will be unable to take any action as there are no subsequent signers to assist."
|
||||
msgstr "Having an assistant as the last signer means they will be unable to take any action as there are no subsequent signers to assist."
|
||||
|
||||
#: apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx
|
||||
#: packages/ui/primitives/document-flow/field-item.tsx
|
||||
msgid "Height:"
|
||||
msgstr "Height:"
|
||||
|
||||
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
|
||||
msgid "Help complete the document for other signers."
|
||||
msgstr "Help complete the document for other signers."
|
||||
@@ -5233,8 +5207,6 @@ msgstr "Hide"
|
||||
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
|
||||
#: apps/remix/app/components/general/folder/folder-grid.tsx
|
||||
#: apps/remix/app/components/general/generic-error-layout.tsx
|
||||
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.folders._index.tsx
|
||||
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.folders._index.tsx
|
||||
msgid "Home"
|
||||
msgstr "Home"
|
||||
|
||||
@@ -5778,7 +5750,6 @@ msgstr "Loading Document..."
|
||||
msgid "Loading suggestions..."
|
||||
msgstr "Loading suggestions..."
|
||||
|
||||
#: apps/remix/app/components/embed/embed-client-loading.tsx
|
||||
#: apps/remix/app/components/general/default-recipients-multiselect-combobox.tsx
|
||||
#: apps/remix/app/components/general/document/document-page-view-recent-activity.tsx
|
||||
msgid "Loading..."
|
||||
@@ -6133,8 +6104,6 @@ msgstr "Multiple access methods can be selected."
|
||||
msgid "My Folder"
|
||||
msgstr "My Folder"
|
||||
|
||||
#: apps/remix/app/components/tables/admin-document-logs-table.tsx
|
||||
#: apps/remix/app/components/tables/document-logs-table.tsx
|
||||
#: apps/remix/app/components/tables/internal-audit-log-table.tsx
|
||||
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
|
||||
#: packages/lib/server-only/pdf/render-audit-logs.ts
|
||||
@@ -6351,10 +6320,6 @@ msgstr "No signature field found"
|
||||
msgid "No Stripe customer attached"
|
||||
msgstr "No Stripe customer attached"
|
||||
|
||||
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
|
||||
msgid "No subscription found"
|
||||
msgstr "No subscription found"
|
||||
|
||||
#: packages/ui/components/recipient/recipient-autocomplete-input.tsx
|
||||
msgid "No suggestions found"
|
||||
msgstr "No suggestions found"
|
||||
@@ -6367,10 +6332,6 @@ msgstr "No team groups found"
|
||||
msgid "No teams yet"
|
||||
msgstr "No teams yet"
|
||||
|
||||
#: apps/remix/app/components/general/webhook-multiselect-combobox.tsx
|
||||
msgid "No triggers available"
|
||||
msgstr "No triggers available"
|
||||
|
||||
#: apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx
|
||||
msgid "No valid direct templates found"
|
||||
msgstr "No valid direct templates found"
|
||||
@@ -6389,13 +6350,11 @@ msgstr "No value found."
|
||||
msgid "No worries, it happens! Enter your email and we'll email you a special link to reset your password."
|
||||
msgstr "No worries, it happens! Enter your email and we'll email you a special link to reset your password."
|
||||
|
||||
#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx
|
||||
#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx
|
||||
#: apps/remix/app/components/forms/editor/editor-field-number-form.tsx
|
||||
#: apps/remix/app/components/tables/admin-claims-table.tsx
|
||||
#: apps/remix/app/components/tables/admin-organisations-table.tsx
|
||||
#: packages/lib/constants/document.ts
|
||||
#: packages/ui/primitives/document-flow/add-subject.tsx
|
||||
msgid "None"
|
||||
msgstr "None"
|
||||
|
||||
@@ -6542,7 +6501,6 @@ msgid "Or continue with"
|
||||
msgstr "Or continue with"
|
||||
|
||||
#: apps/remix/app/components/tables/admin-organisations-table.tsx
|
||||
#: apps/remix/app/components/tables/admin-user-teams-table.tsx
|
||||
#: apps/remix/app/components/tables/user-billing-organisations-table.tsx
|
||||
#: apps/remix/app/components/tables/user-organisations-table.tsx
|
||||
#: apps/remix/app/routes/_unauthenticated+/organisation.sso.confirmation.$token.tsx
|
||||
@@ -6695,7 +6653,6 @@ msgstr "Override organisation settings"
|
||||
#: apps/remix/app/components/tables/organisation-members-table.tsx
|
||||
#: apps/remix/app/components/tables/user-organisations-table.tsx
|
||||
#: apps/remix/app/routes/_authenticated+/admin+/documents._index.tsx
|
||||
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
|
||||
#: apps/remix/app/routes/_authenticated+/dashboard.tsx
|
||||
#: apps/remix/app/routes/_authenticated+/dashboard.tsx
|
||||
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/audit-log.tsx
|
||||
@@ -6726,7 +6683,6 @@ msgstr "Paid"
|
||||
|
||||
#: apps/remix/app/components/forms/signin.tsx
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-dialog.tsx
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
|
||||
msgid "Passkey"
|
||||
msgstr "Passkey"
|
||||
|
||||
@@ -7117,25 +7073,6 @@ msgstr "Please upload a document to continue"
|
||||
msgid "Please upload a logo"
|
||||
msgstr "Please upload a logo"
|
||||
|
||||
#: apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx
|
||||
#: packages/ui/primitives/document-flow/field-item.tsx
|
||||
msgid "Pos X:"
|
||||
msgstr "Pos X:"
|
||||
|
||||
#: apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx
|
||||
#: packages/ui/primitives/document-flow/field-item.tsx
|
||||
msgid "Pos Y:"
|
||||
msgstr "Pos Y:"
|
||||
|
||||
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
|
||||
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-mobile-widget.tsx
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-page-view-v2.tsx
|
||||
#: apps/remix/app/routes/embed+/v1+/multisign+/_index.tsx
|
||||
#: apps/remix/app/routes/embed+/v1+/multisign+/_index.tsx
|
||||
msgid "Powered by"
|
||||
msgstr "Powered by"
|
||||
|
||||
#: apps/remix/app/components/dialogs/template-bulk-send-dialog.tsx
|
||||
msgid "Pre-formatted CSV template with example data."
|
||||
msgstr "Pre-formatted CSV template with example data."
|
||||
@@ -7812,7 +7749,6 @@ msgstr "Right"
|
||||
#: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx
|
||||
#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx
|
||||
#: apps/remix/app/components/tables/admin-organisations-table.tsx
|
||||
#: apps/remix/app/components/tables/admin-user-teams-table.tsx
|
||||
#: apps/remix/app/components/tables/organisation-groups-table.tsx
|
||||
#: apps/remix/app/components/tables/organisation-member-invites-table.tsx
|
||||
#: apps/remix/app/components/tables/organisation-members-table.tsx
|
||||
@@ -8305,7 +8241,6 @@ msgstr "Show templates in your public profile for your audience to sign and get
|
||||
|
||||
#: apps/remix/app/components/dialogs/sign-field-signature-dialog.tsx
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-password.tsx
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auto-sign.tsx
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
|
||||
@@ -8908,7 +8843,6 @@ msgstr "System Requirements"
|
||||
msgid "System Theme"
|
||||
msgstr "System Theme"
|
||||
|
||||
#: apps/remix/app/components/tables/admin-user-teams-table.tsx
|
||||
#: apps/remix/app/components/tables/organisation-insights-table.tsx
|
||||
#: apps/remix/app/components/tables/organisation-teams-table.tsx
|
||||
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
|
||||
@@ -8998,10 +8932,6 @@ msgstr "Team Members"
|
||||
msgid "Team members have been added."
|
||||
msgstr "Team members have been added."
|
||||
|
||||
#: apps/remix/app/routes/_authenticated+/admin+/users.$id.tsx
|
||||
msgid "Team Memberships"
|
||||
msgstr "Team Memberships"
|
||||
|
||||
#: apps/remix/app/components/dialogs/team-create-dialog.tsx
|
||||
#: apps/remix/app/components/forms/team-update-form.tsx
|
||||
#: apps/remix/app/components/tables/organisation-insights-table.tsx
|
||||
@@ -9062,10 +8992,6 @@ msgstr "Teams help you organise your work and collaborate with others. Create yo
|
||||
msgid "Teams that this organisation group is currently assigned to"
|
||||
msgstr "Teams that this organisation group is currently assigned to"
|
||||
|
||||
#: apps/remix/app/routes/_authenticated+/admin+/users.$id.tsx
|
||||
msgid "Teams that this user is a member of and their roles."
|
||||
msgstr "Teams that this user is a member of and their roles."
|
||||
|
||||
#: apps/remix/app/components/dialogs/organisation-member-invite-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx
|
||||
#: apps/remix/app/components/general/template/template-page-view-documents-table.tsx
|
||||
@@ -9380,12 +9306,6 @@ msgstr "The following signers are missing signature fields:"
|
||||
msgid "The following team has been deleted. You will no longer be able to access this team and its documents"
|
||||
msgstr "The following team has been deleted. You will no longer be able to access this team and its documents"
|
||||
|
||||
#. placeholder {0}: form.watch('name')
|
||||
#. placeholder {1}: form.watch('email')
|
||||
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
|
||||
msgid "The next recipient to sign this document will be <0>{0}</0> (<1>{1}</1>)."
|
||||
msgstr "The next recipient to sign this document will be <0>{0}</0> (<1>{1}</1>)."
|
||||
|
||||
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.sso.tsx
|
||||
msgid "The OpenID discovery endpoint URL for your provider"
|
||||
msgstr "The OpenID discovery endpoint URL for your provider"
|
||||
@@ -9905,6 +9825,19 @@ msgstr "Title"
|
||||
msgid "Title cannot be empty"
|
||||
msgstr "Title cannot be empty"
|
||||
|
||||
#. placeholder {0}: actionVerb.toLowerCase()
|
||||
#. placeholder {1}: actionTarget.toLowerCase()
|
||||
#. placeholder {2}: recipient.email
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
|
||||
msgid "To {0} this {1}, you need to be logged in as <0>{2}</0>"
|
||||
msgstr "To {0} this {1}, you need to be logged in as <0>{2}</0>"
|
||||
|
||||
#. placeholder {0}: actionVerb.toLowerCase()
|
||||
#. placeholder {1}: actionTarget.toLowerCase()
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
|
||||
msgid "To {0} this {1}, you need to be logged in."
|
||||
msgstr "To {0} this {1}, you need to be logged in."
|
||||
|
||||
#: apps/remix/app/routes/_unauthenticated+/organisation.invite.$token.tsx
|
||||
msgid "To accept this invitation you must create an account."
|
||||
msgstr "To accept this invitation you must create an account."
|
||||
@@ -9913,42 +9846,6 @@ msgstr "To accept this invitation you must create an account."
|
||||
msgid "To add members to this team, you must first add them to the organisation."
|
||||
msgstr "To add members to this team, you must first add them to the organisation."
|
||||
|
||||
#. placeholder {0}: recipient.email
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
|
||||
msgid "To approve this document, you need to be logged in as <0>{0}</0>"
|
||||
msgstr "To approve this document, you need to be logged in as <0>{0}</0>"
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
|
||||
msgid "To approve this document, you need to be logged in."
|
||||
msgstr "To approve this document, you need to be logged in."
|
||||
|
||||
#. placeholder {0}: recipient.email
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
|
||||
msgid "To approve this field, you need to be logged in as <0>{0}</0>"
|
||||
msgstr "To approve this field, you need to be logged in as <0>{0}</0>"
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
|
||||
msgid "To approve this field, you need to be logged in."
|
||||
msgstr "To approve this field, you need to be logged in."
|
||||
|
||||
#. placeholder {0}: recipient.email
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
|
||||
msgid "To assist with this document, you need to be logged in as <0>{0}</0>"
|
||||
msgstr "To assist with this document, you need to be logged in as <0>{0}</0>"
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
|
||||
msgid "To assist with this document, you need to be logged in."
|
||||
msgstr "To assist with this document, you need to be logged in."
|
||||
|
||||
#. placeholder {0}: recipient.email
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
|
||||
msgid "To assist with this field, you need to be logged in as <0>{0}</0>"
|
||||
msgstr "To assist with this field, you need to be logged in as <0>{0}</0>"
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
|
||||
msgid "To assist with this field, you need to be logged in."
|
||||
msgstr "To assist with this field, you need to be logged in."
|
||||
|
||||
#: apps/remix/app/components/dialogs/team-member-create-dialog.tsx
|
||||
msgid "To be able to add members to a team, you must first add them to the organisation. For more information, please see the <0>documentation</0>."
|
||||
msgstr "To be able to add members to a team, you must first add them to the organisation. For more information, please see the <0>documentation</0>."
|
||||
@@ -9996,24 +9893,6 @@ msgstr "To mark this document as viewed, you need to be logged in."
|
||||
msgid "To proceed further, please set at least one value for the {0} field."
|
||||
msgstr "To proceed further, please set at least one value for the {0} field."
|
||||
|
||||
#. placeholder {0}: recipient.email
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
|
||||
msgid "To sign this document, you need to be logged in as <0>{0}</0>"
|
||||
msgstr "To sign this document, you need to be logged in as <0>{0}</0>"
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
|
||||
msgid "To sign this document, you need to be logged in."
|
||||
msgstr "To sign this document, you need to be logged in."
|
||||
|
||||
#. placeholder {0}: recipient.email
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
|
||||
msgid "To sign this field, you need to be logged in as <0>{0}</0>"
|
||||
msgstr "To sign this field, you need to be logged in as <0>{0}</0>"
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
|
||||
msgid "To sign this field, you need to be logged in."
|
||||
msgstr "To sign this field, you need to be logged in."
|
||||
|
||||
#: apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx
|
||||
msgid "To use our electronic signature service, you must have access to:"
|
||||
msgstr "To use our electronic signature service, you must have access to:"
|
||||
@@ -10022,26 +9901,6 @@ msgstr "To use our electronic signature service, you must have access to:"
|
||||
msgid "To view this document you need to be signed into your account, please sign in to continue."
|
||||
msgstr "To view this document you need to be signed into your account, please sign in to continue."
|
||||
|
||||
#. placeholder {0}: recipient.email
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
|
||||
msgid "To view this document, you need to be logged in as <0>{0}</0>"
|
||||
msgstr "To view this document, you need to be logged in as <0>{0}</0>"
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
|
||||
msgid "To view this document, you need to be logged in."
|
||||
msgstr "To view this document, you need to be logged in."
|
||||
|
||||
#. placeholder {0}: recipient.email
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
|
||||
msgid "To view this field, you need to be logged in as <0>{0}</0>"
|
||||
msgstr "To view this field, you need to be logged in as <0>{0}</0>"
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
|
||||
msgid "To view this field, you need to be logged in."
|
||||
msgstr "To view this field, you need to be logged in."
|
||||
|
||||
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.public-profile.tsx
|
||||
msgid "Toggle the switch to hide your profile from the public."
|
||||
msgstr "Toggle the switch to hide your profile from the public."
|
||||
@@ -10272,7 +10131,6 @@ msgid "Unable to sign in"
|
||||
msgstr "Unable to sign in"
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-password.tsx
|
||||
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings._layout.tsx
|
||||
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings._layout.tsx
|
||||
@@ -11211,7 +11069,6 @@ msgid "We were unable to update your email preferences at this time, please try
|
||||
msgstr "We were unable to update your email preferences at this time, please try again later"
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-password.tsx
|
||||
msgid "We were unable to verify your details. Please try again or contact support"
|
||||
msgstr "We were unable to verify your details. Please try again or contact support"
|
||||
@@ -11381,11 +11238,6 @@ msgstr "Whitelabeling, unlimited members and more"
|
||||
msgid "Who do you want to remind?"
|
||||
msgstr "Who do you want to remind?"
|
||||
|
||||
#: apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx
|
||||
#: packages/ui/primitives/document-flow/field-item.tsx
|
||||
msgid "Width:"
|
||||
msgstr "Width:"
|
||||
|
||||
#: apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx
|
||||
msgid "Withdrawing Consent"
|
||||
msgstr "Withdrawing Consent"
|
||||
@@ -11929,80 +11781,10 @@ msgstr "You need to be logged in as <0>{email}</0> to view this page."
|
||||
msgid "You need to be logged in to view this page."
|
||||
msgstr "You need to be logged in to view this page."
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
|
||||
msgid "You need to setup 2FA to approve this document."
|
||||
msgstr "You need to setup 2FA to approve this document."
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
|
||||
msgid "You need to setup 2FA to approve this field."
|
||||
msgstr "You need to setup 2FA to approve this field."
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
|
||||
msgid "You need to setup 2FA to assist with this document."
|
||||
msgstr "You need to setup 2FA to assist with this document."
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
|
||||
msgid "You need to setup 2FA to assist with this field."
|
||||
msgstr "You need to setup 2FA to assist with this field."
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
|
||||
msgid "You need to setup 2FA to mark this document as viewed."
|
||||
msgstr "You need to setup 2FA to mark this document as viewed."
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
|
||||
msgid "You need to setup 2FA to sign this document."
|
||||
msgstr "You need to setup 2FA to sign this document."
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
|
||||
msgid "You need to setup 2FA to sign this field."
|
||||
msgstr "You need to setup 2FA to sign this field."
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
|
||||
msgid "You need to setup 2FA to view this document."
|
||||
msgstr "You need to setup 2FA to view this document."
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
|
||||
msgid "You need to setup 2FA to view this field."
|
||||
msgstr "You need to setup 2FA to view this field."
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
|
||||
msgid "You need to setup a passkey to approve this document."
|
||||
msgstr "You need to setup a passkey to approve this document."
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
|
||||
msgid "You need to setup a passkey to approve this field."
|
||||
msgstr "You need to setup a passkey to approve this field."
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
|
||||
msgid "You need to setup a passkey to assist with this document."
|
||||
msgstr "You need to setup a passkey to assist with this document."
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
|
||||
msgid "You need to setup a passkey to assist with this field."
|
||||
msgstr "You need to setup a passkey to assist with this field."
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
|
||||
msgid "You need to setup a passkey to mark this document as viewed."
|
||||
msgstr "You need to setup a passkey to mark this document as viewed."
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
|
||||
msgid "You need to setup a passkey to sign this document."
|
||||
msgstr "You need to setup a passkey to sign this document."
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
|
||||
msgid "You need to setup a passkey to sign this field."
|
||||
msgstr "You need to setup a passkey to sign this field."
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
|
||||
msgid "You need to setup a passkey to view this document."
|
||||
msgstr "You need to setup a passkey to view this document."
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
|
||||
msgid "You need to setup a passkey to view this field."
|
||||
msgstr "You need to setup a passkey to view this field."
|
||||
|
||||
#: packages/lib/utils/document-audit-logs.ts
|
||||
msgid "You opened the document"
|
||||
msgstr "You opened the document"
|
||||
@@ -12140,43 +11922,6 @@ msgstr "Your brand website URL"
|
||||
msgid "Your branding preferences have been updated"
|
||||
msgstr "Your branding preferences have been updated"
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
|
||||
msgid "Your browser does not support passkeys, which is required to approve this document."
|
||||
msgstr "Your browser does not support passkeys, which is required to approve this document."
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
|
||||
msgid "Your browser does not support passkeys, which is required to approve this field."
|
||||
msgstr "Your browser does not support passkeys, which is required to approve this field."
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
|
||||
msgid "Your browser does not support passkeys, which is required to assist with this document."
|
||||
msgstr "Your browser does not support passkeys, which is required to assist with this document."
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
|
||||
msgid "Your browser does not support passkeys, which is required to assist with this field."
|
||||
msgstr "Your browser does not support passkeys, which is required to assist with this field."
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
|
||||
msgid "Your browser does not support passkeys, which is required to mark this document as viewed."
|
||||
msgstr "Your browser does not support passkeys, which is required to mark this document as viewed."
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
|
||||
msgid "Your browser does not support passkeys, which is required to sign this document."
|
||||
msgstr "Your browser does not support passkeys, which is required to sign this document."
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
|
||||
msgid "Your browser does not support passkeys, which is required to sign this field."
|
||||
msgstr "Your browser does not support passkeys, which is required to sign this field."
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
|
||||
msgid "Your browser does not support passkeys, which is required to view this document."
|
||||
msgstr "Your browser does not support passkeys, which is required to view this document."
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
|
||||
msgid "Your browser does not support passkeys, which is required to view this field."
|
||||
msgstr "Your browser does not support passkeys, which is required to view this field."
|
||||
|
||||
#: apps/remix/app/components/dialogs/template-bulk-send-dialog.tsx
|
||||
msgid "Your bulk send has been initiated. You will receive an email notification upon completion."
|
||||
msgstr "Your bulk send has been initiated. You will receive an email notification upon completion."
|
||||
|
||||
@@ -243,11 +243,6 @@ msgstr "{0} não pôde ser enviado:"
|
||||
msgid "{0} direct signing templates"
|
||||
msgstr "{0} modelos de assinatura direta"
|
||||
|
||||
#. placeholder {0}: t(FRIENDLY_FIELD_TYPE[field.type])
|
||||
#: packages/ui/components/document/envelope-recipient-field-tooltip.tsx
|
||||
msgid "{0} field"
|
||||
msgstr ""
|
||||
|
||||
#. placeholder {0}: team.name
|
||||
#. placeholder {1}: envelope.title
|
||||
#: packages/lib/jobs/definitions/emails/send-signing-email.handler.ts
|
||||
@@ -741,10 +736,6 @@ msgstr "2FA"
|
||||
msgid "2FA Reset"
|
||||
msgstr "Redefinição de 2FA"
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
|
||||
msgid "2FA token"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/forms/token.tsx
|
||||
msgid "3 months"
|
||||
msgstr "3 meses"
|
||||
@@ -1421,10 +1412,6 @@ msgstr "Todos os destinatários assinaram. O documento está sendo processado e
|
||||
msgid "All recipients will be notified"
|
||||
msgstr "Todos os destinatários serão notificados"
|
||||
|
||||
#: apps/remix/app/components/general/app-nav-mobile.tsx
|
||||
msgid "All rights reserved."
|
||||
msgstr ""
|
||||
|
||||
#: packages/email/template-components/template-document-cancel.tsx
|
||||
msgid "All signatures have been voided."
|
||||
msgstr "Todas as assinaturas foram anuladas."
|
||||
@@ -2039,10 +2026,6 @@ msgstr "Autenticação necessária"
|
||||
msgid "Authenticator app"
|
||||
msgstr "Aplicativo autenticador"
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auto-sign.tsx
|
||||
msgid "Automatically sign fields"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/forms/avatar-image.tsx
|
||||
msgid "Avatar"
|
||||
msgstr "Avatar"
|
||||
@@ -2294,7 +2277,6 @@ msgstr ""
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-password.tsx
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auto-sign.tsx
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
|
||||
@@ -3061,7 +3043,6 @@ msgid "Created"
|
||||
msgstr "Criado"
|
||||
|
||||
#: apps/remix/app/components/tables/admin-organisations-table.tsx
|
||||
#: apps/remix/app/components/tables/admin-user-teams-table.tsx
|
||||
#: apps/remix/app/components/tables/user-organisations-table.tsx
|
||||
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/audit-log.tsx
|
||||
#: packages/lib/server-only/pdf/render-audit-logs.ts
|
||||
@@ -4188,7 +4169,6 @@ msgstr "Divulgação de Assinatura Eletrônica"
|
||||
|
||||
#: apps/remix/app/components/dialogs/ai-field-detection-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/team-email-add-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/team-email-update-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/template-use-dialog.tsx
|
||||
@@ -4226,7 +4206,6 @@ msgstr "Divulgação de Assinatura Eletrônica"
|
||||
#: packages/ui/primitives/document-flow/add-fields.tsx
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx
|
||||
#: packages/ui/primitives/document-flow/add-subject.tsx
|
||||
#: packages/ui/primitives/document-flow/types.ts
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx
|
||||
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx
|
||||
@@ -5147,11 +5126,6 @@ msgstr "Grupos"
|
||||
msgid "Having an assistant as the last signer means they will be unable to take any action as there are no subsequent signers to assist."
|
||||
msgstr "Ter um assistente como o último signatário significa que ele não poderá realizar nenhuma ação, pois não há signatários subsequentes para ajudar."
|
||||
|
||||
#: apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx
|
||||
#: packages/ui/primitives/document-flow/field-item.tsx
|
||||
msgid "Height:"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
|
||||
msgid "Help complete the document for other signers."
|
||||
msgstr "Ajude a completar o documento para outros signatários."
|
||||
@@ -5233,8 +5207,6 @@ msgstr "Ocultar"
|
||||
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
|
||||
#: apps/remix/app/components/general/folder/folder-grid.tsx
|
||||
#: apps/remix/app/components/general/generic-error-layout.tsx
|
||||
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.folders._index.tsx
|
||||
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.folders._index.tsx
|
||||
msgid "Home"
|
||||
msgstr "Início"
|
||||
|
||||
@@ -5778,7 +5750,6 @@ msgstr "Carregando Documento..."
|
||||
msgid "Loading suggestions..."
|
||||
msgstr "Carregando sugestões..."
|
||||
|
||||
#: apps/remix/app/components/embed/embed-client-loading.tsx
|
||||
#: apps/remix/app/components/general/default-recipients-multiselect-combobox.tsx
|
||||
#: apps/remix/app/components/general/document/document-page-view-recent-activity.tsx
|
||||
msgid "Loading..."
|
||||
@@ -6133,8 +6104,6 @@ msgstr "Múltiplos métodos de acesso podem ser selecionados."
|
||||
msgid "My Folder"
|
||||
msgstr "Minha Pasta"
|
||||
|
||||
#: apps/remix/app/components/tables/admin-document-logs-table.tsx
|
||||
#: apps/remix/app/components/tables/document-logs-table.tsx
|
||||
#: apps/remix/app/components/tables/internal-audit-log-table.tsx
|
||||
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
|
||||
#: packages/lib/server-only/pdf/render-audit-logs.ts
|
||||
@@ -6351,10 +6320,6 @@ msgstr "Nenhum campo de assinatura encontrado"
|
||||
msgid "No Stripe customer attached"
|
||||
msgstr "Nenhum cliente Stripe anexado"
|
||||
|
||||
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
|
||||
msgid "No subscription found"
|
||||
msgstr ""
|
||||
|
||||
#: packages/ui/components/recipient/recipient-autocomplete-input.tsx
|
||||
msgid "No suggestions found"
|
||||
msgstr "Nenhuma sugestão encontrada"
|
||||
@@ -6367,10 +6332,6 @@ msgstr "Nenhum grupo de equipe encontrado"
|
||||
msgid "No teams yet"
|
||||
msgstr "Nenhuma equipe ainda"
|
||||
|
||||
#: apps/remix/app/components/general/webhook-multiselect-combobox.tsx
|
||||
msgid "No triggers available"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx
|
||||
msgid "No valid direct templates found"
|
||||
msgstr "Nenhum modelo direto válido encontrado"
|
||||
@@ -6389,13 +6350,11 @@ msgstr "Nenhum valor encontrado."
|
||||
msgid "No worries, it happens! Enter your email and we'll email you a special link to reset your password."
|
||||
msgstr "Não se preocupe, acontece! Digite seu e-mail e enviaremos um link especial para redefinir sua senha."
|
||||
|
||||
#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx
|
||||
#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx
|
||||
#: apps/remix/app/components/forms/editor/editor-field-number-form.tsx
|
||||
#: apps/remix/app/components/tables/admin-claims-table.tsx
|
||||
#: apps/remix/app/components/tables/admin-organisations-table.tsx
|
||||
#: packages/lib/constants/document.ts
|
||||
#: packages/ui/primitives/document-flow/add-subject.tsx
|
||||
msgid "None"
|
||||
msgstr "Nenhum"
|
||||
|
||||
@@ -6542,7 +6501,6 @@ msgid "Or continue with"
|
||||
msgstr "Ou continue com"
|
||||
|
||||
#: apps/remix/app/components/tables/admin-organisations-table.tsx
|
||||
#: apps/remix/app/components/tables/admin-user-teams-table.tsx
|
||||
#: apps/remix/app/components/tables/user-billing-organisations-table.tsx
|
||||
#: apps/remix/app/components/tables/user-organisations-table.tsx
|
||||
#: apps/remix/app/routes/_unauthenticated+/organisation.sso.confirmation.$token.tsx
|
||||
@@ -6695,7 +6653,6 @@ msgstr "Substituir configurações da organização"
|
||||
#: apps/remix/app/components/tables/organisation-members-table.tsx
|
||||
#: apps/remix/app/components/tables/user-organisations-table.tsx
|
||||
#: apps/remix/app/routes/_authenticated+/admin+/documents._index.tsx
|
||||
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
|
||||
#: apps/remix/app/routes/_authenticated+/dashboard.tsx
|
||||
#: apps/remix/app/routes/_authenticated+/dashboard.tsx
|
||||
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/audit-log.tsx
|
||||
@@ -6726,7 +6683,6 @@ msgstr "Pago"
|
||||
|
||||
#: apps/remix/app/components/forms/signin.tsx
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-dialog.tsx
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
|
||||
msgid "Passkey"
|
||||
msgstr "Passkey"
|
||||
|
||||
@@ -7117,25 +7073,6 @@ msgstr "Por favor, faça upload de um documento para continuar"
|
||||
msgid "Please upload a logo"
|
||||
msgstr "Por favor, faça upload de um logotipo"
|
||||
|
||||
#: apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx
|
||||
#: packages/ui/primitives/document-flow/field-item.tsx
|
||||
msgid "Pos X:"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx
|
||||
#: packages/ui/primitives/document-flow/field-item.tsx
|
||||
msgid "Pos Y:"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
|
||||
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-mobile-widget.tsx
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-page-view-v2.tsx
|
||||
#: apps/remix/app/routes/embed+/v1+/multisign+/_index.tsx
|
||||
#: apps/remix/app/routes/embed+/v1+/multisign+/_index.tsx
|
||||
msgid "Powered by"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/template-bulk-send-dialog.tsx
|
||||
msgid "Pre-formatted CSV template with example data."
|
||||
msgstr "Modelo CSV pré-formatado com dados de exemplo."
|
||||
@@ -7812,7 +7749,6 @@ msgstr "Direita"
|
||||
#: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx
|
||||
#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx
|
||||
#: apps/remix/app/components/tables/admin-organisations-table.tsx
|
||||
#: apps/remix/app/components/tables/admin-user-teams-table.tsx
|
||||
#: apps/remix/app/components/tables/organisation-groups-table.tsx
|
||||
#: apps/remix/app/components/tables/organisation-member-invites-table.tsx
|
||||
#: apps/remix/app/components/tables/organisation-members-table.tsx
|
||||
@@ -8305,7 +8241,6 @@ msgstr "Mostre modelos em seu perfil público para seu público assinar e começ
|
||||
|
||||
#: apps/remix/app/components/dialogs/sign-field-signature-dialog.tsx
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-password.tsx
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auto-sign.tsx
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
|
||||
@@ -8908,7 +8843,6 @@ msgstr "Requisitos do Sistema"
|
||||
msgid "System Theme"
|
||||
msgstr "Tema do Sistema"
|
||||
|
||||
#: apps/remix/app/components/tables/admin-user-teams-table.tsx
|
||||
#: apps/remix/app/components/tables/organisation-insights-table.tsx
|
||||
#: apps/remix/app/components/tables/organisation-teams-table.tsx
|
||||
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
|
||||
@@ -8998,10 +8932,6 @@ msgstr "Membros da Equipe"
|
||||
msgid "Team members have been added."
|
||||
msgstr "Membros da equipe foram adicionados."
|
||||
|
||||
#: apps/remix/app/routes/_authenticated+/admin+/users.$id.tsx
|
||||
msgid "Team Memberships"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/team-create-dialog.tsx
|
||||
#: apps/remix/app/components/forms/team-update-form.tsx
|
||||
#: apps/remix/app/components/tables/organisation-insights-table.tsx
|
||||
@@ -9062,10 +8992,6 @@ msgstr "Equipes ajudam você a organizar seu trabalho e colaborar com outros. Cr
|
||||
msgid "Teams that this organisation group is currently assigned to"
|
||||
msgstr "Equipes às quais este grupo da organização está atualmente atribuído"
|
||||
|
||||
#: apps/remix/app/routes/_authenticated+/admin+/users.$id.tsx
|
||||
msgid "Teams that this user is a member of and their roles."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/organisation-member-invite-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx
|
||||
#: apps/remix/app/components/general/template/template-page-view-documents-table.tsx
|
||||
@@ -9380,12 +9306,6 @@ msgstr "Os seguintes signatários estão sem campos de assinatura:"
|
||||
msgid "The following team has been deleted. You will no longer be able to access this team and its documents"
|
||||
msgstr "A seguinte equipe foi excluída. Você não poderá mais acessar esta equipe e seus documentos"
|
||||
|
||||
#. placeholder {0}: form.watch('name')
|
||||
#. placeholder {1}: form.watch('email')
|
||||
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
|
||||
msgid "The next recipient to sign this document will be <0>{0}</0> (<1>{1}</1>)."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.sso.tsx
|
||||
msgid "The OpenID discovery endpoint URL for your provider"
|
||||
msgstr "A URL do endpoint de descoberta OpenID para seu provedor"
|
||||
@@ -9905,6 +9825,19 @@ msgstr "Título"
|
||||
msgid "Title cannot be empty"
|
||||
msgstr "O título não pode estar vazio"
|
||||
|
||||
#. placeholder {0}: actionVerb.toLowerCase()
|
||||
#. placeholder {1}: actionTarget.toLowerCase()
|
||||
#. placeholder {2}: recipient.email
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
|
||||
msgid "To {0} this {1}, you need to be logged in as <0>{2}</0>"
|
||||
msgstr "Para {0} este {1}, você precisa estar logado como <0>{2}</0>"
|
||||
|
||||
#. placeholder {0}: actionVerb.toLowerCase()
|
||||
#. placeholder {1}: actionTarget.toLowerCase()
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
|
||||
msgid "To {0} this {1}, you need to be logged in."
|
||||
msgstr "Para {0} este {1}, você precisa estar logado."
|
||||
|
||||
#: apps/remix/app/routes/_unauthenticated+/organisation.invite.$token.tsx
|
||||
msgid "To accept this invitation you must create an account."
|
||||
msgstr "Para aceitar este convite, você deve criar uma conta."
|
||||
@@ -9913,42 +9846,6 @@ msgstr "Para aceitar este convite, você deve criar uma conta."
|
||||
msgid "To add members to this team, you must first add them to the organisation."
|
||||
msgstr ""
|
||||
|
||||
#. placeholder {0}: recipient.email
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
|
||||
msgid "To approve this document, you need to be logged in as <0>{0}</0>"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
|
||||
msgid "To approve this document, you need to be logged in."
|
||||
msgstr ""
|
||||
|
||||
#. placeholder {0}: recipient.email
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
|
||||
msgid "To approve this field, you need to be logged in as <0>{0}</0>"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
|
||||
msgid "To approve this field, you need to be logged in."
|
||||
msgstr ""
|
||||
|
||||
#. placeholder {0}: recipient.email
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
|
||||
msgid "To assist with this document, you need to be logged in as <0>{0}</0>"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
|
||||
msgid "To assist with this document, you need to be logged in."
|
||||
msgstr ""
|
||||
|
||||
#. placeholder {0}: recipient.email
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
|
||||
msgid "To assist with this field, you need to be logged in as <0>{0}</0>"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
|
||||
msgid "To assist with this field, you need to be logged in."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/team-member-create-dialog.tsx
|
||||
msgid "To be able to add members to a team, you must first add them to the organisation. For more information, please see the <0>documentation</0>."
|
||||
msgstr "Para poder adicionar membros a uma equipe, você deve primeiro adicioná-los à organização. Para mais informações, consulte a <0>documentação</0>."
|
||||
@@ -9996,24 +9893,6 @@ msgstr "Para marcar este documento como visualizado, você precisa estar logado.
|
||||
msgid "To proceed further, please set at least one value for the {0} field."
|
||||
msgstr "Para prosseguir, defina pelo menos um valor para o campo {0}."
|
||||
|
||||
#. placeholder {0}: recipient.email
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
|
||||
msgid "To sign this document, you need to be logged in as <0>{0}</0>"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
|
||||
msgid "To sign this document, you need to be logged in."
|
||||
msgstr ""
|
||||
|
||||
#. placeholder {0}: recipient.email
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
|
||||
msgid "To sign this field, you need to be logged in as <0>{0}</0>"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
|
||||
msgid "To sign this field, you need to be logged in."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx
|
||||
msgid "To use our electronic signature service, you must have access to:"
|
||||
msgstr "Para usar nosso serviço de assinatura eletrônica, você deve ter acesso a:"
|
||||
@@ -10022,26 +9901,6 @@ msgstr "Para usar nosso serviço de assinatura eletrônica, você deve ter acess
|
||||
msgid "To view this document you need to be signed into your account, please sign in to continue."
|
||||
msgstr "Para visualizar este documento, você precisa estar logado em sua conta, faça login para continuar."
|
||||
|
||||
#. placeholder {0}: recipient.email
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
|
||||
msgid "To view this document, you need to be logged in as <0>{0}</0>"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
|
||||
msgid "To view this document, you need to be logged in."
|
||||
msgstr ""
|
||||
|
||||
#. placeholder {0}: recipient.email
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
|
||||
msgid "To view this field, you need to be logged in as <0>{0}</0>"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
|
||||
msgid "To view this field, you need to be logged in."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.public-profile.tsx
|
||||
msgid "Toggle the switch to hide your profile from the public."
|
||||
msgstr "Alterne o botão para ocultar seu perfil do público."
|
||||
@@ -10272,7 +10131,6 @@ msgid "Unable to sign in"
|
||||
msgstr "Não foi possível entrar"
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-password.tsx
|
||||
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings._layout.tsx
|
||||
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings._layout.tsx
|
||||
@@ -11211,7 +11069,6 @@ msgid "We were unable to update your email preferences at this time, please try
|
||||
msgstr "Não conseguimos atualizar suas preferências de e-mail neste momento, por favor, tente novamente mais tarde"
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-password.tsx
|
||||
msgid "We were unable to verify your details. Please try again or contact support"
|
||||
msgstr "Não conseguimos verificar seus detalhes. Por favor, tente novamente ou entre em contato com o suporte"
|
||||
@@ -11381,11 +11238,6 @@ msgstr "Whitelabeling, membros ilimitados e mais"
|
||||
msgid "Who do you want to remind?"
|
||||
msgstr "Quem você deseja lembrar?"
|
||||
|
||||
#: apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx
|
||||
#: packages/ui/primitives/document-flow/field-item.tsx
|
||||
msgid "Width:"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx
|
||||
msgid "Withdrawing Consent"
|
||||
msgstr "Retirada de Consentimento"
|
||||
@@ -11929,80 +11781,10 @@ msgstr "Você precisa estar logado como <0>{email}</0> para visualizar esta pág
|
||||
msgid "You need to be logged in to view this page."
|
||||
msgstr "Você precisa estar logado para visualizar esta página."
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
|
||||
msgid "You need to setup 2FA to approve this document."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
|
||||
msgid "You need to setup 2FA to approve this field."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
|
||||
msgid "You need to setup 2FA to assist with this document."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
|
||||
msgid "You need to setup 2FA to assist with this field."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
|
||||
msgid "You need to setup 2FA to mark this document as viewed."
|
||||
msgstr "Você precisa configurar a 2FA para marcar este documento como visualizado."
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
|
||||
msgid "You need to setup 2FA to sign this document."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
|
||||
msgid "You need to setup 2FA to sign this field."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
|
||||
msgid "You need to setup 2FA to view this document."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
|
||||
msgid "You need to setup 2FA to view this field."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
|
||||
msgid "You need to setup a passkey to approve this document."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
|
||||
msgid "You need to setup a passkey to approve this field."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
|
||||
msgid "You need to setup a passkey to assist with this document."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
|
||||
msgid "You need to setup a passkey to assist with this field."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
|
||||
msgid "You need to setup a passkey to mark this document as viewed."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
|
||||
msgid "You need to setup a passkey to sign this document."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
|
||||
msgid "You need to setup a passkey to sign this field."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
|
||||
msgid "You need to setup a passkey to view this document."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
|
||||
msgid "You need to setup a passkey to view this field."
|
||||
msgstr ""
|
||||
|
||||
#: packages/lib/utils/document-audit-logs.ts
|
||||
msgid "You opened the document"
|
||||
msgstr ""
|
||||
@@ -12140,43 +11922,6 @@ msgstr "URL do site da sua marca"
|
||||
msgid "Your branding preferences have been updated"
|
||||
msgstr "Suas preferências de marca foram atualizadas"
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
|
||||
msgid "Your browser does not support passkeys, which is required to approve this document."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
|
||||
msgid "Your browser does not support passkeys, which is required to approve this field."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
|
||||
msgid "Your browser does not support passkeys, which is required to assist with this document."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
|
||||
msgid "Your browser does not support passkeys, which is required to assist with this field."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
|
||||
msgid "Your browser does not support passkeys, which is required to mark this document as viewed."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
|
||||
msgid "Your browser does not support passkeys, which is required to sign this document."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
|
||||
msgid "Your browser does not support passkeys, which is required to sign this field."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
|
||||
msgid "Your browser does not support passkeys, which is required to view this document."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
|
||||
msgid "Your browser does not support passkeys, which is required to view this field."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/template-bulk-send-dialog.tsx
|
||||
msgid "Your bulk send has been initiated. You will receive an email notification upon completion."
|
||||
msgstr "Seu envio em massa foi iniciado. Você receberá uma notificação por e-mail após a conclusão."
|
||||
|
||||
@@ -37,7 +37,7 @@ const ZCreateFieldBaseSchema = ZEnvelopeFieldAndMetaSchema.and(
|
||||
/**
|
||||
* Position a field using explicit percentage-based coordinates.
|
||||
*/
|
||||
export const ZCoordinatePositionSchema = z.object({
|
||||
const ZCoordinatePositionSchema = z.object({
|
||||
page: ZFieldPageNumberSchema,
|
||||
positionX: ZClampedFieldPositionXSchema,
|
||||
positionY: ZClampedFieldPositionYSchema,
|
||||
@@ -52,7 +52,7 @@ export const ZCoordinatePositionSchema = z.object({
|
||||
* placed at the bounding box of that match. Width and height can optionally be
|
||||
* overridden; when omitted the dimensions of the placeholder text are used.
|
||||
*/
|
||||
export const ZPlaceholderPositionSchema = z.object({
|
||||
const ZPlaceholderPositionSchema = z.object({
|
||||
placeholder: z
|
||||
.string()
|
||||
.describe(
|
||||
@@ -72,10 +72,9 @@ export const ZPlaceholderPositionSchema = z.object({
|
||||
),
|
||||
});
|
||||
|
||||
const ZCreateFieldSchema = z.union([
|
||||
ZCreateFieldBaseSchema.and(ZCoordinatePositionSchema),
|
||||
ZCreateFieldBaseSchema.and(ZPlaceholderPositionSchema),
|
||||
]);
|
||||
const ZCreateFieldSchema = ZCreateFieldBaseSchema.and(
|
||||
z.union([ZCoordinatePositionSchema, ZPlaceholderPositionSchema]),
|
||||
);
|
||||
|
||||
export const ZCreateEnvelopeFieldsRequestSchema = z.object({
|
||||
envelopeId: z.string(),
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZFieldSchema } from '@documenso/lib/types/field';
|
||||
import {
|
||||
ZClampedFieldHeightSchema,
|
||||
ZClampedFieldPositionXSchema,
|
||||
ZClampedFieldPositionYSchema,
|
||||
ZClampedFieldWidthSchema,
|
||||
ZFieldPageNumberSchema,
|
||||
ZFieldSchema,
|
||||
} from '@documenso/lib/types/field';
|
||||
import { ZEnvelopeFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
|
||||
|
||||
import type { TrpcRouteMeta } from '../../trpc';
|
||||
import { ZCoordinatePositionSchema } from './create-envelope-fields.types';
|
||||
|
||||
export const updateEnvelopeFieldsMeta: TrpcRouteMeta = {
|
||||
openapi: {
|
||||
@@ -16,7 +22,7 @@ export const updateEnvelopeFieldsMeta: TrpcRouteMeta = {
|
||||
},
|
||||
};
|
||||
|
||||
const ZUpdateFieldBaseSchema = ZEnvelopeFieldAndMetaSchema.and(
|
||||
const ZUpdateFieldSchema = ZEnvelopeFieldAndMetaSchema.and(
|
||||
z.object({
|
||||
id: z.number().describe('The ID of the field to update.'),
|
||||
envelopeItemId: z
|
||||
@@ -25,18 +31,14 @@ const ZUpdateFieldBaseSchema = ZEnvelopeFieldAndMetaSchema.and(
|
||||
.describe(
|
||||
'The ID of the envelope item to put the field on. If not provided, field will be placed on the first item.',
|
||||
),
|
||||
page: ZFieldPageNumberSchema.optional(),
|
||||
positionX: ZClampedFieldPositionXSchema.optional(),
|
||||
positionY: ZClampedFieldPositionYSchema.optional(),
|
||||
width: ZClampedFieldWidthSchema.optional(),
|
||||
height: ZClampedFieldHeightSchema.optional(),
|
||||
}),
|
||||
);
|
||||
|
||||
// !: Later on we should support placeholders for updates, but since we didn't do the
|
||||
// !: neccesary plumbing for updates in the initial release, we can hold off for now.
|
||||
// const ZUpdateFieldSchema = z.union([
|
||||
// ZUpdateFieldBaseSchema.and(ZCoordinatePositionSchema),
|
||||
// ZUpdateFieldBaseSchema.and(ZPlaceholderPositionSchema),
|
||||
// ]);
|
||||
|
||||
const ZUpdateFieldSchema = ZUpdateFieldBaseSchema.and(ZCoordinatePositionSchema.partial());
|
||||
|
||||
export const ZUpdateEnvelopeFieldsRequestSchema = z.object({
|
||||
envelopeId: z.string(),
|
||||
data: ZUpdateFieldSchema.array(),
|
||||
|
||||
@@ -37,7 +37,7 @@ export const DocumentEmailCheckboxes = ({
|
||||
className="text-muted-foreground ml-2 flex flex-row items-center text-sm"
|
||||
htmlFor={DocumentEmailEvents.RecipientSigned}
|
||||
>
|
||||
<Trans>Email the owner when a recipient signs</Trans>
|
||||
<Trans>Send recipient signed email</Trans>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
@@ -75,7 +75,7 @@ export const DocumentEmailCheckboxes = ({
|
||||
className="text-muted-foreground ml-2 flex flex-row items-center text-sm"
|
||||
htmlFor={DocumentEmailEvents.RecipientSigningRequest}
|
||||
>
|
||||
<Trans>Email recipients with a signing request</Trans>
|
||||
<Trans>Send recipient signing request email</Trans>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
@@ -113,7 +113,7 @@ export const DocumentEmailCheckboxes = ({
|
||||
className="text-muted-foreground ml-2 flex flex-row items-center text-sm"
|
||||
htmlFor={DocumentEmailEvents.RecipientRemoved}
|
||||
>
|
||||
<Trans>Email recipients when they're removed from a pending document</Trans>
|
||||
<Trans>Send recipient removed email</Trans>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
@@ -151,7 +151,7 @@ export const DocumentEmailCheckboxes = ({
|
||||
className="text-muted-foreground ml-2 flex flex-row items-center text-sm"
|
||||
htmlFor={DocumentEmailEvents.DocumentPending}
|
||||
>
|
||||
<Trans>Email the signer if the document is still pending</Trans>
|
||||
<Trans>Send document pending email</Trans>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
@@ -190,7 +190,7 @@ export const DocumentEmailCheckboxes = ({
|
||||
className="text-muted-foreground ml-2 flex flex-row items-center text-sm"
|
||||
htmlFor={DocumentEmailEvents.DocumentCompleted}
|
||||
>
|
||||
<Trans>Email recipients when the document is completed</Trans>
|
||||
<Trans>Send document completed email</Trans>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
@@ -228,7 +228,7 @@ export const DocumentEmailCheckboxes = ({
|
||||
className="text-muted-foreground ml-2 flex flex-row items-center text-sm"
|
||||
htmlFor={DocumentEmailEvents.DocumentDeleted}
|
||||
>
|
||||
<Trans>Email recipients when a pending document is deleted</Trans>
|
||||
<Trans>Send document deleted email</Trans>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
@@ -266,7 +266,7 @@ export const DocumentEmailCheckboxes = ({
|
||||
className="text-muted-foreground ml-2 flex flex-row items-center text-sm"
|
||||
htmlFor={DocumentEmailEvents.OwnerDocumentCompleted}
|
||||
>
|
||||
<Trans>Email the owner when the document is completed</Trans>
|
||||
<Trans>Send document completed email to the owner</Trans>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
|
||||
@@ -186,9 +186,7 @@ export function EnvelopeRecipientFieldTooltip({
|
||||
)}
|
||||
|
||||
<p className="text-center font-semibold">
|
||||
<span>
|
||||
<Trans>{t(FRIENDLY_FIELD_TYPE[field.type])} field</Trans>
|
||||
</span>
|
||||
<span>{t(FRIENDLY_FIELD_TYPE[field.type])} field</span>
|
||||
</p>
|
||||
|
||||
<p className="text-muted-foreground mt-1 text-center text-xs">
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { Suspense, lazy } from 'react';
|
||||
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { type PDFDocumentProxy } from 'pdfjs-dist';
|
||||
|
||||
import type { PdfViewerRendererMode } from './pdf-viewer-konva';
|
||||
@@ -18,13 +17,7 @@ const EnvelopePdfViewer = lazy(async () => import('./pdf-viewer-konva'));
|
||||
|
||||
export const PDFViewerKonvaLazy = (props: PDFViewerProps) => {
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<div>
|
||||
<Trans>Loading...</Trans>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<EnvelopePdfViewer {...props} />
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
@@ -191,10 +191,10 @@ export const AddSubjectFormPartial = ({
|
||||
>
|
||||
<TabsList className="w-full">
|
||||
<TabsTrigger className="w-full" value={DocumentDistributionMethod.EMAIL}>
|
||||
<Trans>Email</Trans>
|
||||
Email
|
||||
</TabsTrigger>
|
||||
<TabsTrigger className="w-full" value={DocumentDistributionMethod.NONE}>
|
||||
<Trans>None</Trans>
|
||||
None
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
|
||||
@@ -2,7 +2,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { FieldType } from '@prisma/client';
|
||||
import { CopyPlus, Settings2, SquareStack, Trash } from 'lucide-react';
|
||||
import { createPortal } from 'react-dom';
|
||||
@@ -325,31 +324,19 @@ export const FieldItem = ({
|
||||
<div className="absolute -top-20 left-1/2 z-50 -translate-x-1/2 rounded-md border border-border bg-background/95 px-2 py-1 shadow-sm backdrop-blur-sm">
|
||||
<div className="flex flex-col gap-0.5 text-[9px]">
|
||||
<span>
|
||||
<span className="text-muted-foreground">
|
||||
<Trans>Pos X:</Trans>
|
||||
</span>
|
||||
|
||||
<span className="text-muted-foreground">Pos X: </span>
|
||||
<span className="font-mono text-foreground">{field.pageX.toFixed(2)}</span>
|
||||
</span>
|
||||
<span>
|
||||
<span className="text-muted-foreground">
|
||||
<Trans>Pos Y:</Trans>
|
||||
</span>
|
||||
|
||||
<span className="text-muted-foreground">Pos Y: </span>
|
||||
<span className="font-mono text-foreground">{field.pageY.toFixed(2)}</span>
|
||||
</span>
|
||||
<span>
|
||||
<span className="text-muted-foreground">
|
||||
<Trans>Width:</Trans>
|
||||
</span>
|
||||
|
||||
<span className="text-muted-foreground">Width: </span>
|
||||
<span className="font-mono text-foreground">{field.pageWidth.toFixed(2)}</span>
|
||||
</span>
|
||||
<span>
|
||||
<span className="text-muted-foreground">
|
||||
<Trans>Height:</Trans>
|
||||
</span>
|
||||
|
||||
<span className="text-muted-foreground">Height: </span>
|
||||
<span className="font-mono text-foreground">{field.pageHeight.toFixed(2)}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -1,19 +1,7 @@
|
||||
import { ClientOnly } from '../../components/client-only';
|
||||
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
|
||||
import { PDFViewer, type PDFViewerProps } from './base.client';
|
||||
|
||||
export const PDFViewerLazy = (props: PDFViewerProps) => {
|
||||
return (
|
||||
<ClientOnly
|
||||
fallback={
|
||||
<div>
|
||||
<Trans>Loading...</Trans>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{() => <PDFViewer {...props} />}
|
||||
</ClientOnly>
|
||||
);
|
||||
return <ClientOnly fallback={<div>Loading...</div>}>{() => <PDFViewer {...props} />}</ClientOnly>;
|
||||
};
|
||||
|
||||