diff --git a/.npmrc b/.npmrc index cbc6b6537..75baad7f0 100644 --- a/.npmrc +++ b/.npmrc @@ -1,3 +1,3 @@ legacy-peer-deps = true prefer-dedupe = true -min-release-age = 7 +# min-release-age = 7 diff --git a/apps/docs/content/docs/developers/api/rate-limits.mdx b/apps/docs/content/docs/developers/api/rate-limits.mdx index ecd50558e..d2e31b1d4 100644 --- a/apps/docs/content/docs/developers/api/rate-limits.mdx +++ b/apps/docs/content/docs/developers/api/rate-limits.mdx @@ -65,3 +65,4 @@ When you exceed a resource limit: - [Authentication](/docs/developers/getting-started/authentication) - API authentication guide - [API Versioning](/docs/developers/api/versioning) - API version management - [First API Call](/docs/developers/getting-started/first-api-call) - Getting started with the API +- [Organisation Limits](/docs/self-hosting/configuration/organisation-limits) - Admins: set per-organisation resource quotas and rate limits (the HTTP rate limit above is separate and not admin-settable) diff --git a/apps/docs/content/docs/policies/enterprise-edition.mdx b/apps/docs/content/docs/policies/enterprise-edition.mdx index a611db5cd..f821e73ec 100644 --- a/apps/docs/content/docs/policies/enterprise-edition.mdx +++ b/apps/docs/content/docs/policies/enterprise-edition.mdx @@ -76,6 +76,8 @@ The Enterprise Edition is required when you: 4. Restart your Documenso instance 5. Verify the license is active in the **Admin Panel** under the **Stats** section + See [Apply Your License Key](/docs/self-hosting/configuration/license) for the full walkthrough, including how to enable individual features once licensed. + @@ -197,7 +199,7 @@ See [Support](/docs/policies/support) for complete support options. 1. Sign the Enterprise license agreement 2. Receive license key and access credentials 3. Deploy using [self-hosting guides](/docs/self-hosting) or access Documenso Cloud - 4. Configure Enterprise features with support assistance + 4. Apply the key — see [Apply Your License Key](/docs/self-hosting/configuration/license) — and configure Enterprise features with support assistance @@ -238,6 +240,7 @@ See [Support](/docs/policies/support) for complete support options. ## Related +- [Apply Your License Key](/docs/self-hosting/configuration/license) - Step-by-step license activation - [Community Edition](/docs/policies/community-edition) - AGPL-3.0 open-source license - [Licenses](/docs/policies/licenses) - Complete licensing overview and FAQ - [Support](/docs/policies/support) - Support channels and response times diff --git a/apps/docs/content/docs/self-hosting/configuration/environment.mdx b/apps/docs/content/docs/self-hosting/configuration/environment.mdx index 4b910b43b..bc4acd1a6 100644 --- a/apps/docs/content/docs/self-hosting/configuration/environment.mdx +++ b/apps/docs/content/docs/self-hosting/configuration/environment.mdx @@ -443,11 +443,11 @@ Telemetry collects only: app version, installation ID, and node ID. No personal ## Enterprise Features -These variables require an active [Enterprise Edition](/docs/policies/enterprise-edition) license. Obtain a license key from [license.documenso.com](https://license.documenso.com) and set it below to unlock enterprise features such as SSO, embed editor, and 21 CFR Part 11 compliance. +These variables require an active [Enterprise Edition](/docs/policies/enterprise-edition) license. Obtain a license key from [license.documenso.com](https://license.documenso.com) and set it below to unlock enterprise features such as SSO, embed editor, and 21 CFR Part 11 compliance. See [Apply Your License Key](/docs/self-hosting/configuration/license) for step-by-step setup. | Variable | Description | | ------------------------------------ | ------------------------------------------------ | -| `NEXT_PRIVATE_DOCUMENSO_LICENSE_KEY` | License key for enterprise features | +| `NEXT_PRIVATE_DOCUMENSO_LICENSE_KEY` | License key for enterprise features — see [Apply Your License Key](/docs/self-hosting/configuration/license) for how to apply it | | `NEXT_PRIVATE_STRIPE_API_KEY` | Stripe API key for billing | | `NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET` | Stripe webhook secret | | `NEXT_PRIVATE_SES_ACCESS_KEY_ID` | AWS SES access key for email domain verification | @@ -510,4 +510,5 @@ NEXT_PRIVATE_SIGNING_PASSPHRASE="your-certificate-password" - [Email Configuration](/docs/self-hosting/configuration/email) - Configure email delivery - [Storage Configuration](/docs/self-hosting/configuration/storage) - Set up S3 storage - [Signing Certificate](/docs/self-hosting/configuration/signing-certificate) - Configure document signing +- [Organisation Limits](/docs/self-hosting/configuration/organisation-limits) - Set per-organisation document, email, and API limits from the admin panel - [Troubleshooting](/docs/self-hosting/maintenance/troubleshooting) - Common configuration issues diff --git a/apps/docs/content/docs/self-hosting/configuration/index.mdx b/apps/docs/content/docs/self-hosting/configuration/index.mdx index 0e80e0efc..de5830a4d 100644 --- a/apps/docs/content/docs/self-hosting/configuration/index.mdx +++ b/apps/docs/content/docs/self-hosting/configuration/index.mdx @@ -29,6 +29,11 @@ description: Configure your self-hosted Documenso instance with environment vari description="Digital signature certificate setup." href="/docs/self-hosting/configuration/signing-certificate" /> + ## Required Configuration diff --git a/apps/docs/content/docs/self-hosting/configuration/license.mdx b/apps/docs/content/docs/self-hosting/configuration/license.mdx new file mode 100644 index 000000000..eea50c154 --- /dev/null +++ b/apps/docs/content/docs/self-hosting/configuration/license.mdx @@ -0,0 +1,107 @@ +--- +title: Apply Your License Key +description: Activate your Enterprise license key to unlock enterprise features on your self-hosted instance. +--- + +import { Accordion, Accordions } from 'fumadocs-ui/components/accordion'; +import { Callout } from 'fumadocs-ui/components/callout'; +import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; + +A license key activates the Enterprise features available to your self-hosted instance, such as CSC signing, SSO, embed white-labelling, and 21 CFR Part 11 compliance. + + + The license key applies to your **whole instance**, not an individual user account. There's one + key per deployment. + + +## Prerequisites + +- An active Enterprise license key — contact [sales](https://documen.so/enterprise) to set up an + Enterprise subscription, then copy your key from [license.documenso.com](https://license.documenso.com). + See [Enterprise Edition](/docs/policies/enterprise-edition) for details. +- A running self-hosted Documenso instance that you're able to restart + +## Step 1: Set the environment variable + +Set `NEXT_PRIVATE_DOCUMENSO_LICENSE_KEY` to your license key. + + + + +Add the variable to your `.env` file (or directly under `environment:` in `compose.yml`): + +```bash +NEXT_PRIVATE_DOCUMENSO_LICENSE_KEY=your-license-key-here +``` + +Then apply it: + +```bash +docker compose up -d +``` + + + + +```bash +docker run -d \ + --name documenso \ + -e NEXT_PRIVATE_DOCUMENSO_LICENSE_KEY=your-license-key-here \ + documenso/documenso:latest +``` + + + + +If you're running Documenso directly (not in a container), add the variable to your `.env` file: + +```bash +NEXT_PRIVATE_DOCUMENSO_LICENSE_KEY=your-license-key-here +``` + + + + +## Step 2: Restart the instance + +The license key is only read once, at process startup. Setting the variable in a running container or shell has no effect until the process restarts. + +```bash +# Docker Compose +docker compose restart documenso + +# Docker +docker restart documenso +``` + +On startup, Documenso validates the key against the Documenso license server and caches the result locally for future startups, so a brief license-server outage won't lock you out. + +## What the license enables + +A valid license doesn't turn every enterprise feature on everywhere — activation depends on the feature: + +- **CSC signing** activates instance-wide automatically once the license is active and CSC transport is configured. See [CSC / QES Signing](/docs/self-hosting/configuration/signing-certificate/csc-qes) for the full setup. +- **SSO, embed white-labelling, 21 CFR Part 11, and similar** are provisioned per organisation. Follow each feature's own guide to configure it once the license is active. + +## Troubleshooting + + + + - Confirm the key is present in the environment the running process actually reads — `docker + exec` into the container and check `env | grep LICENSE` if unsure. + - Confirm the instance was fully restarted after the variable was set, not just reloaded. + - Re-copy the key to rule out truncation or accidental whitespace. + + + Instance-wide features (like CSC signing) also need their own configuration — an active license + alone isn't enough. Check that feature's guide to confirm the required settings are in place. + Per-organisation features additionally need to be provisioned for the organisation that's using + them. + + + +## See Also + +- [Environment Variables](/docs/self-hosting/configuration/environment) - Complete configuration reference +- [Enterprise Edition](/docs/policies/enterprise-edition) - What's included and how to purchase a license +- [CSC / QES Signing](/docs/self-hosting/configuration/signing-certificate/csc-qes) - Enable CSC-based signing diff --git a/apps/docs/content/docs/self-hosting/configuration/meta.json b/apps/docs/content/docs/self-hosting/configuration/meta.json index 32b92f853..6cc250f64 100644 --- a/apps/docs/content/docs/self-hosting/configuration/meta.json +++ b/apps/docs/content/docs/self-hosting/configuration/meta.json @@ -2,12 +2,14 @@ "title": "Configuration", "pages": [ "environment", + "license", "database", "email", "storage", "background-jobs", "signing-certificate", "telemetry", + "organisation-limits", "advanced" ] } diff --git a/apps/docs/content/docs/self-hosting/configuration/organisation-limits.mdx b/apps/docs/content/docs/self-hosting/configuration/organisation-limits.mdx new file mode 100644 index 000000000..2459812ce --- /dev/null +++ b/apps/docs/content/docs/self-hosting/configuration/organisation-limits.mdx @@ -0,0 +1,111 @@ +--- +title: Organisation Limits +description: View and set per-organisation document, email, and API limits on a self-hosted Documenso instance using the admin panel's subscription claims. +--- + +import { Callout } from 'fumadocs-ui/components/callout'; + +Per-organisation limits — document, email, and API usage, plus feature toggles and team/member caps — are controlled by **subscription claims**. You configure them in the admin panel, not through environment variables. + +There are three distinct kinds of limit: + +| Limit | Caps | Admin-settable | +| ---------------------- | ------------------------------------------------- | ----------------------- | +| Resource quota | Documents, emails, and API requests **per month** | Yes — per claim and org | +| Resource rate limit | The same resources over a short window (e.g. `1h`) | Yes — per claim and org | +| Global HTTP rate limit | API requests per IP (100/min, hardcoded) | No — see [Limitations](#limitations) | + +## Prerequisites + +- A running self-hosted Documenso instance. +- An account with the **`ADMIN`** role — an account-level role, separate from organisation and team roles. New accounts are created with the `USER` role only. Grant the first admin by adding `ADMIN` to that user's `roles` directly in the database; after that, an existing admin can grant the role to others under **Admin Panel > Users > _(user)_ > Roles > Update user**. + +Open the admin panel at `/admin`. The sidebar sections used below are **Claims**, **Organisations**, and **Organisation Stats**. + +## Viewing usage + +**One organisation:** open **Admin Panel > Organisations** and select it. The **Organisation usage** section shows the current period's document, email, and API usage against its quotas. + +**All organisations:** open **Admin Panel > Organisation Stats** to sort and filter monthly usage. Filter by **claim** and by **period** (a UTC calendar month, shown as `YYYY-MM`), and switch between **Show usage**, **Show usage with quotas**, and **Show daily averages**. + + + Usage counts **attempts**, not only successful actions. A request that exceeds a quota is still counted before it is rejected, so displayed usage can read higher than the number of actions that succeeded. + + +## Subscription claims + +A subscription claim is a named bundle of limits and feature flags (for example `Free`, `Individual`, `Teams`, `Platform`, or `Enterprise`). Claims are **templates**: when an organisation is created it receives a private copy of its claim and reads from that copy afterwards. Editing a claim template therefore affects organisations created later, not existing ones — to change an existing organisation, [edit it directly](#change-limits-for-one-organisation). + +### Claim fields + +Under **Admin Panel > Claims** (`/admin/claims`), each claim has: + +| Field | Controls | +| ----------------------- | --------------------------------------------------------------------------------- | +| **Name** | The claim's display name. | +| **Team Count** | Teams allowed. `0` = unlimited. | +| **Member Count** | Members allowed. `0` = unlimited. | +| **Envelope Item Count** | Uploaded files allowed per envelope. Minimum `1`. | +| **Recipient Count** | Recipients allowed per document. `0` = unlimited. | +| **Feature Flags** | Feature toggles (see [Feature flags](#feature-flags)). | +| **Limits** | Monthly quota and rate-limit windows for Documents, Emails, and API. | +| **Email transport** | Transport the claim uses. *Default (system mailer)* uses the instance default. | + +### Quotas and rate limits + +The **Limits** section has a column for **Documents**, **Emails**, and **API**, each with two controls: + +- **Monthly quota** — how many of that resource are allowed per calendar month. An **empty** field is unlimited; **`0`** blocks the resource entirely. +- **Rate limit windows** — optional short-window caps, each a duration and a maximum. A window is a number and a unit (`s`, `m`, `h`, `d`), such as `5m`, `1h`, or `24h`, and must be unique within the resource. + + + Quotas and counts use opposite conventions for "unlimited": an **empty** quota is unlimited (and `0` blocks the resource), whereas `0` in the **Team**, **Member**, and **Recipient Count** fields means unlimited. + + +### Feature flags + +The **Feature Flags** section toggles capabilities such as Unlimited documents, Branding, Hide Documenso branding, Email domains, Embed authoring, Embed signing, White label for embed authoring/signing, 21 CFR, HIPAA, Authentication portal, Allow Legacy Envelopes, Signing reminders, QES signing, and Disable emails. + +Some flags are Enterprise features. If your license does not include one, it is marked and cannot be enabled (you can still turn it off). See [Enterprise Edition](/docs/policies/enterprise-edition). + +### Create or edit a claim template + +1. Go to **Admin Panel > Claims**. +2. Select **New claim**, or select an existing claim to edit it. +3. Set the counts, feature flags, and the **Limits** section. +4. Save. Changes apply to organisations created afterwards, not existing ones. + +### Change limits for one organisation + +To change limits for an existing organisation, edit it directly rather than its claim template. + +1. Go to **Admin Panel > Organisations** and open the organisation. +2. Adjust its quota, rate-limit, feature-flag, or email-transport fields. +3. Save. Changes take effect immediately. + +The organisation also shows the **Inherited subscription claim** it was created from. + +## Usage reset + +Monthly quota usage is keyed to the **UTC calendar month**. There is no scheduled reset job — when the month rolls over, the new period's counter starts at `0`. + +## Limitations + +The **global HTTP rate limit is not configurable.** Documenso enforces a hardcoded **100 requests per minute per IP address** on its API endpoint groups (`/api/v1`, `/api/v2`, and the tRPC API are limited separately), returning `429 Too Many Requests`. It is a per-IP safeguard applied at the HTTP layer — not per-organisation, not stored on any claim, and not adjustable from the admin panel. See [Rate Limits](/docs/developers/api/rate-limits). + +## Troubleshooting + +| Symptom | Cause and fix | +| ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------- | +| An organisation hit its limit unexpectedly | Usage counts rejected over-quota attempts. Compare usage against the quota under **Organisation Stats > Show usage with quotas**. | +| A resource is blocked entirely, not just capped | The **Monthly quota** is `0`, which blocks the resource. Leave it empty for unlimited. | +| Emails are not sending for an organisation | Check whether the **Disable emails** flag is enabled on the organisation's claim — it blocks all emails regardless of quota. | +| A claim template edit had no effect | Template edits are not retroactive. Edit the organisation directly under **Admin Panel > Organisations**. | + +--- + +## See Also + +- [Environment Variables](/docs/self-hosting/configuration/environment) - All configuration options +- [Rate Limits](/docs/developers/api/rate-limits) - The global HTTP API rate limit (separate from claims) +- [Enterprise Edition](/docs/policies/enterprise-edition) - Features unlocked by license flags diff --git a/apps/docs/content/docs/self-hosting/configuration/signing-certificate/csc-qes.mdx b/apps/docs/content/docs/self-hosting/configuration/signing-certificate/csc-qes.mdx index 0495430d7..4998baaf7 100644 --- a/apps/docs/content/docs/self-hosting/configuration/signing-certificate/csc-qes.mdx +++ b/apps/docs/content/docs/self-hosting/configuration/signing-certificate/csc-qes.mdx @@ -49,7 +49,7 @@ The callback URL is fixed — Documenso derives it from `NEXT_PUBLIC_WEBAPP_URL` ### Enterprise Edition license -CSC mode is gated by the `instanceCscSigning` license flag. Without a valid Enterprise license, the transport refuses to start (`CSC_UNLICENSED`). +CSC mode is gated by the `instanceCscSigning` license flag. Without a valid Enterprise license, the transport refuses to start (`CSC_UNLICENSED`). See [Apply Your License Key](/docs/self-hosting/configuration/license) to activate one. diff --git a/apps/docs/content/docs/self-hosting/index.mdx b/apps/docs/content/docs/self-hosting/index.mdx index 3f8f8d3c2..99e4ecb01 100644 --- a/apps/docs/content/docs/self-hosting/index.mdx +++ b/apps/docs/content/docs/self-hosting/index.mdx @@ -141,7 +141,7 @@ See the [Quick Start guide](/docs/self-hosting/getting-started/quick-start) for Self-hosted Documenso includes full core functionality under the AGPL-3.0 license. If you need enterprise features such as SSO, embed editor white label, or 21 CFR Part 11 compliance, you can activate them with a license key. -See [Enterprise Edition](/docs/policies/enterprise-edition) for details and [Licenses](/docs/policies/licenses) for a comparison. +See [Enterprise Edition](/docs/policies/enterprise-edition) for details and [Licenses](/docs/policies/licenses) for a comparison. Already have a key? See [Apply Your License Key](/docs/self-hosting/configuration/license). --- diff --git a/apps/docs/package.json b/apps/docs/package.json index 76aecb716..da9966679 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -3,7 +3,7 @@ "version": "0.0.0", "private": true, "scripts": { - "build": "next build", + "build": "NEXT_IGNORE_INCORRECT_LOCKFILE=true next build", "dev": "next dev", "start": "next start", "types:check": "fumadocs-mdx && next typegen && tsc --noEmit", @@ -29,7 +29,7 @@ "@types/node": "^25.1.0", "@types/react": "^19.2.10", "@types/react-dom": "^19.2.3", - "postcss": "^8.5.14", + "postcss": "^8.5.19", "tailwindcss": "^4.1.18", "typescript": "^5.9.3" } diff --git a/apps/docs/src/app/global.css b/apps/docs/src/app/global.css index 7d9c89316..ce71134c8 100644 --- a/apps/docs/src/app/global.css +++ b/apps/docs/src/app/global.css @@ -83,7 +83,7 @@ --accent: hsl(0 0% 27.8431%); --accent-foreground: hsl(95.0847 71.0843% 67.451%); --destructive: hsl(0 86.5979% 61.9608%); - --destructive-foreground: hsl(0 87.6289% 19.0196%); + --destructive-foreground: hsl(0 0% 98.0392%); --border: hsl(0 0% 27.8431%); --input: hsl(0 0% 27.8431%); --ring: hsl(95.0847 71.0843% 67.451%); diff --git a/apps/remix/app/components/dialogs/folder-delete-dialog.tsx b/apps/remix/app/components/dialogs/folder-delete-dialog.tsx index e05f943b3..aca0e7e31 100644 --- a/apps/remix/app/components/dialogs/folder-delete-dialog.tsx +++ b/apps/remix/app/components/dialogs/folder-delete-dialog.tsx @@ -122,7 +122,7 @@ export const FolderDeleteDialog = ({ folder, isOpen, onOpenChange }: FolderDelet Confirm by typing:{' '} - {deleteMessage} + {deleteMessage} diff --git a/apps/remix/app/components/dialogs/organisation-create-dialog.tsx b/apps/remix/app/components/dialogs/organisation-create-dialog.tsx index 183dfbeec..c76bc00a2 100644 --- a/apps/remix/app/components/dialogs/organisation-create-dialog.tsx +++ b/apps/remix/app/components/dialogs/organisation-create-dialog.tsx @@ -336,7 +336,7 @@ const BillingPlanForm = ({ value, onChange, plans, canCreateFreeOrganisation }: >
-

