feat(web): add configurable legal, data protection, and imprint links (#3114) (#3131)

This commit is contained in:
Ayush Dubey
2026-10-01 15:39:36 +10:00
committed by GitHub
parent 7d947de29b
commit 8a41a3bf61
8 changed files with 150 additions and 37 deletions
+41 -22
View File
@@ -1,6 +1,11 @@
import communityCardsImage from '@documenso/assets/images/community-cards.png';
import { authClient } from '@documenso/auth/client';
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import {
NEXT_PUBLIC_IMPRINT_URL,
NEXT_PUBLIC_PRIVACY_POLICY_URL,
NEXT_PUBLIC_TERMS_OF_SERVICE_URL,
} from '@documenso/lib/constants/app';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { ZNameSchema } from '@documenso/lib/types/name';
import { env } from '@documenso/lib/utils/env';
@@ -20,7 +25,8 @@ import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type { TurnstileInstance } from '@marsidev/react-turnstile';
import { Turnstile } from '@marsidev/react-turnstile';
import { useEffect, useRef } from 'react';
import type { ReactNode } from 'react';
import { Fragment, useEffect, useRef } from 'react';
import { useForm } from 'react-hook-form';
import { FaIdCardClip } from 'react-icons/fa6';
import { FcGoogle } from 'react-icons/fc';
@@ -102,6 +108,14 @@ export const SignUpForm = ({
const isSubmitting = form.formState.isSubmitting;
const legalLinks: { href: string | undefined; label: ReactNode }[] = [
{ href: NEXT_PUBLIC_TERMS_OF_SERVICE_URL(), label: <Trans>Terms of Service</Trans> },
{ href: NEXT_PUBLIC_PRIVACY_POLICY_URL(), label: <Trans>Privacy Policy</Trans> },
{ href: NEXT_PUBLIC_IMPRINT_URL(), label: <Trans>Imprint</Trans> },
];
const availableLegalLinks = legalLinks.filter((link): link is { href: string; label: ReactNode } => !!link.href);
const onFormSubmit = async ({ name, email, password, signature }: TSignUpFormSchema) => {
try {
let token: string | undefined;
@@ -406,27 +420,32 @@ export const SignUpForm = ({
)}
</form>
</Form>
<p className="mt-6 text-muted-foreground text-xs">
<Trans>
By proceeding, you agree to our{' '}
<Link
to="https://documen.so/terms"
target="_blank"
className="text-documenso-700 duration-200 hover:opacity-70"
>
Terms of Service
</Link>{' '}
and{' '}
<Link
to="https://documen.so/privacy"
target="_blank"
className="text-documenso-700 duration-200 hover:opacity-70"
>
Privacy Policy
</Link>
.
</Trans>
</p>
{availableLegalLinks.length > 0 && (
<p className="mt-6 text-muted-foreground text-xs">
<Trans>
By proceeding, you agree to our{' '}
{availableLegalLinks.map((link, index) => (
<Fragment key={link.href}>
{index > 0 && (
<>
{' '}
<Trans>and</Trans>{' '}
</>
)}
<Link
to={link.href}
target="_blank"
rel="noopener noreferrer"
className="text-documenso-700 duration-200 hover:opacity-70"
>
{link.label}
</Link>
</Fragment>
))}
.
</Trans>
</p>
)}
</div>
</div>
);
@@ -17,6 +17,7 @@ import { match } from 'ts-pattern';
import { EnvelopeDownloadDialog } from '~/components/dialogs/envelope-download-dialog';
import { useEmbedSigningContext } from '~/components/embed/embed-signing-context';
import { BrandingLogo } from '~/components/general/branding-logo';
import { PublicLegalLinks } from '~/components/general/public-legal-links';
import { BrandingLogoIcon } from '../branding-logo-icon';
import { DocumentSigningRejectDialog } from '../document-signing/document-signing-reject-dialog';
@@ -68,8 +69,10 @@ export const EnvelopeSignerHeader = () => {
</div>
{/* Right side - Desktop content */}
<div className="hidden items-center space-x-2 lg:flex">
<p className="mr-2 flex-shrink-0 text-muted-foreground text-sm">
<div className="hidden items-center space-x-4 lg:flex">
<PublicLegalLinks />
<p className="flex-shrink-0 text-muted-foreground text-sm">
<Plural one="1 Field Remaining" other="# Fields Remaining" value={recipientFieldsRemaining.length} />
</p>
@@ -0,0 +1,60 @@
import {
NEXT_PUBLIC_IMPRINT_URL,
NEXT_PUBLIC_PRIVACY_POLICY_URL,
NEXT_PUBLIC_TERMS_OF_SERVICE_URL,
} from '@documenso/lib/constants/app';
import { cn } from '@documenso/ui/lib/utils';
import { Trans } from '@lingui/react/macro';
import { Link } from 'react-router';
export type PublicLegalLinksProps = {
className?: string;
};
export const PublicLegalLinks = ({ className }: PublicLegalLinksProps) => {
const termsUrl = NEXT_PUBLIC_TERMS_OF_SERVICE_URL();
const privacyUrl = NEXT_PUBLIC_PRIVACY_POLICY_URL();
const imprintUrl = NEXT_PUBLIC_IMPRINT_URL();
return (
<div
className={cn(
'flex flex-wrap items-center justify-center gap-x-4 gap-y-2 text-xs text-muted-foreground',
className,
)}
>
{termsUrl && (
<Link
to={termsUrl}
target="_blank"
rel="noopener noreferrer"
className="duration-200 hover:text-foreground hover:underline"
>
<Trans>Terms of Service</Trans>
</Link>
)}
{privacyUrl && (
<Link
to={privacyUrl}
target="_blank"
rel="noopener noreferrer"
className="duration-200 hover:text-foreground hover:underline"
>
<Trans>Privacy Policy</Trans>
</Link>
)}
{imprintUrl && (
<Link
to={imprintUrl}
target="_blank"
rel="noopener noreferrer"
className="duration-200 hover:text-foreground hover:underline"
>
<Trans>Imprint</Trans>
</Link>
)}
</div>
);
};
+19 -9
View File
@@ -10,6 +10,8 @@ import { useEffect } from 'react';
import { isRouteErrorResponse, Link, Outlet } from 'react-router';
import { Header as AuthenticatedHeader } from '~/components/general/app-header';
import { GenericErrorLayout } from '~/components/general/generic-error-layout';
import { PublicLegalLinks } from '~/components/general/public-legal-links';
import type { Route } from './+types/_layout';
export function meta() {
@@ -35,16 +37,24 @@ export default function RecipientLayout({ matches }: Route.ComponentProps) {
);
return (
<div className="min-h-screen">
{!hideHeader && sessionData?.user && <AuthenticatedHeader />}
<div className="flex min-h-screen flex-col justify-between">
<div>
{!hideHeader && sessionData?.user && <AuthenticatedHeader />}
<main
className={cn({
'mt-8 mb-8 px-4 md:mt-12 md:mb-12 md:px-8': !hideHeader,
})}
>
<Outlet />
</main>
<main
className={cn({
'mt-8 mb-8 px-4 md:mt-12 md:mb-12 md:px-8': !hideHeader,
})}
>
<Outlet />
</main>
</div>
{!hideHeader && (
<footer className="py-4">
<PublicLegalLinks />
</footer>
)}
</div>
);
}
@@ -1,10 +1,12 @@
import backgroundPattern from '@documenso/assets/images/background-pattern.png';
import { Outlet } from 'react-router';
import { PublicLegalLinks } from '~/components/general/public-legal-links';
export default function Layout() {
return (
<main className="relative flex min-h-screen flex-col items-center justify-center overflow-hidden px-4 py-12 md:p-12 lg:p-24">
<div>
<main className="relative flex min-h-screen flex-col items-center justify-between overflow-hidden px-4 py-12 md:p-12 lg:p-24">
<div className="flex w-full flex-1 flex-col items-center justify-center">
<div className="absolute -inset-[min(600px,max(400px,60vw))] -z-[1] flex items-center justify-center opacity-70">
<img
src={backgroundPattern}
@@ -17,10 +19,14 @@ export default function Layout() {
/>
</div>
<div className="relative w-full">
<div className="relative w-full flex items-center justify-center">
<Outlet />
</div>
</div>
<footer className="z-10 mt-8 w-full">
<PublicLegalLinks />
</footer>
</main>
);
}
@@ -54,7 +54,7 @@ export default function SignUp({ loaderData }: Route.ComponentProps) {
return (
<SignUpForm
className="w-screen max-w-screen-2xl px-4 md:px-16 lg:-my-16"
className="w-screen max-w-screen-2xl px-4 md:px-16 lg:-mt-16"
isEmailPasswordSignupEnabled={isEmailPasswordSignupEnabled}
isGoogleSignupEnabled={isGoogleSignupEnabled}
isMicrosoftSignupEnabled={isMicrosoftSignupEnabled}
+8
View File
@@ -45,6 +45,14 @@ export const formatPath = (path: string): string => {
export const NEXT_PUBLIC_SIGNING_CONTACT_INFO = () =>
env('NEXT_PUBLIC_SIGNING_CONTACT_INFO') ?? NEXT_PUBLIC_WEBAPP_URL();
export const NEXT_PUBLIC_TERMS_OF_SERVICE_URL = () =>
env('NEXT_PUBLIC_TERMS_OF_SERVICE_URL') ?? (IS_DOCUMENSO_CLOUD() ? 'https://documen.so/terms' : undefined);
export const NEXT_PUBLIC_PRIVACY_POLICY_URL = () =>
env('NEXT_PUBLIC_PRIVACY_POLICY_URL') ?? (IS_DOCUMENSO_CLOUD() ? 'https://documen.so/privacy' : undefined);
export const NEXT_PUBLIC_IMPRINT_URL = () => env('NEXT_PUBLIC_IMPRINT_URL');
export const NEXT_PRIVATE_USE_LEGACY_SIGNING_SUBFILTER = () =>
env('NEXT_PRIVATE_USE_LEGACY_SIGNING_SUBFILTER') === 'true';
+7
View File
@@ -133,6 +133,13 @@ declare namespace NodeJS {
NEXT_PUBLIC_TURNSTILE_SITE_KEY?: string;
NEXT_PRIVATE_TURNSTILE_SECRET_KEY?: string;
/**
* Legal & Data Protection environment variables
*/
NEXT_PUBLIC_TERMS_OF_SERVICE_URL?: string;
NEXT_PUBLIC_PRIVACY_POLICY_URL?: string;
NEXT_PUBLIC_IMPRINT_URL?: string;
/**
* Google Vertex AI environment variables
*/