diff --git a/apps/remix/app/components/forms/signup.tsx b/apps/remix/app/components/forms/signup.tsx index ea22b2789..7d086382a 100644 --- a/apps/remix/app/components/forms/signup.tsx +++ b/apps/remix/app/components/forms/signup.tsx @@ -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: Terms of Service }, + { href: NEXT_PUBLIC_PRIVACY_POLICY_URL(), label: Privacy Policy }, + { href: NEXT_PUBLIC_IMPRINT_URL(), label: Imprint }, + ]; + + 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 = ({ )} -

- - By proceeding, you agree to our{' '} - - Terms of Service - {' '} - and{' '} - - Privacy Policy - - . - -

+ {availableLegalLinks.length > 0 && ( +

+ + By proceeding, you agree to our{' '} + {availableLegalLinks.map((link, index) => ( + + {index > 0 && ( + <> + {' '} + and{' '} + + )} + + {link.label} + + + ))} + . + +

+ )} ); diff --git a/apps/remix/app/components/general/envelope-signing/envelope-signer-header.tsx b/apps/remix/app/components/general/envelope-signing/envelope-signer-header.tsx index 88ab7f8f4..946eb6ae4 100644 --- a/apps/remix/app/components/general/envelope-signing/envelope-signer-header.tsx +++ b/apps/remix/app/components/general/envelope-signing/envelope-signer-header.tsx @@ -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 = () => { {/* Right side - Desktop content */} -
-

+

+ + +

diff --git a/apps/remix/app/components/general/public-legal-links.tsx b/apps/remix/app/components/general/public-legal-links.tsx new file mode 100644 index 000000000..d21c81949 --- /dev/null +++ b/apps/remix/app/components/general/public-legal-links.tsx @@ -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 ( +
+ {termsUrl && ( + + Terms of Service + + )} + + {privacyUrl && ( + + Privacy Policy + + )} + + {imprintUrl && ( + + Imprint + + )} +
+ ); +}; diff --git a/apps/remix/app/routes/_recipient+/_layout.tsx b/apps/remix/app/routes/_recipient+/_layout.tsx index fde4a7998..b074e4462 100644 --- a/apps/remix/app/routes/_recipient+/_layout.tsx +++ b/apps/remix/app/routes/_recipient+/_layout.tsx @@ -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 ( -
- {!hideHeader && sessionData?.user && } +
+
+ {!hideHeader && sessionData?.user && } -
- -
+
+ +
+
+ + {!hideHeader && ( +
+ +
+ )}
); } diff --git a/apps/remix/app/routes/_unauthenticated+/_layout.tsx b/apps/remix/app/routes/_unauthenticated+/_layout.tsx index 1ad6db22e..dd080fb4b 100644 --- a/apps/remix/app/routes/_unauthenticated+/_layout.tsx +++ b/apps/remix/app/routes/_unauthenticated+/_layout.tsx @@ -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 ( -
-
+
+
-
+
+ +
+ +
); } diff --git a/apps/remix/app/routes/_unauthenticated+/signup.tsx b/apps/remix/app/routes/_unauthenticated+/signup.tsx index f470004d2..6c5be3889 100644 --- a/apps/remix/app/routes/_unauthenticated+/signup.tsx +++ b/apps/remix/app/routes/_unauthenticated+/signup.tsx @@ -54,7 +54,7 @@ export default function SignUp({ loaderData }: Route.ComponentProps) { return ( { 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'; diff --git a/packages/tsconfig/process-env.d.ts b/packages/tsconfig/process-env.d.ts index 31f98cabb..f2a5d9957 100644 --- a/packages/tsconfig/process-env.d.ts +++ b/packages/tsconfig/process-env.d.ts @@ -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 */