+

Free

diff --git a/apps/remix/app/components/dialogs/organisation-email-domain-delete-dialog.tsx b/apps/remix/app/components/dialogs/organisation-email-domain-delete-dialog.tsx index c903cd079..a0308e530 100644 --- a/apps/remix/app/components/dialogs/organisation-email-domain-delete-dialog.tsx +++ b/apps/remix/app/components/dialogs/organisation-email-domain-delete-dialog.tsx @@ -115,7 +115,7 @@ export const OrganisationEmailDomainDeleteDialog = ({ Confirm by typing{' '} - {deleteMessage} + {deleteMessage} diff --git a/apps/remix/app/components/dialogs/organisation-member-invite-dialog.tsx b/apps/remix/app/components/dialogs/organisation-member-invite-dialog.tsx index a033e7bff..d68c0f5d3 100644 --- a/apps/remix/app/components/dialogs/organisation-member-invite-dialog.tsx +++ b/apps/remix/app/components/dialogs/organisation-member-invite-dialog.tsx @@ -370,7 +370,7 @@ export const OrganisationMemberInviteDialog = ({ trigger, ...props }: Organisati + )} + + + { + // Prevent losing the created token by accidentally clicking outside the dialog. + if (createdToken) { + event.preventDefault(); + } + }} + > + {createdToken ? ( + <> + + + Token created + + + + Copy your token now. For security reasons you will not be able to see it again. + + + +
+ +
+ toast({ title: _(msg`Token copied to clipboard`) })} + /> +
+
+ + + + + + ) : ( + <> + + + Create API token + + + + Use API tokens to authenticate with the Documenso API. + + + +
+ +
+ ( + + + Name + + + + + + + + A name to help you identify this token later. + + + + + )} + /> + + ( + + + Expires in + + + + + + + + + )} + /> + + + + + + +
+
+ + + )} +
+ + ); +}; diff --git a/apps/remix/app/components/dialogs/token-delete-dialog.tsx b/apps/remix/app/components/dialogs/token-delete-dialog.tsx index 63f1465b4..0e3553a4d 100644 --- a/apps/remix/app/components/dialogs/token-delete-dialog.tsx +++ b/apps/remix/app/components/dialogs/token-delete-dialog.tsx @@ -105,7 +105,7 @@ export default function TokenDeleteDialog({ token, onDelete, children }: TokenDe - Are you sure you want to delete this token? + Delete token @@ -126,7 +126,7 @@ export default function TokenDeleteDialog({ token, onDelete, children }: TokenDe Confirm by typing:{' '} - {deleteMessage} + {deleteMessage} @@ -139,21 +139,18 @@ export default function TokenDeleteDialog({ token, onDelete, children }: TokenDe /> -
- + - -
+
diff --git a/apps/remix/app/components/dialogs/webhook-delete-dialog.tsx b/apps/remix/app/components/dialogs/webhook-delete-dialog.tsx index d2f8bc059..3401a2083 100644 --- a/apps/remix/app/components/dialogs/webhook-delete-dialog.tsx +++ b/apps/remix/app/components/dialogs/webhook-delete-dialog.tsx @@ -117,7 +117,7 @@ export const WebhookDeleteDialog = ({ webhook, children }: WebhookDeleteDialogPr Confirm by typing:{' '} - {deleteMessage} + {deleteMessage} diff --git a/apps/remix/app/components/embed/authoring/configure-fields-view.tsx b/apps/remix/app/components/embed/authoring/configure-fields-view.tsx index 4a3a7036f..19a2d4b92 100644 --- a/apps/remix/app/components/embed/authoring/configure-fields-view.tsx +++ b/apps/remix/app/components/embed/authoring/configure-fields-view.tsx @@ -503,7 +503,7 @@ export const ConfigureFieldsView = ({ {selectedField && (
; - -type NewlyCreatedToken = { - id: number; - token: string; -}; - -export type ApiTokenFormProps = { - className?: string; - tokens?: Pick[]; -}; - -export const ApiTokenForm = ({ className, tokens }: ApiTokenFormProps) => { - const [, copy] = useCopyToClipboard(); - - const team = useCurrentTeam(); - - const { _ } = useLingui(); - const { toast } = useToast(); - - const [newlyCreatedToken, setNewlyCreatedToken] = useState(); - const [noExpirationDate, setNoExpirationDate] = useState(false); - - const { mutateAsync: createTokenMutation } = trpc.apiToken.create.useMutation({ - onSuccess(data) { - setNewlyCreatedToken(data); - }, - }); - - const form = useForm({ - resolver: zodResolver(ZCreateTokenFormSchema), - defaultValues: { - tokenName: '', - expirationDate: '', - }, - }); - - const copyToken = async (token: string) => { - try { - const copied = await copy(token); - - if (!copied) { - throw new Error('Unable to copy the token'); - } - - toast({ - title: _(msg`Token copied to clipboard`), - description: _(msg`The token was copied to your clipboard.`), - }); - } catch (error) { - toast({ - title: _(msg`Unable to copy token`), - description: _(msg`We were unable to copy the token to your clipboard. Please try again.`), - variant: 'destructive', - }); - } - }; - - const onSubmit = async ({ tokenName, expirationDate }: TCreateTokenFormSchema) => { - try { - await createTokenMutation({ - teamId: team.id, - tokenName, - expirationDate: noExpirationDate ? null : expirationDate, - }); - - toast({ - title: _(msg`Token created`), - description: _(msg`A new token was created successfully.`), - duration: 5000, - }); - - form.reset(); - } catch (err) { - const error = AppError.parseError(err); - - const errorMessage = match(error.code) - .with(AppErrorCode.UNAUTHORIZED, () => msg`You do not have permission to create a token for this team.`) - .otherwise(() => msg`Something went wrong. Please try again later.`); - - toast({ - title: _(msg`An error occurred`), - description: _(errorMessage), - variant: 'destructive', - duration: 5000, - }); - } - }; - - return ( -
-
- -
- ( - - - Token name - - -
- - - -
- - - Please enter a meaningful name for your token. This will help you identify it later. - - - -
- )} - /> - -
- ( - - - Token expiration date - - -
- - - -
- - -
- )} - /> - -
- - Never expire - -
- -
-
-
- - - -
- -
-
-
- - - - {newlyCreatedToken && tokens && tokens.find((token) => token.id === newlyCreatedToken.id) && ( - - - -

- - Your token was created successfully! Make sure to copy it because you won't be able to see it again! - -

- -

- {newlyCreatedToken.token} -

- - -
-
-
- )} -
-
- ); -}; diff --git a/apps/remix/app/components/general/admin-license-card.tsx b/apps/remix/app/components/general/admin-license-card.tsx index a5ed1fd90..c3a3428df 100644 --- a/apps/remix/app/components/general/admin-license-card.tsx +++ b/apps/remix/app/components/general/admin-license-card.tsx @@ -87,7 +87,7 @@ export const AdminLicenseCard = ({ licenseData }: AdminLicenseCardProps) => {
-

+

Documenso License

diff --git a/apps/remix/app/components/general/envelope-editor/envelope-editor-fields-drag-drop.tsx b/apps/remix/app/components/general/envelope-editor/envelope-editor-fields-drag-drop.tsx index 2c2c4516d..b9af073ed 100644 --- a/apps/remix/app/components/general/envelope-editor/envelope-editor-fields-drag-drop.tsx +++ b/apps/remix/app/components/general/envelope-editor/envelope-editor-fields-drag-drop.tsx @@ -270,7 +270,7 @@ export const EnvelopeEditorFieldDragDrop = ({ {selectedField && (
Upload Document : Upload Template} -

+

Drag and drop your document here

diff --git a/apps/remix/app/entry.client.tsx b/apps/remix/app/entry.client.tsx index 86e949d7a..0ffb3602e 100644 --- a/apps/remix/app/entry.client.tsx +++ b/apps/remix/app/entry.client.tsx @@ -3,27 +3,32 @@ import { dynamicActivate } from '@documenso/lib/utils/i18n'; import { i18n } from '@lingui/core'; import { detect, fromHtmlTag } from '@lingui/detect-locale'; import { I18nProvider } from '@lingui/react'; -import { StrictMode, startTransition, useEffect } from 'react'; +import { StrictMode, startTransition } from 'react'; import { hydrateRoot } from 'react-dom/client'; import { HydratedRouter } from 'react-router/dom'; import './utils/polyfills/promise-with-resolvers'; -function PosthogInit() { +/** + * Initialised imperatively (not as a component inside `hydrateRoot`) because + * rendering extra client-only siblings changes the React tree structure + * relative to the server render in `entry.server.tsx`. That shifts every + * `useId` value (used by Radix for `id`/`htmlFor`/`aria-*`), causing hydration + * mismatches which can abort hydration entirely when the user interacts with + * the page early, leaving dead event handlers (broken dropdowns, native form + * submits). + */ +function initPosthog() { const postHogConfig = extractPostHogConfig(); - useEffect(() => { - if (postHogConfig) { - void import('posthog-js').then(({ default: posthog }) => { - posthog.init(postHogConfig.key, { - api_host: postHogConfig.host, - capture_exceptions: true, - }); + if (postHogConfig) { + void import('posthog-js').then(({ default: posthog }) => { + posthog.init(postHogConfig.key, { + api_host: postHogConfig.host, + capture_exceptions: true, }); - } - }, []); - - return null; + }); + } } async function main() { @@ -38,11 +43,11 @@ async function main() { - - , ); }); + + void initPosthog(); } // eslint-disable-next-line @typescript-eslint/no-floating-promises diff --git a/apps/remix/app/root.tsx b/apps/remix/app/root.tsx index 096b1504e..a7c29b4be 100644 --- a/apps/remix/app/root.tsx +++ b/apps/remix/app/root.tsx @@ -119,7 +119,11 @@ export function LayoutContent({ children }: { children: React.ReactNode }) { const isRecipientRoute = matches.some((m) => m.id?.startsWith('routes/_recipient+')); return ( - + // `suppressHydrationWarning` because `remix-themes` intentionally mutates + // `data-theme`/`class` on before hydration (PreventFlashOnWrongTheme), + // so the server-rendered attributes never match the client render when the + // theme is resolved from the system preference. Attribute-only, one level deep. + @@ -173,7 +177,11 @@ export function LayoutContent({ children }: { children: React.ReactNode }) {