diff --git a/apps/remix/package.json b/apps/remix/package.json
index 1f69bf91a..bbbe66ed0 100644
--- a/apps/remix/package.json
+++ b/apps/remix/package.json
@@ -25,26 +25,45 @@
"@hono/node-server": "^1.13.7",
"@hono/trpc-server": "^0.3.4",
"@hono/zod-validator": "^0.4.2",
+ "@hookform/resolvers": "^3.1.0",
"@lingui/core": "^4.11.3",
"@lingui/detect-locale": "^4.11.1",
"@lingui/macro": "^4.11.3",
"@lingui/react": "^4.11.3",
"@oslojs/crypto": "^1.0.1",
"@oslojs/encoding": "^1.1.0",
+ "@simplewebauthn/browser": "^9.0.1",
+ "@simplewebauthn/server": "^9.0.3",
"@react-router/node": "^7.1.5",
"@react-router/serve": "^7.1.5",
"autoprefixer": "^10.4.13",
+ "colord": "^2.9.3",
"framer-motion": "^10.12.8",
"hono": "4.6.15",
"hono-react-router-adapter": "^0.6.2",
+ "input-otp": "^1.2.4",
+ "luxon": "^3.4.0",
+ "lucide-react": "^0.279.0",
"isbot": "^5.1.17",
"jsonwebtoken": "^9.0.2",
+ "papaparse": "^5.4.1",
"posthog-js": "^1.75.3",
"posthog-node": "^3.1.1",
"react": "^18",
"react-dom": "^18",
"react-router": "^7.1.5",
+ "react-call": "^1.3.0",
+ "react-dropzone": "^14.2.3",
+ "react-hook-form": "^7.43.9",
+ "react-hotkeys-hook": "^4.4.1",
+ "react-icons": "^4.11.0",
+ "react-rnd": "^10.4.1",
+ "recharts": "^2.7.2",
+ "remeda": "^2.17.3",
"remix-themes": "^2.0.4",
+ "sharp": "0.32.6",
+ "uqr": "^0.1.2",
+ "ua-parser-js": "^1.0.37",
"tailwindcss": "^3.4.15",
"ts-pattern": "^5.0.5"
},
@@ -55,6 +74,10 @@
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
+ "@simplewebauthn/types": "^9.0.1",
+ "@types/formidable": "^2.0.6",
+ "@types/luxon": "^3.3.1",
+ "@types/ua-parser-js": "^0.7.39",
"cross-env": "^7.0.3",
"remix-flat-routes": "^0.8.4",
"tsx": "^4.11.0",
@@ -63,4 +86,4 @@
"vite-plugin-babel-macros": "^1.0.6",
"vite-tsconfig-paths": "^5.1.4"
}
-}
+}
\ No newline at end of file
diff --git a/apps/web/README.md b/apps/web/README.md
deleted file mode 100644
index 2df72cac5..000000000
--- a/apps/web/README.md
+++ /dev/null
@@ -1 +0,0 @@
-# @documenso/web
diff --git a/apps/web/ambient.d.ts b/apps/web/ambient.d.ts
deleted file mode 100644
index 54b8c1d7c..000000000
--- a/apps/web/ambient.d.ts
+++ /dev/null
@@ -1 +0,0 @@
-declare module '@documenso/tailwind-config';
diff --git a/apps/web/example/cert.p12 b/apps/web/example/cert.p12
deleted file mode 100644
index 532ee19ab..000000000
Binary files a/apps/web/example/cert.p12 and /dev/null differ
diff --git a/apps/web/next-env.d.ts b/apps/web/next-env.d.ts
deleted file mode 100644
index fd36f9494..000000000
--- a/apps/web/next-env.d.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-///
-///
-///
-
-// NOTE: This file should not be edited
-// see https://nextjs.org/docs/basic-features/typescript for more information.
diff --git a/apps/web/next.config.js b/apps/web/next.config.js
deleted file mode 100644
index 5b8f3e60b..000000000
--- a/apps/web/next.config.js
+++ /dev/null
@@ -1,108 +0,0 @@
-/* eslint-disable @typescript-eslint/no-var-requires */
-const fs = require('fs');
-const path = require('path');
-const { version } = require('./package.json');
-const { withAxiom } = require('next-axiom');
-
-const ENV_FILES = ['.env', '.env.local', `.env.${process.env.NODE_ENV || 'development'}`];
-
-ENV_FILES.forEach((file) => {
- require('dotenv').config({
- path: path.join(__dirname, `../../${file}`),
- });
-});
-
-// !: This is a temp hack to get caveat working without placing it back in the public directory.
-// !: By inlining this at build time we should be able to sign faster.
-const FONT_CAVEAT_BYTES = fs.readFileSync(
- path.join(__dirname, '../../packages/assets/fonts/caveat.ttf'),
-);
-
-const FONT_NOTO_SANS_BYTES = fs.readFileSync(
- path.join(__dirname, '../../packages/assets/fonts/noto-sans.ttf'),
-);
-
-/** @type {import('next').NextConfig} */
-const config = {
- output: process.env.DOCKER_OUTPUT ? 'standalone' : undefined,
- experimental: {
- outputFileTracingRoot: path.join(__dirname, '../../'),
- serverComponentsExternalPackages: ['@node-rs/bcrypt', '@documenso/pdf-sign', 'playwright'],
- serverActions: {
- bodySizeLimit: '50mb',
- },
- swcPlugins: [['@lingui/swc-plugin', {}]],
- },
- reactStrictMode: true,
- transpilePackages: [
- '@documenso/assets',
- '@documenso/ee',
- '@documenso/lib',
- '@documenso/prisma',
- '@documenso/tailwind-config',
- '@documenso/trpc',
- '@documenso/ui',
- ],
- env: {
- APP_VERSION: version,
- NEXT_PUBLIC_PROJECT: 'web',
- FONT_CAVEAT_URI: `data:font/ttf;base64,${FONT_CAVEAT_BYTES.toString('base64')}`,
- FONT_NOTO_SANS_URI: `data:font/ttf;base64,${FONT_NOTO_SANS_BYTES.toString('base64')}`,
- },
- modularizeImports: {
- 'lucide-react': {
- transform: 'lucide-react/dist/esm/icons/{{ kebabCase member }}',
- },
- },
- webpack: (config, { isServer }) => {
- // fixes: Module not found: Can’t resolve ‘../build/Release/canvas.node’
- if (isServer) {
- config.resolve.alias.canvas = false;
- }
-
- config.module.rules.push({
- test: /\.po$/,
- use: {
- loader: '@lingui/loader',
- },
- });
-
- return config;
- },
- async rewrites() {
- return [
- {
- source: '/ingest/:path*',
- destination: 'https://eu.posthog.com/:path*',
- },
- ];
- },
- async redirects() {
- return [
- {
- permanent: true,
- source: '/documents/:id/sign',
- destination: '/sign/:token',
- has: [
- {
- type: 'query',
- key: 'token',
- },
- ],
- },
- {
- permanent: true,
- source: '/documents/:id/signed',
- destination: '/sign/:token',
- has: [
- {
- type: 'query',
- key: 'token',
- },
- ],
- },
- ];
- },
-};
-
-module.exports = withAxiom(config);
diff --git a/apps/web/package.json b/apps/web/package.json
deleted file mode 100644
index 9f5053e1e..000000000
--- a/apps/web/package.json
+++ /dev/null
@@ -1,77 +0,0 @@
-{
- "name": "@documenso/web",
- "version": "1.9.0-rc.11",
- "private": true,
- "license": "AGPL-3.0",
- "scripts": {
- "dev": "next dev -p 3000",
- "build": "npm run translate --prefix ../../ && next build",
- "start": "next start",
- "lint": "next lint",
- "e2e:prepare": "next build && next start",
- "lint:fix": "next lint --fix",
- "clean": "rimraf .next && rimraf node_modules",
- "copy:pdfjs": "node ../../scripts/copy-pdfjs.cjs"
- },
- "dependencies": {
- "@documenso/api": "*",
- "@documenso/assets": "*",
- "@documenso/ee": "*",
- "@documenso/lib": "*",
- "@documenso/prisma": "*",
- "@documenso/trpc": "*",
- "@documenso/ui": "*",
- "@hookform/resolvers": "^3.1.0",
- "@lingui/macro": "^4.11.3",
- "@lingui/react": "^4.11.3",
- "@simplewebauthn/browser": "^9.0.1",
- "@simplewebauthn/server": "^9.0.3",
- "colord": "^2.9.3",
- "cookie-es": "^1.0.0",
- "formidable": "^2.1.1",
- "framer-motion": "^10.12.8",
- "input-otp": "^1.2.4",
- "lucide-react": "^0.279.0",
- "luxon": "^3.4.0",
- "micro": "^10.0.1",
- "next": "14.2.6",
- "next-auth": "4.24.5",
- "next-axiom": "^1.5.1",
- "next-plausible": "^3.10.1",
- "next-themes": "^0.2.1",
- "papaparse": "^5.4.1",
- "perfect-freehand": "^1.2.0",
- "posthog-js": "^1.75.3",
- "posthog-node": "^3.1.1",
- "react": "^18",
- "react-call": "^1.3.0",
- "react-dom": "^18",
- "react-dropzone": "^14.2.3",
- "react-hook-form": "^7.43.9",
- "react-hotkeys-hook": "^4.4.1",
- "react-icons": "^4.11.0",
- "react-rnd": "^10.4.1",
- "recharts": "^2.7.2",
- "remeda": "^2.17.3",
- "sharp": "0.32.6",
- "trpc-to-openapi": "2.0.4",
- "ts-pattern": "^5.0.5",
- "ua-parser-js": "^1.0.37",
- "uqr": "^0.1.2",
- "zod": "3.24.1"
- },
- "devDependencies": {
- "@documenso/tailwind-config": "*",
- "@lingui/loader": "^4.11.3",
- "@lingui/swc-plugin": "4.0.8",
- "@simplewebauthn/types": "^9.0.1",
- "@types/formidable": "^2.0.6",
- "@types/luxon": "^3.3.1",
- "@types/node": "^20",
- "@types/papaparse": "^5.3.14",
- "@types/react": "^18",
- "@types/react-dom": "^18",
- "@types/ua-parser-js": "^0.7.39",
- "typescript": "5.6.2"
- }
-}
diff --git a/apps/web/postcss.config.js b/apps/web/postcss.config.js
deleted file mode 100644
index 12a703d90..000000000
--- a/apps/web/postcss.config.js
+++ /dev/null
@@ -1,6 +0,0 @@
-module.exports = {
- plugins: {
- tailwindcss: {},
- autoprefixer: {},
- },
-};
diff --git a/apps/web/process-env.d.ts b/apps/web/process-env.d.ts
deleted file mode 100644
index ae9995df7..000000000
--- a/apps/web/process-env.d.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-declare namespace NodeJS {
- export interface ProcessEnv {
- NEXT_PUBLIC_WEBAPP_URL?: string;
- NEXT_PUBLIC_MARKETING_URL?: string;
- NEXT_PRIVATE_INTERNAL_WEBAPP_URL?:string;
-
- NEXT_PRIVATE_DATABASE_URL: string;
-
- NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID: string;
-
- NEXT_PRIVATE_STRIPE_API_KEY: string;
- NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET: string;
-
- NEXT_PRIVATE_GOOGLE_CLIENT_ID: string;
- NEXT_PRIVATE_GOOGLE_CLIENT_SECRET: string;
-
- NEXT_PRIVATE_OIDC_WELL_KNOWN: string;
- NEXT_PRIVATE_OIDC_CLIENT_ID: string;
- NEXT_PRIVATE_OIDC_CLIENT_SECRET: string;
- NEXT_PRIVATE_OIDC_ALLOW_SIGNUP?: string;
- NEXT_PRIVATE_OIDC_SKIP_VERIFY?: string;
- }
-}
diff --git a/apps/web/src/app/(dashboard)/admin/documents/[id]/admin-actions.tsx b/apps/web/src/app/(dashboard)/admin/documents/[id]/admin-actions.tsx
deleted file mode 100644
index 214cacd38..000000000
--- a/apps/web/src/app/(dashboard)/admin/documents/[id]/admin-actions.tsx
+++ /dev/null
@@ -1,81 +0,0 @@
-'use client';
-
-import Link from 'next/link';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-
-import type { Recipient } from '@documenso/prisma/client';
-import { type Document, SigningStatus } from '@documenso/prisma/client';
-import { trpc } from '@documenso/trpc/react';
-import { cn } from '@documenso/ui/lib/utils';
-import { Button } from '@documenso/ui/primitives/button';
-import {
- Tooltip,
- TooltipContent,
- TooltipProvider,
- TooltipTrigger,
-} from '@documenso/ui/primitives/tooltip';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-export type AdminActionsProps = {
- className?: string;
- document: Document;
- recipients: Recipient[];
-};
-
-export const AdminActions = ({ className, document, recipients }: AdminActionsProps) => {
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const { mutate: resealDocument, isPending: isResealDocumentLoading } =
- trpc.admin.resealDocument.useMutation({
- onSuccess: () => {
- toast({
- title: _(msg`Success`),
- description: _(msg`Document resealed`),
- });
- },
- onError: () => {
- toast({
- title: _(msg`Error`),
- description: _(msg`Failed to reseal document`),
- variant: 'destructive',
- });
- },
- });
-
- return (
-
-
-
-
- recipient.signingStatus !== SigningStatus.SIGNED,
- )}
- onClick={() => resealDocument({ id: document.id })}
- >
- Reseal document
-
-
-
-
-
- Attempts sealing the document again, useful for after a code change has occurred to
- resolve an erroneous document.
-
-
-
-
-
-
-
- Go to owner
-
-
-
- );
-};
diff --git a/apps/web/src/app/(dashboard)/admin/documents/[id]/page.tsx b/apps/web/src/app/(dashboard)/admin/documents/[id]/page.tsx
deleted file mode 100644
index 524dd1f1e..000000000
--- a/apps/web/src/app/(dashboard)/admin/documents/[id]/page.tsx
+++ /dev/null
@@ -1,99 +0,0 @@
-import { Trans } from '@lingui/macro';
-import { DateTime } from 'luxon';
-
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-import { getEntireDocument } from '@documenso/lib/server-only/admin/get-entire-document';
-import {
- Accordion,
- AccordionContent,
- AccordionItem,
- AccordionTrigger,
-} from '@documenso/ui/primitives/accordion';
-import { Badge } from '@documenso/ui/primitives/badge';
-
-import { DocumentStatus } from '~/components/formatter/document-status';
-
-import { AdminActions } from './admin-actions';
-import { RecipientItem } from './recipient-item';
-import { SuperDeleteDocumentDialog } from './super-delete-document-dialog';
-
-type AdminDocumentDetailsPageProps = {
- params: {
- id: string;
- };
-};
-
-export default async function AdminDocumentDetailsPage({ params }: AdminDocumentDetailsPageProps) {
- const { i18n } = await setupI18nSSR();
-
- const document = await getEntireDocument({ id: Number(params.id) });
-
- return (
-
-
-
-
{document.title}
-
-
-
- {document.deletedAt && (
-
- Deleted
-
- )}
-
-
-
-
- Created on : {i18n.date(document.createdAt, DateTime.DATETIME_MED)}
-
-
-
- Last updated at : {i18n.date(document.updatedAt, DateTime.DATETIME_MED)}
-
-
-
-
-
-
- Admin Actions
-
-
-
-
-
-
- Recipients
-
-
-
-
- {document.recipients.map((recipient) => (
-
-
-
-
{recipient.name}
-
- {recipient.email}
-
-
-
-
-
-
-
-
- ))}
-
-
-
-
-
- {document &&
}
-
- );
-}
diff --git a/apps/web/src/app/(dashboard)/admin/documents/[id]/recipient-item.tsx b/apps/web/src/app/(dashboard)/admin/documents/[id]/recipient-item.tsx
deleted file mode 100644
index 8696dab06..000000000
--- a/apps/web/src/app/(dashboard)/admin/documents/[id]/recipient-item.tsx
+++ /dev/null
@@ -1,196 +0,0 @@
-'use client';
-
-import { useMemo } from 'react';
-
-import { useRouter } from 'next/navigation';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { useForm } from 'react-hook-form';
-import { z } from 'zod';
-
-import {
- type Field,
- type Recipient,
- type Signature,
- SigningStatus,
-} from '@documenso/prisma/client';
-import { trpc } from '@documenso/trpc/react';
-import { Button } from '@documenso/ui/primitives/button';
-import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
-import { DataTable } from '@documenso/ui/primitives/data-table';
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from '@documenso/ui/primitives/form/form';
-import { Input } from '@documenso/ui/primitives/input';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-const ZAdminUpdateRecipientFormSchema = z.object({
- name: z.string().min(1),
- email: z.string().email(),
-});
-
-type TAdminUpdateRecipientFormSchema = z.infer;
-
-export type RecipientItemProps = {
- recipient: Recipient & {
- fields: Array<
- Field & {
- signature: Signature | null;
- }
- >;
- };
-};
-
-export const RecipientItem = ({ recipient }: RecipientItemProps) => {
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const router = useRouter();
-
- const form = useForm({
- defaultValues: {
- name: recipient.name,
- email: recipient.email,
- },
- });
-
- const { mutateAsync: updateRecipient } = trpc.admin.updateRecipient.useMutation();
-
- const columns = useMemo(() => {
- return [
- {
- header: 'ID',
- accessorKey: 'id',
- cell: ({ row }) => {row.original.id}
,
- },
- {
- header: _(msg`Type`),
- accessorKey: 'type',
- cell: ({ row }) => {row.original.type}
,
- },
- {
- header: _(msg`Inserted`),
- accessorKey: 'inserted',
- cell: ({ row }) => {row.original.inserted ? 'True' : 'False'}
,
- },
- {
- header: _(msg`Value`),
- accessorKey: 'customText',
- cell: ({ row }) => {row.original.customText}
,
- },
- {
- header: _(msg`Signature`),
- accessorKey: 'signature',
- cell: ({ row }) => (
-
- {row.original.signature?.typedSignature && (
-
{row.original.signature.typedSignature}
- )}
-
- {row.original.signature?.signatureImageAsBase64 && (
-
- )}
-
- ),
- },
- ] satisfies DataTableColumnDef<(typeof recipient)['fields'][number]>[];
- }, []);
-
- const onUpdateRecipientFormSubmit = async ({ name, email }: TAdminUpdateRecipientFormSchema) => {
- try {
- await updateRecipient({
- id: recipient.id,
- name,
- email,
- });
-
- toast({
- title: _(msg`Recipient updated`),
- description: _(msg`The recipient has been updated successfully`),
- });
-
- router.refresh();
- } catch (error) {
- toast({
- title: _(msg`Failed to update recipient`),
- description: error.message,
- variant: 'destructive',
- });
- }
- };
-
- return (
-
-
-
-
-
-
-
- Fields
-
-
-
-
- );
-};
diff --git a/apps/web/src/app/(dashboard)/admin/documents/[id]/super-delete-document-dialog.tsx b/apps/web/src/app/(dashboard)/admin/documents/[id]/super-delete-document-dialog.tsx
deleted file mode 100644
index bf142133f..000000000
--- a/apps/web/src/app/(dashboard)/admin/documents/[id]/super-delete-document-dialog.tsx
+++ /dev/null
@@ -1,135 +0,0 @@
-'use client';
-
-import { useState } from 'react';
-
-import { useRouter } from 'next/navigation';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-
-import type { Document } from '@documenso/prisma/client';
-import { trpc } from '@documenso/trpc/react';
-import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
-import { Button } from '@documenso/ui/primitives/button';
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from '@documenso/ui/primitives/dialog';
-import { Input } from '@documenso/ui/primitives/input';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-export type SuperDeleteDocumentDialogProps = {
- document: Document;
-};
-
-export const SuperDeleteDocumentDialog = ({ document }: SuperDeleteDocumentDialogProps) => {
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const router = useRouter();
-
- const [reason, setReason] = useState('');
-
- const { mutateAsync: deleteDocument, isPending: isDeletingDocument } =
- trpc.admin.deleteDocument.useMutation();
-
- const handleDeleteDocument = async () => {
- try {
- if (!reason) {
- return;
- }
-
- await deleteDocument({ id: document.id, reason });
-
- toast({
- title: _(msg`Document deleted`),
- description: 'The Document has been deleted successfully.',
- duration: 5000,
- });
-
- router.push('/admin/documents');
- } catch (err) {
- toast({
- title: _(msg`An unknown error occurred`),
- variant: 'destructive',
- description:
- 'We encountered an unknown error while attempting to delete your document. Please try again later.',
- });
- }
- };
-
- return (
-
-
-
-
-
- Delete Document
-
-
-
- Delete the document. This action is irreversible so proceed with caution.
-
-
-
-
-
-
-
-
- Delete Document
-
-
-
-
-
-
- Delete Document
-
-
-
-
- This action is not reversible. Please be certain.
-
-
-
-
-
-
- To confirm, please enter the reason
-
-
- setReason(e.target.value)}
- />
-
-
-
-
- Delete document
-
-
-
-
-
-
-
-
- );
-};
diff --git a/apps/web/src/app/(dashboard)/admin/documents/document-results.tsx b/apps/web/src/app/(dashboard)/admin/documents/document-results.tsx
deleted file mode 100644
index 98854f296..000000000
--- a/apps/web/src/app/(dashboard)/admin/documents/document-results.tsx
+++ /dev/null
@@ -1,165 +0,0 @@
-'use client';
-
-import { useMemo, useState } from 'react';
-
-import Link from 'next/link';
-import { useSearchParams } from 'next/navigation';
-
-import { msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { Loader } from 'lucide-react';
-
-import { useDebouncedValue } from '@documenso/lib/client-only/hooks/use-debounced-value';
-import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
-import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
-import { trpc } from '@documenso/trpc/react';
-import { Avatar, AvatarFallback } from '@documenso/ui/primitives/avatar';
-import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
-import { DataTable } from '@documenso/ui/primitives/data-table';
-import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
-import { Input } from '@documenso/ui/primitives/input';
-import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
-
-import { DocumentStatus } from '~/components/formatter/document-status';
-
-// export type AdminDocumentResultsProps = {};
-
-export const AdminDocumentResults = () => {
- const { _, i18n } = useLingui();
-
- const searchParams = useSearchParams();
-
- const updateSearchParams = useUpdateSearchParams();
-
- const [term, setTerm] = useState(() => searchParams?.get?.('term') ?? '');
- const debouncedTerm = useDebouncedValue(term, 500);
-
- const page = searchParams?.get?.('page') ? Number(searchParams.get('page')) : undefined;
- const perPage = searchParams?.get?.('perPage') ? Number(searchParams.get('perPage')) : undefined;
-
- const { data: findDocumentsData, isPending: isFindDocumentsLoading } =
- trpc.admin.findDocuments.useQuery(
- {
- query: debouncedTerm,
- page: page || 1,
- perPage: perPage || 20,
- },
- {
- placeholderData: (previousData) => previousData,
- },
- );
-
- const results = findDocumentsData ?? {
- data: [],
- perPage: 20,
- currentPage: 1,
- totalPages: 1,
- };
-
- const columns = useMemo(() => {
- return [
- {
- header: _(msg`Created`),
- accessorKey: 'createdAt',
- cell: ({ row }) => i18n.date(row.original.createdAt),
- },
- {
- header: _(msg`Title`),
- accessorKey: 'title',
- cell: ({ row }) => {
- return (
-
- {row.original.title}
-
- );
- },
- },
- {
- header: _(msg`Status`),
- accessorKey: 'status',
- cell: ({ row }) => ,
- },
- {
- header: _(msg`Owner`),
- accessorKey: 'owner',
- cell: ({ row }) => {
- const avatarFallbackText = row.original.user.name
- ? extractInitials(row.original.user.name)
- : row.original.user.email.slice(0, 1).toUpperCase();
-
- return (
-
-
-
-
-
- {avatarFallbackText}
-
-
-
-
-
-
-
-
- {avatarFallbackText}
-
-
-
-
- {row.original.user.name}
- {row.original.user.email}
-
-
-
- );
- },
- },
- {
- header: 'Last updated',
- accessorKey: 'updatedAt',
- cell: ({ row }) => i18n.date(row.original.updatedAt),
- },
- ] satisfies DataTableColumnDef<(typeof results)['data'][number]>[];
- }, []);
-
- const onPaginationChange = (newPage: number, newPerPage: number) => {
- updateSearchParams({
- page: newPage,
- perPage: newPerPage,
- });
- };
-
- return (
-
-
setTerm(e.target.value)}
- />
-
-
-
- {(table) => }
-
-
- {isFindDocumentsLoading && (
-
-
-
- )}
-
-
- );
-};
diff --git a/apps/web/src/app/(dashboard)/admin/documents/page.tsx b/apps/web/src/app/(dashboard)/admin/documents/page.tsx
deleted file mode 100644
index b658959bc..000000000
--- a/apps/web/src/app/(dashboard)/admin/documents/page.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import { Trans } from '@lingui/macro';
-
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-
-import { AdminDocumentResults } from './document-results';
-
-export default async function AdminDocumentsPage() {
- await setupI18nSSR();
-
- return (
-
-
- Manage documents
-
-
-
-
- );
-}
diff --git a/apps/web/src/app/(dashboard)/admin/layout.tsx b/apps/web/src/app/(dashboard)/admin/layout.tsx
deleted file mode 100644
index 964267872..000000000
--- a/apps/web/src/app/(dashboard)/admin/layout.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import React from 'react';
-
-import { redirect } from 'next/navigation';
-
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
-import { isAdmin } from '@documenso/lib/next-auth/guards/is-admin';
-
-import { AdminNav } from './nav';
-
-export type AdminSectionLayoutProps = {
- children: React.ReactNode;
-};
-
-export default async function AdminSectionLayout({ children }: AdminSectionLayoutProps) {
- await setupI18nSSR();
-
- const { user } = await getRequiredServerComponentSession();
-
- if (!isAdmin(user)) {
- redirect('/documents');
- }
-
- return (
-
- );
-}
diff --git a/apps/web/src/app/(dashboard)/admin/leaderboard/data-table-leaderboard.tsx b/apps/web/src/app/(dashboard)/admin/leaderboard/data-table-leaderboard.tsx
deleted file mode 100644
index 596f0051d..000000000
--- a/apps/web/src/app/(dashboard)/admin/leaderboard/data-table-leaderboard.tsx
+++ /dev/null
@@ -1,169 +0,0 @@
-'use client';
-
-import { useEffect, useMemo, useState, useTransition } from 'react';
-
-import { msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { ChevronDownIcon as CaretSortIcon, Loader } from 'lucide-react';
-
-import { useDebouncedValue } from '@documenso/lib/client-only/hooks/use-debounced-value';
-import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
-import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
-import { DataTable } from '@documenso/ui/primitives/data-table';
-import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
-import { Input } from '@documenso/ui/primitives/input';
-
-export type SigningVolume = {
- id: number;
- name: string;
- signingVolume: number;
- createdAt: Date;
- planId: string;
-};
-
-type LeaderboardTableProps = {
- signingVolume: SigningVolume[];
- totalPages: number;
- perPage: number;
- page: number;
- sortBy: 'name' | 'createdAt' | 'signingVolume';
- sortOrder: 'asc' | 'desc';
-};
-
-export const LeaderboardTable = ({
- signingVolume,
- totalPages,
- perPage,
- page,
- sortBy,
- sortOrder,
-}: LeaderboardTableProps) => {
- const { _, i18n } = useLingui();
-
- const [isPending, startTransition] = useTransition();
- const updateSearchParams = useUpdateSearchParams();
- const [searchString, setSearchString] = useState('');
- const debouncedSearchString = useDebouncedValue(searchString, 1000);
-
- const columns = useMemo(() => {
- return [
- {
- header: () => (
- handleColumnSort('name')}
- >
- {_(msg`Name`)}
-
-
- ),
- accessorKey: 'name',
- cell: ({ row }) => {
- return (
-
- );
- },
- size: 250,
- },
- {
- header: () => (
- handleColumnSort('signingVolume')}
- >
- {_(msg`Signing Volume`)}
-
-
- ),
- accessorKey: 'signingVolume',
- cell: ({ row }) => {Number(row.getValue('signingVolume'))}
,
- },
- {
- header: () => {
- return (
- handleColumnSort('createdAt')}
- >
- {_(msg`Created`)}
-
-
- );
- },
- accessorKey: 'createdAt',
- cell: ({ row }) => i18n.date(row.original.createdAt),
- },
- ] satisfies DataTableColumnDef[];
- }, [sortOrder]);
-
- useEffect(() => {
- startTransition(() => {
- updateSearchParams({
- search: debouncedSearchString,
- page: 1,
- perPage,
- sortBy,
- sortOrder,
- });
- });
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [debouncedSearchString]);
-
- const onPaginationChange = (page: number, perPage: number) => {
- startTransition(() => {
- updateSearchParams({
- page,
- perPage,
- });
- });
- };
-
- const handleChange = (e: React.ChangeEvent) => {
- setSearchString(e.target.value);
- };
-
- const handleColumnSort = (column: 'name' | 'createdAt' | 'signingVolume') => {
- startTransition(() => {
- updateSearchParams({
- sortBy: column,
- sortOrder: sortBy === column && sortOrder === 'asc' ? 'desc' : 'asc',
- });
- });
- };
-
- return (
-
-
-
- {(table) => }
-
-
- {isPending && (
-
-
-
- )}
-
- );
-};
diff --git a/apps/web/src/app/(dashboard)/admin/leaderboard/fetch-leaderboard.actions.ts b/apps/web/src/app/(dashboard)/admin/leaderboard/fetch-leaderboard.actions.ts
deleted file mode 100644
index 42fc20c97..000000000
--- a/apps/web/src/app/(dashboard)/admin/leaderboard/fetch-leaderboard.actions.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-'use server';
-
-import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
-import { isAdmin } from '@documenso/lib/next-auth/guards/is-admin';
-import { getSigningVolume } from '@documenso/lib/server-only/admin/get-signing-volume';
-
-type SearchOptions = {
- search: string;
- page: number;
- perPage: number;
- sortBy: 'name' | 'createdAt' | 'signingVolume';
- sortOrder: 'asc' | 'desc';
-};
-
-export async function search({ search, page, perPage, sortBy, sortOrder }: SearchOptions) {
- const { user } = await getRequiredServerComponentSession();
-
- if (!isAdmin(user)) {
- throw new Error('Unauthorized');
- }
-
- const results = await getSigningVolume({ search, page, perPage, sortBy, sortOrder });
-
- return results;
-}
diff --git a/apps/web/src/app/(dashboard)/admin/leaderboard/page.tsx b/apps/web/src/app/(dashboard)/admin/leaderboard/page.tsx
deleted file mode 100644
index 0d1c5172a..000000000
--- a/apps/web/src/app/(dashboard)/admin/leaderboard/page.tsx
+++ /dev/null
@@ -1,60 +0,0 @@
-import { Trans } from '@lingui/macro';
-
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
-import { isAdmin } from '@documenso/lib/next-auth/guards/is-admin';
-
-import { LeaderboardTable } from './data-table-leaderboard';
-import { search } from './fetch-leaderboard.actions';
-
-type AdminLeaderboardProps = {
- searchParams?: {
- search?: string;
- page?: number;
- perPage?: number;
- sortBy?: 'name' | 'createdAt' | 'signingVolume';
- sortOrder?: 'asc' | 'desc';
- };
-};
-
-export default async function Leaderboard({ searchParams = {} }: AdminLeaderboardProps) {
- await setupI18nSSR();
-
- const { user } = await getRequiredServerComponentSession();
-
- if (!isAdmin(user)) {
- throw new Error('Unauthorized');
- }
-
- const page = Number(searchParams.page) || 1;
- const perPage = Number(searchParams.perPage) || 10;
- const searchString = searchParams.search || '';
- const sortBy = searchParams.sortBy || 'signingVolume';
- const sortOrder = searchParams.sortOrder || 'desc';
-
- const { leaderboard: signingVolume, totalPages } = await search({
- search: searchString,
- page,
- perPage,
- sortBy,
- sortOrder,
- });
-
- return (
-
-
- Signing Volume
-
-
-
-
-
- );
-}
diff --git a/apps/web/src/app/(dashboard)/admin/nav.tsx b/apps/web/src/app/(dashboard)/admin/nav.tsx
deleted file mode 100644
index bcae0fc75..000000000
--- a/apps/web/src/app/(dashboard)/admin/nav.tsx
+++ /dev/null
@@ -1,112 +0,0 @@
-'use client';
-
-import type { HTMLAttributes } from 'react';
-
-import Link from 'next/link';
-import { usePathname } from 'next/navigation';
-
-import { Trans } from '@lingui/macro';
-import { BarChart3, FileStack, Settings, Trophy, Users, Wallet2 } from 'lucide-react';
-
-import { cn } from '@documenso/ui/lib/utils';
-import { Button } from '@documenso/ui/primitives/button';
-
-export type AdminNavProps = HTMLAttributes;
-
-export const AdminNav = ({ className, ...props }: AdminNavProps) => {
- const pathname = usePathname();
-
- return (
-
-
-
-
- Stats
-
-
-
-
-
-
- Users
-
-
-
-
-
-
- Documents
-
-
-
-
-
-
- Subscriptions
-
-
-
-
-
-
- Leaderboard
-
-
-
-
-
-
- Site Settings
-
-
-
- );
-};
diff --git a/apps/web/src/app/(dashboard)/admin/page.tsx b/apps/web/src/app/(dashboard)/admin/page.tsx
deleted file mode 100644
index 5fe030685..000000000
--- a/apps/web/src/app/(dashboard)/admin/page.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import { redirect } from 'next/navigation';
-
-export default function Admin() {
- redirect('/admin/stats');
-}
diff --git a/apps/web/src/app/(dashboard)/admin/site-settings/banner-form.tsx b/apps/web/src/app/(dashboard)/admin/site-settings/banner-form.tsx
deleted file mode 100644
index 6903f5e17..000000000
--- a/apps/web/src/app/(dashboard)/admin/site-settings/banner-form.tsx
+++ /dev/null
@@ -1,208 +0,0 @@
-'use client';
-
-import { useRouter } from 'next/navigation';
-
-import { zodResolver } from '@hookform/resolvers/zod';
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { useForm } from 'react-hook-form';
-import type { z } from 'zod';
-
-import type { TSiteSettingsBannerSchema } from '@documenso/lib/server-only/site-settings/schemas/banner';
-import {
- SITE_SETTINGS_BANNER_ID,
- ZSiteSettingsBannerSchema,
-} from '@documenso/lib/server-only/site-settings/schemas/banner';
-import { trpc as trpcReact } from '@documenso/trpc/react';
-import { Button } from '@documenso/ui/primitives/button';
-import { ColorPicker } from '@documenso/ui/primitives/color-picker';
-import {
- Form,
- FormControl,
- FormDescription,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from '@documenso/ui/primitives/form/form';
-import { Switch } from '@documenso/ui/primitives/switch';
-import { Textarea } from '@documenso/ui/primitives/textarea';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-const ZBannerFormSchema = ZSiteSettingsBannerSchema;
-
-type TBannerFormSchema = z.infer;
-
-export type BannerFormProps = {
- banner?: TSiteSettingsBannerSchema;
-};
-
-export function BannerForm({ banner }: BannerFormProps) {
- const { toast } = useToast();
- const { _ } = useLingui();
-
- const router = useRouter();
-
- const form = useForm({
- resolver: zodResolver(ZBannerFormSchema),
- defaultValues: {
- id: SITE_SETTINGS_BANNER_ID,
- enabled: banner?.enabled ?? false,
- data: {
- content: banner?.data?.content ?? '',
- bgColor: banner?.data?.bgColor ?? '#000000',
- textColor: banner?.data?.textColor ?? '#FFFFFF',
- },
- },
- });
-
- const enabled = form.watch('enabled');
-
- const { mutateAsync: updateSiteSetting, isPending: isUpdateSiteSettingLoading } =
- trpcReact.admin.updateSiteSetting.useMutation();
-
- const onBannerUpdate = async ({ id, enabled, data }: TBannerFormSchema) => {
- try {
- await updateSiteSetting({
- id,
- enabled,
- data,
- });
-
- toast({
- title: _(msg`Banner Updated`),
- description: _(msg`Your banner has been updated successfully.`),
- duration: 5000,
- });
-
- router.refresh();
- } catch (err) {
- toast({
- title: _(msg`An unknown error occurred`),
- variant: 'destructive',
- description: _(
- msg`We encountered an unknown error while attempting to update the banner. Please try again later.`,
- ),
- });
- }
- };
-
- return (
-
-
- Site Banner
-
-
-
- The site banner is a message that is shown at the top of the site. It can be used to
- display important information to your users.
-
-
-
-
-
-
- );
-}
diff --git a/apps/web/src/app/(dashboard)/admin/site-settings/page.tsx b/apps/web/src/app/(dashboard)/admin/site-settings/page.tsx
deleted file mode 100644
index c78eb87ec..000000000
--- a/apps/web/src/app/(dashboard)/admin/site-settings/page.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import { msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-import { getSiteSettings } from '@documenso/lib/server-only/site-settings/get-site-settings';
-import { SITE_SETTINGS_BANNER_ID } from '@documenso/lib/server-only/site-settings/schemas/banner';
-
-import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
-
-import { BannerForm } from './banner-form';
-
-// import { BannerForm } from './banner-form';
-
-export default async function AdminBannerPage() {
- await setupI18nSSR();
-
- const { _ } = useLingui();
-
- const banner = await getSiteSettings().then((settings) =>
- settings.find((setting) => setting.id === SITE_SETTINGS_BANNER_ID),
- );
-
- return (
-
- );
-}
diff --git a/apps/web/src/app/(dashboard)/admin/stats/page.tsx b/apps/web/src/app/(dashboard)/admin/stats/page.tsx
deleted file mode 100644
index 9ffbfb5dc..000000000
--- a/apps/web/src/app/(dashboard)/admin/stats/page.tsx
+++ /dev/null
@@ -1,158 +0,0 @@
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import {
- File,
- FileCheck,
- FileClock,
- FileCog,
- FileEdit,
- Mail,
- MailOpen,
- PenTool,
- UserPlus,
- UserSquare2,
- Users,
-} from 'lucide-react';
-
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-import { getDocumentStats } from '@documenso/lib/server-only/admin/get-documents-stats';
-import { getRecipientsStats } from '@documenso/lib/server-only/admin/get-recipients-stats';
-import {
- getUserWithSignedDocumentMonthlyGrowth,
- getUsersCount,
- getUsersWithSubscriptionsCount,
-} from '@documenso/lib/server-only/admin/get-users-stats';
-import { getSignerConversionMonthly } from '@documenso/lib/server-only/user/get-signer-conversion';
-
-import { CardMetric } from '~/components/(dashboard)/metric-card/metric-card';
-
-import { SignerConversionChart } from './signer-conversion-chart';
-import { UserWithDocumentChart } from './user-with-document';
-
-export default async function AdminStatsPage() {
- await setupI18nSSR();
-
- const { _ } = useLingui();
-
- const [
- usersCount,
- usersWithSubscriptionsCount,
- docStats,
- recipientStats,
- signerConversionMonthly,
- // userWithAtLeastOneDocumentPerMonth,
- // userWithAtLeastOneDocumentSignedPerMonth,
- MONTHLY_USERS_SIGNED,
- ] = await Promise.all([
- getUsersCount(),
- getUsersWithSubscriptionsCount(),
- getDocumentStats(),
- getRecipientsStats(),
- getSignerConversionMonthly(),
- // getUserWithAtLeastOneDocumentPerMonth(),
- // getUserWithAtLeastOneDocumentSignedPerMonth(),
- getUserWithSignedDocumentMonthlyGrowth(),
- ]);
-
- return (
-
-
- Instance Stats
-
-
-
-
-
-
-
-
-
-
-
-
-
- Document metrics
-
-
-
-
-
-
-
-
-
-
-
- Recipients metrics
-
-
-
-
-
-
-
-
-
-
-
-
-
- Charts
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/apps/web/src/app/(dashboard)/admin/stats/signer-conversion-chart.tsx b/apps/web/src/app/(dashboard)/admin/stats/signer-conversion-chart.tsx
deleted file mode 100644
index 4c5e1bd2c..000000000
--- a/apps/web/src/app/(dashboard)/admin/stats/signer-conversion-chart.tsx
+++ /dev/null
@@ -1,64 +0,0 @@
-'use client';
-
-import { DateTime } from 'luxon';
-import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
-
-import type { GetSignerConversionMonthlyResult } from '@documenso/lib/server-only/user/get-signer-conversion';
-
-export type SignerConversionChartProps = {
- className?: string;
- title: string;
- cummulative?: boolean;
- data: GetSignerConversionMonthlyResult;
-};
-
-export const SignerConversionChart = ({
- className,
- data,
- title,
- cummulative = false,
-}: SignerConversionChartProps) => {
- const formattedData = [...data].reverse().map(({ month, count, cume_count }) => {
- return {
- month: DateTime.fromFormat(month, 'yyyy-MM').toFormat('MMM yyyy'),
- count: Number(count),
- signed_count: Number(cume_count),
- };
- });
-
- return (
-
-
-
-
{title}
-
-
-
-
-
-
-
- [
- Number(value).toLocaleString('en-US'),
- name === 'Recipients',
- ]}
- cursor={{ fill: 'hsl(var(--primary) / 10%)' }}
- />
-
-
-
-
-
-
- );
-};
diff --git a/apps/web/src/app/(dashboard)/admin/stats/user-with-document.tsx b/apps/web/src/app/(dashboard)/admin/stats/user-with-document.tsx
deleted file mode 100644
index bf371b62d..000000000
--- a/apps/web/src/app/(dashboard)/admin/stats/user-with-document.tsx
+++ /dev/null
@@ -1,95 +0,0 @@
-'use client';
-
-import { DateTime } from 'luxon';
-import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
-import type { TooltipProps } from 'recharts';
-import type { NameType, ValueType } from 'recharts/types/component/DefaultTooltipContent';
-
-import type { GetUserWithDocumentMonthlyGrowth } from '@documenso/lib/server-only/admin/get-users-stats';
-
-export type UserWithDocumentChartProps = {
- className?: string;
- title: string;
- data: GetUserWithDocumentMonthlyGrowth;
- completed?: boolean;
- tooltip?: string;
-};
-
-const CustomTooltip = ({
- active,
- payload,
- label,
- tooltip,
-}: TooltipProps & { tooltip?: string }) => {
- if (active && payload && payload.length) {
- return (
-
-
{label}
-
- {`${tooltip} : `}
- {payload[0].value}
-
-
- );
- }
-
- return null;
-};
-
-export const UserWithDocumentChart = ({
- className,
- data,
- title,
- completed = false,
- tooltip,
-}: UserWithDocumentChartProps) => {
- const formattedData = (data: GetUserWithDocumentMonthlyGrowth, completed: boolean) => {
- return [...data].reverse().map(({ month, count, signed_count }) => {
- const formattedMonth = DateTime.fromFormat(month, 'yyyy-MM').toFormat('LLL');
- if (completed) {
- return {
- month: formattedMonth,
- count: Number(signed_count),
- };
- } else {
- return {
- month: formattedMonth,
- count: Number(count),
- };
- }
- });
- };
-
- return (
-
-
-
-
{title}
-
-
-
-
-
-
-
- }
- labelStyle={{
- color: 'hsl(var(--primary-foreground))',
- }}
- cursor={{ fill: 'hsl(var(--primary) / 10%)' }}
- />
-
-
-
-
-
-
- );
-};
diff --git a/apps/web/src/app/(dashboard)/admin/subscriptions/page.tsx b/apps/web/src/app/(dashboard)/admin/subscriptions/page.tsx
deleted file mode 100644
index 7940b6fb5..000000000
--- a/apps/web/src/app/(dashboard)/admin/subscriptions/page.tsx
+++ /dev/null
@@ -1,80 +0,0 @@
-import Link from 'next/link';
-
-import { Trans } from '@lingui/macro';
-
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-import { findSubscriptions } from '@documenso/lib/server-only/admin/get-all-subscriptions';
-import {
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeader,
- TableRow,
-} from '@documenso/ui/primitives/table';
-
-export default async function Subscriptions() {
- await setupI18nSSR();
-
- const subscriptions = await findSubscriptions();
-
- return (
-
-
- Manage subscriptions
-
-
-
-
-
- ID
-
- Status
-
-
- Created At
-
-
- Ends On
-
-
- User ID
-
-
-
-
- {subscriptions.map((subscription, index) => (
-
- {subscription.id}
- {subscription.status}
-
- {subscription.createdAt
- ? new Date(subscription.createdAt).toLocaleDateString(undefined, {
- weekday: 'long',
- year: 'numeric',
- month: 'long',
- day: 'numeric',
- })
- : 'N/A'}
-
-
- {subscription.periodEnd
- ? new Date(subscription.periodEnd).toLocaleDateString(undefined, {
- weekday: 'long',
- year: 'numeric',
- month: 'long',
- day: 'numeric',
- })
- : 'N/A'}
-
-
- {subscription.userId}
-
-
- ))}
-
-
-
-
- );
-}
diff --git a/apps/web/src/app/(dashboard)/admin/users/[id]/delete-user-dialog.tsx b/apps/web/src/app/(dashboard)/admin/users/[id]/delete-user-dialog.tsx
deleted file mode 100644
index 1f45002f2..000000000
--- a/apps/web/src/app/(dashboard)/admin/users/[id]/delete-user-dialog.tsx
+++ /dev/null
@@ -1,142 +0,0 @@
-'use client';
-
-import { useState } from 'react';
-
-import { useRouter } from 'next/navigation';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { match } from 'ts-pattern';
-
-import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
-import type { User } from '@documenso/prisma/client';
-import { trpc } from '@documenso/trpc/react';
-import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
-import { Button } from '@documenso/ui/primitives/button';
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from '@documenso/ui/primitives/dialog';
-import { Input } from '@documenso/ui/primitives/input';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-export type DeleteUserDialogProps = {
- className?: string;
- user: User;
-};
-
-export const DeleteUserDialog = ({ className, user }: DeleteUserDialogProps) => {
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const router = useRouter();
-
- const [email, setEmail] = useState('');
-
- const { mutateAsync: deleteUser, isPending: isDeletingUser } =
- trpc.admin.deleteUser.useMutation();
-
- const onDeleteAccount = async () => {
- try {
- await deleteUser({
- id: user.id,
- });
-
- toast({
- title: _(msg`Account deleted`),
- description: _(msg`The account has been deleted successfully.`),
- duration: 5000,
- });
-
- router.push('/admin/users');
- } catch (err) {
- const error = AppError.parseError(err);
-
- const errorMessage = match(error.code)
- .with(AppErrorCode.NOT_FOUND, () => msg`User not found.`)
- .with(AppErrorCode.UNAUTHORIZED, () => msg`You are not authorized to delete this user.`)
- .otherwise(() => msg`An error occurred while deleting the user.`);
-
- toast({
- title: _(msg`Error`),
- description: _(errorMessage),
- variant: 'destructive',
- duration: 7500,
- });
- }
- };
-
- return (
-
-
-
-
Delete Account
-
-
- Delete the users account and all its contents. This action is irreversible and will
- cancel their subscription, so proceed with caution.
-
-
-
-
-
-
-
-
- Delete Account
-
-
-
-
-
-
- Delete Account
-
-
-
-
- This action is not reversible. Please be certain.
-
-
-
-
-
-
-
- To confirm, please enter the accounts email address ({user.email}).
-
-
-
- setEmail(e.target.value)}
- />
-
-
-
-
- Delete account
-
-
-
-
-
-
-
- );
-};
diff --git a/apps/web/src/app/(dashboard)/admin/users/[id]/disable-user-dialog.tsx b/apps/web/src/app/(dashboard)/admin/users/[id]/disable-user-dialog.tsx
deleted file mode 100644
index 1b09d8432..000000000
--- a/apps/web/src/app/(dashboard)/admin/users/[id]/disable-user-dialog.tsx
+++ /dev/null
@@ -1,141 +0,0 @@
-'use client';
-
-import { useState } from 'react';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { match } from 'ts-pattern';
-
-import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
-import type { User } from '@documenso/prisma/client';
-import { trpc } from '@documenso/trpc/react';
-import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
-import { Button } from '@documenso/ui/primitives/button';
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from '@documenso/ui/primitives/dialog';
-import { Input } from '@documenso/ui/primitives/input';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-export type DisableUserDialogProps = {
- className?: string;
- userToDisable: User;
-};
-
-export const DisableUserDialog = ({ className, userToDisable }: DisableUserDialogProps) => {
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const [email, setEmail] = useState('');
-
- const { mutateAsync: disableUser, isPending: isDisablingUser } =
- trpc.admin.disableUser.useMutation();
-
- const onDisableAccount = async () => {
- try {
- await disableUser({
- id: userToDisable.id,
- });
-
- toast({
- title: _(msg`Account disabled`),
- description: _(msg`The account has been disabled successfully.`),
- duration: 5000,
- });
- } catch (err) {
- const error = AppError.parseError(err);
-
- const errorMessage = match(error.code)
- .with(AppErrorCode.NOT_FOUND, () => msg`User not found.`)
- .with(AppErrorCode.UNAUTHORIZED, () => msg`You are not authorized to disable this user.`)
- .otherwise(() => msg`An error occurred while disabling the user.`);
-
- toast({
- title: _(msg`Error`),
- description: _(errorMessage),
- variant: 'destructive',
- duration: 7500,
- });
- }
- };
-
- return (
-
-
-
-
Disable Account
-
-
- Disabling the user results in the user not being able to use the account. It also
- disables all the related contents such as subscription, webhooks, teams, and API keys.
-
-
-
-
-
-
-
-
- Disable Account
-
-
-
-
-
-
- Disable Account
-
-
-
-
-
- This action is reversible, but please be careful as the account may be
- affected permanently (e.g. their settings and contents not being restored
- properly).
-
-
-
-
-
-
-
-
- To confirm, please enter the accounts email address ({userToDisable.email}
- ).
-
-
-
- setEmail(e.target.value)}
- />
-
-
-
-
- Disable account
-
-
-
-
-
-
-
- );
-};
diff --git a/apps/web/src/app/(dashboard)/admin/users/[id]/enable-user-dialog.tsx b/apps/web/src/app/(dashboard)/admin/users/[id]/enable-user-dialog.tsx
deleted file mode 100644
index c9df59591..000000000
--- a/apps/web/src/app/(dashboard)/admin/users/[id]/enable-user-dialog.tsx
+++ /dev/null
@@ -1,130 +0,0 @@
-'use client';
-
-import { useState } from 'react';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { match } from 'ts-pattern';
-
-import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
-import type { User } from '@documenso/prisma/client';
-import { trpc } from '@documenso/trpc/react';
-import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
-import { Button } from '@documenso/ui/primitives/button';
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from '@documenso/ui/primitives/dialog';
-import { Input } from '@documenso/ui/primitives/input';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-export type EnableUserDialogProps = {
- className?: string;
- userToEnable: User;
-};
-
-export const EnableUserDialog = ({ className, userToEnable }: EnableUserDialogProps) => {
- const { toast } = useToast();
- const { _ } = useLingui();
-
- const [email, setEmail] = useState('');
-
- const { mutateAsync: enableUser, isPending: isEnablingUser } =
- trpc.admin.enableUser.useMutation();
-
- const onEnableAccount = async () => {
- try {
- await enableUser({
- id: userToEnable.id,
- });
-
- toast({
- title: _(msg`Account enabled`),
- description: _(msg`The account has been enabled successfully.`),
- duration: 5000,
- });
- } catch (err) {
- const error = AppError.parseError(err);
-
- const errorMessage = match(error.code)
- .with(AppErrorCode.NOT_FOUND, () => msg`User not found.`)
- .with(AppErrorCode.UNAUTHORIZED, () => msg`You are not authorized to enable this user.`)
- .otherwise(() => msg`An error occurred while enabling the user.`);
-
- toast({
- title: _(msg`Error`),
- description: _(errorMessage),
- variant: 'destructive',
- duration: 7500,
- });
- }
- };
-
- return (
-
-
-
-
Enable Account
-
-
- Enabling the account results in the user being able to use the account again, and all
- the related features such as webhooks, teams, and API keys for example.
-
-
-
-
-
-
-
-
- Enable Account
-
-
-
-
-
-
- Enable Account
-
-
-
-
-
-
- To confirm, please enter the accounts email address ({userToEnable.email}
- ).
-
-
-
- setEmail(e.target.value)}
- />
-
-
-
-
- Enable account
-
-
-
-
-
-
-
- );
-};
diff --git a/apps/web/src/app/(dashboard)/admin/users/[id]/multiselect-role-combobox.tsx b/apps/web/src/app/(dashboard)/admin/users/[id]/multiselect-role-combobox.tsx
deleted file mode 100644
index bf7f85d72..000000000
--- a/apps/web/src/app/(dashboard)/admin/users/[id]/multiselect-role-combobox.tsx
+++ /dev/null
@@ -1,85 +0,0 @@
-import * as React from 'react';
-
-import { Trans } from '@lingui/macro';
-import { Check, ChevronsUpDown } from 'lucide-react';
-
-import { Role } from '@documenso/prisma/client';
-import { cn } from '@documenso/ui/lib/utils';
-import { Button } from '@documenso/ui/primitives/button';
-import {
- Command,
- CommandEmpty,
- CommandGroup,
- CommandInput,
- CommandItem,
-} from '@documenso/ui/primitives/command';
-import { Popover, PopoverContent, PopoverTrigger } from '@documenso/ui/primitives/popover';
-
-type ComboboxProps = {
- listValues: string[];
- onChange: (_values: string[]) => void;
-};
-
-const MultiSelectRoleCombobox = ({ listValues, onChange }: ComboboxProps) => {
- const [open, setOpen] = React.useState(false);
- const [selectedValues, setSelectedValues] = React.useState([]);
- const dbRoles = Object.values(Role);
-
- React.useEffect(() => {
- setSelectedValues(listValues);
- }, [listValues]);
-
- const allRoles = [...new Set([...dbRoles, ...selectedValues])];
-
- const handleSelect = (currentValue: string) => {
- let newSelectedValues;
- if (selectedValues.includes(currentValue)) {
- newSelectedValues = selectedValues.filter((value) => value !== currentValue);
- } else {
- newSelectedValues = [...selectedValues, currentValue];
- }
-
- setSelectedValues(newSelectedValues);
- onChange(newSelectedValues);
- setOpen(false);
- };
-
- return (
-
-
-
- {selectedValues.length > 0 ? selectedValues.join(', ') : 'Select values...'}
-
-
-
-
-
-
-
- No value found.
-
-
- {allRoles.map((value: string, i: number) => (
- handleSelect(value)}>
-
- {value}
-
- ))}
-
-
-
-
- );
-};
-
-export { MultiSelectRoleCombobox };
diff --git a/apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx b/apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx
deleted file mode 100644
index 371726de1..000000000
--- a/apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx
+++ /dev/null
@@ -1,165 +0,0 @@
-'use client';
-
-import { useRouter } from 'next/navigation';
-
-import { zodResolver } from '@hookform/resolvers/zod';
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { useForm } from 'react-hook-form';
-import type { z } from 'zod';
-
-import { trpc } from '@documenso/trpc/react';
-import { ZAdminUpdateProfileMutationSchema } from '@documenso/trpc/server/admin-router/schema';
-import { Button } from '@documenso/ui/primitives/button';
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from '@documenso/ui/primitives/form/form';
-import { Input } from '@documenso/ui/primitives/input';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-import { DeleteUserDialog } from './delete-user-dialog';
-import { DisableUserDialog } from './disable-user-dialog';
-import { EnableUserDialog } from './enable-user-dialog';
-import { MultiSelectRoleCombobox } from './multiselect-role-combobox';
-
-const ZUserFormSchema = ZAdminUpdateProfileMutationSchema.omit({ id: true });
-
-type TUserFormSchema = z.infer;
-
-export default function UserPage({ params }: { params: { id: number } }) {
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const router = useRouter();
-
- const { data: user } = trpc.profile.getUser.useQuery(
- {
- id: Number(params.id),
- },
- {
- enabled: !!params.id,
- },
- );
-
- const roles = user?.roles ?? [];
-
- const { mutateAsync: updateUserMutation } = trpc.admin.updateUser.useMutation();
-
- const form = useForm({
- resolver: zodResolver(ZUserFormSchema),
- values: {
- name: user?.name ?? '',
- email: user?.email ?? '',
- roles: user?.roles ?? [],
- },
- });
-
- const onSubmit = async ({ name, email, roles }: TUserFormSchema) => {
- try {
- await updateUserMutation({
- id: Number(user?.id),
- name,
- email,
- roles,
- });
-
- router.refresh();
-
- toast({
- title: _(msg`Profile updated`),
- description: _(msg`Your profile has been updated.`),
- duration: 5000,
- });
- } catch (e) {
- toast({
- title: _(msg`Error`),
- description: _(msg`An error occurred while updating your profile.`),
- variant: 'destructive',
- });
- }
- };
-
- return (
-
-
- Manage {user?.name}'s profile
-
-
-
-
- (
-
-
- Name
-
-
-
-
-
-
- )}
- />
- (
-
-
- Email
-
-
-
-
-
-
- )}
- />
-
- (
-
-
-
- Roles
-
-
- onChange(values)}
- />
-
-
-
-
- )}
- />
-
-
-
- Update user
-
-
-
-
-
-
-
-
-
- {user && }
- {user && user.disabled && }
- {user && !user.disabled && }
-
-
- );
-}
diff --git a/apps/web/src/app/(dashboard)/admin/users/data-table-users.tsx b/apps/web/src/app/(dashboard)/admin/users/data-table-users.tsx
deleted file mode 100644
index 97a204e91..000000000
--- a/apps/web/src/app/(dashboard)/admin/users/data-table-users.tsx
+++ /dev/null
@@ -1,166 +0,0 @@
-'use client';
-
-import { useEffect, useMemo, useState, useTransition } from 'react';
-
-import Link from 'next/link';
-
-import { msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { Edit, Loader } from 'lucide-react';
-
-import { useDebouncedValue } from '@documenso/lib/client-only/hooks/use-debounced-value';
-import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
-import type { Document, Role, Subscription } from '@documenso/prisma/client';
-import { Button } from '@documenso/ui/primitives/button';
-import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
-import { DataTable } from '@documenso/ui/primitives/data-table';
-import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
-import { Input } from '@documenso/ui/primitives/input';
-
-type UserData = {
- id: number;
- name: string | null;
- email: string;
- roles: Role[];
- subscriptions?: SubscriptionLite[] | null;
- documents: DocumentLite[];
-};
-
-type SubscriptionLite = Pick<
- Subscription,
- 'id' | 'status' | 'planId' | 'priceId' | 'createdAt' | 'periodEnd'
->;
-
-type DocumentLite = Pick;
-
-type UsersDataTableProps = {
- users: UserData[];
- totalPages: number;
- perPage: number;
- page: number;
- individualPriceIds: string[];
-};
-
-export const UsersDataTable = ({
- users,
- totalPages,
- perPage,
- page,
- individualPriceIds,
-}: UsersDataTableProps) => {
- const { _ } = useLingui();
-
- const [isPending, startTransition] = useTransition();
- const updateSearchParams = useUpdateSearchParams();
- const [searchString, setSearchString] = useState('');
- const debouncedSearchString = useDebouncedValue(searchString, 1000);
-
- const columns = useMemo(() => {
- return [
- {
- header: 'ID',
- accessorKey: 'id',
- cell: ({ row }) => {row.original.id}
,
- },
- {
- header: _(msg`Name`),
- accessorKey: 'name',
- cell: ({ row }) => {row.original.name}
,
- },
- {
- header: _(msg`Email`),
- accessorKey: 'email',
- cell: ({ row }) => {row.original.email}
,
- },
- {
- header: _(msg`Roles`),
- accessorKey: 'roles',
- cell: ({ row }) => row.original.roles.join(', '),
- },
- {
- header: _(msg`Subscription`),
- accessorKey: 'subscription',
- cell: ({ row }) => {
- const foundIndividualSubscription = (row.original.subscriptions ?? []).find((sub) =>
- individualPriceIds.includes(sub.priceId),
- );
-
- return foundIndividualSubscription?.status ?? 'NONE';
- },
- },
- {
- header: _(msg`Documents`),
- accessorKey: 'documents',
- cell: ({ row }) => {
- return {row.original.documents?.length}
;
- },
- },
- {
- header: '',
- accessorKey: 'edit',
- cell: ({ row }) => {
- return (
-
-
-
- Edit
-
-
- );
- },
- },
- ] satisfies DataTableColumnDef<(typeof users)[number]>[];
- }, [individualPriceIds]);
-
- useEffect(() => {
- startTransition(() => {
- updateSearchParams({
- search: debouncedSearchString,
- page: 1,
- perPage,
- });
- });
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [debouncedSearchString]);
-
- const onPaginationChange = (page: number, perPage: number) => {
- startTransition(() => {
- updateSearchParams({
- page,
- perPage,
- });
- });
- };
-
- const handleChange = (e: React.ChangeEvent) => {
- setSearchString(e.target.value);
- };
-
- return (
-
-
-
- {(table) => }
-
-
- {isPending && (
-
-
-
- )}
-
- );
-};
diff --git a/apps/web/src/app/(dashboard)/admin/users/fetch-users.actions.ts b/apps/web/src/app/(dashboard)/admin/users/fetch-users.actions.ts
deleted file mode 100644
index 61d5e1829..000000000
--- a/apps/web/src/app/(dashboard)/admin/users/fetch-users.actions.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-'use server';
-
-import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
-import { isAdmin } from '@documenso/lib/next-auth/guards/is-admin';
-import { findUsers } from '@documenso/lib/server-only/user/get-all-users';
-
-export async function search(search: string, page: number, perPage: number) {
- const { user } = await getRequiredServerComponentSession();
-
- if (!isAdmin(user)) {
- throw new Error('Unauthorized');
- }
-
- const results = await findUsers({ username: search, email: search, page, perPage });
-
- return results;
-}
diff --git a/apps/web/src/app/(dashboard)/admin/users/page.tsx b/apps/web/src/app/(dashboard)/admin/users/page.tsx
deleted file mode 100644
index 55803f6f9..000000000
--- a/apps/web/src/app/(dashboard)/admin/users/page.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-import { Trans } from '@lingui/macro';
-
-import { getPricesByPlan } from '@documenso/ee/server-only/stripe/get-prices-by-plan';
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-import { STRIPE_PLAN_TYPE } from '@documenso/lib/constants/billing';
-
-import { UsersDataTable } from './data-table-users';
-import { search } from './fetch-users.actions';
-
-type AdminManageUsersProps = {
- searchParams?: {
- search?: string;
- page?: number;
- perPage?: number;
- };
-};
-
-export default async function AdminManageUsers({ searchParams = {} }: AdminManageUsersProps) {
- await setupI18nSSR();
-
- const page = Number(searchParams.page) || 1;
- const perPage = Number(searchParams.perPage) || 10;
- const searchString = searchParams.search || '';
-
- const [{ users, totalPages }, individualPrices] = await Promise.all([
- search(searchString, page, perPage),
- getPricesByPlan([STRIPE_PLAN_TYPE.REGULAR, STRIPE_PLAN_TYPE.COMMUNITY]).catch(() => []),
- ]);
-
- const individualPriceIds = individualPrices.map((price) => price.id);
-
- return (
-
-
- Manage users
-
-
-
-
- );
-}
diff --git a/apps/web/src/app/(dashboard)/documents/[id]/document-page-view-button.tsx b/apps/web/src/app/(dashboard)/documents/[id]/document-page-view-button.tsx
deleted file mode 100644
index a477d75c6..000000000
--- a/apps/web/src/app/(dashboard)/documents/[id]/document-page-view-button.tsx
+++ /dev/null
@@ -1,121 +0,0 @@
-'use client';
-
-import Link from 'next/link';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { CheckCircle, Download, EyeIcon, Pencil } from 'lucide-react';
-import { useSession } from 'next-auth/react';
-import { match } from 'ts-pattern';
-
-import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
-import { formatDocumentsPath } from '@documenso/lib/utils/teams';
-import type { Document, Recipient, Team, User } from '@documenso/prisma/client';
-import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
-import { trpc as trpcClient } from '@documenso/trpc/client';
-import { Button } from '@documenso/ui/primitives/button';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-export type DocumentPageViewButtonProps = {
- document: Document & {
- user: Pick;
- recipients: Recipient[];
- team: Pick | null;
- };
- team?: Pick;
-};
-
-export const DocumentPageViewButton = ({ document }: DocumentPageViewButtonProps) => {
- const { data: session } = useSession();
- const { toast } = useToast();
- const { _ } = useLingui();
-
- if (!session) {
- return null;
- }
-
- const recipient = document.recipients.find((recipient) => recipient.email === session.user.email);
-
- const isRecipient = !!recipient;
- const isPending = document.status === DocumentStatus.PENDING;
- const isComplete = document.status === DocumentStatus.COMPLETED;
- const isSigned = recipient?.signingStatus === SigningStatus.SIGNED;
- const role = recipient?.role;
-
- const documentsPath = formatDocumentsPath(document.team?.url);
-
- const onDownloadClick = async () => {
- try {
- const documentWithData = await trpcClient.document.getDocumentById.query(
- {
- documentId: document.id,
- },
- {
- context: {
- teamId: document.team?.id?.toString(),
- },
- },
- );
-
- const documentData = documentWithData?.documentData;
-
- if (!documentData) {
- throw new Error('No document available');
- }
-
- await downloadPDF({ documentData, fileName: documentWithData.title });
- } catch (err) {
- toast({
- title: _(msg`Something went wrong`),
- description: _(msg`An error occurred while downloading your document.`),
- variant: 'destructive',
- });
- }
- };
-
- return match({
- isRecipient,
- isPending,
- isComplete,
- isSigned,
- })
- .with({ isRecipient: true, isPending: true, isSigned: false }, () => (
-
-
- {match(role)
- .with(RecipientRole.SIGNER, () => (
- <>
-
- Sign
- >
- ))
- .with(RecipientRole.APPROVER, () => (
- <>
-
- Approve
- >
- ))
- .otherwise(() => (
- <>
-
- View
- >
- ))}
-
-
- ))
- .with({ isComplete: false }, () => (
-
-
- Edit
-
-
- ))
- .with({ isComplete: true }, () => (
-
-
- Download
-
- ))
- .otherwise(() => null);
-};
diff --git a/apps/web/src/app/(dashboard)/documents/[id]/document-page-view-dropdown.tsx b/apps/web/src/app/(dashboard)/documents/[id]/document-page-view-dropdown.tsx
deleted file mode 100644
index 5075f342c..000000000
--- a/apps/web/src/app/(dashboard)/documents/[id]/document-page-view-dropdown.tsx
+++ /dev/null
@@ -1,211 +0,0 @@
-'use client';
-
-import { useState } from 'react';
-
-import Link from 'next/link';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import {
- Copy,
- Download,
- Edit,
- Loader,
- MoreHorizontal,
- ScrollTextIcon,
- Share,
- Trash2,
-} from 'lucide-react';
-import { useSession } from 'next-auth/react';
-
-import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
-import { formatDocumentsPath } from '@documenso/lib/utils/teams';
-import { DocumentStatus } from '@documenso/prisma/client';
-import type { Document, Recipient, Team, TeamEmail, User } from '@documenso/prisma/client';
-import { trpc as trpcClient } from '@documenso/trpc/client';
-import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button';
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuLabel,
- DropdownMenuTrigger,
-} from '@documenso/ui/primitives/dropdown-menu';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-import { DocumentRecipientLinkCopyDialog } from '~/components/document/document-recipient-link-copy-dialog';
-
-import { ResendDocumentActionItem } from '../_action-items/resend-document';
-import { DeleteDocumentDialog } from '../delete-document-dialog';
-import { DuplicateDocumentDialog } from '../duplicate-document-dialog';
-
-export type DocumentPageViewDropdownProps = {
- document: Document & {
- user: Pick;
- recipients: Recipient[];
- team: Pick | null;
- };
- team?: Pick & { teamEmail: TeamEmail | null };
-};
-
-export const DocumentPageViewDropdown = ({ document, team }: DocumentPageViewDropdownProps) => {
- const { data: session } = useSession();
- const { toast } = useToast();
- const { _ } = useLingui();
-
- const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false);
- const [isDuplicateDialogOpen, setDuplicateDialogOpen] = useState(false);
-
- if (!session) {
- return null;
- }
-
- const recipient = document.recipients.find((recipient) => recipient.email === session.user.email);
-
- const isOwner = document.user.id === session.user.id;
- const isDraft = document.status === DocumentStatus.DRAFT;
- const isPending = document.status === DocumentStatus.PENDING;
- const isDeleted = document.deletedAt !== null;
- const isComplete = document.status === DocumentStatus.COMPLETED;
- const isCurrentTeamDocument = team && document.team?.url === team.url;
- const canManageDocument = Boolean(isOwner || isCurrentTeamDocument);
-
- const documentsPath = formatDocumentsPath(team?.url);
-
- const onDownloadClick = async () => {
- try {
- const documentWithData = await trpcClient.document.getDocumentById.query(
- {
- documentId: document.id,
- },
- {
- context: {
- teamId: team?.id?.toString(),
- },
- },
- );
-
- const documentData = documentWithData?.documentData;
-
- if (!documentData) {
- return;
- }
-
- await downloadPDF({ documentData, fileName: document.title });
- } catch (err) {
- toast({
- title: _(msg`Something went wrong`),
- description: _(msg`An error occurred while downloading your document.`),
- variant: 'destructive',
- });
- }
- };
-
- const nonSignedRecipients = document.recipients.filter((item) => item.signingStatus !== 'SIGNED');
-
- return (
-
-
-
-
-
-
-
- Action
-
-
- {(isOwner || isCurrentTeamDocument) && !isComplete && (
-
-
-
- Edit
-
-
- )}
-
- {isComplete && (
-
-
- Download
-
- )}
-
-
-
-
- Audit Log
-
-
-
- setDuplicateDialogOpen(true)}>
-
- Duplicate
-
-
- setDeleteDialogOpen(true)}
- disabled={Boolean(!canManageDocument && team?.teamEmail) || isDeleted}
- >
-
- Delete
-
-
-
- Share
-
-
- {canManageDocument && (
- e.preventDefault()}
- >
-
- Signing Links
-
- }
- />
- )}
-
-
-
- (
- e.preventDefault()}>
-
- {loading ? : }
- Share Signing Card
-
-
- )}
- />
-
-
-
-
- {isDuplicateDialogOpen && (
-
- )}
-
- );
-};
diff --git a/apps/web/src/app/(dashboard)/documents/[id]/document-page-view-information.tsx b/apps/web/src/app/(dashboard)/documents/[id]/document-page-view-information.tsx
deleted file mode 100644
index ebb6482d5..000000000
--- a/apps/web/src/app/(dashboard)/documents/[id]/document-page-view-information.tsx
+++ /dev/null
@@ -1,70 +0,0 @@
-'use client';
-
-import { useMemo } from 'react';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { DateTime } from 'luxon';
-
-import { useIsMounted } from '@documenso/lib/client-only/hooks/use-is-mounted';
-import type { Document, Recipient, User } from '@documenso/prisma/client';
-
-export type DocumentPageViewInformationProps = {
- userId: number;
- document: Document & {
- user: Pick;
- recipients: Recipient[];
- };
-};
-
-export const DocumentPageViewInformation = ({
- document,
- userId,
-}: DocumentPageViewInformationProps) => {
- const isMounted = useIsMounted();
-
- const { _, i18n } = useLingui();
-
- const documentInformation = useMemo(() => {
- return [
- {
- description: msg`Uploaded by`,
- value:
- userId === document.userId ? _(msg`You`) : (document.user.name ?? document.user.email),
- },
- {
- description: msg`Created`,
- value: DateTime.fromJSDate(document.createdAt)
- .setLocale(i18n.locales?.[0] || i18n.locale)
- .toFormat('MMMM d, yyyy'),
- },
- {
- description: msg`Last modified`,
- value: DateTime.fromJSDate(document.updatedAt)
- .setLocale(i18n.locales?.[0] || i18n.locale)
- .toRelative(),
- },
- ];
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [isMounted, document, userId]);
-
- return (
-
-
- Information
-
-
-
- {documentInformation.map((item, i) => (
-
- {_(item.description)}
- {item.value}
-
- ))}
-
-
- );
-};
diff --git a/apps/web/src/app/(dashboard)/documents/[id]/document-page-view-recent-activity.tsx b/apps/web/src/app/(dashboard)/documents/[id]/document-page-view-recent-activity.tsx
deleted file mode 100644
index c6e0787bb..000000000
--- a/apps/web/src/app/(dashboard)/documents/[id]/document-page-view-recent-activity.tsx
+++ /dev/null
@@ -1,166 +0,0 @@
-'use client';
-
-import { useMemo } from 'react';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { AlertTriangle, CheckCheckIcon, CheckIcon, Loader, MailOpen } from 'lucide-react';
-import { DateTime } from 'luxon';
-import { match } from 'ts-pattern';
-
-import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
-import { formatDocumentAuditLogAction } from '@documenso/lib/utils/document-audit-logs';
-import { trpc } from '@documenso/trpc/react';
-import { AnimateGenericFadeInOut } from '@documenso/ui/components/animate/animate-generic-fade-in-out';
-import { cn } from '@documenso/ui/lib/utils';
-
-export type DocumentPageViewRecentActivityProps = {
- documentId: number;
- userId: number;
-};
-
-export const DocumentPageViewRecentActivity = ({
- documentId,
- userId,
-}: DocumentPageViewRecentActivityProps) => {
- const { _ } = useLingui();
-
- const {
- data,
- isLoading,
- isLoadingError,
- refetch,
- hasNextPage,
- fetchNextPage,
- isFetchingNextPage,
- } = trpc.document.findDocumentAuditLogs.useInfiniteQuery(
- {
- documentId,
- filterForRecentActivity: true,
- orderByColumn: 'createdAt',
- orderByDirection: 'asc',
- perPage: 10,
- },
- {
- getNextPageParam: (lastPage) => lastPage.nextCursor,
- },
- );
-
- const documentAuditLogs = useMemo(() => (data?.pages ?? []).flatMap((page) => page.data), [data]);
-
- return (
-
-
-
- Recent activity
-
-
- {/* Can add dropdown menu here for additional options. */}
-
-
- {isLoading && (
-
-
-
- )}
-
- {isLoadingError && (
-
-
- Unable to load document history
-
-
refetch()}
- className="text-foreground/70 hover:text-muted-foreground mt-2 text-sm"
- >
- Click here to retry
-
-
- )}
-
-
- {data && (
-
- )}
-
-
- );
-};
diff --git a/apps/web/src/app/(dashboard)/documents/[id]/document-page-view-recipients.tsx b/apps/web/src/app/(dashboard)/documents/[id]/document-page-view-recipients.tsx
deleted file mode 100644
index ea8ccee15..000000000
--- a/apps/web/src/app/(dashboard)/documents/[id]/document-page-view-recipients.tsx
+++ /dev/null
@@ -1,174 +0,0 @@
-'use client';
-
-import Link from 'next/link';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import {
- AlertTriangle,
- CheckIcon,
- Clock,
- MailIcon,
- MailOpenIcon,
- PenIcon,
- PlusIcon,
-} from 'lucide-react';
-import { match } from 'ts-pattern';
-
-import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
-import { formatSigningLink } from '@documenso/lib/utils/recipients';
-import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
-import type { Document, Recipient } from '@documenso/prisma/client';
-import { CopyTextButton } from '@documenso/ui/components/common/copy-text-button';
-import { SignatureIcon } from '@documenso/ui/icons/signature';
-import { AvatarWithText } from '@documenso/ui/primitives/avatar';
-import { Badge } from '@documenso/ui/primitives/badge';
-import { PopoverHover } from '@documenso/ui/primitives/popover';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-export type DocumentPageViewRecipientsProps = {
- document: Document & {
- recipients: Recipient[];
- };
- documentRootPath: string;
-};
-
-export const DocumentPageViewRecipients = ({
- document,
- documentRootPath,
-}: DocumentPageViewRecipientsProps) => {
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const recipients = document.recipients;
-
- return (
-
-
-
- Recipients
-
-
- {document.status !== DocumentStatus.COMPLETED && (
-
- {recipients.length === 0 ? (
-
- ) : (
-
- )}
-
- )}
-
-
-
- {recipients.length === 0 && (
-
- No recipients
-
- )}
-
- {recipients.map((recipient) => (
-
- {recipient.email}}
- secondaryText={
-
- {_(RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName)}
-
- }
- />
-
-
- {document.status !== DocumentStatus.DRAFT &&
- recipient.signingStatus === SigningStatus.SIGNED && (
-
- {match(recipient.role)
- .with(RecipientRole.APPROVER, () => (
- <>
-
- Approved
- >
- ))
- .with(RecipientRole.CC, () =>
- document.status === DocumentStatus.COMPLETED ? (
- <>
-
- Sent
- >
- ) : (
- <>
-
- Ready
- >
- ),
- )
-
- .with(RecipientRole.SIGNER, () => (
- <>
-
- Signed
- >
- ))
- .with(RecipientRole.VIEWER, () => (
- <>
-
- Viewed
- >
- ))
- .exhaustive()}
-
- )}
-
- {document.status !== DocumentStatus.DRAFT &&
- recipient.signingStatus === SigningStatus.NOT_SIGNED && (
-
-
- Pending
-
- )}
-
- {document.status !== DocumentStatus.DRAFT &&
- recipient.signingStatus === SigningStatus.REJECTED && (
-
-
- Rejected
-
- }
- >
-
- Reason for rejection:
-
-
-
- {recipient.rejectionReason}
-
-
- )}
-
- {document.status === DocumentStatus.PENDING &&
- recipient.signingStatus === SigningStatus.NOT_SIGNED &&
- recipient.role !== RecipientRole.CC && (
-
{
- toast({
- title: _(msg`Copied to clipboard`),
- description: _(msg`The signing link has been copied to your clipboard.`),
- });
- }}
- />
- )}
-
-
- ))}
-
-
- );
-};
diff --git a/apps/web/src/app/(dashboard)/documents/[id]/document-page-view.tsx b/apps/web/src/app/(dashboard)/documents/[id]/document-page-view.tsx
deleted file mode 100644
index 7892e6eba..000000000
--- a/apps/web/src/app/(dashboard)/documents/[id]/document-page-view.tsx
+++ /dev/null
@@ -1,270 +0,0 @@
-import Link from 'next/link';
-import { redirect } from 'next/navigation';
-
-import { Plural, Trans } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { ChevronLeft, Clock9, Users2 } from 'lucide-react';
-import { match } from 'ts-pattern';
-
-import { DOCUMENSO_ENCRYPTION_KEY } from '@documenso/lib/constants/crypto';
-import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
-import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
-import { getServerComponentFlag } from '@documenso/lib/server-only/feature-flags/get-server-component-feature-flag';
-import { getFieldsForDocument } from '@documenso/lib/server-only/field/get-fields-for-document';
-import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
-import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
-import { symmetricDecrypt } from '@documenso/lib/universal/crypto';
-import { formatDocumentsPath } from '@documenso/lib/utils/teams';
-import { DocumentStatus } from '@documenso/prisma/client';
-import type { Team, TeamEmail } from '@documenso/prisma/client';
-import { TeamMemberRole } from '@documenso/prisma/client';
-import { Badge } from '@documenso/ui/primitives/badge';
-import { Button } from '@documenso/ui/primitives/button';
-import { Card, CardContent } from '@documenso/ui/primitives/card';
-import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
-
-import { StackAvatarsWithTooltip } from '~/components/(dashboard)/avatar/stack-avatars-with-tooltip';
-import { DocumentHistorySheet } from '~/components/document/document-history-sheet';
-import { DocumentReadOnlyFields } from '~/components/document/document-read-only-fields';
-import { DocumentRecipientLinkCopyDialog } from '~/components/document/document-recipient-link-copy-dialog';
-import {
- DocumentStatus as DocumentStatusComponent,
- FRIENDLY_STATUS_MAP,
-} from '~/components/formatter/document-status';
-
-import { DocumentPageViewButton } from './document-page-view-button';
-import { DocumentPageViewDropdown } from './document-page-view-dropdown';
-import { DocumentPageViewInformation } from './document-page-view-information';
-import { DocumentPageViewRecentActivity } from './document-page-view-recent-activity';
-import { DocumentPageViewRecipients } from './document-page-view-recipients';
-
-export type DocumentPageViewProps = {
- params: {
- id: string;
- };
- team?: Team & { teamEmail: TeamEmail | null } & { currentTeamMember: { role: TeamMemberRole } };
-};
-
-export const DocumentPageView = async ({ params, team }: DocumentPageViewProps) => {
- const { id } = params;
- const { _ } = useLingui();
-
- const documentId = Number(id);
-
- const documentRootPath = formatDocumentsPath(team?.url);
-
- if (!documentId || Number.isNaN(documentId)) {
- redirect(documentRootPath);
- }
-
- const { user } = await getRequiredServerComponentSession();
-
- const document = await getDocumentById({
- documentId,
- userId: user.id,
- teamId: team?.id,
- }).catch(() => null);
-
- if (document?.teamId && !team?.url) {
- redirect(documentRootPath);
- }
-
- const documentVisibility = document?.visibility;
- const currentTeamMemberRole = team?.currentTeamMember?.role;
- const isRecipient = document?.recipients.find((recipient) => recipient.email === user.email);
- let canAccessDocument = true;
-
- if (team && !isRecipient && document?.userId !== user.id) {
- canAccessDocument = match([documentVisibility, currentTeamMemberRole])
- .with([DocumentVisibility.EVERYONE, TeamMemberRole.ADMIN], () => true)
- .with([DocumentVisibility.EVERYONE, TeamMemberRole.MANAGER], () => true)
- .with([DocumentVisibility.EVERYONE, TeamMemberRole.MEMBER], () => true)
- .with([DocumentVisibility.MANAGER_AND_ABOVE, TeamMemberRole.ADMIN], () => true)
- .with([DocumentVisibility.MANAGER_AND_ABOVE, TeamMemberRole.MANAGER], () => true)
- .with([DocumentVisibility.ADMIN, TeamMemberRole.ADMIN], () => true)
- .otherwise(() => false);
- }
-
- const isDocumentHistoryEnabled = await getServerComponentFlag(
- 'app_document_page_view_history_sheet',
- );
-
- if (!document || !document.documentData || (team && !canAccessDocument)) {
- redirect(documentRootPath);
- }
-
- if (team && !canAccessDocument) {
- redirect(documentRootPath);
- }
-
- const { documentData, documentMeta } = document;
-
- if (documentMeta?.password) {
- const key = DOCUMENSO_ENCRYPTION_KEY;
-
- if (!key) {
- throw new Error('Missing DOCUMENSO_ENCRYPTION_KEY');
- }
-
- const securePassword = Buffer.from(
- symmetricDecrypt({
- key,
- data: documentMeta.password,
- }),
- ).toString('utf-8');
-
- documentMeta.password = securePassword;
- }
-
- const [recipients, fields] = await Promise.all([
- getRecipientsForDocument({
- documentId,
- teamId: team?.id,
- userId: user.id,
- }),
- getFieldsForDocument({
- documentId,
- userId: user.id,
- teamId: team?.id,
- }),
- ]);
-
- const documentWithRecipients = {
- ...document,
- recipients,
- };
-
- return (
-
- {document.status === DocumentStatus.PENDING && (
-
- )}
-
-
-
-
Documents
-
-
-
-
-
- {document.title}
-
-
-
-
-
- {recipients.length > 0 && (
-
-
-
-
-
- {recipients.length} Recipient(s)
-
-
-
- )}
-
- {document.deletedAt && (
-
- Document deleted
-
- )}
-
-
-
- {isDocumentHistoryEnabled && (
-
-
-
-
- Document history
-
-
-
- )}
-
-
-
-
-
-
-
-
-
- {document.status === DocumentStatus.PENDING && (
-
- )}
-
-
-
-
-
-
- {_(FRIENDLY_STATUS_MAP[document.status].labelExtended)}
-
-
-
-
-
-
- {match(document.status)
- .with(DocumentStatus.COMPLETED, () => (
- This document has been signed by all recipients
- ))
- .with(DocumentStatus.DRAFT, () => (
- This document is currently a draft and has not been sent
- ))
- .with(DocumentStatus.PENDING, () => {
- const pendingRecipients = recipients.filter(
- (recipient) => recipient.signingStatus === 'NOT_SIGNED',
- );
-
- return (
-
- );
- })
- .exhaustive()}
-
-
-
-
-
-
-
- {/* Document information section. */}
-
-
- {/* Recipients section. */}
-
-
- {/* Recent activity section. */}
-
-
-
-
-
- );
-};
diff --git a/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx b/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx
deleted file mode 100644
index 7977aa2c6..000000000
--- a/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx
+++ /dev/null
@@ -1,425 +0,0 @@
-'use client';
-
-import { useEffect, useState } from 'react';
-
-import { useRouter, useSearchParams } from 'next/navigation';
-
-import { msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-
-import { isValidLanguageCode } from '@documenso/lib/constants/i18n';
-import {
- DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
- SKIP_QUERY_BATCH_META,
-} from '@documenso/lib/constants/trpc';
-import type { TDocument } from '@documenso/lib/types/document';
-import { DocumentDistributionMethod, DocumentStatus } from '@documenso/prisma/client';
-import { trpc } from '@documenso/trpc/react';
-import { cn } from '@documenso/ui/lib/utils';
-import { Card, CardContent } from '@documenso/ui/primitives/card';
-import { AddFieldsFormPartial } from '@documenso/ui/primitives/document-flow/add-fields';
-import type { TAddFieldsFormSchema } from '@documenso/ui/primitives/document-flow/add-fields.types';
-import { AddSettingsFormPartial } from '@documenso/ui/primitives/document-flow/add-settings';
-import type { TAddSettingsFormSchema } from '@documenso/ui/primitives/document-flow/add-settings.types';
-import { AddSignersFormPartial } from '@documenso/ui/primitives/document-flow/add-signers';
-import type { TAddSignersFormSchema } from '@documenso/ui/primitives/document-flow/add-signers.types';
-import { AddSubjectFormPartial } from '@documenso/ui/primitives/document-flow/add-subject';
-import type { TAddSubjectFormSchema } from '@documenso/ui/primitives/document-flow/add-subject.types';
-import { DocumentFlowFormContainer } from '@documenso/ui/primitives/document-flow/document-flow-root';
-import type { DocumentFlowStep } from '@documenso/ui/primitives/document-flow/types';
-import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
-import { Stepper } from '@documenso/ui/primitives/stepper';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-import { useOptionalCurrentTeam } from '~/providers/team';
-
-export type EditDocumentFormProps = {
- className?: string;
- initialDocument: TDocument;
- documentRootPath: string;
- isDocumentEnterprise: boolean;
-};
-
-type EditDocumentStep = 'settings' | 'signers' | 'fields' | 'subject';
-const EditDocumentSteps: EditDocumentStep[] = ['settings', 'signers', 'fields', 'subject'];
-
-export const EditDocumentForm = ({
- className,
- initialDocument,
- documentRootPath,
- isDocumentEnterprise,
-}: EditDocumentFormProps) => {
- const { toast } = useToast();
- const { _ } = useLingui();
-
- const router = useRouter();
- const searchParams = useSearchParams();
- const team = useOptionalCurrentTeam();
-
- const [isDocumentPdfLoaded, setIsDocumentPdfLoaded] = useState(false);
-
- const utils = trpc.useUtils();
-
- const { data: document, refetch: refetchDocument } =
- trpc.document.getDocumentWithDetailsById.useQuery(
- {
- documentId: initialDocument.id,
- },
- {
- initialData: initialDocument,
- ...SKIP_QUERY_BATCH_META,
- },
- );
-
- const { recipients, fields } = document;
-
- const { mutateAsync: updateDocument } = trpc.document.setSettingsForDocument.useMutation({
- ...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
- onSuccess: (newData) => {
- utils.document.getDocumentWithDetailsById.setData(
- {
- documentId: initialDocument.id,
- },
- (oldData) => ({ ...(oldData || initialDocument), ...newData }),
- );
- },
- });
-
- const { mutateAsync: setSigningOrderForDocument } =
- trpc.document.setSigningOrderForDocument.useMutation({
- ...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
- onSuccess: (newData) => {
- utils.document.getDocumentWithDetailsById.setData(
- {
- documentId: initialDocument.id,
- },
- (oldData) => ({ ...(oldData || initialDocument), ...newData, id: Number(newData.id) }),
- );
- },
- });
-
- const { mutateAsync: addFields } = trpc.field.addFields.useMutation({
- ...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
- onSuccess: ({ fields: newFields }) => {
- utils.document.getDocumentWithDetailsById.setData(
- {
- documentId: initialDocument.id,
- },
- (oldData) => ({ ...(oldData || initialDocument), fields: newFields }),
- );
- },
- });
-
- const { mutateAsync: setRecipients } = trpc.recipient.setDocumentRecipients.useMutation({
- ...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
- onSuccess: ({ recipients: newRecipients }) => {
- utils.document.getDocumentWithDetailsById.setData(
- {
- documentId: initialDocument.id,
- },
- (oldData) => ({ ...(oldData || initialDocument), recipients: newRecipients }),
- );
- },
- });
-
- const { mutateAsync: sendDocument } = trpc.document.sendDocument.useMutation({
- ...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
- onSuccess: (newData) => {
- utils.document.getDocumentWithDetailsById.setData(
- {
- documentId: initialDocument.id,
- },
- (oldData) => ({ ...(oldData || initialDocument), ...newData }),
- );
- },
- });
-
- const { mutateAsync: setPasswordForDocument } =
- trpc.document.setPasswordForDocument.useMutation();
-
- const documentFlow: Record = {
- settings: {
- title: msg`General`,
- description: msg`Configure general settings for the document.`,
- stepIndex: 1,
- },
- signers: {
- title: msg`Add Signers`,
- description: msg`Add the people who will sign the document.`,
- stepIndex: 2,
- },
- fields: {
- title: msg`Add Fields`,
- description: msg`Add all relevant fields for each recipient.`,
- stepIndex: 3,
- },
- subject: {
- title: msg`Distribute Document`,
- description: msg`Choose how the document will reach recipients`,
- stepIndex: 4,
- },
- };
-
- const [step, setStep] = useState(() => {
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
- const searchParamStep = searchParams?.get('step') as EditDocumentStep | undefined;
-
- let initialStep: EditDocumentStep = 'settings';
-
- if (
- searchParamStep &&
- documentFlow[searchParamStep] !== undefined &&
- !(recipients.length === 0 && (searchParamStep === 'subject' || searchParamStep === 'fields'))
- ) {
- initialStep = searchParamStep;
- }
-
- return initialStep;
- });
-
- const onAddSettingsFormSubmit = async (data: TAddSettingsFormSchema) => {
- try {
- const { timezone, dateFormat, redirectUrl, language } = data.meta;
-
- await updateDocument({
- documentId: document.id,
- data: {
- title: data.title,
- externalId: data.externalId || null,
- visibility: data.visibility,
- globalAccessAuth: data.globalAccessAuth ?? null,
- globalActionAuth: data.globalActionAuth ?? null,
- },
- meta: {
- timezone,
- dateFormat,
- redirectUrl,
- language: isValidLanguageCode(language) ? language : undefined,
- },
- });
-
- // Router refresh is here to clear the router cache for when navigating to /documents.
- router.refresh();
-
- setStep('signers');
- } catch (err) {
- console.error(err);
-
- toast({
- title: _(msg`Error`),
- description: _(msg`An error occurred while updating the document settings.`),
- variant: 'destructive',
- });
- }
- };
-
- const onAddSignersFormSubmit = async (data: TAddSignersFormSchema) => {
- try {
- await Promise.all([
- setSigningOrderForDocument({
- documentId: document.id,
- signingOrder: data.signingOrder,
- }),
-
- setRecipients({
- documentId: document.id,
- recipients: data.signers.map((signer) => ({
- ...signer,
- // Explicitly set to null to indicate we want to remove auth if required.
- actionAuth: signer.actionAuth || null,
- })),
- }),
- ]);
-
- // Router refresh is here to clear the router cache for when navigating to /documents.
- router.refresh();
-
- setStep('fields');
- } catch (err) {
- console.error(err);
-
- toast({
- title: _(msg`Error`),
- description: _(msg`An error occurred while adding signers.`),
- variant: 'destructive',
- });
- }
- };
-
- const onAddFieldsFormSubmit = async (data: TAddFieldsFormSchema) => {
- try {
- await addFields({
- documentId: document.id,
- fields: data.fields,
- });
-
- await updateDocument({
- documentId: document.id,
-
- meta: {
- typedSignatureEnabled: data.typedSignatureEnabled,
- },
- });
-
- // Clear all field data from localStorage
- for (let i = 0; i < localStorage.length; i++) {
- const key = localStorage.key(i);
- if (key && key.startsWith('field_')) {
- localStorage.removeItem(key);
- }
- }
-
- // Router refresh is here to clear the router cache for when navigating to /documents.
- router.refresh();
-
- setStep('subject');
- } catch (err) {
- console.error(err);
-
- toast({
- title: _(msg`Error`),
- description: _(msg`An error occurred while adding the fields.`),
- variant: 'destructive',
- });
- }
- };
-
- const onAddSubjectFormSubmit = async (data: TAddSubjectFormSchema) => {
- const { subject, message, distributionMethod, emailSettings } = data.meta;
-
- try {
- await sendDocument({
- documentId: document.id,
- meta: {
- subject,
- message,
- distributionMethod,
- emailSettings,
- },
- });
-
- if (distributionMethod === DocumentDistributionMethod.EMAIL) {
- toast({
- title: _(msg`Document sent`),
- description: _(msg`Your document has been sent successfully.`),
- duration: 5000,
- });
-
- router.push(documentRootPath);
- return;
- }
-
- if (document.status === DocumentStatus.DRAFT) {
- toast({
- title: _(msg`Links Generated`),
- description: _(msg`Signing links have been generated for this document.`),
- duration: 5000,
- });
- } else {
- router.push(`${documentRootPath}/${document.id}`);
- }
- } catch (err) {
- console.error(err);
-
- toast({
- title: _(msg`Error`),
- description: _(msg`An error occurred while sending the document.`),
- variant: 'destructive',
- });
- }
- };
-
- const onPasswordSubmit = async (password: string) => {
- await setPasswordForDocument({
- documentId: document.id,
- password,
- });
- };
-
- const currentDocumentFlow = documentFlow[step];
-
- /**
- * Refresh the data in the background when steps change.
- */
- useEffect(() => {
- void refetchDocument();
-
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [step]);
-
- return (
-
-
-
- setIsDocumentPdfLoaded(true)}
- />
-
-
-
-
-
e.preventDefault()}
- >
- setStep(EditDocumentSteps[step - 1])}
- >
-
-
-
-
-
-
-
-
-
-
-
- );
-};
diff --git a/apps/web/src/app/(dashboard)/documents/[id]/edit/document-edit-page-view.tsx b/apps/web/src/app/(dashboard)/documents/[id]/edit/document-edit-page-view.tsx
deleted file mode 100644
index 70e3323e2..000000000
--- a/apps/web/src/app/(dashboard)/documents/[id]/edit/document-edit-page-view.tsx
+++ /dev/null
@@ -1,147 +0,0 @@
-import Link from 'next/link';
-import { redirect } from 'next/navigation';
-
-import { Plural, Trans } from '@lingui/macro';
-import { ChevronLeft, Users2 } from 'lucide-react';
-import { match } from 'ts-pattern';
-
-import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
-import { DOCUMENSO_ENCRYPTION_KEY } from '@documenso/lib/constants/crypto';
-import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
-import { getDocumentWithDetailsById } from '@documenso/lib/server-only/document/get-document-with-details-by-id';
-import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
-import { symmetricDecrypt } from '@documenso/lib/universal/crypto';
-import { formatDocumentsPath } from '@documenso/lib/utils/teams';
-import type { Team } from '@documenso/prisma/client';
-import { TeamMemberRole } from '@documenso/prisma/client';
-import { DocumentStatus as InternalDocumentStatus } from '@documenso/prisma/client';
-
-import { EditDocumentForm } from '~/app/(dashboard)/documents/[id]/edit-document';
-import { StackAvatarsWithTooltip } from '~/components/(dashboard)/avatar/stack-avatars-with-tooltip';
-import { DocumentStatus } from '~/components/formatter/document-status';
-
-export type DocumentEditPageViewProps = {
- params: {
- id: string;
- };
- team?: Team & { currentTeamMember: { role: TeamMemberRole } };
-};
-
-export const DocumentEditPageView = async ({ params, team }: DocumentEditPageViewProps) => {
- const { id } = params;
-
- const documentId = Number(id);
-
- const documentRootPath = formatDocumentsPath(team?.url);
-
- if (!documentId || Number.isNaN(documentId)) {
- redirect(documentRootPath);
- }
-
- const { user } = await getRequiredServerComponentSession();
-
- const document = await getDocumentWithDetailsById({
- documentId,
- userId: user.id,
- teamId: team?.id,
- }).catch(() => null);
-
- if (document?.teamId && !team?.url) {
- redirect(documentRootPath);
- }
-
- const documentVisibility = document?.visibility;
- const currentTeamMemberRole = team?.currentTeamMember?.role;
- const isRecipient = document?.recipients.find((recipient) => recipient.email === user.email);
- let canAccessDocument = true;
-
- if (!isRecipient && document?.userId !== user.id) {
- canAccessDocument = match([documentVisibility, currentTeamMemberRole])
- .with([DocumentVisibility.EVERYONE, TeamMemberRole.ADMIN], () => true)
- .with([DocumentVisibility.EVERYONE, TeamMemberRole.MANAGER], () => true)
- .with([DocumentVisibility.EVERYONE, TeamMemberRole.MEMBER], () => true)
- .with([DocumentVisibility.MANAGER_AND_ABOVE, TeamMemberRole.ADMIN], () => true)
- .with([DocumentVisibility.MANAGER_AND_ABOVE, TeamMemberRole.MANAGER], () => true)
- .with([DocumentVisibility.ADMIN, TeamMemberRole.ADMIN], () => true)
- .otherwise(() => false);
- }
-
- if (!document) {
- redirect(documentRootPath);
- }
-
- if (team && !canAccessDocument) {
- redirect(documentRootPath);
- }
-
- if (document.status === InternalDocumentStatus.COMPLETED) {
- redirect(`${documentRootPath}/${documentId}`);
- }
-
- const { documentMeta, recipients } = document;
-
- if (documentMeta?.password) {
- const key = DOCUMENSO_ENCRYPTION_KEY;
-
- if (!key) {
- throw new Error('Missing DOCUMENSO_ENCRYPTION_KEY');
- }
-
- const securePassword = Buffer.from(
- symmetricDecrypt({
- key,
- data: documentMeta.password,
- }),
- ).toString('utf-8');
-
- documentMeta.password = securePassword;
- }
-
- const isDocumentEnterprise = await isUserEnterprise({
- userId: user.id,
- teamId: team?.id,
- });
-
- return (
-
-
-
-
Documents
-
-
-
- {document.title}
-
-
-
-
-
- {recipients.length > 0 && (
-
- )}
-
-
-
-
- );
-};
diff --git a/apps/web/src/app/(dashboard)/documents/[id]/edit/page.tsx b/apps/web/src/app/(dashboard)/documents/[id]/edit/page.tsx
deleted file mode 100644
index a20cc8469..000000000
--- a/apps/web/src/app/(dashboard)/documents/[id]/edit/page.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-
-import { DocumentEditPageView } from './document-edit-page-view';
-
-export type DocumentPageProps = {
- params: {
- id: string;
- };
-};
-
-export default async function DocumentEditPage({ params }: DocumentPageProps) {
- await setupI18nSSR();
-
- return ;
-}
diff --git a/apps/web/src/app/(dashboard)/documents/[id]/loading.tsx b/apps/web/src/app/(dashboard)/documents/[id]/loading.tsx
deleted file mode 100644
index b6165436b..000000000
--- a/apps/web/src/app/(dashboard)/documents/[id]/loading.tsx
+++ /dev/null
@@ -1,42 +0,0 @@
-import Link from 'next/link';
-
-import { Trans } from '@lingui/macro';
-import { ChevronLeft, Loader } from 'lucide-react';
-
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-import { Skeleton } from '@documenso/ui/primitives/skeleton';
-
-export default async function Loading() {
- await setupI18nSSR();
-
- return (
-
-
-
-
Documents
-
-
-
- Loading Document...
-
-
-
-
-
-
-
-
-
-
-
-
- Loading document...
-
-
-
-
-
-
-
- );
-}
diff --git a/apps/web/src/app/(dashboard)/documents/[id]/logs/document-logs-data-table.tsx b/apps/web/src/app/(dashboard)/documents/[id]/logs/document-logs-data-table.tsx
deleted file mode 100644
index 45097b594..000000000
--- a/apps/web/src/app/(dashboard)/documents/[id]/logs/document-logs-data-table.tsx
+++ /dev/null
@@ -1,163 +0,0 @@
-'use client';
-
-import { useMemo } from 'react';
-
-import { useSearchParams } from 'next/navigation';
-
-import { msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { DateTime } from 'luxon';
-import type { DateTimeFormatOptions } from 'luxon';
-import { UAParser } from 'ua-parser-js';
-
-import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
-import { ZUrlSearchParamsSchema } from '@documenso/lib/types/search-params';
-import { formatDocumentAuditLogAction } from '@documenso/lib/utils/document-audit-logs';
-import { trpc } from '@documenso/trpc/react';
-import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
-import { DataTable } from '@documenso/ui/primitives/data-table';
-import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
-import { Skeleton } from '@documenso/ui/primitives/skeleton';
-import { TableCell } from '@documenso/ui/primitives/table';
-
-export type DocumentLogsDataTableProps = {
- documentId: number;
-};
-
-const dateFormat: DateTimeFormatOptions = {
- ...DateTime.DATETIME_SHORT,
- hourCycle: 'h12',
-};
-
-export const DocumentLogsDataTable = ({ documentId }: DocumentLogsDataTableProps) => {
- const { _, i18n } = useLingui();
-
- const searchParams = useSearchParams();
- const updateSearchParams = useUpdateSearchParams();
-
- const parsedSearchParams = ZUrlSearchParamsSchema.parse(Object.fromEntries(searchParams ?? []));
-
- const { data, isLoading, isLoadingError } = trpc.document.findDocumentAuditLogs.useQuery(
- {
- documentId,
- page: parsedSearchParams.page,
- perPage: parsedSearchParams.perPage,
- },
- {
- placeholderData: (previousData) => previousData,
- },
- );
-
- const onPaginationChange = (page: number, perPage: number) => {
- updateSearchParams({
- page,
- perPage,
- });
- };
-
- const results = data ?? {
- data: [],
- perPage: 10,
- currentPage: 1,
- totalPages: 1,
- };
-
- const columns = useMemo(() => {
- const parser = new UAParser();
-
- return [
- {
- header: _(msg`Time`),
- accessorKey: 'createdAt',
- cell: ({ row }) => i18n.date(row.original.createdAt, dateFormat),
- },
- {
- header: _(msg`User`),
- accessorKey: 'name',
- cell: ({ row }) =>
- row.original.name || row.original.email ? (
-
- {row.original.name && (
-
- {row.original.name}
-
- )}
-
- {row.original.email && (
-
- {row.original.email}
-
- )}
-
- ) : (
- N/A
- ),
- },
- {
- header: _(msg`Action`),
- accessorKey: 'type',
- cell: ({ row }) => {formatDocumentAuditLogAction(_, row.original).description} ,
- },
- {
- header: 'IP Address',
- accessorKey: 'ipAddress',
- },
- {
- header: 'Browser',
- cell: ({ row }) => {
- if (!row.original.userAgent) {
- return 'N/A';
- }
-
- parser.setUA(row.original.userAgent);
-
- const result = parser.getResult();
-
- return result.browser.name ?? 'N/A';
- },
- },
- ] satisfies DataTableColumnDef<(typeof results)['data'][number]>[];
- }, []);
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- >
- ),
- }}
- >
- {(table) => }
-
- );
-};
diff --git a/apps/web/src/app/(dashboard)/documents/[id]/logs/document-logs-page-view.tsx b/apps/web/src/app/(dashboard)/documents/[id]/logs/document-logs-page-view.tsx
deleted file mode 100644
index 877dad583..000000000
--- a/apps/web/src/app/(dashboard)/documents/[id]/logs/document-logs-page-view.tsx
+++ /dev/null
@@ -1,179 +0,0 @@
-import Link from 'next/link';
-import { redirect } from 'next/navigation';
-
-import type { MessageDescriptor } from '@lingui/core';
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { ChevronLeft } from 'lucide-react';
-import { DateTime } from 'luxon';
-
-import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
-import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
-import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
-import { formatDocumentsPath } from '@documenso/lib/utils/teams';
-import type { Recipient, Team } from '@documenso/prisma/client';
-import { Card } from '@documenso/ui/primitives/card';
-
-import {
- DocumentStatus as DocumentStatusComponent,
- FRIENDLY_STATUS_MAP,
-} from '~/components/formatter/document-status';
-
-import { DocumentLogsDataTable } from './document-logs-data-table';
-import { DownloadAuditLogButton } from './download-audit-log-button';
-import { DownloadCertificateButton } from './download-certificate-button';
-
-export type DocumentLogsPageViewProps = {
- params: {
- id: string;
- };
- team?: Team;
-};
-
-export const DocumentLogsPageView = async ({ params, team }: DocumentLogsPageViewProps) => {
- const { _, i18n } = useLingui();
-
- const { id } = params;
-
- const documentId = Number(id);
-
- const documentRootPath = formatDocumentsPath(team?.url);
-
- if (!documentId || Number.isNaN(documentId)) {
- redirect(documentRootPath);
- }
-
- const { user } = await getRequiredServerComponentSession();
-
- const [document, recipients] = await Promise.all([
- getDocumentById({
- documentId,
- userId: user.id,
- teamId: team?.id,
- }).catch(() => null),
- getRecipientsForDocument({
- documentId,
- userId: user.id,
- teamId: team?.id,
- }),
- ]);
-
- if (!document || !document.documentData) {
- redirect(documentRootPath);
- }
-
- const documentInformation: { description: MessageDescriptor; value: string }[] = [
- {
- description: msg`Document title`,
- value: document.title,
- },
- {
- description: msg`Document ID`,
- value: document.id.toString(),
- },
- {
- description: msg`Document status`,
- value: _(FRIENDLY_STATUS_MAP[document.status].label),
- },
- {
- description: msg`Created by`,
- value: document.user.name
- ? `${document.user.name} (${document.user.email})`
- : document.user.email,
- },
- {
- description: msg`Date created`,
- value: DateTime.fromJSDate(document.createdAt)
- .setLocale(i18n.locales?.[0] || i18n.locale)
- .toLocaleString(DateTime.DATETIME_MED_WITH_SECONDS),
- },
- {
- description: msg`Last updated`,
- value: DateTime.fromJSDate(document.updatedAt)
- .setLocale(i18n.locales?.[0] || i18n.locale)
- .toLocaleString(DateTime.DATETIME_MED_WITH_SECONDS),
- },
- {
- description: msg`Time zone`,
- value: document.documentMeta?.timezone ?? 'N/A',
- },
- ];
-
- const formatRecipientText = (recipient: Recipient) => {
- let text = recipient.email;
-
- if (recipient.name) {
- text = `${recipient.name} (${recipient.email})`;
- }
-
- return `[${recipient.role}] ${text}`;
- };
-
- return (
-
-
-
-
Document
-
-
-
-
-
- {document.title}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {documentInformation.map((info, i) => (
-
-
{_(info.description)}
-
{info.value}
-
- ))}
-
-
-
Recipients
-
- {recipients.map((recipient) => (
-
- {formatRecipientText(recipient)}
-
- ))}
-
-
-
-
-
-
-
- );
-};
diff --git a/apps/web/src/app/(dashboard)/documents/[id]/logs/download-audit-log-button.tsx b/apps/web/src/app/(dashboard)/documents/[id]/logs/download-audit-log-button.tsx
deleted file mode 100644
index d6be5318c..000000000
--- a/apps/web/src/app/(dashboard)/documents/[id]/logs/download-audit-log-button.tsx
+++ /dev/null
@@ -1,80 +0,0 @@
-'use client';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { DownloadIcon } from 'lucide-react';
-
-import { trpc } from '@documenso/trpc/react';
-import { cn } from '@documenso/ui/lib/utils';
-import { Button } from '@documenso/ui/primitives/button';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-export type DownloadAuditLogButtonProps = {
- className?: string;
- teamId?: number;
- documentId: number;
-};
-
-export const DownloadAuditLogButton = ({ className, documentId }: DownloadAuditLogButtonProps) => {
- const { toast } = useToast();
- const { _ } = useLingui();
-
- const { mutateAsync: downloadAuditLogs, isPending } =
- trpc.document.downloadAuditLogs.useMutation();
-
- const onDownloadAuditLogsClick = async () => {
- try {
- const { url } = await downloadAuditLogs({ documentId });
-
- const iframe = Object.assign(document.createElement('iframe'), {
- src: url,
- });
-
- Object.assign(iframe.style, {
- position: 'fixed',
- top: '0',
- left: '0',
- width: '0',
- height: '0',
- });
-
- const onLoaded = () => {
- if (iframe.contentDocument?.readyState === 'complete') {
- iframe.contentWindow?.print();
-
- iframe.contentWindow?.addEventListener('afterprint', () => {
- document.body.removeChild(iframe);
- });
- }
- };
-
- // When the iframe has loaded, print the iframe and remove it from the dom
- iframe.addEventListener('load', onLoaded);
-
- document.body.appendChild(iframe);
-
- onLoaded();
- } catch (error) {
- console.error(error);
-
- toast({
- title: _(msg`Something went wrong`),
- description: _(
- msg`Sorry, we were unable to download the audit logs. Please try again later.`,
- ),
- variant: 'destructive',
- });
- }
- };
-
- return (
- void onDownloadAuditLogsClick()}
- >
- {!isPending && }
- Download Audit Logs
-
- );
-};
diff --git a/apps/web/src/app/(dashboard)/documents/[id]/logs/download-certificate-button.tsx b/apps/web/src/app/(dashboard)/documents/[id]/logs/download-certificate-button.tsx
deleted file mode 100644
index 18eff7258..000000000
--- a/apps/web/src/app/(dashboard)/documents/[id]/logs/download-certificate-button.tsx
+++ /dev/null
@@ -1,89 +0,0 @@
-'use client';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { DownloadIcon } from 'lucide-react';
-
-import { DocumentStatus } from '@documenso/prisma/client';
-import { trpc } from '@documenso/trpc/react';
-import { cn } from '@documenso/ui/lib/utils';
-import { Button } from '@documenso/ui/primitives/button';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-export type DownloadCertificateButtonProps = {
- className?: string;
- documentId: number;
- documentStatus: DocumentStatus;
- teamId?: number;
-};
-
-export const DownloadCertificateButton = ({
- className,
- documentId,
- documentStatus,
- teamId,
-}: DownloadCertificateButtonProps) => {
- const { toast } = useToast();
- const { _ } = useLingui();
-
- const { mutateAsync: downloadCertificate, isPending } =
- trpc.document.downloadCertificate.useMutation();
-
- const onDownloadCertificatesClick = async () => {
- try {
- const { url } = await downloadCertificate({ documentId });
-
- const iframe = Object.assign(document.createElement('iframe'), {
- src: url,
- });
-
- Object.assign(iframe.style, {
- position: 'fixed',
- top: '0',
- left: '0',
- width: '0',
- height: '0',
- });
-
- const onLoaded = () => {
- if (iframe.contentDocument?.readyState === 'complete') {
- iframe.contentWindow?.print();
-
- iframe.contentWindow?.addEventListener('afterprint', () => {
- document.body.removeChild(iframe);
- });
- }
- };
-
- // When the iframe has loaded, print the iframe and remove it from the dom
- iframe.addEventListener('load', onLoaded);
-
- document.body.appendChild(iframe);
-
- onLoaded();
- } catch (error) {
- console.error(error);
-
- toast({
- title: _(msg`Something went wrong`),
- description: _(
- msg`Sorry, we were unable to download the certificate. Please try again later.`,
- ),
- variant: 'destructive',
- });
- }
- };
-
- return (
- void onDownloadCertificatesClick()}
- >
- {!isPending && }
- Download Certificate
-
- );
-};
diff --git a/apps/web/src/app/(dashboard)/documents/[id]/logs/page.tsx b/apps/web/src/app/(dashboard)/documents/[id]/logs/page.tsx
deleted file mode 100644
index c7c489ef1..000000000
--- a/apps/web/src/app/(dashboard)/documents/[id]/logs/page.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-
-import { DocumentLogsPageView } from './document-logs-page-view';
-
-export type DocumentsLogsPageProps = {
- params: {
- id: string;
- };
-};
-
-export default async function DocumentsLogsPage({ params }: DocumentsLogsPageProps) {
- await setupI18nSSR();
-
- return ;
-}
diff --git a/apps/web/src/app/(dashboard)/documents/[id]/page.tsx b/apps/web/src/app/(dashboard)/documents/[id]/page.tsx
deleted file mode 100644
index 4e570effe..000000000
--- a/apps/web/src/app/(dashboard)/documents/[id]/page.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-
-import { DocumentPageView } from './document-page-view';
-
-export type DocumentPageProps = {
- params: {
- id: string;
- };
-};
-
-export default async function DocumentPage({ params }: DocumentPageProps) {
- await setupI18nSSR();
-
- return ;
-}
diff --git a/apps/web/src/app/(dashboard)/documents/[id]/sent/page.tsx b/apps/web/src/app/(dashboard)/documents/[id]/sent/page.tsx
deleted file mode 100644
index 9d6e20535..000000000
--- a/apps/web/src/app/(dashboard)/documents/[id]/sent/page.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import Link from 'next/link';
-
-import { Trans } from '@lingui/macro';
-import { ChevronLeft } from 'lucide-react';
-
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-
-export default async function DocumentSentPage() {
- await setupI18nSSR();
-
- return (
-
-
-
- Documents
-
-
-
- Loading Document...
-
-
- );
-}
diff --git a/apps/web/src/app/(dashboard)/documents/_action-items/resend-document.tsx b/apps/web/src/app/(dashboard)/documents/_action-items/resend-document.tsx
deleted file mode 100644
index f45a3262f..000000000
--- a/apps/web/src/app/(dashboard)/documents/_action-items/resend-document.tsx
+++ /dev/null
@@ -1,197 +0,0 @@
-'use client';
-
-import { useState } from 'react';
-
-import { zodResolver } from '@hookform/resolvers/zod';
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { History } from 'lucide-react';
-import { useSession } from 'next-auth/react';
-import { useForm } from 'react-hook-form';
-import * as z from 'zod';
-
-import { getRecipientType } from '@documenso/lib/client-only/recipient-type';
-import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter';
-import type { Team } from '@documenso/prisma/client';
-import { type Document, type Recipient, SigningStatus } from '@documenso/prisma/client';
-import { trpc as trpcReact } from '@documenso/trpc/react';
-import { cn } from '@documenso/ui/lib/utils';
-import { Button } from '@documenso/ui/primitives/button';
-import { Checkbox } from '@documenso/ui/primitives/checkbox';
-import {
- Dialog,
- DialogClose,
- DialogContent,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from '@documenso/ui/primitives/dialog';
-import { DropdownMenuItem } from '@documenso/ui/primitives/dropdown-menu';
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
-} from '@documenso/ui/primitives/form/form';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-import { StackAvatar } from '~/components/(dashboard)/avatar/stack-avatar';
-
-const FORM_ID = 'resend-email';
-
-export type ResendDocumentActionItemProps = {
- document: Document & {
- team: Pick | null;
- };
- recipients: Recipient[];
- team?: Pick;
-};
-
-export const ZResendDocumentFormSchema = z.object({
- recipients: z.array(z.number()).min(1, {
- message: 'You must select at least one item.',
- }),
-});
-
-export type TResendDocumentFormSchema = z.infer;
-
-export const ResendDocumentActionItem = ({
- document,
- recipients,
- team,
-}: ResendDocumentActionItemProps) => {
- const { data: session } = useSession();
- const { toast } = useToast();
- const { _ } = useLingui();
-
- const [isOpen, setIsOpen] = useState(false);
- const isOwner = document.userId === session?.user?.id;
- const isCurrentTeamDocument = team && document.team?.url === team.url;
-
- const isDisabled =
- (!isOwner && !isCurrentTeamDocument) ||
- document.status !== 'PENDING' ||
- !recipients.some((r) => r.signingStatus === SigningStatus.NOT_SIGNED);
-
- const { mutateAsync: resendDocument } = trpcReact.document.resendDocument.useMutation();
-
- const form = useForm({
- resolver: zodResolver(ZResendDocumentFormSchema),
- defaultValues: {
- recipients: [],
- },
- });
-
- const {
- handleSubmit,
- formState: { isSubmitting },
- } = form;
-
- const onFormSubmit = async ({ recipients }: TResendDocumentFormSchema) => {
- try {
- await resendDocument({ documentId: document.id, recipients });
-
- toast({
- title: _(msg`Document re-sent`),
- description: _(msg`Your document has been re-sent successfully.`),
- duration: 5000,
- });
-
- setIsOpen(false);
- } catch (err) {
- toast({
- title: _(msg`Something went wrong`),
- description: _(msg`This document could not be re-sent at this time. Please try again.`),
- variant: 'destructive',
- duration: 7500,
- });
- }
- };
-
- return (
-
-
- e.preventDefault()}>
-
- Resend
-
-
-
-
-
-
-
- Who do you want to remind?
-
-
-
-
-
-
- (
- <>
- {recipients.map((recipient) => (
-
-
-
- {recipient.email}
-
-
-
-
- checked
- ? onChange([...value, recipient.id])
- : onChange(value.filter((v) => v !== recipient.id))
- }
- />
-
-
- ))}
- >
- )}
- />
-
-
-
-
-
-
-
- Cancel
-
-
-
-
- Send reminder
-
-
-
-
-
- );
-};
diff --git a/apps/web/src/app/(dashboard)/documents/data-table-action-button.tsx b/apps/web/src/app/(dashboard)/documents/data-table-action-button.tsx
deleted file mode 100644
index 1194dfd01..000000000
--- a/apps/web/src/app/(dashboard)/documents/data-table-action-button.tsx
+++ /dev/null
@@ -1,146 +0,0 @@
-'use client';
-
-import Link from 'next/link';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { CheckCircle, Download, Edit, EyeIcon, Pencil } from 'lucide-react';
-import { useSession } from 'next-auth/react';
-import { match } from 'ts-pattern';
-
-import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
-import { formatDocumentsPath } from '@documenso/lib/utils/teams';
-import type { Document, Recipient, Team, User } from '@documenso/prisma/client';
-import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
-import { trpc as trpcClient } from '@documenso/trpc/client';
-import { Button } from '@documenso/ui/primitives/button';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-export type DataTableActionButtonProps = {
- row: Document & {
- user: Pick;
- recipients: Recipient[];
- team: Pick | null;
- };
- team?: Pick;
-};
-
-export const DataTableActionButton = ({ row, team }: DataTableActionButtonProps) => {
- const { data: session } = useSession();
- const { toast } = useToast();
- const { _ } = useLingui();
-
- if (!session) {
- return null;
- }
-
- const recipient = row.recipients.find((recipient) => recipient.email === session.user.email);
-
- const isOwner = row.user.id === session.user.id;
- const isRecipient = !!recipient;
- const isDraft = row.status === DocumentStatus.DRAFT;
- const isPending = row.status === DocumentStatus.PENDING;
- const isComplete = row.status === DocumentStatus.COMPLETED;
- const isSigned = recipient?.signingStatus === SigningStatus.SIGNED;
- const role = recipient?.role;
- const isCurrentTeamDocument = team && row.team?.url === team.url;
-
- const documentsPath = formatDocumentsPath(team?.url);
-
- const onDownloadClick = async () => {
- try {
- const document = !recipient
- ? await trpcClient.document.getDocumentById.query(
- {
- documentId: row.id,
- },
- {
- context: {
- teamId: team?.id?.toString(),
- },
- },
- )
- : await trpcClient.document.getDocumentByToken.query({
- token: recipient.token,
- });
-
- const documentData = document?.documentData;
-
- if (!documentData) {
- throw Error('No document available');
- }
-
- await downloadPDF({ documentData, fileName: row.title });
- } catch (err) {
- toast({
- title: _(msg`Something went wrong`),
- description: _(msg`An error occurred while downloading your document.`),
- variant: 'destructive',
- });
- }
- };
-
- // TODO: Consider if want to keep this logic for hiding viewing for CC'ers
- if (recipient?.role === RecipientRole.CC && isComplete === false) {
- return null;
- }
-
- return match({
- isOwner,
- isRecipient,
- isDraft,
- isPending,
- isComplete,
- isSigned,
- isCurrentTeamDocument,
- })
- .with(
- isOwner ? { isDraft: true, isOwner: true } : { isDraft: true, isCurrentTeamDocument: true },
- () => (
-
-
-
- Edit
-
-
- ),
- )
- .with({ isRecipient: true, isPending: true, isSigned: false }, () => (
-
-
- {match(role)
- .with(RecipientRole.SIGNER, () => (
- <>
-
- Sign
- >
- ))
- .with(RecipientRole.APPROVER, () => (
- <>
-
- Approve
- >
- ))
- .otherwise(() => (
- <>
-
- View
- >
- ))}
-
-
- ))
- .with({ isPending: true, isSigned: true }, () => (
-
-
- View
-
- ))
- .with({ isComplete: true }, () => (
-
-
- Download
-
- ))
- .otherwise(() =>
);
-};
diff --git a/apps/web/src/app/(dashboard)/documents/data-table-action-dropdown.tsx b/apps/web/src/app/(dashboard)/documents/data-table-action-dropdown.tsx
deleted file mode 100644
index 567d8dcd8..000000000
--- a/apps/web/src/app/(dashboard)/documents/data-table-action-dropdown.tsx
+++ /dev/null
@@ -1,244 +0,0 @@
-'use client';
-
-import { useState } from 'react';
-
-import Link from 'next/link';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import {
- CheckCircle,
- Copy,
- Download,
- Edit,
- EyeIcon,
- Loader,
- MoreHorizontal,
- MoveRight,
- Pencil,
- Share,
- Trash2,
-} from 'lucide-react';
-import { useSession } from 'next-auth/react';
-
-import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
-import { formatDocumentsPath } from '@documenso/lib/utils/teams';
-import type { Document, Recipient, Team, User } from '@documenso/prisma/client';
-import { DocumentStatus, RecipientRole } from '@documenso/prisma/client';
-import { trpc as trpcClient } from '@documenso/trpc/client';
-import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button';
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuLabel,
- DropdownMenuTrigger,
-} from '@documenso/ui/primitives/dropdown-menu';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-import { DocumentRecipientLinkCopyDialog } from '~/components/document/document-recipient-link-copy-dialog';
-
-import { ResendDocumentActionItem } from './_action-items/resend-document';
-import { DeleteDocumentDialog } from './delete-document-dialog';
-import { DuplicateDocumentDialog } from './duplicate-document-dialog';
-import { MoveDocumentDialog } from './move-document-dialog';
-
-export type DataTableActionDropdownProps = {
- row: Document & {
- user: Pick;
- recipients: Recipient[];
- team: Pick | null;
- };
- team?: Pick & { teamEmail?: string };
-};
-
-export const DataTableActionDropdown = ({ row, team }: DataTableActionDropdownProps) => {
- const { data: session } = useSession();
- const { toast } = useToast();
- const { _ } = useLingui();
-
- const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false);
- const [isDuplicateDialogOpen, setDuplicateDialogOpen] = useState(false);
- const [isMoveDialogOpen, setMoveDialogOpen] = useState(false);
-
- if (!session) {
- return null;
- }
-
- const recipient = row.recipients.find((recipient) => recipient.email === session.user.email);
-
- const isOwner = row.user.id === session.user.id;
- // const isRecipient = !!recipient;
- const isDraft = row.status === DocumentStatus.DRAFT;
- const isPending = row.status === DocumentStatus.PENDING;
- const isComplete = row.status === DocumentStatus.COMPLETED;
- // const isSigned = recipient?.signingStatus === SigningStatus.SIGNED;
- const isCurrentTeamDocument = team && row.team?.url === team.url;
- const canManageDocument = Boolean(isOwner || isCurrentTeamDocument);
-
- const documentsPath = formatDocumentsPath(team?.url);
-
- const onDownloadClick = async () => {
- try {
- const document = !recipient
- ? await trpcClient.document.getDocumentById.query({
- documentId: row.id,
- })
- : await trpcClient.document.getDocumentByToken.query({
- token: recipient.token,
- });
-
- const documentData = document?.documentData;
-
- if (!documentData) {
- return;
- }
-
- await downloadPDF({ documentData, fileName: row.title });
- } catch (err) {
- toast({
- title: _(msg`Something went wrong`),
- description: _(msg`An error occurred while downloading your document.`),
- variant: 'destructive',
- });
- }
- };
-
- const nonSignedRecipients = row.recipients.filter((item) => item.signingStatus !== 'SIGNED');
-
- return (
-
-
-
-
-
-
-
- Action
-
-
- {!isDraft && recipient && recipient?.role !== RecipientRole.CC && (
-
-
- {recipient?.role === RecipientRole.VIEWER && (
- <>
-
- View
- >
- )}
-
- {recipient?.role === RecipientRole.SIGNER && (
- <>
-
- Sign
- >
- )}
-
- {recipient?.role === RecipientRole.APPROVER && (
- <>
-
- Approve
- >
- )}
-
-
- )}
-
-
-
-
- Edit
-
-
-
-
-
- Download
-
-
- setDuplicateDialogOpen(true)}>
-
- Duplicate
-
-
- {/* We don't want to allow teams moving documents across at the moment. */}
- {!team && (
- setMoveDialogOpen(true)}>
-
- Move to Team
-
- )}
-
- {/* No point displaying this if there's no functionality. */}
- {/*
-
- Void
- */}
-
- setDeleteDialogOpen(true)}
- disabled={Boolean(!canManageDocument && team?.teamEmail)}
- >
-
- {canManageDocument ? _(msg`Delete`) : _(msg`Hide`)}
-
-
-
- Share
-
-
- {canManageDocument && (
- e.preventDefault()}>
-
-
- Signing Links
-
-
- }
- />
- )}
-
-
-
- (
- e.preventDefault()}>
-
- {loading ? : }
- Share Signing Card
-
-
- )}
- />
-
-
-
-
-
-
-
-
- );
-};
diff --git a/apps/web/src/app/(dashboard)/documents/data-table-sender-filter.tsx b/apps/web/src/app/(dashboard)/documents/data-table-sender-filter.tsx
deleted file mode 100644
index 8003c20b8..000000000
--- a/apps/web/src/app/(dashboard)/documents/data-table-sender-filter.tsx
+++ /dev/null
@@ -1,70 +0,0 @@
-'use client';
-
-import { usePathname, useRouter, useSearchParams } from 'next/navigation';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-
-import { useIsMounted } from '@documenso/lib/client-only/hooks/use-is-mounted';
-import { parseToIntegerArray } from '@documenso/lib/utils/params';
-import { trpc } from '@documenso/trpc/react';
-import { MultiSelectCombobox } from '@documenso/ui/primitives/multi-select-combobox';
-
-type DataTableSenderFilterProps = {
- teamId: number;
-};
-
-export const DataTableSenderFilter = ({ teamId }: DataTableSenderFilterProps) => {
- const { _ } = useLingui();
-
- const pathname = usePathname();
- const searchParams = useSearchParams();
- const router = useRouter();
-
- const isMounted = useIsMounted();
-
- const senderIds = parseToIntegerArray(searchParams?.get('senderIds') ?? '');
-
- const { data, isLoading } = trpc.team.getTeamMembers.useQuery({
- teamId,
- });
-
- const comboBoxOptions = (data ?? []).map((member) => ({
- label: member.user.name ?? member.user.email,
- value: member.user.id,
- }));
-
- const onChange = (newSenderIds: number[]) => {
- if (!pathname) {
- return;
- }
-
- const params = new URLSearchParams(searchParams?.toString());
-
- params.set('senderIds', newSenderIds.join(','));
-
- if (newSenderIds.length === 0) {
- params.delete('senderIds');
- }
-
- router.push(`${pathname}?${params.toString()}`, { scroll: false });
- };
-
- return (
-
-
- Sender: All
-
-
- }
- enableClearAllButton={true}
- inputPlaceholder={msg`Search`}
- loading={!isMounted || isLoading}
- options={comboBoxOptions}
- selectedValues={senderIds}
- onChange={onChange}
- />
- );
-};
diff --git a/apps/web/src/app/(dashboard)/documents/data-table-title.tsx b/apps/web/src/app/(dashboard)/documents/data-table-title.tsx
deleted file mode 100644
index 39af81195..000000000
--- a/apps/web/src/app/(dashboard)/documents/data-table-title.tsx
+++ /dev/null
@@ -1,63 +0,0 @@
-'use client';
-
-import Link from 'next/link';
-
-import { useSession } from 'next-auth/react';
-import { match } from 'ts-pattern';
-
-import { formatDocumentsPath } from '@documenso/lib/utils/teams';
-import type { Document, Recipient, Team, User } from '@documenso/prisma/client';
-
-export type DataTableTitleProps = {
- row: Document & {
- user: Pick;
- team: Pick | null;
- recipients: Recipient[];
- };
- teamUrl?: string;
-};
-
-export const DataTableTitle = ({ row, teamUrl }: DataTableTitleProps) => {
- const { data: session } = useSession();
-
- if (!session) {
- return null;
- }
-
- const recipient = row.recipients.find((recipient) => recipient.email === session.user.email);
-
- const isOwner = row.user.id === session.user.id;
- const isRecipient = !!recipient;
- const isCurrentTeamDocument = teamUrl && row.team?.url === teamUrl;
-
- const documentsPath = formatDocumentsPath(isCurrentTeamDocument ? teamUrl : undefined);
-
- return match({
- isOwner,
- isRecipient,
- isCurrentTeamDocument,
- })
- .with({ isOwner: true }, { isCurrentTeamDocument: true }, () => (
-
- {row.title}
-
- ))
- .with({ isRecipient: true }, () => (
-
- {row.title}
-
- ))
- .otherwise(() => (
-
- {row.title}
-
- ));
-};
diff --git a/apps/web/src/app/(dashboard)/documents/data-table.tsx b/apps/web/src/app/(dashboard)/documents/data-table.tsx
deleted file mode 100644
index 4051b7d1d..000000000
--- a/apps/web/src/app/(dashboard)/documents/data-table.tsx
+++ /dev/null
@@ -1,127 +0,0 @@
-'use client';
-
-import { useMemo, useTransition } from 'react';
-
-import { msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { Loader } from 'lucide-react';
-import { DateTime } from 'luxon';
-import { useSession } from 'next-auth/react';
-
-import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
-import type { Team } from '@documenso/prisma/client';
-import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
-import type { TFindDocumentsResponse } from '@documenso/trpc/server/document-router/schema';
-import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
-import { DataTable } from '@documenso/ui/primitives/data-table';
-import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
-
-import { StackAvatarsWithTooltip } from '~/components/(dashboard)/avatar/stack-avatars-with-tooltip';
-import { DocumentStatus } from '~/components/formatter/document-status';
-
-import { DataTableActionButton } from './data-table-action-button';
-import { DataTableActionDropdown } from './data-table-action-dropdown';
-import { DataTableTitle } from './data-table-title';
-
-export type DocumentsDataTableProps = {
- results: TFindDocumentsResponse;
- showSenderColumn?: boolean;
- team?: Pick & { teamEmail?: string };
-};
-
-export const DocumentsDataTable = ({
- results,
- showSenderColumn,
- team,
-}: DocumentsDataTableProps) => {
- const { _, i18n } = useLingui();
-
- const { data: session } = useSession();
-
- const [isPending, startTransition] = useTransition();
-
- const updateSearchParams = useUpdateSearchParams();
-
- const columns = useMemo(() => {
- return [
- {
- header: _(msg`Created`),
- accessorKey: 'createdAt',
- cell: ({ row }) =>
- i18n.date(row.original.createdAt, { ...DateTime.DATETIME_SHORT, hourCycle: 'h12' }),
- },
- {
- header: _(msg`Title`),
- cell: ({ row }) => ,
- },
- {
- id: 'sender',
- header: _(msg`Sender`),
- cell: ({ row }) => row.original.user.name ?? row.original.user.email,
- },
- {
- header: _(msg`Recipient`),
- accessorKey: 'recipient',
- cell: ({ row }) => (
-
- ),
- },
- {
- header: _(msg`Status`),
- accessorKey: 'status',
- cell: ({ row }) => ,
- size: 140,
- },
- {
- header: _(msg`Actions`),
- cell: ({ row }) =>
- (!row.original.deletedAt || row.original.status === ExtendedDocumentStatus.COMPLETED) && (
-
-
-
-
- ),
- },
- ] satisfies DataTableColumnDef<(typeof results)['data'][number]>[];
- }, [team]);
-
- const onPaginationChange = (page: number, perPage: number) => {
- startTransition(() => {
- updateSearchParams({
- page,
- perPage,
- });
- });
- };
-
- if (!session) {
- return null;
- }
-
- return (
-
-
- {(table) => }
-
-
- {isPending && (
-
-
-
- )}
-
- );
-};
diff --git a/apps/web/src/app/(dashboard)/documents/delete-document-dialog.tsx b/apps/web/src/app/(dashboard)/documents/delete-document-dialog.tsx
deleted file mode 100644
index 50c0b83eb..000000000
--- a/apps/web/src/app/(dashboard)/documents/delete-document-dialog.tsx
+++ /dev/null
@@ -1,207 +0,0 @@
-import { useEffect, useState } from 'react';
-
-import { useRouter } from 'next/navigation';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { match } from 'ts-pattern';
-
-import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
-import { DocumentStatus } from '@documenso/prisma/client';
-import { trpc as trpcReact } from '@documenso/trpc/react';
-import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
-import { Button } from '@documenso/ui/primitives/button';
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from '@documenso/ui/primitives/dialog';
-import { Input } from '@documenso/ui/primitives/input';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-type DeleteDocumentDialogProps = {
- id: number;
- open: boolean;
- onOpenChange: (_open: boolean) => void;
- status: DocumentStatus;
- documentTitle: string;
- teamId?: number;
- canManageDocument: boolean;
-};
-
-export const DeleteDocumentDialog = ({
- id,
- open,
- onOpenChange,
- status,
- documentTitle,
- canManageDocument,
-}: DeleteDocumentDialogProps) => {
- const router = useRouter();
-
- const { toast } = useToast();
- const { refreshLimits } = useLimits();
- const { _ } = useLingui();
-
- const deleteMessage = msg`delete`;
-
- const [inputValue, setInputValue] = useState('');
- const [isDeleteEnabled, setIsDeleteEnabled] = useState(status === DocumentStatus.DRAFT);
-
- const { mutateAsync: deleteDocument, isPending } = trpcReact.document.deleteDocument.useMutation({
- onSuccess: () => {
- router.refresh();
- void refreshLimits();
-
- toast({
- title: _(msg`Document deleted`),
- description: _(msg`"${documentTitle}" has been successfully deleted`),
- duration: 5000,
- });
-
- onOpenChange(false);
- },
- });
-
- useEffect(() => {
- if (open) {
- setInputValue('');
- setIsDeleteEnabled(status === DocumentStatus.DRAFT);
- }
- }, [open, status]);
-
- const onDelete = async () => {
- try {
- await deleteDocument({ documentId: id });
- } catch {
- toast({
- title: _(msg`Something went wrong`),
- description: _(msg`This document could not be deleted at this time. Please try again.`),
- variant: 'destructive',
- duration: 7500,
- });
- }
- };
-
- const onInputChange = (event: React.ChangeEvent) => {
- setInputValue(event.target.value);
- setIsDeleteEnabled(event.target.value === _(deleteMessage));
- };
-
- return (
- !isPending && onOpenChange(value)}>
-
-
-
- Are you sure?
-
-
-
- {canManageDocument ? (
-
- You are about to delete "{documentTitle}"
-
- ) : (
-
- You are about to hide "{documentTitle}"
-
- )}
-
-
-
- {canManageDocument ? (
-
- {match(status)
- .with(DocumentStatus.DRAFT, () => (
-
-
- Please note that this action is irreversible . Once confirmed,
- this document will be permanently deleted.
-
-
- ))
- .with(DocumentStatus.PENDING, () => (
-
-
-
- Please note that this action is irreversible .
-
-
-
-
- Once confirmed, the following will occur:
-
-
-
-
- Document will be permanently deleted
-
-
- Document signing process will be cancelled
-
-
- All inserted signatures will be voided
-
-
- All recipients will be notified
-
-
-
- ))
- .with(DocumentStatus.COMPLETED, () => (
-
-
- By deleting this document, the following will occur:
-
-
-
-
- The document will be hidden from your account
-
-
- Recipients will still retain their copy of the document
-
-
-
- ))
- .exhaustive()}
-
- ) : (
-
-
- Please contact support if you would like to revert this action.
-
-
- )}
-
- {status !== DocumentStatus.DRAFT && canManageDocument && (
-
- )}
-
-
- onOpenChange(false)}>
- Cancel
-
-
-
- {canManageDocument ? _(msg`Delete`) : _(msg`Hide`)}
-
-
-
-
- );
-};
diff --git a/apps/web/src/app/(dashboard)/documents/documents-page-view.tsx b/apps/web/src/app/(dashboard)/documents/documents-page-view.tsx
deleted file mode 100644
index 65465c27c..000000000
--- a/apps/web/src/app/(dashboard)/documents/documents-page-view.tsx
+++ /dev/null
@@ -1,174 +0,0 @@
-import Link from 'next/link';
-
-import { Trans } from '@lingui/macro';
-
-import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
-import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
-import { findDocuments } from '@documenso/lib/server-only/document/find-documents';
-import type { PeriodSelectorValue } from '@documenso/lib/server-only/document/find-documents';
-import type { GetStatsInput } from '@documenso/lib/server-only/document/get-stats';
-import { getStats } from '@documenso/lib/server-only/document/get-stats';
-import { parseToIntegerArray } from '@documenso/lib/utils/params';
-import { formatDocumentsPath } from '@documenso/lib/utils/teams';
-import type { Team, TeamEmail, TeamMemberRole } from '@documenso/prisma/client';
-import { isExtendedDocumentStatus } from '@documenso/prisma/guards/is-extended-document-status';
-import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
-import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
-import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
-
-import { DocumentSearch } from '~/components/(dashboard)/document-search/document-search';
-import { PeriodSelector } from '~/components/(dashboard)/period-selector/period-selector';
-import { isPeriodSelectorValue } from '~/components/(dashboard)/period-selector/types';
-import { DocumentStatus } from '~/components/formatter/document-status';
-
-import { DocumentsDataTable } from './data-table';
-import { DataTableSenderFilter } from './data-table-sender-filter';
-import { EmptyDocumentState } from './empty-state';
-import { UploadDocument } from './upload-document';
-
-export interface DocumentsPageViewProps {
- searchParams?: {
- status?: ExtendedDocumentStatus;
- period?: PeriodSelectorValue;
- page?: string;
- perPage?: string;
- senderIds?: string;
- search?: string;
- };
- team?: Team & { teamEmail?: TeamEmail | null } & { currentTeamMember?: { role: TeamMemberRole } };
-}
-
-export const DocumentsPageView = async ({ searchParams = {}, team }: DocumentsPageViewProps) => {
- const { user } = await getRequiredServerComponentSession();
-
- const status = isExtendedDocumentStatus(searchParams.status) ? searchParams.status : 'ALL';
- const period = isPeriodSelectorValue(searchParams.period) ? searchParams.period : '';
- const page = Number(searchParams.page) || 1;
- const perPage = Number(searchParams.perPage) || 20;
- const senderIds = parseToIntegerArray(searchParams.senderIds ?? '');
- const search = searchParams.search || '';
- const currentTeam = team
- ? { id: team.id, url: team.url, teamEmail: team.teamEmail?.email }
- : undefined;
- const currentTeamMemberRole = team?.currentTeamMember?.role;
-
- const getStatOptions: GetStatsInput = {
- user,
- period,
- search,
- };
-
- if (team) {
- getStatOptions.team = {
- teamId: team.id,
- teamEmail: team.teamEmail?.email,
- senderIds,
- currentTeamMemberRole,
- currentUserEmail: user.email,
- userId: user.id,
- };
- }
-
- const stats = await getStats(getStatOptions);
-
- const results = await findDocuments({
- userId: user.id,
- teamId: team?.id,
- status,
- orderBy: {
- column: 'createdAt',
- direction: 'desc',
- },
- page,
- perPage,
- period,
- senderIds,
- query: search,
- });
-
- const getTabHref = (value: typeof status) => {
- const params = new URLSearchParams(searchParams);
-
- params.set('status', value);
-
- if (params.has('page')) {
- params.delete('page');
- }
-
- return `${formatDocumentsPath(team?.url)}?${params.toString()}`;
- };
-
- return (
-
-
-
-
-
- {team && (
-
- {team.avatarImageId && (
-
- )}
-
- {team.name.slice(0, 1)}
-
-
- )}
-
-
- Documents
-
-
-
-
-
-
- {[
- ExtendedDocumentStatus.INBOX,
- ExtendedDocumentStatus.PENDING,
- ExtendedDocumentStatus.COMPLETED,
- ExtendedDocumentStatus.DRAFT,
- ExtendedDocumentStatus.ALL,
- ].map((value) => (
-
-
-
-
- {value !== ExtendedDocumentStatus.ALL && (
- {stats[value]}
- )}
-
-
- ))}
-
-
-
- {team &&
}
-
-
-
-
-
-
-
-
-
- {results.count > 0 && (
-
- )}
- {results.count === 0 && }
-
-
- );
-};
diff --git a/apps/web/src/app/(dashboard)/documents/duplicate-document-dialog.tsx b/apps/web/src/app/(dashboard)/documents/duplicate-document-dialog.tsx
deleted file mode 100644
index b124da08b..000000000
--- a/apps/web/src/app/(dashboard)/documents/duplicate-document-dialog.tsx
+++ /dev/null
@@ -1,124 +0,0 @@
-import { useRouter } from 'next/navigation';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-
-import { formatDocumentsPath } from '@documenso/lib/utils/teams';
-import type { Team } from '@documenso/prisma/client';
-import { trpc as trpcReact } from '@documenso/trpc/react';
-import { Button } from '@documenso/ui/primitives/button';
-import {
- Dialog,
- DialogContent,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from '@documenso/ui/primitives/dialog';
-import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-type DuplicateDocumentDialogProps = {
- id: number;
- open: boolean;
- onOpenChange: (_open: boolean) => void;
- team?: Pick;
-};
-
-export const DuplicateDocumentDialog = ({
- id,
- open,
- onOpenChange,
- team,
-}: DuplicateDocumentDialogProps) => {
- const router = useRouter();
-
- const { toast } = useToast();
- const { _ } = useLingui();
-
- const { data: document, isLoading } = trpcReact.document.getDocumentById.useQuery({
- documentId: id,
- });
-
- const documentData = document?.documentData
- ? {
- ...document.documentData,
- data: document.documentData.initialData,
- }
- : undefined;
-
- const documentsPath = formatDocumentsPath(team?.url);
-
- const { mutateAsync: duplicateDocument, isPending: isDuplicateLoading } =
- trpcReact.document.duplicateDocument.useMutation({
- onSuccess: ({ documentId }) => {
- router.push(`${documentsPath}/${documentId}/edit`);
-
- toast({
- title: _(msg`Document Duplicated`),
- description: _(msg`Your document has been successfully duplicated.`),
- duration: 5000,
- });
-
- onOpenChange(false);
- },
- });
-
- const onDuplicate = async () => {
- try {
- await duplicateDocument({ documentId: id });
- } catch {
- toast({
- title: _(msg`Something went wrong`),
- description: _(msg`This document could not be duplicated at this time. Please try again.`),
- variant: 'destructive',
- duration: 7500,
- });
- }
- };
-
- return (
- !isLoading && onOpenChange(value)}>
-
-
-
- Duplicate
-
-
- {!documentData || isLoading ? (
-
-
- Loading Document...
-
-
- ) : (
-
-
-
- )}
-
-
-
- onOpenChange(false)}
- className="flex-1"
- >
- Cancel
-
-
-
- Duplicate
-
-
-
-
-
- );
-};
diff --git a/apps/web/src/app/(dashboard)/documents/empty-state.tsx b/apps/web/src/app/(dashboard)/documents/empty-state.tsx
deleted file mode 100644
index f97527d0b..000000000
--- a/apps/web/src/app/(dashboard)/documents/empty-state.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-import { msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { Bird, CheckCircle2 } from 'lucide-react';
-import { match } from 'ts-pattern';
-
-import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
-
-export type EmptyDocumentProps = { status: ExtendedDocumentStatus };
-
-export const EmptyDocumentState = ({ status }: EmptyDocumentProps) => {
- const { _ } = useLingui();
-
- const {
- title,
- message,
- icon: Icon,
- } = match(status)
- .with(ExtendedDocumentStatus.COMPLETED, () => ({
- title: msg`Nothing to do`,
- message: msg`There are no completed documents yet. Documents that you have created or received will appear here once completed.`,
- icon: CheckCircle2,
- }))
- .with(ExtendedDocumentStatus.DRAFT, () => ({
- title: msg`No active drafts`,
- message: msg`There are no active drafts at the current moment. You can upload a document to start drafting.`,
- icon: CheckCircle2,
- }))
- .with(ExtendedDocumentStatus.ALL, () => ({
- title: msg`We're all empty`,
- message: msg`You have not yet created or received any documents. To create a document please upload one.`,
- icon: Bird,
- }))
- .otherwise(() => ({
- title: msg`Nothing to do`,
- message: msg`All documents have been processed. Any new documents that are sent or received will show here.`,
- icon: CheckCircle2,
- }));
-
- return (
-
-
-
-
-
{_(title)}
-
-
{_(message)}
-
-
- );
-};
diff --git a/apps/web/src/app/(dashboard)/documents/move-document-dialog.tsx b/apps/web/src/app/(dashboard)/documents/move-document-dialog.tsx
deleted file mode 100644
index abdd9d817..000000000
--- a/apps/web/src/app/(dashboard)/documents/move-document-dialog.tsx
+++ /dev/null
@@ -1,129 +0,0 @@
-import { useState } from 'react';
-
-import { useRouter } from 'next/navigation';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-
-import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
-import { trpc } from '@documenso/trpc/react';
-import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
-import { Button } from '@documenso/ui/primitives/button';
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from '@documenso/ui/primitives/dialog';
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from '@documenso/ui/primitives/select';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-type MoveDocumentDialogProps = {
- documentId: number;
- open: boolean;
- onOpenChange: (_open: boolean) => void;
-};
-
-export const MoveDocumentDialog = ({ documentId, open, onOpenChange }: MoveDocumentDialogProps) => {
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const router = useRouter();
-
- const [selectedTeamId, setSelectedTeamId] = useState(null);
-
- const { data: teams, isLoading: isLoadingTeams } = trpc.team.getTeams.useQuery();
-
- const { mutateAsync: moveDocument, isPending } = trpc.document.moveDocumentToTeam.useMutation({
- onSuccess: () => {
- router.refresh();
- toast({
- title: _(msg`Document moved`),
- description: _(msg`The document has been successfully moved to the selected team.`),
- duration: 5000,
- });
- onOpenChange(false);
- },
- onError: (error) => {
- toast({
- title: _(msg`Error`),
- description: error.message || _(msg`An error occurred while moving the document.`),
- variant: 'destructive',
- duration: 7500,
- });
- },
- });
-
- const onMove = async () => {
- if (!selectedTeamId) {
- return;
- }
-
- await moveDocument({ documentId, teamId: selectedTeamId });
- };
-
- return (
-
-
-
-
- Move Document to Team
-
-
- Select a team to move this document to. This action cannot be undone.
-
-
-
- setSelectedTeamId(Number(value))}>
-
-
-
-
- {isLoadingTeams ? (
-
- Loading teams...
-
- ) : (
- teams?.map((team) => (
-
-
-
- {team.avatarImageId && (
-
- )}
-
-
- {team.name.slice(0, 1).toUpperCase()}
-
-
-
-
{team.name}
-
-
- ))
- )}
-
-
-
-
- onOpenChange(false)}>
- Cancel
-
-
- {isPending ? Moving... : Move }
-
-
-
-
- );
-};
diff --git a/apps/web/src/app/(dashboard)/documents/page.tsx b/apps/web/src/app/(dashboard)/documents/page.tsx
deleted file mode 100644
index 41cbf3df9..000000000
--- a/apps/web/src/app/(dashboard)/documents/page.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import type { Metadata } from 'next';
-
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
-
-import type { DocumentsPageViewProps } from './documents-page-view';
-import { DocumentsPageView } from './documents-page-view';
-import { UpcomingProfileClaimTeaser } from './upcoming-profile-claim-teaser';
-
-export type DocumentsPageProps = {
- searchParams?: DocumentsPageViewProps['searchParams'];
-};
-
-export const metadata: Metadata = {
- title: 'Documents',
-};
-
-export default async function DocumentsPage({ searchParams = {} }: DocumentsPageProps) {
- await setupI18nSSR();
-
- const { user } = await getRequiredServerComponentSession();
-
- return (
- <>
-
-
- >
- );
-}
diff --git a/apps/web/src/app/(dashboard)/documents/upcoming-profile-claim-teaser.tsx b/apps/web/src/app/(dashboard)/documents/upcoming-profile-claim-teaser.tsx
deleted file mode 100644
index dbb9cd52c..000000000
--- a/apps/web/src/app/(dashboard)/documents/upcoming-profile-claim-teaser.tsx
+++ /dev/null
@@ -1,59 +0,0 @@
-'use client';
-
-import { useCallback, useEffect, useState } from 'react';
-
-import { msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-
-import type { User } from '@documenso/prisma/client';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-import { ClaimPublicProfileDialogForm } from '~/components/forms/public-profile-claim-dialog';
-
-export type UpcomingProfileClaimTeaserProps = {
- user: User;
-};
-
-export const UpcomingProfileClaimTeaser = ({ user }: UpcomingProfileClaimTeaserProps) => {
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const [open, setOpen] = useState(false);
- const [claimed, setClaimed] = useState(false);
-
- const onOpenChange = useCallback(
- (open: boolean) => {
- if (!open && !claimed) {
- toast({
- title: _(msg`Claim your profile later`),
- description: _(
- msg`You can claim your profile later on by going to your profile settings!`,
- ),
- });
- }
-
- setOpen(open);
- localStorage.setItem('app.hasShownProfileClaimDialog', 'true');
- },
- // eslint-disable-next-line react-hooks/exhaustive-deps
- [claimed, toast],
- );
-
- useEffect(() => {
- const hasShownProfileClaimDialog =
- localStorage.getItem('app.hasShownProfileClaimDialog') === 'true';
-
- if (!user.url && !hasShownProfileClaimDialog) {
- onOpenChange(true);
- }
- }, [onOpenChange, user.url]);
-
- return (
- setClaimed(true)}
- user={user}
- />
- );
-};
diff --git a/apps/web/src/app/(dashboard)/documents/upload-document.tsx b/apps/web/src/app/(dashboard)/documents/upload-document.tsx
deleted file mode 100644
index c8fc800a0..000000000
--- a/apps/web/src/app/(dashboard)/documents/upload-document.tsx
+++ /dev/null
@@ -1,159 +0,0 @@
-'use client';
-
-import { useMemo, useState } from 'react';
-
-import { useRouter } from 'next/navigation';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { Loader } from 'lucide-react';
-import { useSession } from 'next-auth/react';
-import { match } from 'ts-pattern';
-
-import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
-import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
-import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
-import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
-import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
-import { createDocumentData } from '@documenso/lib/server-only/document-data/create-document-data';
-import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
-import { formatDocumentsPath } from '@documenso/lib/utils/teams';
-import { trpc } from '@documenso/trpc/react';
-import { cn } from '@documenso/ui/lib/utils';
-import { DocumentDropzone } from '@documenso/ui/primitives/document-dropzone';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-export type UploadDocumentProps = {
- className?: string;
- team?: {
- id: number;
- url: string;
- };
-};
-
-export const UploadDocument = ({ className, team }: UploadDocumentProps) => {
- const router = useRouter();
- const analytics = useAnalytics();
- const userTimezone =
- TIME_ZONES.find((timezone) => timezone === Intl.DateTimeFormat().resolvedOptions().timeZone) ??
- DEFAULT_DOCUMENT_TIME_ZONE;
-
- const { data: session } = useSession();
-
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const { quota, remaining, refreshLimits } = useLimits();
-
- const [isLoading, setIsLoading] = useState(false);
-
- const { mutateAsync: createDocument } = trpc.document.createDocument.useMutation();
-
- const disabledMessage = useMemo(() => {
- if (remaining.documents === 0) {
- return team
- ? msg`Document upload disabled due to unpaid invoices`
- : msg`You have reached your document limit.`;
- }
-
- if (!session?.user.emailVerified) {
- return msg`Verify your email to upload documents.`;
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [remaining.documents, session?.user.emailVerified, team]);
-
- const onFileDrop = async (file: File) => {
- try {
- setIsLoading(true);
-
- const { type, data } = await putPdfFile(file);
-
- const { id: documentDataId } = await createDocumentData({
- type,
- data,
- });
-
- const { id } = await createDocument({
- title: file.name,
- documentDataId,
- timezone: userTimezone,
- });
-
- void refreshLimits();
-
- toast({
- title: _(msg`Document uploaded`),
- description: _(msg`Your document has been uploaded successfully.`),
- duration: 5000,
- });
-
- analytics.capture('App: Document Uploaded', {
- userId: session?.user.id,
- documentId: id,
- timestamp: new Date().toISOString(),
- });
-
- router.push(`${formatDocumentsPath(team?.url)}/${id}/edit`);
- } catch (err) {
- const error = AppError.parseError(err);
-
- console.error(err);
-
- const errorMessage = match(error.code)
- .with('INVALID_DOCUMENT_FILE', () => msg`You cannot upload encrypted PDFs`)
- .with(
- AppErrorCode.LIMIT_EXCEEDED,
- () => msg`You have reached your document limit for this month. Please upgrade your plan.`,
- )
- .otherwise(() => msg`An error occurred while uploading your document.`);
-
- toast({
- title: _(msg`Error`),
- description: _(errorMessage),
- variant: 'destructive',
- duration: 7500,
- });
- } finally {
- setIsLoading(false);
- }
- };
-
- const onFileDropRejected = () => {
- toast({
- title: _(msg`Your document failed to upload.`),
- description: _(msg`File cannot be larger than ${APP_DOCUMENT_UPLOAD_SIZE_LIMIT}MB`),
- duration: 5000,
- variant: 'destructive',
- });
- };
-
- return (
-
-
-
-
- {team?.id === undefined &&
- remaining.documents > 0 &&
- Number.isFinite(remaining.documents) && (
-
-
- {remaining.documents} of {quota.documents} documents remaining this month.
-
-
- )}
-
-
- {isLoading && (
-
-
-
- )}
-
- );
-};
diff --git a/apps/web/src/app/(dashboard)/layout.tsx b/apps/web/src/app/(dashboard)/layout.tsx
deleted file mode 100644
index 90096b0e3..000000000
--- a/apps/web/src/app/(dashboard)/layout.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-import React from 'react';
-
-import { redirect } from 'next/navigation';
-
-import { getServerSession } from 'next-auth';
-
-import { LimitsProvider } from '@documenso/ee/server-only/limits/provider/server';
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-import { NEXT_AUTH_OPTIONS } from '@documenso/lib/next-auth/auth-options';
-import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
-import { getTeams } from '@documenso/lib/server-only/team/get-teams';
-
-import { Banner } from '~/components/(dashboard)/layout/banner';
-import { Header } from '~/components/(dashboard)/layout/header';
-import { VerifyEmailBanner } from '~/components/(dashboard)/layout/verify-email-banner';
-import { RefreshOnFocus } from '~/components/(dashboard)/refresh-on-focus/refresh-on-focus';
-import { NextAuthProvider } from '~/providers/next-auth';
-
-export type AuthenticatedDashboardLayoutProps = {
- children: React.ReactNode;
-};
-
-export default async function AuthenticatedDashboardLayout({
- children,
-}: AuthenticatedDashboardLayoutProps) {
- await setupI18nSSR();
-
- const session = await getServerSession(NEXT_AUTH_OPTIONS);
-
- if (!session) {
- redirect('/signin');
- }
-
- const [{ user }, teams] = await Promise.all([
- getRequiredServerComponentSession(),
- getTeams({ userId: session.user.id }),
- ]);
-
- return (
-
-
- {!user.emailVerified && }
-
-
-
-
-
- {children}
-
-
-
-
- );
-}
diff --git a/apps/web/src/app/(dashboard)/settings/billing/billing-plans.tsx b/apps/web/src/app/(dashboard)/settings/billing/billing-plans.tsx
deleted file mode 100644
index 537a0c97b..000000000
--- a/apps/web/src/app/(dashboard)/settings/billing/billing-plans.tsx
+++ /dev/null
@@ -1,138 +0,0 @@
-'use client';
-
-import { useState } from 'react';
-
-import type { MessageDescriptor } from '@lingui/core';
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { AnimatePresence, motion } from 'framer-motion';
-
-import type { PriceIntervals } from '@documenso/ee/server-only/stripe/get-prices-by-interval';
-import { useIsMounted } from '@documenso/lib/client-only/hooks/use-is-mounted';
-import { toHumanPrice } from '@documenso/lib/universal/stripe/to-human-price';
-import { Button } from '@documenso/ui/primitives/button';
-import { Card, CardContent, CardTitle } from '@documenso/ui/primitives/card';
-import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-import { createCheckout } from './create-checkout.action';
-
-type Interval = keyof PriceIntervals;
-
-const INTERVALS: Interval[] = ['day', 'week', 'month', 'year'];
-
-// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
-const isInterval = (value: unknown): value is Interval => INTERVALS.includes(value as Interval);
-
-const FRIENDLY_INTERVALS: Record = {
- day: msg`Daily`,
- week: msg`Weekly`,
- month: msg`Monthly`,
- year: msg`Yearly`,
-};
-
-const MotionCard = motion(Card);
-
-export type BillingPlansProps = {
- prices: PriceIntervals;
-};
-
-export const BillingPlans = ({ prices }: BillingPlansProps) => {
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const isMounted = useIsMounted();
-
- const [interval, setInterval] = useState('month');
- const [checkoutSessionPriceId, setCheckoutSessionPriceId] = useState(null);
-
- const onSubscribeClick = async (priceId: string) => {
- try {
- setCheckoutSessionPriceId(priceId);
-
- const url = await createCheckout({ priceId });
-
- if (!url) {
- throw new Error('Unable to create session');
- }
-
- window.open(url);
- } catch (_err) {
- toast({
- title: _(msg`Something went wrong`),
- description: _(msg`An error occurred while trying to create a checkout session.`),
- variant: 'destructive',
- });
- } finally {
- setCheckoutSessionPriceId(null);
- }
- };
-
- return (
-
-
isInterval(value) && setInterval(value)}>
-
- {INTERVALS.map(
- (interval) =>
- prices[interval].length > 0 && (
-
- {_(FRIENDLY_INTERVALS[interval])}
-
- ),
- )}
-
-
-
-
-
- {prices[interval].map((price) => (
-
-
- {price.product.name}
-
-
- ${toHumanPrice(price.unit_amount ?? 0)} {price.currency.toUpperCase()}{' '}
- per {interval}
-
-
-
- {price.product.description}
-
-
- {price.product.features && price.product.features.length > 0 && (
-
-
Includes:
-
-
- {price.product.features.map((feature, index) => (
-
- {feature.name}
-
- ))}
-
-
- )}
-
-
-
- void onSubscribeClick(price.id)}
- >
- Subscribe
-
-
-
- ))}
-
-
-
- );
-};
diff --git a/apps/web/src/app/(dashboard)/settings/billing/billing-portal-button.tsx b/apps/web/src/app/(dashboard)/settings/billing/billing-portal-button.tsx
deleted file mode 100644
index c8ab8c132..000000000
--- a/apps/web/src/app/(dashboard)/settings/billing/billing-portal-button.tsx
+++ /dev/null
@@ -1,70 +0,0 @@
-'use client';
-
-import { useState } from 'react';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-
-import { Button } from '@documenso/ui/primitives/button';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-import { createBillingPortal } from './create-billing-portal.action';
-
-export type BillingPortalButtonProps = {
- buttonProps?: React.ComponentProps;
- children?: React.ReactNode;
-};
-
-export const BillingPortalButton = ({ buttonProps, children }: BillingPortalButtonProps) => {
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const [isFetchingPortalUrl, setIsFetchingPortalUrl] = useState(false);
-
- const handleFetchPortalUrl = async () => {
- if (isFetchingPortalUrl) {
- return;
- }
-
- setIsFetchingPortalUrl(true);
-
- try {
- const sessionUrl = await createBillingPortal();
-
- if (!sessionUrl) {
- throw new Error('NO_SESSION');
- }
-
- window.open(sessionUrl, '_blank');
- } catch (e) {
- let description = _(
- msg`We are unable to proceed to the billing portal at this time. Please try again, or contact support.`,
- );
-
- if (e.message === 'CUSTOMER_NOT_FOUND') {
- description = _(
- msg`You do not currently have a customer record, this should not happen. Please contact support for assistance.`,
- );
- }
-
- toast({
- title: _(msg`Something went wrong`),
- description,
- variant: 'destructive',
- duration: 10000,
- });
- }
-
- setIsFetchingPortalUrl(false);
- };
-
- return (
- handleFetchPortalUrl()}
- loading={isFetchingPortalUrl}
- >
- {children || Manage Subscription }
-
- );
-};
diff --git a/apps/web/src/app/(dashboard)/settings/billing/create-billing-portal.action.ts b/apps/web/src/app/(dashboard)/settings/billing/create-billing-portal.action.ts
deleted file mode 100644
index 5435aefb1..000000000
--- a/apps/web/src/app/(dashboard)/settings/billing/create-billing-portal.action.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-'use server';
-
-import { getStripeCustomerByUser } from '@documenso/ee/server-only/stripe/get-customer';
-import { getPortalSession } from '@documenso/ee/server-only/stripe/get-portal-session';
-import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
-import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
-
-export const createBillingPortal = async () => {
- const { user } = await getRequiredServerComponentSession();
-
- const { stripeCustomer } = await getStripeCustomerByUser(user);
-
- return getPortalSession({
- customerId: stripeCustomer.id,
- returnUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/settings/billing`,
- });
-};
diff --git a/apps/web/src/app/(dashboard)/settings/billing/create-checkout.action.ts b/apps/web/src/app/(dashboard)/settings/billing/create-checkout.action.ts
deleted file mode 100644
index 90dd77e79..000000000
--- a/apps/web/src/app/(dashboard)/settings/billing/create-checkout.action.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-'use server';
-
-import { getCheckoutSession } from '@documenso/ee/server-only/stripe/get-checkout-session';
-import { getStripeCustomerByUser } from '@documenso/ee/server-only/stripe/get-customer';
-import { getPortalSession } from '@documenso/ee/server-only/stripe/get-portal-session';
-import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
-import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
-import { getSubscriptionsByUserId } from '@documenso/lib/server-only/subscription/get-subscriptions-by-user-id';
-
-export type CreateCheckoutOptions = {
- priceId: string;
-};
-
-export const createCheckout = async ({ priceId }: CreateCheckoutOptions) => {
- const session = await getRequiredServerComponentSession();
-
- const { user, stripeCustomer } = await getStripeCustomerByUser(session.user);
-
- const existingSubscriptions = await getSubscriptionsByUserId({ userId: user.id });
-
- const foundSubscription = existingSubscriptions.find(
- (subscription) =>
- subscription.priceId === priceId &&
- subscription.periodEnd &&
- subscription.periodEnd >= new Date(),
- );
-
- if (foundSubscription) {
- return getPortalSession({
- customerId: stripeCustomer.id,
- returnUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/settings/billing`,
- });
- }
-
- return getCheckoutSession({
- customerId: stripeCustomer.id,
- priceId,
- returnUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/settings/billing`,
- });
-};
diff --git a/apps/web/src/app/(dashboard)/settings/billing/page.tsx b/apps/web/src/app/(dashboard)/settings/billing/page.tsx
deleted file mode 100644
index 128761589..000000000
--- a/apps/web/src/app/(dashboard)/settings/billing/page.tsx
+++ /dev/null
@@ -1,146 +0,0 @@
-import type { Metadata } from 'next';
-import { redirect } from 'next/navigation';
-
-import { Trans } from '@lingui/macro';
-import { match } from 'ts-pattern';
-
-import { getStripeCustomerByUser } from '@documenso/ee/server-only/stripe/get-customer';
-import { getPricesByInterval } from '@documenso/ee/server-only/stripe/get-prices-by-interval';
-import { getPrimaryAccountPlanPrices } from '@documenso/ee/server-only/stripe/get-primary-account-plan-prices';
-import { getProductByPriceId } from '@documenso/ee/server-only/stripe/get-product-by-price-id';
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-import { STRIPE_PLAN_TYPE } from '@documenso/lib/constants/billing';
-import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
-import { getServerComponentFlag } from '@documenso/lib/server-only/feature-flags/get-server-component-feature-flag';
-import { type Stripe } from '@documenso/lib/server-only/stripe';
-import { getSubscriptionsByUserId } from '@documenso/lib/server-only/subscription/get-subscriptions-by-user-id';
-import { SubscriptionStatus } from '@documenso/prisma/client';
-
-import { BillingPlans } from './billing-plans';
-import { BillingPortalButton } from './billing-portal-button';
-
-export const metadata: Metadata = {
- title: 'Billing',
-};
-
-export default async function BillingSettingsPage() {
- const { i18n } = await setupI18nSSR();
-
- let { user } = await getRequiredServerComponentSession();
-
- const isBillingEnabled = await getServerComponentFlag('app_billing');
-
- // Redirect if subscriptions are not enabled.
- if (!isBillingEnabled) {
- redirect('/settings/profile');
- }
-
- if (!user.customerId) {
- user = await getStripeCustomerByUser(user).then((result) => result.user);
- }
-
- const [subscriptions, prices, primaryAccountPlanPrices] = await Promise.all([
- getSubscriptionsByUserId({ userId: user.id }),
- getPricesByInterval({ plans: [STRIPE_PLAN_TYPE.REGULAR, STRIPE_PLAN_TYPE.PLATFORM] }),
- getPrimaryAccountPlanPrices(),
- ]);
-
- const primaryAccountPlanPriceIds = primaryAccountPlanPrices.map(({ id }) => id);
-
- let subscriptionProduct: Stripe.Product | null = null;
-
- const primaryAccountPlanSubscriptions = subscriptions.filter(({ priceId }) =>
- primaryAccountPlanPriceIds.includes(priceId),
- );
-
- const subscription =
- primaryAccountPlanSubscriptions.find(({ status }) => status === SubscriptionStatus.ACTIVE) ??
- primaryAccountPlanSubscriptions[0];
-
- if (subscription?.priceId) {
- subscriptionProduct = await getProductByPriceId({ priceId: subscription.priceId }).catch(
- () => null,
- );
- }
-
- const isMissingOrInactiveOrFreePlan =
- !subscription || subscription.status === SubscriptionStatus.INACTIVE;
-
- return (
-
-
-
-
- Billing
-
-
-
- {isMissingOrInactiveOrFreePlan && (
-
-
- You are currently on the Free Plan .
-
-
- )}
-
- {/* Todo: Translation */}
- {!isMissingOrInactiveOrFreePlan &&
- match(subscription.status)
- .with('ACTIVE', () => (
-
- {subscriptionProduct ? (
-
- You are currently subscribed to{' '}
- {subscriptionProduct.name}
-
- ) : (
- You currently have an active plan
- )}
-
- {subscription.periodEnd && (
-
- {' '}
- which is set to{' '}
- {subscription.cancelAtPeriodEnd ? (
-
- end on{' '}
-
- {i18n.date(subscription.periodEnd)}.
-
-
- ) : (
-
- automatically renew on{' '}
-
- {i18n.date(subscription.periodEnd)}.
-
-
- )}
-
- )}
-
- ))
- .with('PAST_DUE', () => (
-
-
- Your current plan is past due. Please update your payment information.
-
-
- ))
- .otherwise(() => null)}
-
-
-
- {isMissingOrInactiveOrFreePlan && (
-
- Manage billing
-
- )}
-
-
-
-
- {isMissingOrInactiveOrFreePlan ?
:
}
-
- );
-}
diff --git a/apps/web/src/app/(dashboard)/settings/layout.tsx b/apps/web/src/app/(dashboard)/settings/layout.tsx
deleted file mode 100644
index 9c472cf07..000000000
--- a/apps/web/src/app/(dashboard)/settings/layout.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import React from 'react';
-
-import { Trans } from '@lingui/macro';
-
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-
-import { DesktopNav } from '~/components/(dashboard)/settings/layout/desktop-nav';
-import { MobileNav } from '~/components/(dashboard)/settings/layout/mobile-nav';
-
-export type DashboardSettingsLayoutProps = {
- children: React.ReactNode;
-};
-
-export default async function DashboardSettingsLayout({ children }: DashboardSettingsLayoutProps) {
- await setupI18nSSR();
-
- return (
-
- );
-}
diff --git a/apps/web/src/app/(dashboard)/settings/page.tsx b/apps/web/src/app/(dashboard)/settings/page.tsx
deleted file mode 100644
index d1c944938..000000000
--- a/apps/web/src/app/(dashboard)/settings/page.tsx
+++ /dev/null
@@ -1,8 +0,0 @@
-import { redirect } from 'next/navigation';
-
-export default function SettingsPage() {
- redirect('/settings/profile');
-
- // Page is intentionally empty because it will be redirected to /settings/profile
- return
;
-}
diff --git a/apps/web/src/app/(dashboard)/settings/password/page.tsx b/apps/web/src/app/(dashboard)/settings/password/page.tsx
deleted file mode 100644
index dd344a1d1..000000000
--- a/apps/web/src/app/(dashboard)/settings/password/page.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import { redirect } from 'next/navigation';
-
-export default function PasswordSettingsPage() {
- redirect('/settings/security');
-}
diff --git a/apps/web/src/app/(dashboard)/settings/profile/delete-account-dialog.tsx b/apps/web/src/app/(dashboard)/settings/profile/delete-account-dialog.tsx
deleted file mode 100644
index 2bb37e57b..000000000
--- a/apps/web/src/app/(dashboard)/settings/profile/delete-account-dialog.tsx
+++ /dev/null
@@ -1,155 +0,0 @@
-'use client';
-
-import { useState } from 'react';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { signOut } from 'next-auth/react';
-
-import type { User } from '@documenso/prisma/client';
-import { trpc } from '@documenso/trpc/react';
-import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
-import { Button } from '@documenso/ui/primitives/button';
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from '@documenso/ui/primitives/dialog';
-import { Input } from '@documenso/ui/primitives/input';
-import { Label } from '@documenso/ui/primitives/label';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-export type DeleteAccountDialogProps = {
- className?: string;
- user: User;
-};
-
-export const DeleteAccountDialog = ({ className, user }: DeleteAccountDialogProps) => {
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const hasTwoFactorAuthentication = user.twoFactorEnabled;
-
- const [enteredEmail, setEnteredEmail] = useState('');
-
- const { mutateAsync: deleteAccount, isPending: isDeletingAccount } =
- trpc.profile.deleteAccount.useMutation();
-
- const onDeleteAccount = async () => {
- try {
- await deleteAccount();
-
- toast({
- title: _(msg`Account deleted`),
- description: _(msg`Your account has been deleted successfully.`),
- duration: 5000,
- });
-
- return await signOut({ callbackUrl: '/' });
- } catch (err) {
- toast({
- title: _(msg`An unknown error occurred`),
- variant: 'destructive',
- description: _(
- msg`We encountered an unknown error while attempting to delete your account. Please try again later.`,
- ),
- });
- }
- };
-
- return (
-
-
-
-
- Delete Account
-
-
-
- Delete your account and all its contents, including completed documents. This action
- is irreversible and will cancel your subscription, so proceed with caution.
-
-
-
-
-
-
setEnteredEmail('')}>
-
-
- Delete Account
-
-
-
-
-
-
- Delete Account
-
-
-
-
- This action is not reversible. Please be certain.
-
-
-
- {hasTwoFactorAuthentication && (
-
-
- Disable Two Factor Authentication before deleting your account.
-
-
- )}
-
-
-
- Documenso will delete{' '}
- all of your documents , along with all of
- your completed documents, signatures, and all other resources belonging to your
- Account.
-
-
-
-
- {!hasTwoFactorAuthentication && (
-
-
-
- Please type{' '}
- {user.email} to
- confirm.
-
-
-
- setEnteredEmail(e.target.value)}
- />
-
- )}
-
-
- {isDeletingAccount ? _(msg`Deleting account...`) : _(msg`Confirm Deletion`)}
-
-
-
-
-
-
-
- );
-};
diff --git a/apps/web/src/app/(dashboard)/settings/profile/page.tsx b/apps/web/src/app/(dashboard)/settings/profile/page.tsx
deleted file mode 100644
index 214f73ad2..000000000
--- a/apps/web/src/app/(dashboard)/settings/profile/page.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-import type { Metadata } from 'next';
-
-import { msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
-
-import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
-import { AvatarImageForm } from '~/components/forms/avatar-image';
-import { ProfileForm } from '~/components/forms/profile';
-
-import { DeleteAccountDialog } from './delete-account-dialog';
-
-export const metadata: Metadata = {
- title: 'Profile',
-};
-
-export default async function ProfileSettingsPage() {
- await setupI18nSSR();
-
- const { _ } = useLingui();
- const { user } = await getRequiredServerComponentSession();
-
- return (
-
- );
-}
diff --git a/apps/web/src/app/(dashboard)/settings/public-profile/page.tsx b/apps/web/src/app/(dashboard)/settings/public-profile/page.tsx
deleted file mode 100644
index 8fcf1bca2..000000000
--- a/apps/web/src/app/(dashboard)/settings/public-profile/page.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
-import { getUserPublicProfile } from '@documenso/lib/server-only/user/get-user-public-profile';
-
-import { PublicProfilePageView } from './public-profile-page-view';
-
-export default async function Page() {
- await setupI18nSSR();
-
- const { user } = await getRequiredServerComponentSession();
-
- const { profile } = await getUserPublicProfile({
- userId: user.id,
- });
-
- return ;
-}
diff --git a/apps/web/src/app/(dashboard)/settings/public-profile/public-profile-page-view.tsx b/apps/web/src/app/(dashboard)/settings/public-profile/public-profile-page-view.tsx
deleted file mode 100644
index 0795c29ca..000000000
--- a/apps/web/src/app/(dashboard)/settings/public-profile/public-profile-page-view.tsx
+++ /dev/null
@@ -1,227 +0,0 @@
-'use client';
-
-import { useEffect, useMemo, useState } from 'react';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-
-import type {
- Team,
- TeamProfile,
- TemplateDirectLink,
- User,
- UserProfile,
-} from '@documenso/prisma/client';
-import { TemplateType } from '@documenso/prisma/client';
-import { trpc } from '@documenso/trpc/react';
-import type { FindTemplateRow } from '@documenso/trpc/server/template-router/schema';
-import { cn } from '@documenso/ui/lib/utils';
-import { Button } from '@documenso/ui/primitives/button';
-import { Switch } from '@documenso/ui/primitives/switch';
-import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
-import type { TPublicProfileFormSchema } from '~/components/forms/public-profile-form';
-import { PublicProfileForm } from '~/components/forms/public-profile-form';
-import { ManagePublicTemplateDialog } from '~/components/templates/manage-public-template-dialog';
-
-import { PublicTemplatesDataTable } from './public-templates-data-table';
-
-export type PublicProfilePageViewOptions = {
- user: User;
- team?: Team;
- profile: UserProfile | TeamProfile;
-};
-
-type DirectTemplate = FindTemplateRow & {
- directLink: Pick;
-};
-
-const userProfileText = {
- settingsTitle: msg`Public Profile`,
- settingsSubtitle: msg`You can choose to enable or disable your profile for public view.`,
- templatesTitle: msg`My templates`,
- templatesSubtitle: msg`Show templates in your public profile for your audience to sign and get started quickly`,
-};
-
-const teamProfileText = {
- settingsTitle: msg`Team Public Profile`,
- settingsSubtitle: msg`You can choose to enable or disable your team profile for public view.`,
- templatesTitle: msg`Team templates`,
- templatesSubtitle: msg`Show templates in your team public profile for your audience to sign and get started quickly`,
-};
-
-export const PublicProfilePageView = ({ user, team, profile }: PublicProfilePageViewOptions) => {
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const [isPublicProfileVisible, setIsPublicProfileVisible] = useState(profile.enabled);
- const [isTooltipOpen, setIsTooltipOpen] = useState(false);
-
- const { data } = trpc.template.findTemplates.useQuery({
- perPage: 100,
- });
-
- const { mutateAsync: updateUserProfile, isPending: isUpdatingUserProfile } =
- trpc.profile.updatePublicProfile.useMutation();
-
- const { mutateAsync: updateTeamProfile, isPending: isUpdatingTeamProfile } =
- trpc.team.updateTeamPublicProfile.useMutation();
-
- const isUpdating = isUpdatingUserProfile || isUpdatingTeamProfile;
- const profileText = team ? teamProfileText : userProfileText;
-
- const enabledPrivateDirectTemplates = useMemo(
- () =>
- (data?.data ?? []).filter(
- (template): template is DirectTemplate =>
- template.directLink?.enabled === true && template.type !== TemplateType.PUBLIC,
- ),
- [data],
- );
-
- const onProfileUpdate = async (data: TPublicProfileFormSchema) => {
- if (team) {
- await updateTeamProfile({
- teamId: team.id,
- ...data,
- });
- } else {
- await updateUserProfile(data);
- }
-
- if (data.enabled === undefined && !isPublicProfileVisible) {
- setIsTooltipOpen(true);
- }
- };
-
- const togglePublicProfileVisibility = async (isVisible: boolean) => {
- setIsTooltipOpen(false);
-
- if (isUpdating) {
- return;
- }
-
- if (isVisible && !user.url) {
- toast({
- title: _(msg`You must set a profile URL before enabling your public profile.`),
- variant: 'destructive',
- });
-
- return;
- }
-
- setIsPublicProfileVisible(isVisible);
-
- try {
- await onProfileUpdate({
- enabled: isVisible,
- });
- } catch {
- toast({
- title: _(msg`Something went wrong`),
- description: _(msg`We were unable to set your public profile to public. Please try again.`),
- variant: 'destructive',
- });
-
- setIsPublicProfileVisible(!isVisible);
- }
- };
-
- useEffect(() => {
- setIsPublicProfileVisible(profile.enabled);
- }, [profile.enabled]);
-
- return (
-
-
-
-
- *:first-child]:text-muted-foreground': !isPublicProfileVisible,
- '[&>*:last-child]:text-muted-foreground': isPublicProfileVisible,
- },
- )}
- >
-
- Hide
-
-
-
- Show
-
-
-
-
-
- {isPublicProfileVisible ? (
- <>
-
-
- Profile is currently visible .
-
-
-
-
- Toggle the switch to hide your profile from the public.
-
- >
- ) : (
- <>
-
-
- Profile is currently hidden .
-
-
-
-
- Toggle the switch to show your profile to the public.
-
- >
- )}
-
-
-
-
-
-
-
-
-
- Link template
-
- }
- />
-
-
-
-
-
- );
-};
diff --git a/apps/web/src/app/(dashboard)/settings/public-profile/public-templates-data-table.tsx b/apps/web/src/app/(dashboard)/settings/public-profile/public-templates-data-table.tsx
deleted file mode 100644
index b8f3034b8..000000000
--- a/apps/web/src/app/(dashboard)/settings/public-profile/public-templates-data-table.tsx
+++ /dev/null
@@ -1,209 +0,0 @@
-'use client';
-
-import { useMemo, useState } from 'react';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { EditIcon, FileIcon, LinkIcon, MoreHorizontalIcon, Trash2Icon } from 'lucide-react';
-
-import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard';
-import { formatDirectTemplatePath } from '@documenso/lib/utils/templates';
-import type { TemplateDirectLink } from '@documenso/prisma/client';
-import { TemplateType } from '@documenso/prisma/client';
-import { trpc } from '@documenso/trpc/react';
-import type { FindTemplateRow } from '@documenso/trpc/server/template-router/schema';
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuLabel,
- DropdownMenuTrigger,
-} from '@documenso/ui/primitives/dropdown-menu';
-import { Skeleton } from '@documenso/ui/primitives/skeleton';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-import { ManagePublicTemplateDialog } from '~/components/templates/manage-public-template-dialog';
-
-type DirectTemplate = FindTemplateRow & {
- directLink: Pick;
-};
-
-export const PublicTemplatesDataTable = () => {
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const [, copy] = useCopyToClipboard();
-
- const [publicTemplateDialogPayload, setPublicTemplateDialogPayload] = useState<{
- step: 'MANAGE' | 'CONFIRM_DISABLE';
- templateId: number;
- } | null>(null);
-
- const { data, isLoading, isLoadingError, refetch } = trpc.template.findTemplates.useQuery(
- {},
- {
- placeholderData: (previousData) => previousData,
- },
- );
-
- const { directTemplates, publicDirectTemplates, privateDirectTemplates } = useMemo(() => {
- const directTemplates = (data?.data ?? []).filter(
- (template): template is DirectTemplate => template.directLink?.enabled === true,
- );
-
- const publicDirectTemplates = directTemplates.filter(
- (template) => template.directLink?.enabled === true && template.type === TemplateType.PUBLIC,
- );
-
- const privateDirectTemplates = directTemplates.filter(
- (template) => template.directLink?.enabled === true && template.type === TemplateType.PRIVATE,
- );
-
- return {
- directTemplates,
- publicDirectTemplates,
- privateDirectTemplates,
- };
- }, [data]);
-
- const onCopyClick = async (token: string) =>
- copy(formatDirectTemplatePath(token)).then(() => {
- toast({
- title: _(msg`Copied to clipboard`),
- description: _(msg`The direct link has been copied to your clipboard`),
- });
- });
-
- return (
-
-
- {/* Loading and error handling states. */}
- {publicDirectTemplates.length === 0 && (
- <>
- {isLoading &&
- Array(3)
- .fill(0)
- .map((_, index) => (
-
- ))}
-
- {isLoadingError && (
-
- Unable to load your public profile templates at this time
- {
- e.preventDefault();
- void refetch();
- }}
- >
- Click here to retry
-
-
- )}
-
- {!isLoading && (
-
- No public profile templates found
-
- Click here to get started
-
- }
- />
-
- )}
- >
- )}
-
- {/* Public templates list. */}
- {publicDirectTemplates.map((template) => (
-
-
-
-
-
-
{template.publicTitle}
-
{template.publicDescription}
-
-
-
-
-
-
-
-
-
-
- Action
-
-
- void onCopyClick(template.directLink.token)}>
-
- Copy sharable link
-
-
- {
- setPublicTemplateDialogPayload({
- step: 'MANAGE',
- templateId: template.id,
- });
- }}
- >
-
- Update
-
-
-
- setPublicTemplateDialogPayload({
- step: 'CONFIRM_DISABLE',
- templateId: template.id,
- })
- }
- >
-
- Remove
-
-
-
-
- ))}
-
-
-
{
- if (!value) {
- setPublicTemplateDialogPayload(null);
- }
- }}
- />
-
- );
-};
diff --git a/apps/web/src/app/(dashboard)/settings/security/activity/page.tsx b/apps/web/src/app/(dashboard)/settings/security/activity/page.tsx
deleted file mode 100644
index e16b71659..000000000
--- a/apps/web/src/app/(dashboard)/settings/security/activity/page.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import type { Metadata } from 'next';
-
-import { msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-
-import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
-
-import { UserSecurityActivityDataTable } from './user-security-activity-data-table';
-
-export const metadata: Metadata = {
- title: 'Security activity',
-};
-
-export default async function SettingsSecurityActivityPage() {
- await setupI18nSSR();
-
- const { _ } = useLingui();
-
- return (
-
-
- {/* Todo */}
- {/* */}
-
-
-
-
-
-
- );
-}
diff --git a/apps/web/src/app/(dashboard)/settings/security/activity/user-security-activity-data-table.tsx b/apps/web/src/app/(dashboard)/settings/security/activity/user-security-activity-data-table.tsx
deleted file mode 100644
index 660c2346f..000000000
--- a/apps/web/src/app/(dashboard)/settings/security/activity/user-security-activity-data-table.tsx
+++ /dev/null
@@ -1,162 +0,0 @@
-'use client';
-
-import { useMemo } from 'react';
-
-import { usePathname, useRouter, useSearchParams } from 'next/navigation';
-
-import { msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import type { DateTimeFormatOptions } from 'luxon';
-import { DateTime } from 'luxon';
-import { UAParser } from 'ua-parser-js';
-
-import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
-import { USER_SECURITY_AUDIT_LOG_MAP } from '@documenso/lib/constants/auth';
-import { ZUrlSearchParamsSchema } from '@documenso/lib/types/search-params';
-import { trpc } from '@documenso/trpc/react';
-import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
-import { DataTable } from '@documenso/ui/primitives/data-table';
-import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
-import { Skeleton } from '@documenso/ui/primitives/skeleton';
-import { TableCell } from '@documenso/ui/primitives/table';
-
-const dateFormat: DateTimeFormatOptions = {
- ...DateTime.DATETIME_SHORT,
- hourCycle: 'h12',
-};
-
-export const UserSecurityActivityDataTable = () => {
- const { _, i18n } = useLingui();
-
- const pathname = usePathname();
- const router = useRouter();
- const searchParams = useSearchParams();
- const updateSearchParams = useUpdateSearchParams();
-
- const parsedSearchParams = ZUrlSearchParamsSchema.parse(Object.fromEntries(searchParams ?? []));
-
- const { data, isLoading, isLoadingError } = trpc.profile.findUserSecurityAuditLogs.useQuery(
- {
- page: parsedSearchParams.page,
- perPage: parsedSearchParams.perPage,
- },
- {
- placeholderData: (previousData) => previousData,
- },
- );
-
- const onPaginationChange = (page: number, perPage: number) => {
- updateSearchParams({
- page,
- perPage,
- });
- };
-
- const results = data ?? {
- data: [],
- perPage: 10,
- currentPage: 1,
- totalPages: 1,
- };
-
- const columns = useMemo(() => {
- const parser = new UAParser();
-
- return [
- {
- header: _(msg`Date`),
- accessorKey: 'createdAt',
- cell: ({ row }) => i18n.date(row.original.createdAt, dateFormat),
- },
- {
- header: _(msg`Device`),
- cell: ({ row }) => {
- if (!row.original.userAgent) {
- return 'N/A';
- }
-
- parser.setUA(row.original.userAgent);
-
- const result = parser.getResult();
-
- let output = result.os.name;
-
- if (!output) {
- return 'N/A';
- }
-
- if (result.os.version) {
- output += ` (${result.os.version})`;
- }
-
- return output;
- },
- },
- {
- header: _(msg`Browser`),
- cell: ({ row }) => {
- if (!row.original.userAgent) {
- return 'N/A';
- }
-
- parser.setUA(row.original.userAgent);
-
- const result = parser.getResult();
-
- return result.browser.name ?? 'N/A';
- },
- },
- {
- header: 'IP Address',
- accessorKey: 'ipAddress',
- cell: ({ row }) => row.original.ipAddress ?? 'N/A',
- },
- {
- header: _(msg`Action`),
- accessorKey: 'type',
- cell: ({ row }) => USER_SECURITY_AUDIT_LOG_MAP[row.original.type],
- },
- ] satisfies DataTableColumnDef<(typeof results)['data'][number]>[];
- }, []);
-
- return (
- router.push(pathname ?? '/')}
- error={{
- enable: isLoadingError,
- }}
- skeleton={{
- enable: isLoading,
- rows: 3,
- component: (
- <>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- >
- ),
- }}
- >
- {(table) => }
-
- );
-};
diff --git a/apps/web/src/app/(dashboard)/settings/security/page.tsx b/apps/web/src/app/(dashboard)/settings/security/page.tsx
deleted file mode 100644
index 1f3d4de08..000000000
--- a/apps/web/src/app/(dashboard)/settings/security/page.tsx
+++ /dev/null
@@ -1,146 +0,0 @@
-import type { Metadata } from 'next';
-import Link from 'next/link';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
-import { getServerComponentFlag } from '@documenso/lib/server-only/feature-flags/get-server-component-feature-flag';
-import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
-import { Button } from '@documenso/ui/primitives/button';
-
-import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
-import { DisableAuthenticatorAppDialog } from '~/components/forms/2fa/disable-authenticator-app-dialog';
-import { EnableAuthenticatorAppDialog } from '~/components/forms/2fa/enable-authenticator-app-dialog';
-import { ViewRecoveryCodesDialog } from '~/components/forms/2fa/view-recovery-codes-dialog';
-import { PasswordForm } from '~/components/forms/password';
-
-export const metadata: Metadata = {
- title: 'Security',
-};
-
-export default async function SecuritySettingsPage() {
- await setupI18nSSR();
-
- const { _ } = useLingui();
- const { user } = await getRequiredServerComponentSession();
-
- const isPasskeyEnabled = await getServerComponentFlag('app_passkey');
-
- return (
-
-
-
- {user.identityProvider === 'DOCUMENSO' && (
- <>
-
-
-
- >
- )}
-
-
-
-
- Two factor authentication
-
-
-
- {user.identityProvider === 'DOCUMENSO' ? (
-
- Add an authenticator to serve as a secondary authentication method when signing in,
- or when signing documents.
-
- ) : (
-
- Add an authenticator to serve as a secondary authentication method for signing
- documents.
-
- )}
-
-
-
- {user.twoFactorEnabled ? (
-
- ) : (
-
- )}
-
-
- {user.twoFactorEnabled && (
-
-
-
- Recovery codes
-
-
-
-
- Two factor authentication recovery codes are used to access your account in the
- event that you lose access to your authenticator app.
-
-
-
-
-
-
- )}
-
- {isPasskeyEnabled && (
-
-
-
- Passkeys
-
-
-
-
- Allows authenticating using biometrics, password managers, hardware keys, etc.
-
-
-
-
-
-
- Manage passkeys
-
-
-
- )}
-
-
-
-
- Recent activity
-
-
-
- View all recent security activity related to your account.
-
-
-
-
-
- View activity
-
-
-
-
- );
-}
diff --git a/apps/web/src/app/(dashboard)/settings/security/passkeys/create-passkey-dialog.tsx b/apps/web/src/app/(dashboard)/settings/security/passkeys/create-passkey-dialog.tsx
deleted file mode 100644
index 44b87ae2f..000000000
--- a/apps/web/src/app/(dashboard)/settings/security/passkeys/create-passkey-dialog.tsx
+++ /dev/null
@@ -1,261 +0,0 @@
-'use client';
-
-import { useEffect, useState } from 'react';
-
-import { zodResolver } from '@hookform/resolvers/zod';
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import type * as DialogPrimitive from '@radix-ui/react-dialog';
-import { startRegistration } from '@simplewebauthn/browser';
-import { KeyRoundIcon } from 'lucide-react';
-import { useForm } from 'react-hook-form';
-import { match } from 'ts-pattern';
-import { UAParser } from 'ua-parser-js';
-import { z } from 'zod';
-
-import { MAXIMUM_PASSKEYS } from '@documenso/lib/constants/auth';
-import { AppError } from '@documenso/lib/errors/app-error';
-import { trpc } from '@documenso/trpc/react';
-import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
-import { Button } from '@documenso/ui/primitives/button';
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from '@documenso/ui/primitives/dialog';
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from '@documenso/ui/primitives/form/form';
-import { Input } from '@documenso/ui/primitives/input';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-export type CreatePasskeyDialogProps = {
- trigger?: React.ReactNode;
- onSuccess?: () => void;
-} & Omit;
-
-const ZCreatePasskeyFormSchema = z.object({
- passkeyName: z.string().min(3),
-});
-
-type TCreatePasskeyFormSchema = z.infer;
-
-const parser = new UAParser();
-
-export const CreatePasskeyDialog = ({ trigger, onSuccess, ...props }: CreatePasskeyDialogProps) => {
- const [open, setOpen] = useState(false);
- const [formError, setFormError] = useState(null);
-
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const form = useForm({
- resolver: zodResolver(ZCreatePasskeyFormSchema),
- defaultValues: {
- passkeyName: '',
- },
- });
-
- const { mutateAsync: createPasskeyRegistrationOptions, isPending } =
- trpc.auth.createPasskeyRegistrationOptions.useMutation();
-
- const { mutateAsync: createPasskey } = trpc.auth.createPasskey.useMutation();
-
- const onFormSubmit = async ({ passkeyName }: TCreatePasskeyFormSchema) => {
- setFormError(null);
-
- try {
- const passkeyRegistrationOptions = await createPasskeyRegistrationOptions();
-
- const registrationResult = await startRegistration(passkeyRegistrationOptions);
-
- await createPasskey({
- passkeyName,
- verificationResponse: registrationResult,
- });
-
- toast({
- description: _(msg`Successfully created passkey`),
- duration: 5000,
- });
-
- onSuccess?.();
- setOpen(false);
- } catch (err) {
- if (err.name === 'NotAllowedError') {
- return;
- }
-
- const error = AppError.parseError(err);
-
- setFormError(err.code || error.code);
- }
- };
-
- const extractDefaultPasskeyName = () => {
- if (!window || !window.navigator) {
- return;
- }
-
- parser.setUA(window.navigator.userAgent);
-
- const result = parser.getResult();
- const operatingSystem = result.os.name;
- const browser = result.browser.name;
-
- let passkeyName = '';
-
- if (operatingSystem && browser) {
- passkeyName = `${browser} (${operatingSystem})`;
- }
-
- return passkeyName;
- };
-
- useEffect(() => {
- if (!open) {
- const defaultPasskeyName = extractDefaultPasskeyName();
-
- form.reset({
- passkeyName: defaultPasskeyName,
- });
-
- setFormError(null);
- }
- }, [open, form]);
-
- return (
- !form.formState.isSubmitting && setOpen(value)}
- >
- e.stopPropagation()} asChild={true}>
- {trigger ?? (
-
-
- Add passkey
-
- )}
-
-
-
-
-
- Add passkey
-
-
-
-
- Passkeys allow you to sign in and authenticate using biometrics, password managers,
- etc.
-
-
-
-
-
-
-
- (
-
-
- Passkey name
-
-
-
-
-
-
- )}
- />
-
-
-
-
- When you click continue, you will be prompted to add the first available
- authenticator on your system.
-
-
-
-
-
- If you do not want to use the authenticator prompted, you can close it, which
- will then display the next available authenticator.
-
-
-
-
- {formError && (
-
- {match(formError)
- .with('ERROR_AUTHENTICATOR_PREVIOUSLY_REGISTERED', () => (
-
- This passkey has already been registered.
-
- ))
- .with('TOO_MANY_PASSKEYS', () => (
-
- You cannot have more than {MAXIMUM_PASSKEYS} passkeys.
-
- ))
- .with('InvalidStateError', () => (
- <>
-
-
- Passkey creation cancelled due to one of the following reasons:
-
-
-
-
-
- Cancelled by user
-
-
- Passkey already exists for the provided authenticator
-
-
- Exceeded timeout
-
-
-
- >
- ))
- .otherwise(() => (
-
- Something went wrong. Please try again or contact support.
-
- ))}
-
- )}
-
-
- setOpen(false)}>
- Cancel
-
-
-
- Continue
-
-
-
-
-
-
-
- );
-};
diff --git a/apps/web/src/app/(dashboard)/settings/security/passkeys/page.tsx b/apps/web/src/app/(dashboard)/settings/security/passkeys/page.tsx
deleted file mode 100644
index c9ec4153a..000000000
--- a/apps/web/src/app/(dashboard)/settings/security/passkeys/page.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-import type { Metadata } from 'next';
-import { redirect } from 'next/navigation';
-
-import { msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-import { getServerComponentFlag } from '@documenso/lib/server-only/feature-flags/get-server-component-feature-flag';
-
-import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
-
-import { CreatePasskeyDialog } from './create-passkey-dialog';
-import { UserPasskeysDataTable } from './user-passkeys-data-table';
-
-export const metadata: Metadata = {
- title: 'Manage passkeys',
-};
-
-export default async function SettingsManagePasskeysPage() {
- await setupI18nSSR();
-
- const { _ } = useLingui();
- const isPasskeyEnabled = await getServerComponentFlag('app_passkey');
-
- if (!isPasskeyEnabled) {
- redirect('/settings/security');
- }
-
- return (
-
- );
-}
diff --git a/apps/web/src/app/(dashboard)/settings/security/passkeys/user-passkeys-data-table-actions.tsx b/apps/web/src/app/(dashboard)/settings/security/passkeys/user-passkeys-data-table-actions.tsx
deleted file mode 100644
index 72142da41..000000000
--- a/apps/web/src/app/(dashboard)/settings/security/passkeys/user-passkeys-data-table-actions.tsx
+++ /dev/null
@@ -1,219 +0,0 @@
-import { useState } from 'react';
-
-import { zodResolver } from '@hookform/resolvers/zod';
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { useForm } from 'react-hook-form';
-import { z } from 'zod';
-
-import { trpc } from '@documenso/trpc/react';
-import { cn } from '@documenso/ui/lib/utils';
-import { Button } from '@documenso/ui/primitives/button';
-import {
- Dialog,
- DialogClose,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from '@documenso/ui/primitives/dialog';
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from '@documenso/ui/primitives/form/form';
-import { Input } from '@documenso/ui/primitives/input';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-export type UserPasskeysDataTableActionsProps = {
- className?: string;
- passkeyId: string;
- passkeyName: string;
-};
-
-const ZUpdatePasskeySchema = z.object({
- name: z.string(),
-});
-
-type TUpdatePasskeySchema = z.infer;
-
-export const UserPasskeysDataTableActions = ({
- className,
- passkeyId,
- passkeyName,
-}: UserPasskeysDataTableActionsProps) => {
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
- const [isUpdateDialogOpen, setIsUpdateDialogOpen] = useState(false);
-
- const form = useForm({
- resolver: zodResolver(ZUpdatePasskeySchema),
- defaultValues: {
- name: passkeyName,
- },
- });
-
- const { mutateAsync: updatePasskey, isPending: isUpdatingPasskey } =
- trpc.auth.updatePasskey.useMutation({
- onSuccess: () => {
- toast({
- title: _(msg`Success`),
- description: _(msg`Passkey has been updated`),
- });
- },
- onError: () => {
- toast({
- title: _(msg`Something went wrong`),
- description: _(
- msg`We are unable to update this passkey at the moment. Please try again later.`,
- ),
- duration: 10000,
- variant: 'destructive',
- });
- },
- });
-
- const { mutateAsync: deletePasskey, isPending: isDeletingPasskey } =
- trpc.auth.deletePasskey.useMutation({
- onSuccess: () => {
- toast({
- title: _(msg`Success`),
- description: _(msg`Passkey has been removed`),
- });
- },
- onError: () => {
- toast({
- title: _(msg`Something went wrong`),
- description: _(
- msg`We are unable to remove this passkey at the moment. Please try again later.`,
- ),
- duration: 10000,
- variant: 'destructive',
- });
- },
- });
-
- return (
-
- !isUpdatingPasskey && setIsUpdateDialogOpen(value)}
- >
- e.stopPropagation()} asChild>
-
- Edit
-
-
-
-
-
-
- Update passkey
-
-
-
-
- You are currently updating the {passkeyName} passkey.
-
-
-
-
-
-
- updatePasskey({
- passkeyId,
- name,
- }),
- )}
- >
-
- (
-
-
- Name
-
-
-
-
-
-
- )}
- />
-
-
-
-
- Cancel
-
-
-
-
- Update
-
-
-
-
-
-
-
-
- !isDeletingPasskey && setIsDeleteDialogOpen(value)}
- >
- e.stopPropagation()} asChild={true}>
-
- Delete
-
-
-
-
-
-
- Delete passkey
-
-
-
-
- Are you sure you want to remove the {passkeyName} passkey.
-
-
-
-
-
-
-
-
- Cancel
-
-
-
-
- deletePasskey({
- passkeyId,
- })
- }
- variant="destructive"
- loading={isDeletingPasskey}
- >
- Delete
-
-
-
-
-
-
- );
-};
diff --git a/apps/web/src/app/(dashboard)/settings/security/passkeys/user-passkeys-data-table.tsx b/apps/web/src/app/(dashboard)/settings/security/passkeys/user-passkeys-data-table.tsx
deleted file mode 100644
index 169630f20..000000000
--- a/apps/web/src/app/(dashboard)/settings/security/passkeys/user-passkeys-data-table.tsx
+++ /dev/null
@@ -1,129 +0,0 @@
-'use client';
-
-import { useMemo } from 'react';
-
-import { usePathname, useRouter, useSearchParams } from 'next/navigation';
-
-import { msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { DateTime } from 'luxon';
-
-import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
-import { ZUrlSearchParamsSchema } from '@documenso/lib/types/search-params';
-import { trpc } from '@documenso/trpc/react';
-import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
-import { DataTable } from '@documenso/ui/primitives/data-table';
-import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
-import { Skeleton } from '@documenso/ui/primitives/skeleton';
-import { TableCell } from '@documenso/ui/primitives/table';
-
-import { UserPasskeysDataTableActions } from './user-passkeys-data-table-actions';
-
-export const UserPasskeysDataTable = () => {
- const { _ } = useLingui();
-
- const pathname = usePathname();
- const router = useRouter();
- const searchParams = useSearchParams();
- const updateSearchParams = useUpdateSearchParams();
-
- const parsedSearchParams = ZUrlSearchParamsSchema.parse(Object.fromEntries(searchParams ?? []));
-
- const { data, isLoading, isLoadingError } = trpc.auth.findPasskeys.useQuery(
- {
- page: parsedSearchParams.page,
- perPage: parsedSearchParams.perPage,
- },
- {
- placeholderData: (previousData) => previousData,
- },
- );
-
- const onPaginationChange = (page: number, perPage: number) => {
- updateSearchParams({
- page,
- perPage,
- });
- };
-
- const results = data ?? {
- data: [],
- perPage: 10,
- currentPage: 1,
- totalPages: 1,
- };
-
- const columns = useMemo(() => {
- return [
- {
- header: _(msg`Name`),
- accessorKey: 'name',
- },
- {
- header: _(msg`Created`),
- accessorKey: 'createdAt',
- cell: ({ row }) => DateTime.fromJSDate(row.original.createdAt).toRelative(),
- },
-
- {
- header: _(msg`Last used`),
- accessorKey: 'updatedAt',
- cell: ({ row }) =>
- row.original.lastUsedAt
- ? DateTime.fromJSDate(row.original.lastUsedAt).toRelative()
- : _(msg`Never`),
- },
- {
- id: 'actions',
- cell: ({ row }) => (
-
- ),
- },
- ] satisfies DataTableColumnDef<(typeof results)['data'][number]>[];
- }, []);
-
- return (
- router.push(pathname ?? '/')}
- error={{
- enable: isLoadingError,
- }}
- skeleton={{
- enable: isLoading,
- rows: 3,
- component: (
- <>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- >
- ),
- }}
- >
- {(table) => }
-
- );
-};
diff --git a/apps/web/src/app/(dashboard)/settings/teams/accept-team-invitation-button.tsx b/apps/web/src/app/(dashboard)/settings/teams/accept-team-invitation-button.tsx
deleted file mode 100644
index 7d737026b..000000000
--- a/apps/web/src/app/(dashboard)/settings/teams/accept-team-invitation-button.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-'use client';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-
-import { trpc } from '@documenso/trpc/react';
-import { Button } from '@documenso/ui/primitives/button';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-export type AcceptTeamInvitationButtonProps = {
- teamId: number;
-};
-
-export const AcceptTeamInvitationButton = ({ teamId }: AcceptTeamInvitationButtonProps) => {
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const {
- mutateAsync: acceptTeamInvitation,
- isPending,
- isSuccess,
- } = trpc.team.acceptTeamInvitation.useMutation({
- onSuccess: () => {
- toast({
- title: _(msg`Success`),
- description: _(msg`Accepted team invitation`),
- duration: 5000,
- });
- },
- onError: () => {
- toast({
- title: _(msg`Something went wrong`),
- description: _(msg`Unable to join this team at this time.`),
- variant: 'destructive',
- duration: 10000,
- });
- },
- });
-
- return (
- acceptTeamInvitation({ teamId })}
- loading={isPending}
- disabled={isPending || isSuccess}
- >
- Accept
-
- );
-};
diff --git a/apps/web/src/app/(dashboard)/settings/teams/decline-team-invitation-button.tsx b/apps/web/src/app/(dashboard)/settings/teams/decline-team-invitation-button.tsx
deleted file mode 100644
index 02c5c38aa..000000000
--- a/apps/web/src/app/(dashboard)/settings/teams/decline-team-invitation-button.tsx
+++ /dev/null
@@ -1,50 +0,0 @@
-'use client';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-
-import { trpc } from '@documenso/trpc/react';
-import { Button } from '@documenso/ui/primitives/button';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-export type DeclineTeamInvitationButtonProps = {
- teamId: number;
-};
-
-export const DeclineTeamInvitationButton = ({ teamId }: DeclineTeamInvitationButtonProps) => {
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const {
- mutateAsync: declineTeamInvitation,
- isPending,
- isSuccess,
- } = trpc.team.declineTeamInvitation.useMutation({
- onSuccess: () => {
- toast({
- title: _(msg`Success`),
- description: _(msg`Declined team invitation`),
- duration: 5000,
- });
- },
- onError: () => {
- toast({
- title: _(msg`Something went wrong`),
- description: _(msg`Unable to decline this team invitation at this time.`),
- variant: 'destructive',
- duration: 10000,
- });
- },
- });
-
- return (
- declineTeamInvitation({ teamId })}
- loading={isPending}
- disabled={isPending || isSuccess}
- variant="ghost"
- >
- Decline
-
- );
-};
diff --git a/apps/web/src/app/(dashboard)/settings/teams/page.tsx b/apps/web/src/app/(dashboard)/settings/teams/page.tsx
deleted file mode 100644
index 439bc9713..000000000
--- a/apps/web/src/app/(dashboard)/settings/teams/page.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-'use client';
-
-import { msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { AnimatePresence } from 'framer-motion';
-
-import { trpc } from '@documenso/trpc/react';
-import { AnimateGenericFadeInOut } from '@documenso/ui/components/animate/animate-generic-fade-in-out';
-
-import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
-import { CreateTeamDialog } from '~/components/(teams)/dialogs/create-team-dialog';
-import { UserSettingsTeamsPageDataTable } from '~/components/(teams)/tables/user-settings-teams-page-data-table';
-
-import { TeamEmailUsage } from './team-email-usage';
-import { TeamInvitations } from './team-invitations';
-
-export default function TeamsSettingsPage() {
- const { _ } = useLingui();
-
- const { data: teamEmail } = trpc.team.getTeamEmailByEmail.useQuery();
-
- return (
-
-
-
-
-
-
-
-
-
- {teamEmail && (
-
-
-
- )}
-
-
-
-
-
- );
-}
diff --git a/apps/web/src/app/(dashboard)/settings/teams/team-email-usage.tsx b/apps/web/src/app/(dashboard)/settings/teams/team-email-usage.tsx
deleted file mode 100644
index 3f7fadf26..000000000
--- a/apps/web/src/app/(dashboard)/settings/teams/team-email-usage.tsx
+++ /dev/null
@@ -1,126 +0,0 @@
-'use client';
-
-import { useState } from 'react';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-
-import type { TeamEmail } from '@documenso/prisma/client';
-import { trpc } from '@documenso/trpc/react';
-import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
-import { Button } from '@documenso/ui/primitives/button';
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from '@documenso/ui/primitives/dialog';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-export type TeamEmailUsageProps = {
- teamEmail: TeamEmail & { team: { name: string; url: string } };
-};
-
-export const TeamEmailUsage = ({ teamEmail }: TeamEmailUsageProps) => {
- const [open, setOpen] = useState(false);
-
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const { mutateAsync: deleteTeamEmail, isPending: isDeletingTeamEmail } =
- trpc.team.deleteTeamEmail.useMutation({
- onSuccess: () => {
- toast({
- title: _(msg`Success`),
- description: _(msg`You have successfully revoked access.`),
- duration: 5000,
- });
- },
- onError: () => {
- toast({
- title: _(msg`Something went wrong`),
- description: _(
- msg`We encountered an unknown error while attempting to revoke access. Please try again or contact support.`,
- ),
- variant: 'destructive',
- duration: 10000,
- });
- },
- });
-
- return (
-
-
-
- Team Email
-
-
-
-
- Your email is currently being used by team{' '}
- {teamEmail.team.name} ({teamEmail.team.url}
- ).
-
-
-
-
- They have permission on your behalf to:
-
-
-
-
- Display your name and email in documents
-
-
- View all documents sent to your account
-
-
-
-
-
- !isDeletingTeamEmail && setOpen(value)}>
-
-
- Revoke access
-
-
-
-
-
-
- Are you sure?
-
-
-
-
- You are about to revoke access for team{' '}
- {teamEmail.team.name} ({teamEmail.team.url})
- to use your email.
-
-
-
-
-
-
- setOpen(false)}>
- Cancel
-
-
- deleteTeamEmail({ teamId: teamEmail.teamId })}
- >
- Revoke
-
-
-
-
-
-
- );
-};
diff --git a/apps/web/src/app/(dashboard)/settings/teams/team-invitations.tsx b/apps/web/src/app/(dashboard)/settings/teams/team-invitations.tsx
deleted file mode 100644
index 1cef7ea30..000000000
--- a/apps/web/src/app/(dashboard)/settings/teams/team-invitations.tsx
+++ /dev/null
@@ -1,115 +0,0 @@
-'use client';
-
-import { Plural, Trans } from '@lingui/macro';
-import { AnimatePresence } from 'framer-motion';
-import { BellIcon } from 'lucide-react';
-
-import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
-import { formatTeamUrl } from '@documenso/lib/utils/teams';
-import { trpc } from '@documenso/trpc/react';
-import { AnimateGenericFadeInOut } from '@documenso/ui/components/animate/animate-generic-fade-in-out';
-import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
-import { AvatarWithText } from '@documenso/ui/primitives/avatar';
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from '@documenso/ui/primitives/dialog';
-
-import { AcceptTeamInvitationButton } from './accept-team-invitation-button';
-import { DeclineTeamInvitationButton } from './decline-team-invitation-button';
-
-export const TeamInvitations = () => {
- const { data, isLoading } = trpc.team.getTeamInvitations.useQuery();
-
- return (
-
- {data && data.length > 0 && !isLoading && (
-
-
-
-
-
-
-
- You have 1 pending team invitation
-
- }
- other={
-
- You have # pending team invitations
-
- }
- />
-
-
-
-
-
- View invites
-
-
-
-
-
-
- Pending invitations
-
-
-
-
- You have 1 pending team invitation
-
- }
- other={
-
- You have # pending team invitations
-
- }
- />
-
-
-
-
- {data.map((invitation) => (
-
-
- {invitation.team.name}
-
- }
- secondaryText={formatTeamUrl(invitation.team.url)}
- rightSideComponent={
-
- }
- />
-
- ))}
-
-
-
-
-
-
- )}
-
- );
-};
diff --git a/apps/web/src/app/(dashboard)/settings/tokens/page.tsx b/apps/web/src/app/(dashboard)/settings/tokens/page.tsx
deleted file mode 100644
index ad90b54d5..000000000
--- a/apps/web/src/app/(dashboard)/settings/tokens/page.tsx
+++ /dev/null
@@ -1,94 +0,0 @@
-import { Trans } from '@lingui/macro';
-import { DateTime } from 'luxon';
-
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
-import { getUserTokens } from '@documenso/lib/server-only/public-api/get-all-user-tokens';
-import { Button } from '@documenso/ui/primitives/button';
-
-import DeleteTokenDialog from '~/components/(dashboard)/settings/token/delete-token-dialog';
-import { ApiTokenForm } from '~/components/forms/token';
-
-export default async function ApiTokensPage() {
- const { i18n } = await setupI18nSSR();
-
- const { user } = await getRequiredServerComponentSession();
-
- const tokens = await getUserTokens({ userId: user.id });
-
- return (
-
-
- API Tokens
-
-
-
-
- On this page, you can create new API tokens and manage the existing ones.
- Also see our{' '}
-
- Documentation
-
- .
-
-
-
-
-
-
-
-
-
-
- Your existing tokens
-
-
- {tokens.length === 0 && (
-
-
- Your tokens will be shown here once you create them.
-
-
- )}
-
- {tokens.length > 0 && (
-
- {tokens.map((token) => (
-
-
-
-
{token.name}
-
-
- Created on {i18n.date(token.createdAt, DateTime.DATETIME_FULL)}
-
- {token.expires ? (
-
- Expires on {i18n.date(token.expires, DateTime.DATETIME_FULL)}
-
- ) : (
-
- Token doesn't have an expiration date
-
- )}
-
-
-
-
-
- Delete
-
-
-
-
-
- ))}
-
- )}
-
- );
-}
diff --git a/apps/web/src/app/(dashboard)/settings/webhooks/[id]/page.tsx b/apps/web/src/app/(dashboard)/settings/webhooks/[id]/page.tsx
deleted file mode 100644
index 3800bdd67..000000000
--- a/apps/web/src/app/(dashboard)/settings/webhooks/[id]/page.tsx
+++ /dev/null
@@ -1,216 +0,0 @@
-'use client';
-
-import { useRouter } from 'next/navigation';
-
-import { zodResolver } from '@hookform/resolvers/zod';
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { Loader } from 'lucide-react';
-import { useForm } from 'react-hook-form';
-import type { z } from 'zod';
-
-import { trpc } from '@documenso/trpc/react';
-import { ZEditWebhookMutationSchema } from '@documenso/trpc/server/webhook-router/schema';
-import { Button } from '@documenso/ui/primitives/button';
-import {
- Form,
- FormControl,
- FormDescription,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from '@documenso/ui/primitives/form/form';
-import { Input } from '@documenso/ui/primitives/input';
-import { PasswordInput } from '@documenso/ui/primitives/password-input';
-import { Switch } from '@documenso/ui/primitives/switch';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
-import { TriggerMultiSelectCombobox } from '~/components/(dashboard)/settings/webhooks/trigger-multiselect-combobox';
-
-const ZEditWebhookFormSchema = ZEditWebhookMutationSchema.omit({ id: true });
-
-type TEditWebhookFormSchema = z.infer;
-
-export type WebhookPageOptions = {
- params: {
- id: string;
- };
-};
-
-export default function WebhookPage({ params }: WebhookPageOptions) {
- const { _ } = useLingui();
- const { toast } = useToast();
- const router = useRouter();
-
- const { data: webhook, isLoading } = trpc.webhook.getWebhookById.useQuery(
- {
- id: params.id,
- },
- { enabled: !!params.id },
- );
-
- const { mutateAsync: updateWebhook } = trpc.webhook.editWebhook.useMutation();
-
- const form = useForm({
- resolver: zodResolver(ZEditWebhookFormSchema),
- values: {
- webhookUrl: webhook?.webhookUrl ?? '',
- eventTriggers: webhook?.eventTriggers ?? [],
- secret: webhook?.secret ?? '',
- enabled: webhook?.enabled ?? true,
- },
- });
-
- const onSubmit = async (data: TEditWebhookFormSchema) => {
- try {
- await updateWebhook({
- id: params.id,
- ...data,
- });
-
- toast({
- title: _(msg`Webhook updated`),
- description: _(msg`The webhook has been updated successfully.`),
- duration: 5000,
- });
-
- router.refresh();
- } catch (err) {
- toast({
- title: _(msg`Failed to update webhook`),
- description: _(
- msg`We encountered an error while updating the webhook. Please try again later.`,
- ),
- variant: 'destructive',
- });
- }
- };
-
- return (
-
-
-
- {isLoading && (
-
-
-
- )}
-
-
-
-
-
-
- (
-
-
- Triggers
-
-
- {
- onChange(values);
- }}
- />
-
-
-
- The events that will trigger a webhook to be sent to your URL.
-
-
-
-
- )}
- />
-
- (
-
-
- Secret
-
-
-
-
-
-
-
- A secret that will be sent to your URL so you can verify that the request has
- been sent by Documenso.
-
-
-
-
- )}
- />
-
-
-
- Update webhook
-
-
-
-
-
-
- );
-}
diff --git a/apps/web/src/app/(dashboard)/settings/webhooks/page.tsx b/apps/web/src/app/(dashboard)/settings/webhooks/page.tsx
deleted file mode 100644
index 5da11e6f9..000000000
--- a/apps/web/src/app/(dashboard)/settings/webhooks/page.tsx
+++ /dev/null
@@ -1,111 +0,0 @@
-'use client';
-
-import Link from 'next/link';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { Loader } from 'lucide-react';
-import { DateTime } from 'luxon';
-
-import { toFriendlyWebhookEventName } from '@documenso/lib/universal/webhook/to-friendly-webhook-event-name';
-import { trpc } from '@documenso/trpc/react';
-import { cn } from '@documenso/ui/lib/utils';
-import { Badge } from '@documenso/ui/primitives/badge';
-import { Button } from '@documenso/ui/primitives/button';
-
-import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
-import { CreateWebhookDialog } from '~/components/(dashboard)/settings/webhooks/create-webhook-dialog';
-import { DeleteWebhookDialog } from '~/components/(dashboard)/settings/webhooks/delete-webhook-dialog';
-
-export default function WebhookPage() {
- const { _, i18n } = useLingui();
-
- const { data: webhooks, isLoading } = trpc.webhook.getWebhooks.useQuery();
-
- return (
-
-
-
-
-
- {isLoading && (
-
-
-
- )}
-
- {webhooks && webhooks.length === 0 && (
- // TODO: Perhaps add some illustrations here to make the page more engaging
-
-
-
- You have no webhooks yet. Your webhooks will be shown here once you create them.
-
-
-
- )}
-
- {webhooks && webhooks.length > 0 && (
-
- {webhooks?.map((webhook) => (
-
-
-
-
{webhook.id}
-
-
-
- {webhook.webhookUrl}
-
-
-
- {webhook.enabled ? Enabled : Disabled }
-
-
-
-
-
- Listening to{' '}
- {webhook.eventTriggers
- .map((trigger) => toFriendlyWebhookEventName(trigger))
- .join(', ')}
-
-
-
-
- Created on {i18n.date(webhook.createdAt, DateTime.DATETIME_FULL)}
-
-
-
-
-
-
- Edit
-
-
-
-
- Delete
-
-
-
-
-
- ))}
-
- )}
-
- );
-}
diff --git a/apps/web/src/app/(dashboard)/templates/[id]/edit/edit-template.tsx b/apps/web/src/app/(dashboard)/templates/[id]/edit/edit-template.tsx
deleted file mode 100644
index fc0439c00..000000000
--- a/apps/web/src/app/(dashboard)/templates/[id]/edit/edit-template.tsx
+++ /dev/null
@@ -1,304 +0,0 @@
-'use client';
-
-import { useEffect, useState } from 'react';
-
-import { useRouter } from 'next/navigation';
-
-import { msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-
-import { isValidLanguageCode } from '@documenso/lib/constants/i18n';
-import {
- DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
- SKIP_QUERY_BATCH_META,
-} from '@documenso/lib/constants/trpc';
-import type { TTemplate } from '@documenso/lib/types/template';
-import { trpc } from '@documenso/trpc/react';
-import { cn } from '@documenso/ui/lib/utils';
-import { Card, CardContent } from '@documenso/ui/primitives/card';
-import { DocumentFlowFormContainer } from '@documenso/ui/primitives/document-flow/document-flow-root';
-import type { DocumentFlowStep } from '@documenso/ui/primitives/document-flow/types';
-import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
-import { Stepper } from '@documenso/ui/primitives/stepper';
-import { AddTemplateFieldsFormPartial } from '@documenso/ui/primitives/template-flow/add-template-fields';
-import type { TAddTemplateFieldsFormSchema } from '@documenso/ui/primitives/template-flow/add-template-fields.types';
-import { AddTemplatePlaceholderRecipientsFormPartial } from '@documenso/ui/primitives/template-flow/add-template-placeholder-recipients';
-import type { TAddTemplatePlacholderRecipientsFormSchema } from '@documenso/ui/primitives/template-flow/add-template-placeholder-recipients.types';
-import { AddTemplateSettingsFormPartial } from '@documenso/ui/primitives/template-flow/add-template-settings';
-import type { TAddTemplateSettingsFormSchema } from '@documenso/ui/primitives/template-flow/add-template-settings.types';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-import { useOptionalCurrentTeam } from '~/providers/team';
-
-export type EditTemplateFormProps = {
- className?: string;
- initialTemplate: TTemplate;
- isEnterprise: boolean;
- templateRootPath: string;
-};
-
-type EditTemplateStep = 'settings' | 'signers' | 'fields';
-const EditTemplateSteps: EditTemplateStep[] = ['settings', 'signers', 'fields'];
-
-export const EditTemplateForm = ({
- initialTemplate,
- className,
- isEnterprise,
- templateRootPath,
-}: EditTemplateFormProps) => {
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const router = useRouter();
-
- const team = useOptionalCurrentTeam();
-
- const [step, setStep] = useState('settings');
-
- const [isDocumentPdfLoaded, setIsDocumentPdfLoaded] = useState(false);
-
- const utils = trpc.useUtils();
-
- const { data: template, refetch: refetchTemplate } = trpc.template.getTemplateById.useQuery(
- {
- templateId: initialTemplate.id,
- },
- {
- initialData: initialTemplate,
- ...SKIP_QUERY_BATCH_META,
- },
- );
-
- const { recipients, fields, templateDocumentData } = template;
-
- const documentFlow: Record = {
- settings: {
- title: msg`General`,
- description: msg`Configure general settings for the template.`,
- stepIndex: 1,
- },
- signers: {
- title: msg`Add Placeholders`,
- description: msg`Add all relevant placeholders for each recipient.`,
- stepIndex: 2,
- },
- fields: {
- title: msg`Add Fields`,
- description: msg`Add all relevant fields for each recipient.`,
- stepIndex: 3,
- },
- };
-
- const currentDocumentFlow = documentFlow[step];
-
- const { mutateAsync: updateTemplateSettings } = trpc.template.updateTemplate.useMutation({
- ...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
- onSuccess: (newData) => {
- utils.template.getTemplateById.setData(
- {
- templateId: initialTemplate.id,
- },
- (oldData) => ({ ...(oldData || initialTemplate), ...newData }),
- );
- },
- });
-
- const { mutateAsync: addTemplateFields } = trpc.field.addTemplateFields.useMutation({
- ...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
- onSuccess: (newData) => {
- utils.template.getTemplateById.setData(
- {
- templateId: initialTemplate.id,
- },
- (oldData) => ({ ...(oldData || initialTemplate), ...newData }),
- );
- },
- });
-
- const { mutateAsync: setRecipients } = trpc.recipient.setTemplateRecipients.useMutation({
- ...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
- onSuccess: (newData) => {
- utils.template.getTemplateById.setData(
- {
- templateId: initialTemplate.id,
- },
- (oldData) => ({ ...(oldData || initialTemplate), ...newData }),
- );
- },
- });
-
- const onAddSettingsFormSubmit = async (data: TAddTemplateSettingsFormSchema) => {
- try {
- await updateTemplateSettings({
- templateId: template.id,
- data: {
- title: data.title,
- externalId: data.externalId || null,
- visibility: data.visibility,
- globalAccessAuth: data.globalAccessAuth ?? null,
- globalActionAuth: data.globalActionAuth ?? null,
- },
- meta: {
- ...data.meta,
- language: isValidLanguageCode(data.meta.language) ? data.meta.language : undefined,
- },
- });
-
- // Router refresh is here to clear the router cache for when navigating to /documents.
- router.refresh();
-
- setStep('signers');
- } catch (err) {
- console.error(err);
-
- toast({
- title: _(msg`Error`),
- description: _(msg`An error occurred while updating the document settings.`),
- variant: 'destructive',
- });
- }
- };
-
- const onAddTemplatePlaceholderFormSubmit = async (
- data: TAddTemplatePlacholderRecipientsFormSchema,
- ) => {
- try {
- await Promise.all([
- updateTemplateSettings({
- templateId: template.id,
- meta: {
- signingOrder: data.signingOrder,
- },
- }),
-
- setRecipients({
- templateId: template.id,
- recipients: data.signers,
- }),
- ]);
-
- // Router refresh is here to clear the router cache for when navigating to /documents.
- router.refresh();
-
- setStep('fields');
- } catch (err) {
- toast({
- title: _(msg`Error`),
- description: _(msg`An error occurred while adding signers.`),
- variant: 'destructive',
- });
- }
- };
-
- const onAddFieldsFormSubmit = async (data: TAddTemplateFieldsFormSchema) => {
- try {
- await addTemplateFields({
- templateId: template.id,
- fields: data.fields,
- });
-
- await updateTemplateSettings({
- templateId: template.id,
- meta: {
- typedSignatureEnabled: data.typedSignatureEnabled,
- },
- });
-
- // Clear all field data from localStorage
- for (let i = 0; i < localStorage.length; i++) {
- const key = localStorage.key(i);
- if (key && key.startsWith('field_')) {
- localStorage.removeItem(key);
- }
- }
-
- toast({
- title: _(msg`Template saved`),
- description: _(msg`Your templates has been saved successfully.`),
- duration: 5000,
- });
-
- router.push(templateRootPath);
- } catch (err) {
- console.error(err);
-
- toast({
- title: _(msg`Error`),
- description: _(msg`An error occurred while adding fields.`),
- variant: 'destructive',
- });
- }
- };
-
- /**
- * Refresh the data in the background when steps change.
- */
- useEffect(() => {
- void refetchTemplate();
-
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [step]);
-
- return (
-
-
-
- setIsDocumentPdfLoaded(true)}
- />
-
-
-
-
-
e.preventDefault()}
- >
- setStep(EditTemplateSteps[step - 1])}
- >
-
-
-
-
-
-
-
-
-
- );
-};
diff --git a/apps/web/src/app/(dashboard)/templates/[id]/edit/page.tsx b/apps/web/src/app/(dashboard)/templates/[id]/edit/page.tsx
deleted file mode 100644
index ed463b2cf..000000000
--- a/apps/web/src/app/(dashboard)/templates/[id]/edit/page.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-import React from 'react';
-
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-
-import type { TemplateEditPageViewProps } from './template-edit-page-view';
-import { TemplateEditPageView } from './template-edit-page-view';
-
-type TemplateEditPageProps = Pick;
-
-export default async function TemplateEditPage({ params }: TemplateEditPageProps) {
- await setupI18nSSR();
-
- return ;
-}
diff --git a/apps/web/src/app/(dashboard)/templates/[id]/edit/template-edit-page-view.tsx b/apps/web/src/app/(dashboard)/templates/[id]/edit/template-edit-page-view.tsx
deleted file mode 100644
index 6f8cdd667..000000000
--- a/apps/web/src/app/(dashboard)/templates/[id]/edit/template-edit-page-view.tsx
+++ /dev/null
@@ -1,100 +0,0 @@
-import React from 'react';
-
-import Link from 'next/link';
-import { redirect } from 'next/navigation';
-
-import { Trans } from '@lingui/macro';
-import { ChevronLeft } from 'lucide-react';
-
-import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
-import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
-import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id';
-import { formatTemplatesPath } from '@documenso/lib/utils/teams';
-import type { Team } from '@documenso/prisma/client';
-
-import { TemplateType } from '~/components/formatter/template-type';
-
-import { TemplateDirectLinkBadge } from '../../template-direct-link-badge';
-import { TemplateDirectLinkDialogWrapper } from '../template-direct-link-dialog-wrapper';
-import { EditTemplateForm } from './edit-template';
-
-export type TemplateEditPageViewProps = {
- params: {
- id: string;
- };
- team?: Team;
-};
-
-export const TemplateEditPageView = async ({ params, team }: TemplateEditPageViewProps) => {
- const { id } = params;
-
- const templateId = Number(id);
- const templateRootPath = formatTemplatesPath(team?.url);
-
- if (!templateId || Number.isNaN(templateId)) {
- redirect(templateRootPath);
- }
-
- const { user } = await getRequiredServerComponentSession();
-
- const template = await getTemplateById({
- id: templateId,
- userId: user.id,
- teamId: team?.id,
- }).catch(() => null);
-
- if (!template || !template.templateDocumentData) {
- redirect(templateRootPath);
- }
-
- const isTemplateEnterprise = await isUserEnterprise({
- userId: user.id,
- teamId: team?.id,
- });
-
- return (
-
-
-
-
-
-
Template
-
-
-
- {template.title}
-
-
-
-
-
- {template.directLink?.token && (
-
- )}
-
-
-
-
-
-
-
-
-
-
- );
-};
diff --git a/apps/web/src/app/(dashboard)/templates/[id]/page.tsx b/apps/web/src/app/(dashboard)/templates/[id]/page.tsx
deleted file mode 100644
index 6133f5d7d..000000000
--- a/apps/web/src/app/(dashboard)/templates/[id]/page.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-
-import { TemplatePageView } from './template-page-view';
-
-export type TemplatePageProps = {
- params: {
- id: string;
- };
-};
-
-export default async function TemplatePage({ params }: TemplatePageProps) {
- await setupI18nSSR();
-
- return ;
-}
diff --git a/apps/web/src/app/(dashboard)/templates/[id]/template-direct-link-dialog-wrapper.tsx b/apps/web/src/app/(dashboard)/templates/[id]/template-direct-link-dialog-wrapper.tsx
deleted file mode 100644
index 3a00c302c..000000000
--- a/apps/web/src/app/(dashboard)/templates/[id]/template-direct-link-dialog-wrapper.tsx
+++ /dev/null
@@ -1,48 +0,0 @@
-'use client';
-
-import React, { useState } from 'react';
-
-import { Trans } from '@lingui/macro';
-import { LinkIcon } from 'lucide-react';
-
-import type { Recipient, Template, TemplateDirectLink } from '@documenso/prisma/client';
-import { Button } from '@documenso/ui/primitives/button';
-
-import { TemplateDirectLinkDialog } from '../template-direct-link-dialog';
-
-export type TemplateDirectLinkDialogWrapperProps = {
- template: Template & { directLink?: TemplateDirectLink | null; recipients: Recipient[] };
-};
-
-export const TemplateDirectLinkDialogWrapper = ({
- template,
-}: TemplateDirectLinkDialogWrapperProps) => {
- const [isTemplateDirectLinkOpen, setTemplateDirectLinkOpen] = useState(false);
-
- return (
-
- {
- e.preventDefault();
- setTemplateDirectLinkOpen(true);
- }}
- >
-
-
- {template.directLink ? (
- Manage Direct Link
- ) : (
- Create Direct Link
- )}
-
-
-
-
- );
-};
diff --git a/apps/web/src/app/(dashboard)/templates/[id]/template-page-view-documents-table.tsx b/apps/web/src/app/(dashboard)/templates/[id]/template-page-view-documents-table.tsx
deleted file mode 100644
index 4b4b1e57b..000000000
--- a/apps/web/src/app/(dashboard)/templates/[id]/template-page-view-documents-table.tsx
+++ /dev/null
@@ -1,275 +0,0 @@
-'use client';
-
-import { useMemo } from 'react';
-
-import { useSearchParams } from 'next/navigation';
-
-import type { MessageDescriptor } from '@lingui/core';
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { InfoIcon } from 'lucide-react';
-import { DateTime } from 'luxon';
-import { z } from 'zod';
-
-import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
-import { ZUrlSearchParamsSchema } from '@documenso/lib/types/search-params';
-import type { Team } from '@documenso/prisma/client';
-import { DocumentSource, DocumentStatus as DocumentStatusEnum } from '@documenso/prisma/client';
-import { trpc } from '@documenso/trpc/react';
-import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
-import { DataTable } from '@documenso/ui/primitives/data-table';
-import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
-import { SelectItem } from '@documenso/ui/primitives/select';
-import { Skeleton } from '@documenso/ui/primitives/skeleton';
-import { TableCell } from '@documenso/ui/primitives/table';
-import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
-
-import { StackAvatarsWithTooltip } from '~/components/(dashboard)/avatar/stack-avatars-with-tooltip';
-import { DocumentSearch } from '~/components/(dashboard)/document-search/document-search';
-import { PeriodSelector } from '~/components/(dashboard)/period-selector/period-selector';
-import { DocumentStatus } from '~/components/formatter/document-status';
-import { SearchParamSelector } from '~/components/forms/search-param-selector';
-
-import { DataTableActionButton } from '../../documents/data-table-action-button';
-import { DataTableActionDropdown } from '../../documents/data-table-action-dropdown';
-import { DataTableTitle } from '../../documents/data-table-title';
-
-const DOCUMENT_SOURCE_LABELS: { [key in DocumentSource]: MessageDescriptor } = {
- DOCUMENT: msg`Document`,
- TEMPLATE: msg`Template`,
- TEMPLATE_DIRECT_LINK: msg`Direct link`,
-};
-
-const ZDocumentSearchParamsSchema = ZUrlSearchParamsSchema.extend({
- source: z
- .nativeEnum(DocumentSource)
- .optional()
- .catch(() => undefined),
- status: z
- .nativeEnum(DocumentStatusEnum)
- .optional()
- .catch(() => undefined),
-});
-
-type TemplatePageViewDocumentsTableProps = {
- templateId: number;
- team?: Team;
-};
-
-export const TemplatePageViewDocumentsTable = ({
- templateId,
- team,
-}: TemplatePageViewDocumentsTableProps) => {
- const { _, i18n } = useLingui();
-
- const searchParams = useSearchParams();
- const updateSearchParams = useUpdateSearchParams();
-
- const parsedSearchParams = ZDocumentSearchParamsSchema.parse(
- Object.fromEntries(searchParams ?? []),
- );
-
- const { data, isLoading, isLoadingError } = trpc.document.findDocuments.useQuery(
- {
- templateId,
- page: parsedSearchParams.page,
- perPage: parsedSearchParams.perPage,
- query: parsedSearchParams.query,
- source: parsedSearchParams.source,
- status: parsedSearchParams.status,
- },
- {
- placeholderData: (previousData) => previousData,
- },
- );
-
- const onPaginationChange = (page: number, perPage: number) => {
- updateSearchParams({
- page,
- perPage,
- });
- };
-
- const results = data ?? {
- data: [],
- perPage: 10,
- currentPage: 1,
- totalPages: 1,
- };
-
- const columns = useMemo(() => {
- return [
- {
- header: _(msg`Created`),
- accessorKey: 'createdAt',
- cell: ({ row }) =>
- i18n.date(row.original.createdAt, { ...DateTime.DATETIME_SHORT, hourCycle: 'h12' }),
- },
- {
- header: _(msg`Title`),
- cell: ({ row }) => ,
- },
-
- {
- header: _(msg`Recipient`),
- accessorKey: 'recipient',
- cell: ({ row }) => (
-
- ),
- },
- {
- header: _(msg`Status`),
- accessorKey: 'status',
- cell: ({ row }) => ,
- size: 140,
- },
- {
- header: () => (
-
-
Source
-
-
-
-
-
-
-
-
-
-
- ),
- accessorKey: 'type',
- cell: ({ row }) => (
-
- {_(DOCUMENT_SOURCE_LABELS[row.original.source])}
-
- ),
- },
- {
- id: 'actions',
- header: _(msg`Actions`),
- cell: ({ row }) => (
-
-
-
-
-
- ),
- },
- ] satisfies DataTableColumnDef<(typeof results)['data'][number]>[];
- }, []);
-
- return (
-
-
-
-
-
- [...DocumentStatusEnum.COMPLETED].includes(value as unknown as string)
- }
- >
-
- Any Status
-
-
- Completed
-
-
- Pending
-
-
- Draft
-
-
-
-
- [...DocumentSource.TEMPLATE].includes(value as unknown as string)
- }
- >
-
- Any Source
-
-
- Template
-
-
- Direct Link
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- >
- ),
- }}
- >
- {(table) => }
-
-
- );
-};
diff --git a/apps/web/src/app/(dashboard)/templates/[id]/template-page-view-information.tsx b/apps/web/src/app/(dashboard)/templates/[id]/template-page-view-information.tsx
deleted file mode 100644
index 8f59ff028..000000000
--- a/apps/web/src/app/(dashboard)/templates/[id]/template-page-view-information.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-'use client';
-
-import { useMemo } from 'react';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { DateTime } from 'luxon';
-
-import { useIsMounted } from '@documenso/lib/client-only/hooks/use-is-mounted';
-import type { Template, User } from '@documenso/prisma/client';
-
-export type TemplatePageViewInformationProps = {
- userId: number;
- template: Template & {
- user: Pick;
- };
-};
-
-export const TemplatePageViewInformation = ({
- template,
- userId,
-}: TemplatePageViewInformationProps) => {
- const isMounted = useIsMounted();
-
- const { _, i18n } = useLingui();
-
- const templateInformation = useMemo(() => {
- return [
- {
- description: msg`Uploaded by`,
- value:
- userId === template.userId ? _(msg`You`) : (template.user.name ?? template.user.email),
- },
- {
- description: msg`Created`,
- value: i18n.date(template.createdAt, { dateStyle: 'medium' }),
- },
- {
- description: msg`Last modified`,
- value: DateTime.fromJSDate(template.updatedAt)
- .setLocale(i18n.locales?.[0] || i18n.locale)
- .toRelative(),
- },
- ];
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [isMounted, template, userId]);
-
- return (
-
-
- Information
-
-
-
- {templateInformation.map((item, i) => (
-
- {_(item.description)}
- {item.value}
-
- ))}
-
-
- );
-};
diff --git a/apps/web/src/app/(dashboard)/templates/[id]/template-page-view-recent-activity.tsx b/apps/web/src/app/(dashboard)/templates/[id]/template-page-view-recent-activity.tsx
deleted file mode 100644
index dbdc099ee..000000000
--- a/apps/web/src/app/(dashboard)/templates/[id]/template-page-view-recent-activity.tsx
+++ /dev/null
@@ -1,159 +0,0 @@
-'use client';
-
-import Link from 'next/link';
-
-import { Trans } from '@lingui/macro';
-import { Loader } from 'lucide-react';
-import { DateTime } from 'luxon';
-import { match } from 'ts-pattern';
-
-import { DocumentSource } from '@documenso/prisma/client';
-import { trpc } from '@documenso/trpc/react';
-import { cn } from '@documenso/ui/lib/utils';
-import { Button } from '@documenso/ui/primitives/button';
-
-export type TemplatePageViewRecentActivityProps = {
- templateId: number;
- teamId?: number;
- documentRootPath: string;
-};
-
-export const TemplatePageViewRecentActivity = ({
- templateId,
- documentRootPath,
-}: TemplatePageViewRecentActivityProps) => {
- const { data, isLoading, isLoadingError, refetch } = trpc.document.findDocuments.useQuery({
- templateId,
- orderByColumn: 'createdAt',
- orderByDirection: 'asc',
- perPage: 5,
- });
-
- const results = data ?? {
- data: [],
- perPage: 10,
- currentPage: 1,
- totalPages: 1,
- };
-
- return (
-
-
-
- Recent documents
-
-
- {/* Can add dropdown menu here for additional options. */}
-
-
- {isLoading && (
-
-
-
- )}
-
- {isLoadingError && (
-
-
- Unable to load documents
-
-
refetch()}
- className="text-foreground/70 hover:text-muted-foreground mt-2 text-sm"
- >
- Click here to retry
-
-
- )}
-
- {data && (
- <>
-
- {data.data.length > 0 && results.totalPages > 1 && (
-
-
-
-
-
- {
- window.scrollTo({
- top: document.getElementById('documents')?.offsetTop,
- behavior: 'smooth',
- });
- }}
- className="text-foreground/70 hover:text-muted-foreground flex items-center text-xs"
- >
- View more
-
-
- )}
-
- {results.data.length === 0 && (
-
-
- No recent documents
-
-
- )}
-
- {results.data.map((document, documentIndex) => (
-
-
-
-
-
-
- {match(document.source)
- .with(DocumentSource.DOCUMENT, DocumentSource.TEMPLATE, () => (
-
- Document created by {document.user.name}
-
- ))
- .with(DocumentSource.TEMPLATE_DIRECT_LINK, () => (
-
- Document created using a direct link
-
- ))
- .exhaustive()}
-
-
-
- {DateTime.fromJSDate(document.createdAt).toRelative({ style: 'short' })}
-
-
- ))}
-
-
- {
- window.scrollTo({
- top: document.getElementById('documents')?.offsetTop,
- behavior: 'smooth',
- });
- }}
- >
- View all related documents
-
- >
- )}
-
- );
-};
diff --git a/apps/web/src/app/(dashboard)/templates/[id]/template-page-view-recipients.tsx b/apps/web/src/app/(dashboard)/templates/[id]/template-page-view-recipients.tsx
deleted file mode 100644
index ecbfa9a7f..000000000
--- a/apps/web/src/app/(dashboard)/templates/[id]/template-page-view-recipients.tsx
+++ /dev/null
@@ -1,69 +0,0 @@
-import Link from 'next/link';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { PenIcon, PlusIcon } from 'lucide-react';
-
-import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
-import type { Recipient, Template } from '@documenso/prisma/client';
-import { AvatarWithText } from '@documenso/ui/primitives/avatar';
-
-export type TemplatePageViewRecipientsProps = {
- template: Template & {
- recipients: Recipient[];
- };
- templateRootPath: string;
-};
-
-export const TemplatePageViewRecipients = ({
- template,
- templateRootPath,
-}: TemplatePageViewRecipientsProps) => {
- const { _ } = useLingui();
-
- const recipients = template.recipients;
-
- return (
-
-
-
- Recipients
-
-
-
- {recipients.length === 0 ? (
-
- ) : (
-
- )}
-
-
-
-
- {recipients.length === 0 && (
-
- No recipients
-
- )}
-
- {recipients.map((recipient) => (
-
- {recipient.email}}
- secondaryText={
-
- {_(RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName)}
-
- }
- />
-
- ))}
-
-
- );
-};
diff --git a/apps/web/src/app/(dashboard)/templates/[id]/template-page-view.tsx b/apps/web/src/app/(dashboard)/templates/[id]/template-page-view.tsx
deleted file mode 100644
index 895eed438..000000000
--- a/apps/web/src/app/(dashboard)/templates/[id]/template-page-view.tsx
+++ /dev/null
@@ -1,204 +0,0 @@
-import Link from 'next/link';
-import { redirect } from 'next/navigation';
-
-import { Trans } from '@lingui/macro';
-import { ChevronLeft, LucideEdit } from 'lucide-react';
-
-import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
-import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id';
-import { formatDocumentsPath, formatTemplatesPath } from '@documenso/lib/utils/teams';
-import { DocumentSigningOrder, SigningStatus, type Team } from '@documenso/prisma/client';
-import { Button } from '@documenso/ui/primitives/button';
-import { Card, CardContent } from '@documenso/ui/primitives/card';
-import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
-
-import { DocumentReadOnlyFields } from '~/components/document/document-read-only-fields';
-import { TemplateType } from '~/components/formatter/template-type';
-
-import { DataTableActionDropdown } from '../data-table-action-dropdown';
-import { TemplateDirectLinkBadge } from '../template-direct-link-badge';
-import { UseTemplateDialog } from '../use-template-dialog';
-import { TemplateDirectLinkDialogWrapper } from './template-direct-link-dialog-wrapper';
-import { TemplatePageViewDocumentsTable } from './template-page-view-documents-table';
-import { TemplatePageViewInformation } from './template-page-view-information';
-import { TemplatePageViewRecentActivity } from './template-page-view-recent-activity';
-import { TemplatePageViewRecipients } from './template-page-view-recipients';
-
-export type TemplatePageViewProps = {
- params: {
- id: string;
- };
- team?: Team;
-};
-
-export const TemplatePageView = async ({ params, team }: TemplatePageViewProps) => {
- const { id } = params;
-
- const templateId = Number(id);
- const templateRootPath = formatTemplatesPath(team?.url);
- const documentRootPath = formatDocumentsPath(team?.url);
-
- if (!templateId || Number.isNaN(templateId)) {
- redirect(templateRootPath);
- }
-
- const { user } = await getRequiredServerComponentSession();
-
- const template = await getTemplateById({
- id: templateId,
- userId: user.id,
- teamId: team?.id,
- }).catch(() => null);
-
- if (!template || !template.templateDocumentData || (template?.teamId && !team?.url)) {
- redirect(templateRootPath);
- }
-
- const { templateDocumentData, fields, recipients, templateMeta } = template;
-
- // Remap to fit the DocumentReadOnlyFields component.
- const readOnlyFields = fields.map((field) => {
- const recipient = recipients.find((recipient) => recipient.id === field.recipientId) || {
- name: '',
- email: '',
- signingStatus: SigningStatus.NOT_SIGNED,
- };
-
- return {
- ...field,
- recipient,
- signature: null,
- };
- });
-
- const mockedDocumentMeta = templateMeta
- ? {
- ...templateMeta,
- signingOrder: templateMeta.signingOrder || DocumentSigningOrder.SEQUENTIAL,
- documentId: 0,
- }
- : undefined;
-
- return (
-
-
-
-
Templates
-
-
-
-
-
- {template.title}
-
-
-
-
-
- {template.directLink?.token && (
-
- )}
-
-
-
-
-
-
-
-
-
- Edit Template
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Manage and view template
-
-
-
-
- Use
-
- }
- />
-
-
-
- {/* Template information section. */}
-
-
- {/* Recipients section. */}
-
-
- {/* Recent activity section. */}
-
-
-
-
-
-
-
- Documents created from template
-
-
-
-
-
- );
-};
diff --git a/apps/web/src/app/(dashboard)/templates/data-table-action-dropdown.tsx b/apps/web/src/app/(dashboard)/templates/data-table-action-dropdown.tsx
deleted file mode 100644
index 28fac6118..000000000
--- a/apps/web/src/app/(dashboard)/templates/data-table-action-dropdown.tsx
+++ /dev/null
@@ -1,125 +0,0 @@
-'use client';
-
-import { useState } from 'react';
-
-import Link from 'next/link';
-
-import { Trans } from '@lingui/macro';
-import { Copy, Edit, MoreHorizontal, MoveRight, Share2Icon, Trash2 } from 'lucide-react';
-import { useSession } from 'next-auth/react';
-
-import type { Recipient, Template, TemplateDirectLink } from '@documenso/prisma/client';
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuLabel,
- DropdownMenuTrigger,
-} from '@documenso/ui/primitives/dropdown-menu';
-
-import { DeleteTemplateDialog } from './delete-template-dialog';
-import { DuplicateTemplateDialog } from './duplicate-template-dialog';
-import { MoveTemplateDialog } from './move-template-dialog';
-import { TemplateDirectLinkDialog } from './template-direct-link-dialog';
-
-export type DataTableActionDropdownProps = {
- row: Template & {
- directLink?: Pick | null;
- recipients: Recipient[];
- };
- templateRootPath: string;
- teamId?: number;
-};
-
-export const DataTableActionDropdown = ({
- row,
- templateRootPath,
- teamId,
-}: DataTableActionDropdownProps) => {
- const { data: session } = useSession();
-
- const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false);
- const [isTemplateDirectLinkDialogOpen, setTemplateDirectLinkDialogOpen] = useState(false);
- const [isDuplicateDialogOpen, setDuplicateDialogOpen] = useState(false);
- const [isMoveDialogOpen, setMoveDialogOpen] = useState(false);
-
- if (!session) {
- return null;
- }
-
- const isOwner = row.userId === session.user.id;
- const isTeamTemplate = row.teamId === teamId;
-
- return (
-
-
-
-
-
-
- Action
-
-
-
-
- Edit
-
-
-
- setDuplicateDialogOpen(true)}
- >
-
- Duplicate
-
-
- setTemplateDirectLinkDialogOpen(true)}>
-
- Direct link
-
-
- {!teamId && (
- setMoveDialogOpen(true)}>
-
- Move to Team
-
- )}
-
- setDeleteDialogOpen(true)}
- >
-
- Delete
-
-
-
-
-
-
-
-
-
-
-
- );
-};
diff --git a/apps/web/src/app/(dashboard)/templates/data-table-templates.tsx b/apps/web/src/app/(dashboard)/templates/data-table-templates.tsx
deleted file mode 100644
index d198cdab5..000000000
--- a/apps/web/src/app/(dashboard)/templates/data-table-templates.tsx
+++ /dev/null
@@ -1,211 +0,0 @@
-'use client';
-
-import { useMemo, useTransition } from 'react';
-
-import Link from 'next/link';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { AlertTriangle, Globe2Icon, InfoIcon, Link2Icon, Loader, LockIcon } from 'lucide-react';
-
-import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
-import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
-import type { FindTemplateRow } from '@documenso/trpc/server/template-router/schema';
-import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
-import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
-import { DataTable } from '@documenso/ui/primitives/data-table';
-import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
-import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
-
-import { TemplateType } from '~/components/formatter/template-type';
-
-import { DataTableActionDropdown } from './data-table-action-dropdown';
-import { DataTableTitle } from './data-table-title';
-import { TemplateDirectLinkBadge } from './template-direct-link-badge';
-import { UseTemplateDialog } from './use-template-dialog';
-
-type TemplatesDataTableProps = {
- templates: FindTemplateRow[];
- perPage: number;
- page: number;
- totalPages: number;
- documentRootPath: string;
- templateRootPath: string;
- teamId?: number;
-};
-
-export const TemplatesDataTable = ({
- templates,
- perPage,
- page,
- totalPages,
- documentRootPath,
- templateRootPath,
- teamId,
-}: TemplatesDataTableProps) => {
- const [isPending, startTransition] = useTransition();
-
- const updateSearchParams = useUpdateSearchParams();
-
- const { _, i18n } = useLingui();
- const { remaining } = useLimits();
-
- const columns = useMemo(() => {
- return [
- {
- header: _(msg`Created`),
- accessorKey: 'createdAt',
- cell: ({ row }) => i18n.date(row.original.createdAt),
- },
- {
- header: _(msg`Title`),
- cell: ({ row }) => ,
- },
- {
- header: () => (
-
-
Type
-
-
-
-
-
-
-
-
-
-
- Public
-
-
-
-
- Public templates are connected to your public profile. Any modifications to
- public templates will also appear in your public profile.
-
-
-
-
-
-
- direct link
-
-
-
-
- Direct link templates contain one dynamic recipient placeholder. Anyone with
- access to this link can sign the document, and it will then appear on your
- documents page.
-
-
-
-
-
-
- {teamId ? Team Only : Private }
-
-
-
- {teamId ? (
-
- Team only templates are not linked anywhere and are visible only to your
- team.
-
- ) : (
- Private templates can only be modified and viewed by you.
- )}
-
-
-
-
-
-
- ),
- accessorKey: 'type',
- cell: ({ row }) => (
-
-
-
- {row.original.directLink?.token && (
-
- )}
-
- ),
- },
- {
- header: _(msg`Actions`),
- accessorKey: 'actions',
- cell: ({ row }) => {
- return (
-
-
-
-
-
- );
- },
- },
- ] satisfies DataTableColumnDef<(typeof templates)[number]>[];
- }, [documentRootPath, teamId, templateRootPath]);
-
- const onPaginationChange = (page: number, perPage: number) => {
- startTransition(() => {
- updateSearchParams({
- page,
- perPage,
- });
- });
- };
-
- return (
-
- {remaining.documents === 0 && (
-
-
-
- Document Limit Exceeded!
-
-
-
- You have reached your document limit.{' '}
-
- Upgrade your account to continue!
-
-
-
-
- )}
-
-
- {(table) => }
-
-
- {isPending && (
-
-
-
- )}
-
- );
-};
diff --git a/apps/web/src/app/(dashboard)/templates/data-table-title.tsx b/apps/web/src/app/(dashboard)/templates/data-table-title.tsx
deleted file mode 100644
index 69855ca1e..000000000
--- a/apps/web/src/app/(dashboard)/templates/data-table-title.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-'use client';
-
-import Link from 'next/link';
-
-import { useSession } from 'next-auth/react';
-
-import { formatTemplatesPath } from '@documenso/lib/utils/teams';
-import type { Template } from '@documenso/prisma/client';
-
-import { useOptionalCurrentTeam } from '~/providers/team';
-
-export type DataTableTitleProps = {
- row: Template & {
- team: { id: number; url: string } | null;
- };
-};
-
-export const DataTableTitle = ({ row }: DataTableTitleProps) => {
- const { data: session } = useSession();
- const team = useOptionalCurrentTeam();
-
- if (!session) {
- return null;
- }
-
- const isCurrentTeamTemplate = team?.url && row.team?.url === team?.url;
-
- const templatesPath = formatTemplatesPath(isCurrentTeamTemplate ? team?.url : undefined);
-
- return (
-
- {row.title}
-
- );
-};
diff --git a/apps/web/src/app/(dashboard)/templates/delete-template-dialog.tsx b/apps/web/src/app/(dashboard)/templates/delete-template-dialog.tsx
deleted file mode 100644
index f5a4750b4..000000000
--- a/apps/web/src/app/(dashboard)/templates/delete-template-dialog.tsx
+++ /dev/null
@@ -1,91 +0,0 @@
-import { useRouter } from 'next/navigation';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-
-import { trpc as trpcReact } from '@documenso/trpc/react';
-import { Button } from '@documenso/ui/primitives/button';
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from '@documenso/ui/primitives/dialog';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-type DeleteTemplateDialogProps = {
- id: number;
- teamId?: number;
- open: boolean;
- onOpenChange: (_open: boolean) => void;
-};
-
-export const DeleteTemplateDialog = ({ id, open, onOpenChange }: DeleteTemplateDialogProps) => {
- const router = useRouter();
-
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const { mutateAsync: deleteTemplate, isPending } = trpcReact.template.deleteTemplate.useMutation({
- onSuccess: () => {
- router.refresh();
-
- toast({
- title: _(msg`Template deleted`),
- description: _(msg`Your template has been successfully deleted.`),
- duration: 5000,
- });
-
- onOpenChange(false);
- },
- onError: () => {
- toast({
- title: _(msg`Something went wrong`),
- description: _(msg`This template could not be deleted at this time. Please try again.`),
- variant: 'destructive',
- duration: 7500,
- });
- },
- });
-
- return (
- !isPending && onOpenChange(value)}>
-
-
-
- Do you want to delete this template?
-
-
-
-
- Please note that this action is irreversible. Once confirmed, your template will be
- permanently deleted.
-
-
-
-
-
- onOpenChange(false)}
- >
- Cancel
-
-
- deleteTemplate({ templateId: id })}
- >
- Delete
-
-
-
-
- );
-};
diff --git a/apps/web/src/app/(dashboard)/templates/duplicate-template-dialog.tsx b/apps/web/src/app/(dashboard)/templates/duplicate-template-dialog.tsx
deleted file mode 100644
index 34beee309..000000000
--- a/apps/web/src/app/(dashboard)/templates/duplicate-template-dialog.tsx
+++ /dev/null
@@ -1,95 +0,0 @@
-import { useRouter } from 'next/navigation';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-
-import { trpc as trpcReact } from '@documenso/trpc/react';
-import { Button } from '@documenso/ui/primitives/button';
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from '@documenso/ui/primitives/dialog';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-type DuplicateTemplateDialogProps = {
- id: number;
- teamId?: number;
- open: boolean;
- onOpenChange: (_open: boolean) => void;
-};
-
-export const DuplicateTemplateDialog = ({
- id,
- open,
- onOpenChange,
-}: DuplicateTemplateDialogProps) => {
- const router = useRouter();
-
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const { mutateAsync: duplicateTemplate, isPending } =
- trpcReact.template.duplicateTemplate.useMutation({
- onSuccess: () => {
- router.refresh();
-
- toast({
- title: _(msg`Template duplicated`),
- description: _(msg`Your template has been duplicated successfully.`),
- duration: 5000,
- });
-
- onOpenChange(false);
- },
- onError: () => {
- toast({
- title: _(msg`Error`),
- description: _(msg`An error occurred while duplicating template.`),
- variant: 'destructive',
- });
- },
- });
-
- return (
- !isPending && onOpenChange(value)}>
-
-
-
- Do you want to duplicate this template?
-
-
-
- Your template will be duplicated.
-
-
-
-
- onOpenChange(false)}
- >
- Cancel
-
-
-
- duplicateTemplate({
- templateId: id,
- })
- }
- >
- Duplicate
-
-
-
-
- );
-};
diff --git a/apps/web/src/app/(dashboard)/templates/empty-state.tsx b/apps/web/src/app/(dashboard)/templates/empty-state.tsx
deleted file mode 100644
index a16460e88..000000000
--- a/apps/web/src/app/(dashboard)/templates/empty-state.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import { Trans } from '@lingui/macro';
-import { Bird } from 'lucide-react';
-
-export const EmptyTemplateState = () => {
- return (
-
-
-
-
-
- We're all empty
-
-
-
-
- You have not yet created any templates. To create a template please upload one.
-
-
-
-
- );
-};
diff --git a/apps/web/src/app/(dashboard)/templates/move-template-dialog.tsx b/apps/web/src/app/(dashboard)/templates/move-template-dialog.tsx
deleted file mode 100644
index 9a00b9d5b..000000000
--- a/apps/web/src/app/(dashboard)/templates/move-template-dialog.tsx
+++ /dev/null
@@ -1,140 +0,0 @@
-import { useState } from 'react';
-
-import { useRouter } from 'next/navigation';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { match } from 'ts-pattern';
-
-import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
-import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
-import { trpc } from '@documenso/trpc/react';
-import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
-import { Button } from '@documenso/ui/primitives/button';
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from '@documenso/ui/primitives/dialog';
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from '@documenso/ui/primitives/select';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-type MoveTemplateDialogProps = {
- templateId: number;
- open: boolean;
- onOpenChange: (_open: boolean) => void;
-};
-
-export const MoveTemplateDialog = ({ templateId, open, onOpenChange }: MoveTemplateDialogProps) => {
- const router = useRouter();
-
- const { toast } = useToast();
- const { _ } = useLingui();
-
- const [selectedTeamId, setSelectedTeamId] = useState(null);
-
- const { data: teams, isLoading: isLoadingTeams } = trpc.team.getTeams.useQuery();
- const { mutateAsync: moveTemplate, isPending } = trpc.template.moveTemplateToTeam.useMutation({
- onSuccess: () => {
- router.refresh();
- toast({
- title: _(msg`Template moved`),
- description: _(msg`The template has been successfully moved to the selected team.`),
- duration: 5000,
- });
- onOpenChange(false);
- },
- onError: (err) => {
- const error = AppError.parseError(err);
-
- const errorMessage = match(error.code)
- .with(
- AppErrorCode.NOT_FOUND,
- () => msg`Template not found or already associated with a team.`,
- )
- .with(AppErrorCode.UNAUTHORIZED, () => msg`You are not a member of this team.`)
- .otherwise(() => msg`An error occurred while moving the template.`);
-
- toast({
- title: _(msg`Error`),
- description: _(errorMessage),
- variant: 'destructive',
- duration: 7500,
- });
- },
- });
-
- const onMove = async () => {
- if (!selectedTeamId) {
- return;
- }
-
- await moveTemplate({ templateId, teamId: selectedTeamId });
- };
-
- return (
-
-
-
-
- Move Template to Team
-
-
- Select a team to move this template to. This action cannot be undone.
-
-
-
- setSelectedTeamId(Number(value))}>
-
-
-
-
- {isLoadingTeams ? (
-
- Loading teams...
-
- ) : (
- teams?.map((team) => (
-
-
-
- {team.avatarImageId && (
-
- )}
-
-
- {team.name.slice(0, 1).toUpperCase()}
-
-
-
-
{team.name}
-
-
- ))
- )}
-
-
-
-
- onOpenChange(false)}>
- Cancel
-
-
- {isPending ? Moving... : Move }
-
-
-
-
- );
-};
diff --git a/apps/web/src/app/(dashboard)/templates/new-template-dialog.tsx b/apps/web/src/app/(dashboard)/templates/new-template-dialog.tsx
deleted file mode 100644
index f6e31c3e0..000000000
--- a/apps/web/src/app/(dashboard)/templates/new-template-dialog.tsx
+++ /dev/null
@@ -1,132 +0,0 @@
-'use client';
-
-import React, { useState } from 'react';
-
-import { useRouter } from 'next/navigation';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { FilePlus, Loader } from 'lucide-react';
-import { useSession } from 'next-auth/react';
-
-import { createDocumentData } from '@documenso/lib/server-only/document-data/create-document-data';
-import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
-import { trpc } from '@documenso/trpc/react';
-import { Button } from '@documenso/ui/primitives/button';
-import {
- Dialog,
- DialogClose,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from '@documenso/ui/primitives/dialog';
-import { DocumentDropzone } from '@documenso/ui/primitives/document-dropzone';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-type NewTemplateDialogProps = {
- teamId?: number;
- templateRootPath: string;
-};
-
-export const NewTemplateDialog = ({ templateRootPath }: NewTemplateDialogProps) => {
- const router = useRouter();
-
- const { data: session } = useSession();
- const { toast } = useToast();
- const { _ } = useLingui();
-
- const { mutateAsync: createTemplate } = trpc.template.createTemplate.useMutation();
-
- const [showNewTemplateDialog, setShowNewTemplateDialog] = useState(false);
- const [isUploadingFile, setIsUploadingFile] = useState(false);
-
- const onFileDrop = async (file: File) => {
- if (isUploadingFile) {
- return;
- }
-
- setIsUploadingFile(true);
-
- try {
- const { type, data } = await putPdfFile(file);
- const { id: templateDocumentDataId } = await createDocumentData({
- type,
- data,
- });
-
- const { id } = await createTemplate({
- title: file.name,
- templateDocumentDataId,
- });
-
- toast({
- title: _(msg`Template document uploaded`),
- description: _(
- msg`Your document has been uploaded successfully. You will be redirected to the template page.`,
- ),
- duration: 5000,
- });
-
- setShowNewTemplateDialog(false);
-
- router.push(`${templateRootPath}/${id}/edit`);
- } catch {
- toast({
- title: _(msg`Something went wrong`),
- description: _(msg`Please try again later.`),
- variant: 'destructive',
- });
-
- setIsUploadingFile(false);
- }
- };
-
- return (
- !isUploadingFile && setShowNewTemplateDialog(value)}
- >
-
-
-
- New Template
-
-
-
-
-
-
- New Template
-
-
-
- Templates allow you to quickly generate documents with pre-filled recipients and
- fields.
-
-
-
-
-
-
-
- {isUploadingFile && (
-
-
-
- )}
-
-
-
-
-
- Close
-
-
-
-
-
- );
-};
diff --git a/apps/web/src/app/(dashboard)/templates/page.tsx b/apps/web/src/app/(dashboard)/templates/page.tsx
deleted file mode 100644
index 886eba88e..000000000
--- a/apps/web/src/app/(dashboard)/templates/page.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import React from 'react';
-
-import type { Metadata } from 'next';
-
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-
-import { TemplatesPageView } from './templates-page-view';
-import type { TemplatesPageViewProps } from './templates-page-view';
-
-type TemplatesPageProps = {
- searchParams?: TemplatesPageViewProps['searchParams'];
-};
-
-export const metadata: Metadata = {
- title: 'Templates',
-};
-
-export default async function TemplatesPage({ searchParams = {} }: TemplatesPageProps) {
- await setupI18nSSR();
-
- return ;
-}
diff --git a/apps/web/src/app/(dashboard)/templates/template-direct-link-badge.tsx b/apps/web/src/app/(dashboard)/templates/template-direct-link-badge.tsx
deleted file mode 100644
index 66dee8643..000000000
--- a/apps/web/src/app/(dashboard)/templates/template-direct-link-badge.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-'use client';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { Link2Icon } from 'lucide-react';
-
-import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard';
-import { formatDirectTemplatePath } from '@documenso/lib/utils/templates';
-import { cn } from '@documenso/ui/lib/utils';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-type TemplateDirectLinkBadgeProps = {
- token: string;
- enabled: boolean;
- className?: string;
-};
-
-export const TemplateDirectLinkBadge = ({
- token,
- enabled,
- className,
-}: TemplateDirectLinkBadgeProps) => {
- const [, copy] = useCopyToClipboard();
-
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const onCopyClick = async (token: string) =>
- copy(formatDirectTemplatePath(token)).then(() => {
- toast({
- title: _(msg`Copied to clipboard`),
- description: _(msg`The direct link has been copied to your clipboard`),
- });
- });
-
- return (
- onCopyClick(token)}
- >
-
- {enabled ? direct link : direct link disabled }
-
- );
-};
diff --git a/apps/web/src/app/(dashboard)/templates/template-direct-link-dialog.tsx b/apps/web/src/app/(dashboard)/templates/template-direct-link-dialog.tsx
deleted file mode 100644
index f603f20be..000000000
--- a/apps/web/src/app/(dashboard)/templates/template-direct-link-dialog.tsx
+++ /dev/null
@@ -1,485 +0,0 @@
-import { useEffect, useMemo, useState } from 'react';
-
-import Link from 'next/link';
-import { useRouter } from 'next/navigation';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { CircleDotIcon, CircleIcon, ClipboardCopyIcon, InfoIcon, LoaderIcon } from 'lucide-react';
-import { P, match } from 'ts-pattern';
-
-import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
-import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard';
-import { DIRECT_TEMPLATE_RECIPIENT_EMAIL } from '@documenso/lib/constants/direct-templates';
-import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
-import { DIRECT_TEMPLATE_DOCUMENTATION } from '@documenso/lib/constants/template';
-import { formatDirectTemplatePath } from '@documenso/lib/utils/templates';
-import {
- type Recipient,
- RecipientRole,
- type Template,
- type TemplateDirectLink,
-} from '@documenso/prisma/client';
-import { trpc as trpcReact } from '@documenso/trpc/react';
-import { AnimateGenericFadeInOut } from '@documenso/ui/components/animate/animate-generic-fade-in-out';
-import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
-import { Button } from '@documenso/ui/primitives/button';
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from '@documenso/ui/primitives/dialog';
-import { Input } from '@documenso/ui/primitives/input';
-import { Label } from '@documenso/ui/primitives/label';
-import { Switch } from '@documenso/ui/primitives/switch';
-import {
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeader,
- TableRow,
-} from '@documenso/ui/primitives/table';
-import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-type TemplateDirectLinkDialogProps = {
- template: Template & {
- directLink?: Pick | null;
- recipients: Recipient[];
- };
- open: boolean;
- onOpenChange: (_open: boolean) => void;
-};
-
-type TemplateDirectLinkStep = 'ONBOARD' | 'SELECT_RECIPIENT' | 'MANAGE' | 'CONFIRM_DELETE';
-
-export const TemplateDirectLinkDialog = ({
- template,
- open,
- onOpenChange,
-}: TemplateDirectLinkDialogProps) => {
- const { toast } = useToast();
- const { quota, remaining } = useLimits();
- const { _ } = useLingui();
-
- const [, copy] = useCopyToClipboard();
- const router = useRouter();
-
- const [isEnabled, setIsEnabled] = useState(template.directLink?.enabled ?? false);
- const [token, setToken] = useState(template.directLink?.token ?? null);
- const [selectedRecipientId, setSelectedRecipientId] = useState(null);
- const [currentStep, setCurrentStep] = useState(
- token ? 'MANAGE' : 'ONBOARD',
- );
-
- const validDirectTemplateRecipients = useMemo(
- () => template.recipients.filter((recipient) => recipient.role !== RecipientRole.CC),
- [template.recipients],
- );
-
- const {
- mutateAsync: createTemplateDirectLink,
- isPending: isCreatingTemplateDirectLink,
- reset: resetCreateTemplateDirectLink,
- } = trpcReact.template.createTemplateDirectLink.useMutation({
- onSuccess: (data) => {
- setToken(data.token);
- setIsEnabled(data.enabled);
- setCurrentStep('MANAGE');
-
- router.refresh();
- },
- onError: () => {
- setSelectedRecipientId(null);
-
- toast({
- title: _(msg`Something went wrong`),
- description: _(msg`Unable to create direct template access. Please try again later.`),
- variant: 'destructive',
- });
- },
- });
-
- const { mutateAsync: toggleTemplateDirectLink, isPending: isTogglingTemplateAccess } =
- trpcReact.template.toggleTemplateDirectLink.useMutation({
- onSuccess: (data) => {
- const enabledDescription = msg`Direct link signing has been enabled`;
- const disabledDescription = msg`Direct link signing has been disabled`;
-
- toast({
- title: _(msg`Success`),
- description: _(data.enabled ? enabledDescription : disabledDescription),
- });
- },
- onError: (_ctx, data) => {
- const enabledDescription = msg`An error occurred while enabling direct link signing.`;
- const disabledDescription = msg`An error occurred while disabling direct link signing.`;
-
- toast({
- title: _(msg`Something went wrong`),
- description: _(data.enabled ? enabledDescription : disabledDescription),
- variant: 'destructive',
- });
- },
- });
-
- const { mutateAsync: deleteTemplateDirectLink, isPending: isDeletingTemplateDirectLink } =
- trpcReact.template.deleteTemplateDirectLink.useMutation({
- onSuccess: () => {
- onOpenChange(false);
- setToken(null);
-
- toast({
- title: _(msg`Success`),
- description: _(msg`Direct template link deleted`),
- duration: 5000,
- });
-
- router.refresh();
- setToken(null);
- },
- onError: () => {
- toast({
- title: _(msg`Something went wrong`),
- description: _(
- msg`We encountered an error while removing the direct template link. Please try again later.`,
- ),
- variant: 'destructive',
- });
- },
- });
-
- const onCopyClick = async (token: string) =>
- copy(formatDirectTemplatePath(token)).then(() => {
- toast({
- title: _(msg`Copied to clipboard`),
- description: _(msg`The direct link has been copied to your clipboard`),
- });
- });
-
- const onRecipientTableRowClick = async (recipientId: number) => {
- if (isLoading) {
- return;
- }
-
- setSelectedRecipientId(recipientId);
-
- await createTemplateDirectLink({
- templateId: template.id,
- directRecipientId: recipientId,
- });
- };
-
- const isLoading =
- isCreatingTemplateDirectLink || isTogglingTemplateAccess || isDeletingTemplateDirectLink;
-
- useEffect(() => {
- resetCreateTemplateDirectLink();
- setCurrentStep(token ? 'MANAGE' : 'ONBOARD');
- setSelectedRecipientId(null);
-
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [open]);
-
- return (
- !isLoading && onOpenChange(value)}>
-
-
- {match({ token, currentStep })
- .with({ token: P.nullish, currentStep: 'ONBOARD' }, () => (
-
-
-
- Create Direct Signing Link
-
-
-
- Here's how it works:
-
-
-
-
- {DIRECT_TEMPLATE_DOCUMENTATION.map((step, index) => (
-
-
-
- {_(step.title)}
- {_(step.description)}
-
- ))}
-
-
- {remaining.directTemplates === 0 && (
-
-
-
- Direct template link usage exceeded ({quota.directTemplates}/
- {quota.directTemplates})
-
-
-
-
- You have reached the maximum limit of {quota.directTemplates} direct
- templates.{' '}
-
- Upgrade your account to continue!
-
-
-
-
- )}
-
- {remaining.directTemplates !== 0 && (
-
- setCurrentStep('SELECT_RECIPIENT')}>
- Enable direct link signing
-
-
- )}
-
- ))
- .with({ token: P.nullish, currentStep: 'SELECT_RECIPIENT' }, () => (
-
- {isCreatingTemplateDirectLink && validDirectTemplateRecipients.length !== 0 && (
-
-
-
- )}
-
-
-
- Choose Direct Link Recipient
-
-
-
- Choose an existing recipient from below to continue
-
-
-
-
-
-
-
-
- Recipient
-
-
- Role
-
-
-
-
-
- {validDirectTemplateRecipients.length === 0 && (
-
-
-
- No valid recipients found
-
-
-
- )}
-
- {validDirectTemplateRecipients.map((row) => (
- onRecipientTableRowClick(row.id)}
- >
-
-
-
{row.name}
-
{row.email}
-
-
-
-
- {_(RECIPIENT_ROLES_DESCRIPTION[row.role].roleName)}
-
-
-
- {selectedRecipientId === row.id ? (
-
- ) : (
-
- )}
-
-
- ))}
-
-
-
-
- {/* Prevent creating placeholder direct template recipient if the email already exists. */}
- {!template.recipients.some(
- (recipient) => recipient.email === DIRECT_TEMPLATE_RECIPIENT_EMAIL,
- ) && (
-
-
- {validDirectTemplateRecipients.length !== 0 && (
-
- Or
-
- )}
-
-
- createTemplateDirectLink({
- templateId: template.id,
- })
- }
- >
- Create one automatically
-
-
-
- )}
-
- ))
- .with({ token: P.string, currentStep: 'MANAGE' }, ({ token }) => (
-
-
-
- Direct Link Signing
-
-
-
- Manage the direct link signing for this template
-
-
-
-
-
-
- Enable Direct Link Signing
-
-
-
-
-
-
- Disabling direct link signing will prevent anyone from accessing the
- link.
-
-
-
-
-
- setIsEnabled(value)}
- />
-
-
-
-
- Copy Shareable Link
-
-
-
-
-
-
- void onCopyClick(token)}
- >
-
-
-
-
-
-
-
-
- setCurrentStep('CONFIRM_DELETE')}
- >
- Remove
-
-
- {
- await toggleTemplateDirectLink({
- templateId: template.id,
- enabled: isEnabled,
- }).catch((e) => null);
-
- onOpenChange(false);
- }}
- >
- Save
-
-
-
- ))
- .with({ token: P.string, currentStep: 'CONFIRM_DELETE' }, () => (
-
-
-
- Are you sure?
-
-
-
-
- Please note that proceeding will remove direct linking recipient and turn it
- into a placeholder.
-
-
-
-
-
- setCurrentStep('MANAGE')}
- >
- Cancel
-
-
- void deleteTemplateDirectLink({ templateId: template.id })}
- >
- Confirm
-
-
-
- ))
- .otherwise(() => null)}
-
-
-
- );
-};
diff --git a/apps/web/src/app/(dashboard)/templates/templates-page-view.tsx b/apps/web/src/app/(dashboard)/templates/templates-page-view.tsx
deleted file mode 100644
index b1d95c426..000000000
--- a/apps/web/src/app/(dashboard)/templates/templates-page-view.tsx
+++ /dev/null
@@ -1,81 +0,0 @@
-import React from 'react';
-
-import { Trans } from '@lingui/macro';
-
-import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
-import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
-import { findTemplates } from '@documenso/lib/server-only/template/find-templates';
-import { formatDocumentsPath, formatTemplatesPath } from '@documenso/lib/utils/teams';
-import type { Team } from '@documenso/prisma/client';
-import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
-
-import { TemplatesDataTable } from './data-table-templates';
-import { EmptyTemplateState } from './empty-state';
-import { NewTemplateDialog } from './new-template-dialog';
-
-export type TemplatesPageViewProps = {
- searchParams?: {
- page?: number;
- perPage?: number;
- };
- team?: Team;
-};
-
-export const TemplatesPageView = async ({ searchParams = {}, team }: TemplatesPageViewProps) => {
- const { user } = await getRequiredServerComponentSession();
- const page = Number(searchParams.page) || 1;
- const perPage = Number(searchParams.perPage) || 10;
-
- const documentRootPath = formatDocumentsPath(team?.url);
- const templateRootPath = formatTemplatesPath(team?.url);
-
- const { data: templates, totalPages } = await findTemplates({
- userId: user.id,
- teamId: team?.id,
- page: page,
- perPage: perPage,
- });
-
- return (
-
-
-
- {team && (
-
- {team.avatarImageId && (
-
- )}
-
- {team.name.slice(0, 1)}
-
-
- )}
-
-
- Templates
-
-
-
-
-
-
-
-
-
- {templates.length > 0 ? (
-
- ) : (
-
- )}
-
-
- );
-};
diff --git a/apps/web/src/app/(dashboard)/templates/use-template-dialog.tsx b/apps/web/src/app/(dashboard)/templates/use-template-dialog.tsx
deleted file mode 100644
index 6c7508327..000000000
--- a/apps/web/src/app/(dashboard)/templates/use-template-dialog.tsx
+++ /dev/null
@@ -1,576 +0,0 @@
-'use client';
-
-import { useEffect, useState } from 'react';
-
-import { useRouter } from 'next/navigation';
-
-import { zodResolver } from '@hookform/resolvers/zod';
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { InfoIcon, Plus, Upload, X } from 'lucide-react';
-import { useFieldArray, useForm } from 'react-hook-form';
-import * as z from 'zod';
-
-import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
-import {
- TEMPLATE_RECIPIENT_EMAIL_PLACEHOLDER_REGEX,
- TEMPLATE_RECIPIENT_NAME_PLACEHOLDER_REGEX,
-} from '@documenso/lib/constants/template';
-import { AppError } from '@documenso/lib/errors/app-error';
-import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
-import type { Recipient } from '@documenso/prisma/client';
-import { DocumentDistributionMethod, DocumentSigningOrder } from '@documenso/prisma/client';
-import { trpc } from '@documenso/trpc/react';
-import { cn } from '@documenso/ui/lib/utils';
-import { Button } from '@documenso/ui/primitives/button';
-import { Checkbox } from '@documenso/ui/primitives/checkbox';
-import {
- Dialog,
- DialogClose,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from '@documenso/ui/primitives/dialog';
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from '@documenso/ui/primitives/form/form';
-import { Input } from '@documenso/ui/primitives/input';
-import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
-import type { Toast } from '@documenso/ui/primitives/use-toast';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-const ZAddRecipientsForNewDocumentSchema = z
- .object({
- distributeDocument: z.boolean(),
- useCustomDocument: z.boolean().default(false),
- customDocumentData: z
- .any()
- .refine((data) => data instanceof File || data === undefined)
- .optional(),
- recipients: z.array(
- z.object({
- id: z.number(),
- email: z.string().email(),
- name: z.string(),
- signingOrder: z.number().optional(),
- }),
- ),
- })
- // Display exactly which rows are duplicates.
- .superRefine((items, ctx) => {
- const uniqueEmails = new Map();
-
- for (const [index, recipients] of items.recipients.entries()) {
- const email = recipients.email.toLowerCase();
-
- const firstFoundIndex = uniqueEmails.get(email);
-
- if (firstFoundIndex === undefined) {
- uniqueEmails.set(email, index);
- continue;
- }
-
- ctx.addIssue({
- code: z.ZodIssueCode.custom,
- message: 'Emails must be unique',
- path: ['recipients', index, 'email'],
- });
-
- ctx.addIssue({
- code: z.ZodIssueCode.custom,
- message: 'Emails must be unique',
- path: ['recipients', firstFoundIndex, 'email'],
- });
- }
- });
-
-type TAddRecipientsForNewDocumentSchema = z.infer;
-
-export type UseTemplateDialogProps = {
- templateId: number;
- templateSigningOrder?: DocumentSigningOrder | null;
- recipients: Recipient[];
- documentDistributionMethod?: DocumentDistributionMethod;
- documentRootPath: string;
- trigger?: React.ReactNode;
-};
-
-export function UseTemplateDialog({
- recipients,
- documentDistributionMethod = DocumentDistributionMethod.EMAIL,
- documentRootPath,
- templateId,
- templateSigningOrder,
- trigger,
-}: UseTemplateDialogProps) {
- const router = useRouter();
-
- const { toast } = useToast();
- const { _ } = useLingui();
-
- const [open, setOpen] = useState(false);
-
- const form = useForm({
- resolver: zodResolver(ZAddRecipientsForNewDocumentSchema),
- defaultValues: {
- distributeDocument: false,
- useCustomDocument: false,
- customDocumentData: undefined,
- recipients: recipients
- .sort((a, b) => (a.signingOrder || 0) - (b.signingOrder || 0))
- .map((recipient) => {
- const isRecipientEmailPlaceholder = recipient.email.match(
- TEMPLATE_RECIPIENT_EMAIL_PLACEHOLDER_REGEX,
- );
-
- const isRecipientNamePlaceholder = recipient.name.match(
- TEMPLATE_RECIPIENT_NAME_PLACEHOLDER_REGEX,
- );
-
- return {
- id: recipient.id,
- name: !isRecipientNamePlaceholder ? recipient.name : '',
- email: !isRecipientEmailPlaceholder ? recipient.email : '',
- signingOrder: recipient.signingOrder ?? undefined,
- };
- }),
- },
- });
-
- const { mutateAsync: createDocumentFromTemplate } =
- trpc.template.createDocumentFromTemplate.useMutation();
-
- const onSubmit = async (data: TAddRecipientsForNewDocumentSchema) => {
- try {
- let customDocumentDataId: string | undefined = undefined;
-
- if (data.useCustomDocument && data.customDocumentData) {
- const customDocumentData = await putPdfFile(data.customDocumentData);
- customDocumentDataId = customDocumentData.id;
- }
-
- const { id } = await createDocumentFromTemplate({
- templateId,
- recipients: data.recipients,
- distributeDocument: data.distributeDocument,
- customDocumentDataId,
- });
-
- toast({
- title: _(msg`Document created`),
- description: _(msg`Your document has been created from the template successfully.`),
- duration: 5000,
- });
-
- let documentPath = `${documentRootPath}/${id}`;
-
- if (
- data.distributeDocument &&
- documentDistributionMethod === DocumentDistributionMethod.NONE
- ) {
- documentPath += '?action=view-signing-links';
- }
-
- router.push(documentPath);
- } catch (err) {
- const error = AppError.parseError(err);
-
- const toastPayload: Toast = {
- title: _(msg`Error`),
- description: _(msg`An error occurred while creating document from template.`),
- variant: 'destructive',
- };
-
- if (error.code === 'DOCUMENT_SEND_FAILED') {
- toastPayload.description = _(
- msg`The document was created but could not be sent to recipients.`,
- );
- }
-
- toast(toastPayload);
- }
- };
-
- const { fields: formRecipients } = useFieldArray({
- control: form.control,
- name: 'recipients',
- });
-
- useEffect(() => {
- if (!open) {
- form.reset();
- }
- }, [open, form]);
-
- return (
- !form.formState.isSubmitting && setOpen(value)}>
-
- {trigger || (
-
-
- Use Template
-
- )}
-
-
-
-
- Create document from template
-
-
- {recipients.length === 0 ? (
- A draft document will be created
- ) : (
- Add the recipients to create the document with
- )}
-
-
-
-
-
-
-
- {formRecipients.map((recipient, index) => (
-
- {templateSigningOrder === DocumentSigningOrder.SEQUENTIAL && (
- (
-
-
-
-
-
-
- )}
- />
- )}
-
- (
-
- {index === 0 && (
-
- Email
-
- )}
-
-
-
-
-
-
- )}
- />
-
- (
-
- {index === 0 && (
-
- Name
-
- )}
-
-
-
-
-
-
- )}
- />
-
- ))}
-
- {recipients.length > 0 && (
-
-
(
-
-
-
-
- {documentDistributionMethod === DocumentDistributionMethod.EMAIL && (
-
- Send document
-
-
-
-
-
-
-
-
- The document will be immediately sent to recipients if this
- is checked.
-
-
-
-
-
- Otherwise, the document will be created as a draft.
-
-
-
-
-
- )}
-
- {documentDistributionMethod === DocumentDistributionMethod.NONE && (
-
- Create as pending
-
-
-
-
-
-
-
- Create the document as pending and ready to sign.
-
-
-
-
- We won't send anything to notify recipients.
-
-
-
-
- We will generate signing links for you, which you can send
- to the recipients through your method of choice.
-
-
-
-
-
- )}
-
-
- )}
- />
-
- )}
-
-
(
-
-
-
{
- field.onChange(checked);
- if (!checked) {
- form.setValue('customDocumentData', undefined);
- }
- }}
- />
-
- Upload custom document
-
-
-
-
-
-
-
- Upload a custom document to use instead of the template's default
- document
-
-
-
-
-
-
-
- )}
- />
-
- {form.watch('useCustomDocument') && (
-
-
(
-
-
-
-
-
- {!field.value && (
- <>
-
-
-
-
-
- Click to upload
- {' '}
- or drag and drop
-
-
-
-
- PDF files only
-
- >
- )}
-
- {field.value && (
-
-
{field.value.name}
-
- {(field.value.size / (1024 * 1024)).toFixed(2)} MB
-
-
- )}
-
-
- {
- const file = e.target.files?.[0];
-
- if (!file) {
- field.onChange(undefined);
-
- return;
- }
-
- if (file.type !== 'application/pdf') {
- form.setError('customDocumentData', {
- type: 'manual',
- message: _(msg`Please select a PDF file`),
- });
-
- return;
- }
-
- if (file.size > APP_DOCUMENT_UPLOAD_SIZE_LIMIT * 1024 * 1024) {
- form.setError('customDocumentData', {
- type: 'manual',
- message: _(
- msg`File size exceeds the limit of ${APP_DOCUMENT_UPLOAD_SIZE_LIMIT} MB`,
- ),
- });
-
- return;
- }
-
- field.onChange(file);
- }}
- />
-
- {field.value && (
-
-
{
- e.preventDefault();
- field.onChange(undefined);
- }}
- >
-
-
- Clear file
-
-
-
- )}
-
-
-
-
-
- )}
- />
-
- )}
-
-
-
-
-
- Close
-
-
-
-
- {!form.getValues('distributeDocument') ? (
- Create as draft
- ) : documentDistributionMethod === DocumentDistributionMethod.EMAIL ? (
- Create and send
- ) : (
- Create signing links
- )}
-
-
-
-
-
-
-
- );
-}
diff --git a/apps/web/src/app/(internal)/%5F%5Fhtmltopdf/audit-log/data-table.tsx b/apps/web/src/app/(internal)/%5F%5Fhtmltopdf/audit-log/data-table.tsx
deleted file mode 100644
index a3c77c15e..000000000
--- a/apps/web/src/app/(internal)/%5F%5Fhtmltopdf/audit-log/data-table.tsx
+++ /dev/null
@@ -1,95 +0,0 @@
-import { msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { DateTime } from 'luxon';
-import type { DateTimeFormatOptions } from 'luxon';
-import { UAParser } from 'ua-parser-js';
-
-import { APP_I18N_OPTIONS } from '@documenso/lib/constants/i18n';
-import type { TDocumentAuditLog } from '@documenso/lib/types/document-audit-logs';
-import { formatDocumentAuditLogAction } from '@documenso/lib/utils/document-audit-logs';
-import {
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeader,
- TableRow,
-} from '@documenso/ui/primitives/table';
-
-export type AuditLogDataTableProps = {
- logs: TDocumentAuditLog[];
-};
-
-const dateFormat: DateTimeFormatOptions = {
- ...DateTime.DATETIME_SHORT,
- hourCycle: 'h12',
-};
-
-/**
- * DO NOT USE TRANS. YOU MUST USE _ FOR THIS FILE AND ALL CHILDREN COMPONENTS.
- */
-export const AuditLogDataTable = ({ logs }: AuditLogDataTableProps) => {
- const { _ } = useLingui();
-
- const parser = new UAParser();
-
- const uppercaseFistLetter = (text: string) => {
- return text.charAt(0).toUpperCase() + text.slice(1);
- };
-
- return (
-
-
-
- {_(msg`Time`)}
- {_(msg`User`)}
- {_(msg`Action`)}
- {_(msg`IP Address`)}
- {_(msg`Browser`)}
-
-
-
-
- {logs.map((log, i) => (
-
-
- {DateTime.fromJSDate(log.createdAt)
- .setLocale(APP_I18N_OPTIONS.defaultLocale)
- .toLocaleString(dateFormat)}
-
-
-
- {log.name || log.email ? (
-
- {log.name && (
-
- {log.name}
-
- )}
-
- {log.email && (
-
- {log.email}
-
- )}
-
- ) : (
- N/A
- )}
-
-
-
- {uppercaseFistLetter(formatDocumentAuditLogAction(_, log).description)}
-
-
- {log.ipAddress}
-
-
- {log.userAgent ? parser.setUA(log.userAgent).getBrowser().name : 'N/A'}
-
-
- ))}
-
-
- );
-};
diff --git a/apps/web/src/app/(internal)/%5F%5Fhtmltopdf/audit-log/page.tsx b/apps/web/src/app/(internal)/%5F%5Fhtmltopdf/audit-log/page.tsx
deleted file mode 100644
index d420ab456..000000000
--- a/apps/web/src/app/(internal)/%5F%5Fhtmltopdf/audit-log/page.tsx
+++ /dev/null
@@ -1,169 +0,0 @@
-import React from 'react';
-
-import { redirect } from 'next/navigation';
-
-import { msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { DateTime } from 'luxon';
-
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-import { DOCUMENT_STATUS } from '@documenso/lib/constants/document';
-import { APP_I18N_OPTIONS, ZSupportedLanguageCodeSchema } from '@documenso/lib/constants/i18n';
-import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
-import { getEntireDocument } from '@documenso/lib/server-only/admin/get-entire-document';
-import { decryptSecondaryData } from '@documenso/lib/server-only/crypto/decrypt';
-import { findDocumentAuditLogs } from '@documenso/lib/server-only/document/find-document-audit-logs';
-import { dynamicActivate } from '@documenso/lib/utils/i18n';
-import { Card, CardContent } from '@documenso/ui/primitives/card';
-
-import { Logo } from '~/components/branding/logo';
-
-import { AuditLogDataTable } from './data-table';
-
-type AuditLogProps = {
- searchParams: {
- d: string;
- };
-};
-
-/**
- * DO NOT USE TRANS. YOU MUST USE _ FOR THIS FILE AND ALL CHILDREN COMPONENTS.
- *
- * Cannot use dynamicActivate by itself to translate this specific page and all
- * children components because `not-found.tsx` page runs and overrides the i18n.
- */
-export default async function AuditLog({ searchParams }: AuditLogProps) {
- const { i18n } = await setupI18nSSR();
-
- const { _ } = useLingui();
-
- const { d } = searchParams;
-
- if (typeof d !== 'string' || !d) {
- return redirect('/');
- }
-
- const rawDocumentId = decryptSecondaryData(d);
-
- if (!rawDocumentId || isNaN(Number(rawDocumentId))) {
- return redirect('/');
- }
-
- const documentId = Number(rawDocumentId);
-
- const document = await getEntireDocument({
- id: documentId,
- }).catch(() => null);
-
- if (!document) {
- return redirect('/');
- }
-
- const documentLanguage = ZSupportedLanguageCodeSchema.parse(document.documentMeta?.language);
-
- await dynamicActivate(i18n, documentLanguage);
-
- const { data: auditLogs } = await findDocumentAuditLogs({
- documentId: documentId,
- userId: document.userId,
- teamId: document.teamId || undefined,
- perPage: 100_000,
- });
-
- return (
-
-
-
{_(msg`Version History`)}
-
-
-
-
-
- {_(msg`Document ID`)}
-
- {document.id}
-
-
-
- {_(msg`Enclosed Document`)}
-
- {document.title}
-
-
-
- {_(msg`Status`)}
-
-
- {_(
- document.deletedAt ? msg`Deleted` : DOCUMENT_STATUS[document.status].description,
- ).toUpperCase()}
-
-
-
-
- {_(msg`Owner`)}
-
-
- {document.user.name} ({document.user.email})
-
-
-
-
- {_(msg`Created At`)}
-
-
- {DateTime.fromJSDate(document.createdAt)
- .setLocale(APP_I18N_OPTIONS.defaultLocale)
- .toFormat('yyyy-mm-dd hh:mm:ss a (ZZZZ)')}
-
-
-
-
- {_(msg`Last Updated`)}
-
-
- {DateTime.fromJSDate(document.updatedAt)
- .setLocale(APP_I18N_OPTIONS.defaultLocale)
- .toFormat('yyyy-mm-dd hh:mm:ss a (ZZZZ)')}
-
-
-
-
- {_(msg`Time Zone`)}
-
-
- {document.documentMeta?.timezone ?? 'N/A'}
-
-
-
-
-
{_(msg`Recipients`)}
-
-
- {document.recipients.map((recipient) => (
-
-
- [{_(RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName)}]
- {' '}
- {recipient.name} ({recipient.email})
-
- ))}
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/apps/web/src/app/(internal)/%5F%5Fhtmltopdf/certificate/page.tsx b/apps/web/src/app/(internal)/%5F%5Fhtmltopdf/certificate/page.tsx
deleted file mode 100644
index 79339082d..000000000
--- a/apps/web/src/app/(internal)/%5F%5Fhtmltopdf/certificate/page.tsx
+++ /dev/null
@@ -1,321 +0,0 @@
-import React from 'react';
-
-import { redirect } from 'next/navigation';
-
-import { msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { DateTime } from 'luxon';
-import { match } from 'ts-pattern';
-import { UAParser } from 'ua-parser-js';
-
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-import { APP_I18N_OPTIONS, ZSupportedLanguageCodeSchema } from '@documenso/lib/constants/i18n';
-import {
- RECIPIENT_ROLES_DESCRIPTION,
- RECIPIENT_ROLE_SIGNING_REASONS,
-} from '@documenso/lib/constants/recipient-roles';
-import { getEntireDocument } from '@documenso/lib/server-only/admin/get-entire-document';
-import { decryptSecondaryData } from '@documenso/lib/server-only/crypto/decrypt';
-import { getDocumentCertificateAuditLogs } from '@documenso/lib/server-only/document/get-document-certificate-audit-logs';
-import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
-import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
-import { dynamicActivate } from '@documenso/lib/utils/i18n';
-import { FieldType } from '@documenso/prisma/client';
-import { Card, CardContent } from '@documenso/ui/primitives/card';
-import {
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeader,
- TableRow,
-} from '@documenso/ui/primitives/table';
-
-import { Logo } from '~/components/branding/logo';
-
-type SigningCertificateProps = {
- searchParams: {
- d: string;
- };
-};
-
-const FRIENDLY_SIGNING_REASONS = {
- ['__OWNER__']: msg`I am the owner of this document`,
- ...RECIPIENT_ROLE_SIGNING_REASONS,
-};
-
-/**
- * DO NOT USE TRANS. YOU MUST USE _ FOR THIS FILE AND ALL CHILDREN COMPONENTS.
- *
- * Cannot use dynamicActivate by itself to translate this specific page and all
- * children components because `not-found.tsx` page runs and overrides the i18n.
- */
-export default async function SigningCertificate({ searchParams }: SigningCertificateProps) {
- const { i18n } = await setupI18nSSR();
-
- const { _ } = useLingui();
-
- const { d } = searchParams;
-
- if (typeof d !== 'string' || !d) {
- return redirect('/');
- }
-
- const rawDocumentId = decryptSecondaryData(d);
-
- if (!rawDocumentId || isNaN(Number(rawDocumentId))) {
- return redirect('/');
- }
-
- const documentId = Number(rawDocumentId);
-
- const document = await getEntireDocument({
- id: documentId,
- }).catch(() => null);
-
- if (!document) {
- return redirect('/');
- }
-
- const documentLanguage = ZSupportedLanguageCodeSchema.parse(document.documentMeta?.language);
-
- await dynamicActivate(i18n, documentLanguage);
-
- const auditLogs = await getDocumentCertificateAuditLogs({
- id: documentId,
- });
-
- const isOwner = (email: string) => {
- return email.toLowerCase() === document.user.email.toLowerCase();
- };
-
- const getDevice = (userAgent?: string | null) => {
- if (!userAgent) {
- return 'Unknown';
- }
-
- const parser = new UAParser(userAgent);
-
- parser.setUA(userAgent);
-
- const result = parser.getResult();
-
- return `${result.os.name} - ${result.browser.name} ${result.browser.version}`;
- };
-
- const getAuthenticationLevel = (recipientId: number) => {
- const recipient = document.recipients.find((recipient) => recipient.id === recipientId);
-
- if (!recipient) {
- return 'Unknown';
- }
-
- const extractedAuthMethods = extractDocumentAuthMethods({
- documentAuth: document.authOptions,
- recipientAuth: recipient.authOptions,
- });
-
- let authLevel = match(extractedAuthMethods.derivedRecipientActionAuth)
- .with('ACCOUNT', () => _(msg`Account Re-Authentication`))
- .with('TWO_FACTOR_AUTH', () => _(msg`Two-Factor Re-Authentication`))
- .with('PASSKEY', () => _(msg`Passkey Re-Authentication`))
- .with('EXPLICIT_NONE', () => _(msg`Email`))
- .with(null, () => null)
- .exhaustive();
-
- if (!authLevel) {
- authLevel = match(extractedAuthMethods.derivedRecipientAccessAuth)
- .with('ACCOUNT', () => _(msg`Account Authentication`))
- .with(null, () => _(msg`Email`))
- .exhaustive();
- }
-
- return authLevel;
- };
-
- const getRecipientAuditLogs = (recipientId: number) => {
- return {
- [DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT]: auditLogs[DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT].filter(
- (log) =>
- log.type === DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT && log.data.recipientId === recipientId,
- ),
- [DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED]: auditLogs[
- DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED
- ].filter(
- (log) =>
- log.type === DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED &&
- log.data.recipientId === recipientId,
- ),
- [DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED]: auditLogs[
- DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED
- ].filter(
- (log) =>
- log.type === DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED &&
- log.data.recipientId === recipientId,
- ),
- };
- };
-
- const getRecipientSignatureField = (recipientId: number) => {
- return document.recipients
- .find((recipient) => recipient.id === recipientId)
- ?.fields.find(
- (field) => field.type === FieldType.SIGNATURE || field.type === FieldType.FREE_SIGNATURE,
- );
- };
-
- return (
-
-
-
{_(msg`Signing Certificate`)}
-
-
-
-
-
-
-
- {_(msg`Signer Events`)}
- {_(msg`Signature`)}
- {_(msg`Details`)}
- {/* Security */}
-
-
-
-
- {document.recipients.map((recipient, i) => {
- const logs = getRecipientAuditLogs(recipient.id);
- const signature = getRecipientSignatureField(recipient.id);
-
- return (
-
-
- {recipient.name}
- {recipient.email}
-
- {_(RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName)}
-
-
-
- {_(msg`Authentication Level`)}: {' '}
- {getAuthenticationLevel(recipient.id)}
-
-
-
-
- {signature ? (
- <>
-
- {signature.signature?.signatureImageAsBase64 && (
-
- )}
-
- {signature.signature?.typedSignature && (
-
- {signature.signature?.typedSignature}
-
- )}
-
-
-
- {_(msg`Signature ID`)}: {' '}
-
- {signature.secondaryId}
-
-
-
-
- {_(msg`IP Address`)}: {' '}
-
- {logs.DOCUMENT_RECIPIENT_COMPLETED[0]?.ipAddress ?? _(msg`Unknown`)}
-
-
-
-
- {_(msg`Device`)}: {' '}
-
- {getDevice(logs.DOCUMENT_RECIPIENT_COMPLETED[0]?.userAgent)}
-
-
- >
- ) : (
- N/A
- )}
-
-
-
-
-
- {_(msg`Sent`)}: {' '}
-
- {logs.EMAIL_SENT[0]
- ? DateTime.fromJSDate(logs.EMAIL_SENT[0].createdAt)
- .setLocale(APP_I18N_OPTIONS.defaultLocale)
- .toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')
- : _(msg`Unknown`)}
-
-
-
-
- {_(msg`Viewed`)}: {' '}
-
- {logs.DOCUMENT_OPENED[0]
- ? DateTime.fromJSDate(logs.DOCUMENT_OPENED[0].createdAt)
- .setLocale(APP_I18N_OPTIONS.defaultLocale)
- .toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')
- : _(msg`Unknown`)}
-
-
-
-
- {_(msg`Signed`)}: {' '}
-
- {logs.DOCUMENT_RECIPIENT_COMPLETED[0]
- ? DateTime.fromJSDate(logs.DOCUMENT_RECIPIENT_COMPLETED[0].createdAt)
- .setLocale(APP_I18N_OPTIONS.defaultLocale)
- .toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')
- : _(msg`Unknown`)}
-
-
-
-
- {_(msg`Reason`)}: {' '}
-
- {_(
- isOwner(recipient.email)
- ? FRIENDLY_SIGNING_REASONS['__OWNER__']
- : FRIENDLY_SIGNING_REASONS[recipient.role],
- )}
-
-
-
-
-
- );
- })}
-
-
-
-
-
-
-
-
- {_(msg`Signing certificate provided by`)}:
-
-
-
-
-
-
- );
-}
diff --git a/apps/web/src/app/(profile)/layout.tsx b/apps/web/src/app/(profile)/layout.tsx
deleted file mode 100644
index d43e44172..000000000
--- a/apps/web/src/app/(profile)/layout.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import React from 'react';
-
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
-import { getTeams } from '@documenso/lib/server-only/team/get-teams';
-
-import { RefreshOnFocus } from '~/components/(dashboard)/refresh-on-focus/refresh-on-focus';
-import { NextAuthProvider } from '~/providers/next-auth';
-
-import { ProfileHeader } from './profile-header';
-
-type PublicProfileLayoutProps = {
- children: React.ReactNode;
-};
-
-export default async function PublicProfileLayout({ children }: PublicProfileLayoutProps) {
- await setupI18nSSR();
-
- const { user, session } = await getServerComponentSession();
-
- // I wouldn't typically do this but it's better than the `let` statement
- const teams = user && session ? await getTeams({ userId: user.id }) : undefined;
-
- return (
-
-
-
-
-
- );
-}
diff --git a/apps/web/src/app/(profile)/p/[url]/not-found.tsx b/apps/web/src/app/(profile)/p/[url]/not-found.tsx
deleted file mode 100644
index 991c0d12a..000000000
--- a/apps/web/src/app/(profile)/p/[url]/not-found.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-'use client';
-
-import Link from 'next/link';
-
-import { Trans } from '@lingui/macro';
-import { ChevronLeft } from 'lucide-react';
-
-import { Button } from '@documenso/ui/primitives/button';
-
-export default function NotFound() {
- return (
-
-
-
- 404 Profile not found
-
-
-
- Oops! Something went wrong.
-
-
-
- The profile you are looking for could not be found.
-
-
-
-
-
-
- Go Back
-
-
-
-
-
- );
-}
diff --git a/apps/web/src/app/(profile)/p/[url]/page.tsx b/apps/web/src/app/(profile)/p/[url]/page.tsx
deleted file mode 100644
index 67226c5cb..000000000
--- a/apps/web/src/app/(profile)/p/[url]/page.tsx
+++ /dev/null
@@ -1,206 +0,0 @@
-import Image from 'next/image';
-import Link from 'next/link';
-import { notFound, redirect } from 'next/navigation';
-
-import { Trans } from '@lingui/macro';
-import { FileIcon } from 'lucide-react';
-import { DateTime } from 'luxon';
-
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
-import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
-import { getPublicProfileByUrl } from '@documenso/lib/server-only/profile/get-public-profile-by-url';
-import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
-import { formatDirectTemplatePath } from '@documenso/lib/utils/templates';
-import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
-import { Button } from '@documenso/ui/primitives/button';
-import {
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeader,
- TableRow,
-} from '@documenso/ui/primitives/table';
-import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
-
-export type PublicProfilePageProps = {
- params: {
- url: string;
- };
-};
-
-const BADGE_DATA = {
- Premium: {
- imageSrc: '/static/premium-user-badge.svg',
- name: 'Premium',
- },
- EarlySupporter: {
- imageSrc: '/static/early-supporter-badge.svg',
- name: 'Early supporter',
- },
-};
-
-export default async function PublicProfilePage({ params }: PublicProfilePageProps) {
- await setupI18nSSR();
-
- const { url: profileUrl } = params;
-
- if (!profileUrl) {
- redirect('/');
- }
-
- const publicProfile = await getPublicProfileByUrl({
- profileUrl,
- }).catch(() => null);
-
- if (!publicProfile || !publicProfile.profile.enabled) {
- notFound();
- }
-
- const { user } = await getServerComponentSession();
-
- const { profile, templates } = publicProfile;
-
- return (
-
-
-
- {publicProfile.avatarImageId && (
-
- )}
-
-
- {extractInitials(publicProfile.name)}
-
-
-
-
-
{publicProfile.name}
-
- {publicProfile.badge && (
-
-
-
-
-
-
-
-
-
-
- {BADGE_DATA[publicProfile.badge.type].name}
-
-
-
- Since {DateTime.fromJSDate(publicProfile.badge.since).toFormat('LLL ‘yy')}
-
-
-
-
-
- )}
-
-
-
- {(profile.bio ?? '').split('\n').map((line, index) => (
-
- {line}
-
- ))}
-
-
-
- {templates.length === 0 && (
-
-
-
- It looks like {publicProfile.name} hasn't added any documents to their profile yet.
- {' '}
- {!user?.id && (
-
-
- While waiting for them to do so you can create your own Documenso account and get
- started with document signing right away.
-
-
- )}
- {'userId' in profile && user?.id === profile.userId && (
-
-
- Go to your{' '}
-
- public profile settings
- {' '}
- to add documents.
-
-
- )}
-
-
- )}
-
- {templates.length > 0 && (
-
-
-
-
-
- Documents
-
-
-
-
- {templates.map((template) => (
-
-
-
-
-
-
-
-
- {template.publicTitle}
-
-
- {template.publicDescription}
-
-
-
-
-
- Sign
-
-
-
-
-
-
- ))}
-
-
-
- )}
-
- );
-}
diff --git a/apps/web/src/app/(profile)/profile-header.tsx b/apps/web/src/app/(profile)/profile-header.tsx
deleted file mode 100644
index 9c1bd3d7b..000000000
--- a/apps/web/src/app/(profile)/profile-header.tsx
+++ /dev/null
@@ -1,91 +0,0 @@
-'use client';
-
-import { useEffect, useState } from 'react';
-
-import Image from 'next/image';
-import Link from 'next/link';
-
-import { Trans } from '@lingui/macro';
-import { PlusIcon } from 'lucide-react';
-
-import LogoIcon from '@documenso/assets/logo_icon.png';
-import type { TGetTeamsResponse } from '@documenso/lib/server-only/team/get-teams';
-import type { User } from '@documenso/prisma/client';
-import { cn } from '@documenso/ui/lib/utils';
-import { Button } from '@documenso/ui/primitives/button';
-
-import { Header as AuthenticatedHeader } from '~/components/(dashboard)/layout/header';
-import { Logo } from '~/components/branding/logo';
-
-type ProfileHeaderProps = {
- user?: User | null;
- teams?: TGetTeamsResponse;
-};
-
-export const ProfileHeader = ({ user, teams = [] }: ProfileHeaderProps) => {
- const [scrollY, setScrollY] = useState(0);
-
- useEffect(() => {
- const onScroll = () => {
- setScrollY(window.scrollY);
- };
-
- window.addEventListener('scroll', onScroll);
-
- return () => window.removeEventListener('scroll', onScroll);
- }, []);
-
- if (user) {
- return ;
- }
-
- return (
- 5 && 'border-b-border',
- )}
- >
-
-
-
-
-
-
-
-
-
-
- Want your own public profile?
-
-
- Like to have your own public profile with agreements?
-
-
-
-
-
-
-
-
- Create
-
-
-
-
-
-
- );
-};
diff --git a/apps/web/src/app/(recipient)/d/[token]/configure-direct-template.tsx b/apps/web/src/app/(recipient)/d/[token]/configure-direct-template.tsx
deleted file mode 100644
index f9b7019f3..000000000
--- a/apps/web/src/app/(recipient)/d/[token]/configure-direct-template.tsx
+++ /dev/null
@@ -1,161 +0,0 @@
-'use client';
-
-import { zodResolver } from '@hookform/resolvers/zod';
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { useSession } from 'next-auth/react';
-import { useForm } from 'react-hook-form';
-import { z } from 'zod';
-
-import type { TTemplate } from '@documenso/lib/types/template';
-import type { Recipient } from '@documenso/prisma/client';
-import type { Field } from '@documenso/prisma/client';
-import {
- DocumentFlowFormContainerActions,
- DocumentFlowFormContainerContent,
- DocumentFlowFormContainerFooter,
- DocumentFlowFormContainerHeader,
- DocumentFlowFormContainerStep,
-} from '@documenso/ui/primitives/document-flow/document-flow-root';
-import { ShowFieldItem } from '@documenso/ui/primitives/document-flow/show-field-item';
-import type { DocumentFlowStep } from '@documenso/ui/primitives/document-flow/types';
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from '@documenso/ui/primitives/form/form';
-import { Input } from '@documenso/ui/primitives/input';
-import { useStep } from '@documenso/ui/primitives/stepper';
-
-import { useRequiredDocumentAuthContext } from '~/app/(signing)/sign/[token]/document-auth-provider';
-
-const ZConfigureDirectTemplateFormSchema = z.object({
- email: z.string().email('Email is invalid'),
-});
-
-export type TConfigureDirectTemplateFormSchema = z.infer;
-
-export type ConfigureDirectTemplateFormProps = {
- flowStep: DocumentFlowStep;
- isDocumentPdfLoaded: boolean;
- template: Omit;
- directTemplateRecipient: Recipient & { fields: Field[] };
- initialEmail?: string;
- onSubmit: (_data: TConfigureDirectTemplateFormSchema) => void;
-};
-
-export const ConfigureDirectTemplateFormPartial = ({
- flowStep,
- isDocumentPdfLoaded,
- template,
- directTemplateRecipient,
- initialEmail,
- onSubmit,
-}: ConfigureDirectTemplateFormProps) => {
- const { _ } = useLingui();
- const { data: session } = useSession();
-
- const { recipients } = template;
- const { derivedRecipientAccessAuth } = useRequiredDocumentAuthContext();
-
- const recipientsWithBlankDirectRecipientEmail = recipients.map((recipient) => {
- if (recipient.id === directTemplateRecipient.id) {
- return {
- ...recipient,
- email: '',
- };
- }
-
- return recipient;
- });
-
- const form = useForm({
- resolver: zodResolver(
- ZConfigureDirectTemplateFormSchema.superRefine((items, ctx) => {
- if (template.recipients.map((recipient) => recipient.email).includes(items.email)) {
- ctx.addIssue({
- code: z.ZodIssueCode.custom,
- message: _(msg`Email cannot already exist in the template`),
- path: ['email'],
- });
- }
- }),
- ),
- defaultValues: {
- email: initialEmail || '',
- },
- });
-
- const { stepIndex, currentStep, totalSteps, previousStep } = useStep();
-
- return (
- <>
-
-
-
- {isDocumentPdfLoaded &&
- directTemplateRecipient.fields.map((field, index) => (
-
- ))}
-
-
-
- (
-
-
- Email
-
-
-
-
-
-
- {!fieldState.error && (
-
- Enter your email address to receive the completed document.
-
- )}
-
-
-
- )}
- />
-
-
-
-
-
-
-
-
-
- >
- );
-};
diff --git a/apps/web/src/app/(recipient)/d/[token]/direct-template.tsx b/apps/web/src/app/(recipient)/d/[token]/direct-template.tsx
deleted file mode 100644
index 5a2a99e31..000000000
--- a/apps/web/src/app/(recipient)/d/[token]/direct-template.tsx
+++ /dev/null
@@ -1,176 +0,0 @@
-'use client';
-
-import { useState } from 'react';
-
-import { useRouter, useSearchParams } from 'next/navigation';
-
-import { msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-
-import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
-import type { TTemplate } from '@documenso/lib/types/template';
-import type { Field } from '@documenso/prisma/client';
-import { type Recipient } from '@documenso/prisma/client';
-import { trpc } from '@documenso/trpc/react';
-import { Card, CardContent } from '@documenso/ui/primitives/card';
-import { DocumentFlowFormContainer } from '@documenso/ui/primitives/document-flow/document-flow-root';
-import type { DocumentFlowStep } from '@documenso/ui/primitives/document-flow/types';
-import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
-import { Stepper } from '@documenso/ui/primitives/stepper';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-import { useRequiredDocumentAuthContext } from '~/app/(signing)/sign/[token]/document-auth-provider';
-import { useRequiredSigningContext } from '~/app/(signing)/sign/[token]/provider';
-
-import type { TConfigureDirectTemplateFormSchema } from './configure-direct-template';
-import { ConfigureDirectTemplateFormPartial } from './configure-direct-template';
-import type { DirectTemplateLocalField } from './sign-direct-template';
-import { SignDirectTemplateForm } from './sign-direct-template';
-
-export type TemplatesDirectPageViewProps = {
- template: Omit;
- directTemplateToken: string;
- directTemplateRecipient: Recipient & { fields: Field[] };
-};
-
-type DirectTemplateStep = 'configure' | 'sign';
-const DirectTemplateSteps: DirectTemplateStep[] = ['configure', 'sign'];
-
-export const DirectTemplatePageView = ({
- template,
- directTemplateRecipient,
- directTemplateToken,
-}: TemplatesDirectPageViewProps) => {
- const router = useRouter();
- const searchParams = useSearchParams();
-
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const { email, fullName, setEmail } = useRequiredSigningContext();
- const { recipient, setRecipient } = useRequiredDocumentAuthContext();
-
- const [step, setStep] = useState('configure');
- const [isDocumentPdfLoaded, setIsDocumentPdfLoaded] = useState(false);
-
- const recipientActionVerb = _(
- RECIPIENT_ROLES_DESCRIPTION[directTemplateRecipient.role].actionVerb,
- );
-
- const directTemplateFlow: Record = {
- configure: {
- title: msg`General`,
- description: msg`Preview and configure template.`,
- stepIndex: 1,
- },
- sign: {
- title: msg`${recipientActionVerb} document`,
- description: msg`${recipientActionVerb} the document to complete the process.`,
- stepIndex: 2,
- },
- };
-
- const { mutateAsync: createDocumentFromDirectTemplate } =
- trpc.template.createDocumentFromDirectTemplate.useMutation();
-
- /**
- * Set the email into a temporary recipient so it can be used for reauth and signing email fields.
- */
- const onConfigureDirectTemplateSubmit = ({ email }: TConfigureDirectTemplateFormSchema) => {
- setEmail(email);
-
- setRecipient({
- ...recipient,
- email,
- });
-
- setStep('sign');
- };
-
- const onSignDirectTemplateSubmit = async (fields: DirectTemplateLocalField[]) => {
- try {
- let directTemplateExternalId = searchParams?.get('externalId') || undefined;
-
- if (directTemplateExternalId) {
- directTemplateExternalId = decodeURIComponent(directTemplateExternalId);
- }
-
- const { token } = await createDocumentFromDirectTemplate({
- directTemplateToken,
- directTemplateExternalId,
- directRecipientName: fullName,
- directRecipientEmail: recipient.email,
- templateUpdatedAt: template.updatedAt,
- signedFieldValues: fields.map((field) => {
- if (!field.signedValue) {
- throw new Error('Invalid configuration');
- }
-
- return field.signedValue;
- }),
- });
-
- const redirectUrl = template.templateMeta?.redirectUrl;
-
- redirectUrl ? router.push(redirectUrl) : router.push(`/sign/${token}/complete`);
- } catch (err) {
- toast({
- title: _(msg`Something went wrong`),
- description: _(
- msg`We were unable to submit this document at this time. Please try again later.`,
- ),
- variant: 'destructive',
- });
-
- throw err;
- }
- };
-
- const currentDocumentFlow = directTemplateFlow[step];
-
- return (
-
-
-
- setIsDocumentPdfLoaded(true)}
- />
-
-
-
-
- e.preventDefault()}
- >
- setStep(DirectTemplateSteps[step - 1])}
- >
-
-
-
-
-
-
-
- );
-};
diff --git a/apps/web/src/app/(recipient)/d/[token]/not-found.tsx b/apps/web/src/app/(recipient)/d/[token]/not-found.tsx
deleted file mode 100644
index ef22a2562..000000000
--- a/apps/web/src/app/(recipient)/d/[token]/not-found.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-'use client';
-
-import Link from 'next/link';
-
-import { Trans } from '@lingui/macro';
-import { ChevronLeft } from 'lucide-react';
-
-import { Button } from '@documenso/ui/primitives/button';
-
-export default function NotFound() {
- return (
-
-
-
- 404 Template not found
-
-
-
- Oops! Something went wrong.
-
-
-
-
- The template you are looking for may have been disabled, deleted or may have never
- existed.
-
-
-
-
-
-
-
- Go Back
-
-
-
-
-
- );
-}
diff --git a/apps/web/src/app/(recipient)/d/[token]/page.tsx b/apps/web/src/app/(recipient)/d/[token]/page.tsx
deleted file mode 100644
index 333e93649..000000000
--- a/apps/web/src/app/(recipient)/d/[token]/page.tsx
+++ /dev/null
@@ -1,97 +0,0 @@
-import { notFound, redirect } from 'next/navigation';
-
-import { Plural } from '@lingui/macro';
-import { UsersIcon } from 'lucide-react';
-import { match } from 'ts-pattern';
-
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
-import { getTemplateByDirectLinkToken } from '@documenso/lib/server-only/template/get-template-by-direct-link-token';
-import { DocumentAccessAuth } from '@documenso/lib/types/document-auth';
-import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
-
-import { DocumentAuthProvider } from '~/app/(signing)/sign/[token]/document-auth-provider';
-import { SigningProvider } from '~/app/(signing)/sign/[token]/provider';
-
-import { DirectTemplatePageView } from './direct-template';
-import { DirectTemplateAuthPageView } from './signing-auth-page';
-
-export type TemplatesDirectPageProps = {
- params: {
- token: string;
- };
-};
-
-export default async function TemplatesDirectPage({ params }: TemplatesDirectPageProps) {
- await setupI18nSSR();
-
- const { token } = params;
-
- if (!token) {
- redirect('/');
- }
-
- const { user } = await getServerComponentSession();
-
- const template = await getTemplateByDirectLinkToken({
- token,
- }).catch(() => null);
-
- if (!template || !template.directLink?.enabled) {
- notFound();
- }
-
- const directTemplateRecipient = template.recipients.find(
- (recipient) => recipient.id === template.directLink?.directTemplateRecipientId,
- );
-
- if (!directTemplateRecipient) {
- notFound();
- }
-
- const { derivedRecipientAccessAuth } = extractDocumentAuthMethods({
- documentAuth: template.authOptions,
- });
-
- // Ensure typesafety when we add more options.
- const isAccessAuthValid = match(derivedRecipientAccessAuth)
- .with(DocumentAccessAuth.ACCOUNT, () => user !== null)
- .with(null, () => true)
- .exhaustive();
-
- if (!isAccessAuthValid) {
- return ;
- }
-
- return (
-
-
-
-
- {template.title}
-
-
-
-
-
-
-
-
- );
-}
diff --git a/apps/web/src/app/(recipient)/d/[token]/sign-direct-template.tsx b/apps/web/src/app/(recipient)/d/[token]/sign-direct-template.tsx
deleted file mode 100644
index 9e379ab41..000000000
--- a/apps/web/src/app/(recipient)/d/[token]/sign-direct-template.tsx
+++ /dev/null
@@ -1,388 +0,0 @@
-import { useMemo, useState } from 'react';
-
-import { Trans } from '@lingui/macro';
-import { DateTime } from 'luxon';
-import { match } from 'ts-pattern';
-
-import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
-import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
-import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones';
-import {
- ZCheckboxFieldMeta,
- ZDropdownFieldMeta,
- ZNumberFieldMeta,
- ZRadioFieldMeta,
- ZTextFieldMeta,
-} from '@documenso/lib/types/field-meta';
-import type { TTemplate } from '@documenso/lib/types/template';
-import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields';
-import type { Field, Recipient, Signature } from '@documenso/prisma/client';
-import { FieldType } from '@documenso/prisma/client';
-import type {
- TRemovedSignedFieldWithTokenMutationSchema,
- TSignFieldWithTokenMutationSchema,
-} from '@documenso/trpc/server/field-router/schema';
-import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
-import { Button } from '@documenso/ui/primitives/button';
-import { Card, CardContent } from '@documenso/ui/primitives/card';
-import {
- DocumentFlowFormContainerContent,
- DocumentFlowFormContainerFooter,
- DocumentFlowFormContainerHeader,
- DocumentFlowFormContainerStep,
-} from '@documenso/ui/primitives/document-flow/document-flow-root';
-import type { DocumentFlowStep } from '@documenso/ui/primitives/document-flow/types';
-import { ElementVisible } from '@documenso/ui/primitives/element-visible';
-import { Input } from '@documenso/ui/primitives/input';
-import { Label } from '@documenso/ui/primitives/label';
-import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
-import { useStep } from '@documenso/ui/primitives/stepper';
-
-import { CheckboxField } from '~/app/(signing)/sign/[token]/checkbox-field';
-import { DateField } from '~/app/(signing)/sign/[token]/date-field';
-import { DropdownField } from '~/app/(signing)/sign/[token]/dropdown-field';
-import { EmailField } from '~/app/(signing)/sign/[token]/email-field';
-import { InitialsField } from '~/app/(signing)/sign/[token]/initials-field';
-import { NameField } from '~/app/(signing)/sign/[token]/name-field';
-import { NumberField } from '~/app/(signing)/sign/[token]/number-field';
-import { useRequiredSigningContext } from '~/app/(signing)/sign/[token]/provider';
-import { RadioField } from '~/app/(signing)/sign/[token]/radio-field';
-import { SignDialog } from '~/app/(signing)/sign/[token]/sign-dialog';
-import { SignatureField } from '~/app/(signing)/sign/[token]/signature-field';
-import { TextField } from '~/app/(signing)/sign/[token]/text-field';
-
-export type SignDirectTemplateFormProps = {
- flowStep: DocumentFlowStep;
- directRecipient: Recipient;
- directRecipientFields: Field[];
- template: Omit;
- onSubmit: (_data: DirectTemplateLocalField[]) => Promise;
-};
-
-export type DirectTemplateLocalField = Field & {
- signedValue?: TSignFieldWithTokenMutationSchema;
- signature?: Signature;
-};
-
-export const SignDirectTemplateForm = ({
- flowStep,
- directRecipient,
- directRecipientFields,
- template,
- onSubmit,
-}: SignDirectTemplateFormProps) => {
- const { fullName, signature, signatureValid, setFullName, setSignature } =
- useRequiredSigningContext();
-
- const [localFields, setLocalFields] = useState(directRecipientFields);
- const [validateUninsertedFields, setValidateUninsertedFields] = useState(false);
- const [isSubmitting, setIsSubmitting] = useState(false);
-
- const { currentStep, totalSteps, previousStep } = useStep();
-
- const onSignField = (value: TSignFieldWithTokenMutationSchema) => {
- setLocalFields(
- localFields.map((field) => {
- if (field.id !== value.fieldId) {
- return field;
- }
-
- const tempField: DirectTemplateLocalField = {
- ...field,
- customText: value.value,
- inserted: true,
- signedValue: value,
- };
-
- if (field.type === FieldType.SIGNATURE) {
- tempField.signature = {
- id: 1,
- created: new Date(),
- recipientId: 1,
- fieldId: 1,
- signatureImageAsBase64: value.value.startsWith('data:') ? value.value : null,
- typedSignature: value.value.startsWith('data:') ? null : value.value,
- } satisfies Signature;
- }
-
- if (field.type === FieldType.DATE) {
- tempField.customText = DateTime.now()
- .setZone(template.templateMeta?.timezone ?? DEFAULT_DOCUMENT_TIME_ZONE)
- .toFormat(template.templateMeta?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT);
- }
- return tempField;
- }),
- );
- };
-
- const onUnsignField = (value: TRemovedSignedFieldWithTokenMutationSchema) => {
- setLocalFields(
- localFields.map((field) => {
- if (field.id !== value.fieldId) {
- return field;
- }
-
- return {
- ...field,
- customText: '',
- inserted: false,
- signedValue: undefined,
- signature: undefined,
- };
- }),
- );
- };
-
- const hasSignatureField = localFields.some((field) => field.type === FieldType.SIGNATURE);
-
- const uninsertedFields = useMemo(() => {
- return sortFieldsByPosition(localFields.filter((field) => !field.inserted));
- }, [localFields]);
-
- const fieldsValidated = () => {
- setValidateUninsertedFields(true);
- validateFieldsInserted(localFields);
- };
-
- const handleSubmit = async () => {
- setValidateUninsertedFields(true);
-
- if (hasSignatureField && !signatureValid) {
- return;
- }
-
- const isFieldsValid = validateFieldsInserted(localFields);
-
- if (!isFieldsValid) {
- return;
- }
-
- setIsSubmitting(true);
-
- try {
- await onSubmit(localFields);
- } catch {
- setIsSubmitting(false);
- }
-
- // Do not reset to false since we do a redirect.
- };
-
- return (
- <>
-
-
-
-
- {validateUninsertedFields && uninsertedFields[0] && (
-
- Click to insert field
-
- )}
-
- {localFields.map((field) =>
- match(field.type)
- .with(FieldType.SIGNATURE, () => (
-
- ))
- .with(FieldType.INITIALS, () => (
-
- ))
- .with(FieldType.NAME, () => (
-
- ))
- .with(FieldType.DATE, () => (
-
- ))
- .with(FieldType.EMAIL, () => (
-
- ))
- .with(FieldType.TEXT, () => {
- const parsedFieldMeta = field.fieldMeta
- ? ZTextFieldMeta.parse(field.fieldMeta)
- : null;
-
- return (
-
- );
- })
- .with(FieldType.NUMBER, () => {
- const parsedFieldMeta = field.fieldMeta
- ? ZNumberFieldMeta.parse(field.fieldMeta)
- : null;
-
- return (
-
- );
- })
- .with(FieldType.DROPDOWN, () => {
- const parsedFieldMeta = field.fieldMeta
- ? ZDropdownFieldMeta.parse(field.fieldMeta)
- : null;
-
- return (
-
- );
- })
- .with(FieldType.RADIO, () => {
- const parsedFieldMeta = field.fieldMeta
- ? ZRadioFieldMeta.parse(field.fieldMeta)
- : null;
-
- return (
-
- );
- })
- .with(FieldType.CHECKBOX, () => {
- const parsedFieldMeta = field.fieldMeta
- ? ZCheckboxFieldMeta.parse(field.fieldMeta)
- : null;
-
- return (
-
- );
- })
- .otherwise(() => null),
- )}
-
-
-
-
-
-
- Full Name
-
-
- setFullName(e.target.value.trimStart())}
- />
-
-
-
-
- Signature
-
-
-
-
- {
- setSignature(value);
- }}
- />
-
-
-
-
-
-
-
-
-
-
-
-
- Back
-
-
-
-
-
- >
- );
-};
diff --git a/apps/web/src/app/(recipient)/d/[token]/signing-auth-page.tsx b/apps/web/src/app/(recipient)/d/[token]/signing-auth-page.tsx
deleted file mode 100644
index 9300d8f46..000000000
--- a/apps/web/src/app/(recipient)/d/[token]/signing-auth-page.tsx
+++ /dev/null
@@ -1,59 +0,0 @@
-'use client';
-
-import { useState } from 'react';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { signOut } from 'next-auth/react';
-
-import { Button } from '@documenso/ui/primitives/button';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-export const DirectTemplateAuthPageView = () => {
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const [isSigningOut, setIsSigningOut] = useState(false);
-
- const handleChangeAccount = async () => {
- try {
- setIsSigningOut(true);
-
- await signOut({
- callbackUrl: '/signin',
- });
- } catch {
- toast({
- title: _(msg`Something went wrong`),
- description: _(msg`We were unable to log you out at this time.`),
- duration: 10000,
- variant: 'destructive',
- });
- }
-
- setIsSigningOut(false);
- };
-
- return (
-
-
-
- Authentication required
-
-
-
- You need to be logged in to view this page.
-
-
-
handleChangeAccount()}
- loading={isSigningOut}
- >
- Login
-
-
-
- );
-};
diff --git a/apps/web/src/app/(recipient)/layout.tsx b/apps/web/src/app/(recipient)/layout.tsx
deleted file mode 100644
index 51ba41a15..000000000
--- a/apps/web/src/app/(recipient)/layout.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import React from 'react';
-
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
-import type { TGetTeamsResponse } from '@documenso/lib/server-only/team/get-teams';
-import { getTeams } from '@documenso/lib/server-only/team/get-teams';
-
-import { Header as AuthenticatedHeader } from '~/components/(dashboard)/layout/header';
-import { NextAuthProvider } from '~/providers/next-auth';
-
-type RecipientLayoutProps = {
- children: React.ReactNode;
-};
-
-/**
- * A layout to handle scenarios where the user is a recipient of a given resource
- * where we do not care whether they are authenticated or not.
- *
- * Such as direct template access, or signing.
- */
-export default async function RecipientLayout({ children }: RecipientLayoutProps) {
- await setupI18nSSR();
-
- const { user, session } = await getServerComponentSession();
-
- let teams: TGetTeamsResponse = [];
-
- if (user && session) {
- teams = await getTeams({ userId: user.id });
- }
-
- return (
-
-
- {user &&
}
-
-
{children}
-
-
- );
-}
diff --git a/apps/web/src/app/(share)/share/[slug]/opengraph/route.tsx b/apps/web/src/app/(share)/share/[slug]/opengraph/route.tsx
deleted file mode 100644
index cfdd60b03..000000000
--- a/apps/web/src/app/(share)/share/[slug]/opengraph/route.tsx
+++ /dev/null
@@ -1,154 +0,0 @@
-import { ImageResponse } from 'next/og';
-import { NextResponse } from 'next/server';
-
-import { P, match } from 'ts-pattern';
-
-import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
-
-import type { ShareHandlerAPIResponse } from '~/pages/api/share';
-
-export const runtime = 'edge';
-
-const CARD_OFFSET_TOP = 173;
-const CARD_OFFSET_LEFT = 307;
-const CARD_WIDTH = 590;
-const CARD_HEIGHT = 337;
-
-const IMAGE_SIZE = {
- width: 1200,
- height: 630,
-};
-
-type SharePageOpenGraphImageProps = {
- params: { slug: string };
-};
-
-export async function GET(_request: Request, { params: { slug } }: SharePageOpenGraphImageProps) {
- const [interSemiBold, interRegular, caveatRegular, shareFrameImage] = await Promise.all([
- fetch(new URL('@documenso/assets/fonts/inter-semibold.ttf', import.meta.url)).then(
- async (res) => res.arrayBuffer(),
- ),
- fetch(new URL('@documenso/assets/fonts/inter-regular.ttf', import.meta.url)).then(async (res) =>
- res.arrayBuffer(),
- ),
- fetch(new URL('@documenso/assets/fonts/caveat-regular.ttf', import.meta.url)).then(
- async (res) => res.arrayBuffer(),
- ),
- fetch(new URL('@documenso/assets/static/og-share-frame2.png', import.meta.url)).then(
- async (res) => res.arrayBuffer(),
- ),
- ]);
-
- const baseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
-
- const recipientOrSender: ShareHandlerAPIResponse = await fetch(
- new URL(`/api/share?slug=${slug}`, baseUrl),
- ).then(async (res) => res.json());
-
- if ('error' in recipientOrSender) {
- return NextResponse.json({ error: 'Not found' }, { status: 404 });
- }
-
- const isRecipient = 'Signature' in recipientOrSender;
-
- const signatureImage = match(recipientOrSender)
- .with({ signatures: P.array(P._) }, (recipient) => {
- return recipient.signatures?.[0]?.signatureImageAsBase64 || null;
- })
- .otherwise((sender) => {
- return sender.signature || null;
- });
-
- const signatureName = match(recipientOrSender)
- .with({ signatures: P.array(P._) }, (recipient) => {
- return recipient.name || recipient.email;
- })
- .otherwise((sender) => {
- return sender.name || sender.email;
- });
-
- return new ImageResponse(
- (
-
- {/* @ts-expect-error Lack of typing from ImageResponse */}
-
-
- {signatureImage ? (
-
-
-
- ) : (
-
- {signatureName}
-
- )}
-
-
-
- {isRecipient ? 'Document Signed!' : 'Document Sent!'}
-
-
-
- ),
- {
- ...IMAGE_SIZE,
- fonts: [
- {
- name: 'Caveat',
- data: caveatRegular,
- style: 'italic',
- },
- {
- name: 'Inter',
- data: interRegular,
- style: 'normal',
- weight: 400,
- },
- {
- name: 'Inter',
- data: interSemiBold,
- style: 'normal',
- weight: 600,
- },
- ],
- headers: {
- 'Access-Control-Allow-Origin': '*',
- 'Access-Control-Allow-Methods': 'GET, OPTIONS',
- },
- },
- );
-}
diff --git a/apps/web/src/app/(share)/share/[slug]/page.tsx b/apps/web/src/app/(share)/share/[slug]/page.tsx
deleted file mode 100644
index efcd75a41..000000000
--- a/apps/web/src/app/(share)/share/[slug]/page.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import type { Metadata } from 'next';
-import { headers } from 'next/headers';
-import { redirect } from 'next/navigation';
-
-import { NEXT_PUBLIC_MARKETING_URL } from '@documenso/lib/constants/app';
-
-type SharePageProps = {
- params: { slug: string };
-};
-
-export function generateMetadata({ params: { slug } }: SharePageProps) {
- return {
- title: 'Documenso - Share',
- description: 'I just signed a document in style with Documenso!',
- openGraph: {
- title: 'Documenso - Join the open source signing revolution',
- description: 'I just signed with Documenso!',
- type: 'website',
- images: [`/share/${slug}/opengraph`],
- },
- twitter: {
- site: '@documenso',
- card: 'summary_large_image',
- images: [`/share/${slug}/opengraph`],
- description: 'I just signed with Documenso!',
- },
- } satisfies Metadata;
-}
-
-export default function SharePage() {
- const userAgent = headers().get('User-Agent') ?? '';
-
- // https://stackoverflow.com/questions/47026171/how-to-detect-bots-for-open-graph-with-user-agent
- if (/bot|facebookexternalhit|WhatsApp|google|bing|duckduckbot|MetaInspector/i.test(userAgent)) {
- return null;
- }
-
- redirect(NEXT_PUBLIC_MARKETING_URL() ?? 'http://localhost:3001');
-}
diff --git a/apps/web/src/app/(signing)/sign/[token]/auto-sign.tsx b/apps/web/src/app/(signing)/sign/[token]/auto-sign.tsx
deleted file mode 100644
index d404d0d6b..000000000
--- a/apps/web/src/app/(signing)/sign/[token]/auto-sign.tsx
+++ /dev/null
@@ -1,237 +0,0 @@
-'use client';
-
-import { useState, useTransition } from 'react';
-
-import { useRouter } from 'next/navigation';
-
-import { Plural, Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { useForm } from 'react-hook-form';
-import { P, match } from 'ts-pattern';
-
-import { unsafe_useEffectOnce } from '@documenso/lib/client-only/hooks/use-effect-once';
-import { DocumentAuth } from '@documenso/lib/types/document-auth';
-import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
-import type { Field, Recipient } from '@documenso/prisma/client';
-import { FieldType } from '@documenso/prisma/client';
-import { trpc } from '@documenso/trpc/react';
-import { Button } from '@documenso/ui/primitives/button';
-import {
- Dialog,
- DialogContent,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from '@documenso/ui/primitives/dialog';
-import { FRIENDLY_FIELD_TYPE } from '@documenso/ui/primitives/document-flow/types';
-import { Form } from '@documenso/ui/primitives/form/form';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-import { SigningDisclosure } from '~/components/general/signing-disclosure';
-
-import { useRequiredDocumentAuthContext } from './document-auth-provider';
-import { useRequiredSigningContext } from './provider';
-
-const AUTO_SIGNABLE_FIELD_TYPES: string[] = [
- FieldType.NAME,
- FieldType.INITIALS,
- FieldType.EMAIL,
- FieldType.DATE,
-];
-
-// The action auth types that are not allowed to be auto signed
-//
-// Reasoning: If the action auth is a passkey or 2FA, it's likely that the owner of the document
-// intends on having the user manually sign due to the additional security measures employed for
-// other field types.
-const NON_AUTO_SIGNABLE_ACTION_AUTH_TYPES: string[] = [
- DocumentAuth.PASSKEY,
- DocumentAuth.TWO_FACTOR_AUTH,
-];
-
-// The threshold for the number of fields that could be autosigned before displaying the dialog
-//
-// Reasoning: If there aren't that many fields, it's likely going to be easier to manually sign each one
-// while for larger documents with many fields it will be beneficial to sign away the boilerplate fields.
-const AUTO_SIGN_THRESHOLD = 5;
-
-export type AutoSignProps = {
- recipient: Pick;
- fields: Field[];
-};
-
-export const AutoSign = ({ recipient, fields }: AutoSignProps) => {
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const router = useRouter();
-
- const { email, fullName } = useRequiredSigningContext();
- const { derivedRecipientActionAuth } = useRequiredDocumentAuthContext();
-
- const [open, setOpen] = useState(false);
- const [isPending, startTransition] = useTransition();
-
- const form = useForm();
-
- const { mutateAsync: signFieldWithToken } = trpc.field.signFieldWithToken.useMutation();
-
- const autoSignableFields = fields.filter((field) => {
- if (field.inserted) {
- return false;
- }
-
- if (!AUTO_SIGNABLE_FIELD_TYPES.includes(field.type)) {
- return false;
- }
-
- if (field.type === FieldType.NAME && !fullName) {
- return false;
- }
-
- if (field.type === FieldType.INITIALS && !fullName) {
- return false;
- }
-
- if (field.type === FieldType.EMAIL && !email) {
- return false;
- }
-
- return true;
- });
-
- const actionAuthAllowsAutoSign = !NON_AUTO_SIGNABLE_ACTION_AUTH_TYPES.includes(
- derivedRecipientActionAuth ?? '',
- );
-
- const onSubmit = async () => {
- const results = await Promise.allSettled(
- autoSignableFields.map(async (field) => {
- const value = match(field.type)
- .with(FieldType.NAME, () => fullName)
- .with(FieldType.INITIALS, () => extractInitials(fullName))
- .with(FieldType.EMAIL, () => email)
- .with(FieldType.DATE, () => new Date().toISOString())
- .otherwise(() => '');
-
- const authOptions = match(derivedRecipientActionAuth)
- .with(DocumentAuth.ACCOUNT, () => ({
- type: DocumentAuth.ACCOUNT,
- }))
- .with(DocumentAuth.EXPLICIT_NONE, () => ({
- type: DocumentAuth.EXPLICIT_NONE,
- }))
- .with(null, () => undefined)
- .with(
- P.union(DocumentAuth.PASSKEY, DocumentAuth.TWO_FACTOR_AUTH),
- // This is a bit dirty, but the sentinel value used here is incredibly short-lived.
- () => 'NOT_SUPPORTED' as const,
- )
- .exhaustive();
-
- if (authOptions === 'NOT_SUPPORTED') {
- throw new Error('Action auth is not supported for auto signing');
- }
-
- if (!value) {
- throw new Error('No value to sign');
- }
-
- return await signFieldWithToken({
- token: recipient.token,
- fieldId: field.id,
- value,
- isBase64: false,
- authOptions,
- });
- }),
- );
-
- if (results.some((result) => result.status === 'rejected')) {
- toast({
- title: _(msg`Error`),
- description: _(
- msg`An error occurred while auto-signing the document, some fields may not be signed. Please review and manually sign any remaining fields.`,
- ),
- duration: 5000,
- variant: 'destructive',
- });
- }
-
- startTransition(() => {
- router.refresh();
-
- setOpen(false);
- });
- };
-
- unsafe_useEffectOnce(() => {
- if (actionAuthAllowsAutoSign && autoSignableFields.length > AUTO_SIGN_THRESHOLD) {
- setOpen(true);
- }
- });
-
- return (
-
-
-
- Automatically sign fields
-
-
-
-
-
- When you sign a document, we can automatically fill in and sign the following fields
- using information that has already been provided. You can also manually sign or remove
- any automatically signed fields afterwards if you desire.
-
-
-
-
- {AUTO_SIGNABLE_FIELD_TYPES.map((fieldType) => (
-
- {_(FRIENDLY_FIELD_TYPE[fieldType as FieldType])}
-
- (
- f.type === fieldType).length}
- one="1 matching field"
- other="# matching fields"
- />
- )
-
-
- ))}
-
-
-
-
-
-
-
-
- {
- setOpen(false);
- }}
- >
- Cancel
-
-
-
- Sign
-
-
-
-
-
-
- );
-};
diff --git a/apps/web/src/app/(signing)/sign/[token]/checkbox-field.tsx b/apps/web/src/app/(signing)/sign/[token]/checkbox-field.tsx
deleted file mode 100644
index 2bf96afdd..000000000
--- a/apps/web/src/app/(signing)/sign/[token]/checkbox-field.tsx
+++ /dev/null
@@ -1,299 +0,0 @@
-'use client';
-
-import { useEffect, useMemo, useState, useTransition } from 'react';
-
-import { useRouter } from 'next/navigation';
-
-import { msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { Loader } from 'lucide-react';
-
-import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
-import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
-import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
-import { ZCheckboxFieldMeta } from '@documenso/lib/types/field-meta';
-import { fromCheckboxValue, toCheckboxValue } from '@documenso/lib/universal/field-checkbox';
-import type { Recipient } from '@documenso/prisma/client';
-import type { FieldWithSignatureAndFieldMeta } from '@documenso/prisma/types/field-with-signature-and-fieldmeta';
-import { trpc } from '@documenso/trpc/react';
-import type {
- TRemovedSignedFieldWithTokenMutationSchema,
- TSignFieldWithTokenMutationSchema,
-} from '@documenso/trpc/server/field-router/schema';
-import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
-import { Checkbox } from '@documenso/ui/primitives/checkbox';
-import { checkboxValidationSigns } from '@documenso/ui/primitives/document-flow/field-items-advanced-settings/constants';
-import { Label } from '@documenso/ui/primitives/label';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-import { useRequiredDocumentAuthContext } from './document-auth-provider';
-import { SigningFieldContainer } from './signing-field-container';
-
-export type CheckboxFieldProps = {
- field: FieldWithSignatureAndFieldMeta;
- recipient: Recipient;
- onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise | void;
- onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise | void;
-};
-
-export const CheckboxField = ({
- field,
- recipient,
- onSignField,
- onUnsignField,
-}: CheckboxFieldProps) => {
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const router = useRouter();
- const [isPending, startTransition] = useTransition();
- const { executeActionAuthProcedure } = useRequiredDocumentAuthContext();
-
- const parsedFieldMeta = ZCheckboxFieldMeta.parse(field.fieldMeta);
-
- const values = parsedFieldMeta.values?.map((item) => ({
- ...item,
- value: item.value.length > 0 ? item.value : `empty-value-${item.id}`,
- }));
-
- const [checkedValues, setCheckedValues] = useState(
- values
- ?.map((item) =>
- item.checked ? (item.value.length > 0 ? item.value : `empty-value-${item.id}`) : '',
- )
- .filter(Boolean) || [],
- );
-
- const isReadOnly = parsedFieldMeta.readOnly;
-
- const checkboxValidationRule = parsedFieldMeta.validationRule;
- const checkboxValidationLength = parsedFieldMeta.validationLength;
- const validationSign = checkboxValidationSigns.find(
- (sign) => sign.label === checkboxValidationRule,
- );
-
- const isLengthConditionMet = useMemo(() => {
- if (!validationSign) return true;
- return (
- (validationSign.value === '>=' && checkedValues.length >= (checkboxValidationLength || 0)) ||
- (validationSign.value === '=' && checkedValues.length === (checkboxValidationLength || 0)) ||
- (validationSign.value === '<=' && checkedValues.length <= (checkboxValidationLength || 0))
- );
- }, [checkedValues, validationSign, checkboxValidationLength]);
-
- const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
- trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
-
- const {
- mutateAsync: removeSignedFieldWithToken,
- isPending: isRemoveSignedFieldWithTokenLoading,
- } = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
-
- const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;
- const shouldAutoSignField =
- (!field.inserted && checkedValues.length > 0 && isLengthConditionMet) ||
- (!field.inserted && isReadOnly && isLengthConditionMet);
-
- const onSign = async (authOptions?: TRecipientActionAuth) => {
- try {
- const payload: TSignFieldWithTokenMutationSchema = {
- token: recipient.token,
- fieldId: field.id,
- value: toCheckboxValue(checkedValues),
- isBase64: true,
- authOptions,
- };
-
- if (onSignField) {
- await onSignField(payload);
- } else {
- await signFieldWithToken(payload);
- }
-
- startTransition(() => router.refresh());
- } catch (err) {
- const error = AppError.parseError(err);
-
- if (error.code === AppErrorCode.UNAUTHORIZED) {
- throw error;
- }
-
- console.error(err);
-
- toast({
- title: _(msg`Error`),
- description: _(msg`An error occurred while signing the document.`),
- variant: 'destructive',
- });
- }
- };
-
- const onRemove = async (fieldType?: string) => {
- try {
- const payload: TRemovedSignedFieldWithTokenMutationSchema = {
- token: recipient.token,
- fieldId: field.id,
- };
-
- if (onUnsignField) {
- await onUnsignField(payload);
- } else {
- await removeSignedFieldWithToken(payload);
- }
-
- if (fieldType === 'Checkbox') {
- setCheckedValues([]);
- }
-
- startTransition(() => router.refresh());
- } catch (err) {
- console.error(err);
-
- toast({
- title: _(msg`Error`),
- description: _(msg`An error occurred while removing the signature.`),
- variant: 'destructive',
- });
- }
- };
-
- const handleCheckboxChange = (value: string, itemId: number) => {
- const updatedValue = value || `empty-value-${itemId}`;
- const updatedValues = checkedValues.includes(updatedValue)
- ? checkedValues.filter((v) => v !== updatedValue)
- : [...checkedValues, updatedValue];
-
- setCheckedValues(updatedValues);
- };
-
- const handleCheckboxOptionClick = async (item: {
- id: number;
- checked: boolean;
- value: string;
- }) => {
- let updatedValues: string[] = [];
-
- try {
- const isChecked = checkedValues.includes(
- item.value.length > 0 ? item.value : `empty-value-${item.id}`,
- );
-
- if (!isChecked) {
- updatedValues = [
- ...checkedValues,
- item.value.length > 0 ? item.value : `empty-value-${item.id}`,
- ];
-
- await removeSignedFieldWithToken({
- token: recipient.token,
- fieldId: field.id,
- });
-
- if (isLengthConditionMet) {
- await signFieldWithToken({
- token: recipient.token,
- fieldId: field.id,
- value: toCheckboxValue(checkedValues),
- isBase64: true,
- });
- }
- } else {
- updatedValues = checkedValues.filter(
- (v) => v !== item.value && v !== `empty-value-${item.id}`,
- );
-
- await removeSignedFieldWithToken({
- token: recipient.token,
- fieldId: field.id,
- });
- }
- } catch (err) {
- console.error(err);
-
- toast({
- title: _(msg`Error`),
- description: _(msg`An error occurred while updating the signature.`),
- variant: 'destructive',
- });
- } finally {
- setCheckedValues(updatedValues);
- startTransition(() => router.refresh());
- }
- };
-
- useEffect(() => {
- if (shouldAutoSignField) {
- void executeActionAuthProcedure({
- onReauthFormSubmit: async (authOptions) => await onSign(authOptions),
- actionTarget: field.type,
- });
- }
- }, [checkedValues, isLengthConditionMet, field.inserted]);
-
- const parsedCheckedValues = useMemo(
- () => fromCheckboxValue(field.customText),
- [field.customText],
- );
-
- return (
-
- {isLoading && (
-
-
-
- )}
-
- {!field.inserted && (
- <>
- {!isLengthConditionMet && (
-
- {validationSign?.label} {checkboxValidationLength}
-
- )}
-
- {values?.map((item: { id: number; value: string; checked: boolean }, index: number) => {
- const itemValue = item.value || `empty-value-${item.id}`;
-
- return (
-
- handleCheckboxChange(item.value, item.id)}
- />
-
- {item.value.includes('empty-value-') ? '' : item.value}
-
-
- );
- })}
-
- >
- )}
-
- {field.inserted && (
-
- {values?.map((item: { id: number; value: string; checked: boolean }, index: number) => {
- const itemValue = item.value || `empty-value-${item.id}`;
-
- return (
-
- void handleCheckboxOptionClick(item)}
- />
-
- {item.value.includes('empty-value-') ? '' : item.value}
-
-
- );
- })}
-
- )}
-
- );
-};
diff --git a/apps/web/src/app/(signing)/sign/[token]/complete/claim-account.tsx b/apps/web/src/app/(signing)/sign/[token]/complete/claim-account.tsx
deleted file mode 100644
index 9f2b85858..000000000
--- a/apps/web/src/app/(signing)/sign/[token]/complete/claim-account.tsx
+++ /dev/null
@@ -1,166 +0,0 @@
-'use client';
-
-import { useRouter } from 'next/navigation';
-
-import { zodResolver } from '@hookform/resolvers/zod';
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { useForm } from 'react-hook-form';
-import { z } from 'zod';
-
-import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
-import { AppError } from '@documenso/lib/errors/app-error';
-import { trpc } from '@documenso/trpc/react';
-import { ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
-import { Button } from '@documenso/ui/primitives/button';
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from '@documenso/ui/primitives/form/form';
-import { Input } from '@documenso/ui/primitives/input';
-import { PasswordInput } from '@documenso/ui/primitives/password-input';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-import { signupErrorMessages } from '~/components/forms/v2/signup';
-
-export type ClaimAccountProps = {
- defaultName: string;
- defaultEmail: string;
- trigger?: React.ReactNode;
-};
-
-export const ZClaimAccountFormSchema = z
- .object({
- name: z
- .string()
- .trim()
- .min(1, { message: msg`Please enter a valid name.`.id }),
- email: z.string().email().min(1),
- password: ZPasswordSchema,
- })
- .refine(
- (data) => {
- const { name, email, password } = data;
- return !password.includes(name) && !password.includes(email.split('@')[0]);
- },
- {
- message: msg`Password should not be common or based on personal information`.id,
- path: ['password'],
- },
- );
-
-export type TClaimAccountFormSchema = z.infer;
-
-export const ClaimAccount = ({ defaultName, defaultEmail }: ClaimAccountProps) => {
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const analytics = useAnalytics();
- const router = useRouter();
-
- const { mutateAsync: signup } = trpc.auth.signup.useMutation();
-
- const form = useForm({
- values: {
- name: defaultName ?? '',
- email: defaultEmail,
- password: '',
- },
- resolver: zodResolver(ZClaimAccountFormSchema),
- });
-
- const onFormSubmit = async ({ name, email, password }: TClaimAccountFormSchema) => {
- try {
- await signup({ name, email, password });
-
- router.push(`/unverified-account`);
-
- toast({
- title: _(msg`Registration Successful`),
- description: _(
- msg`You have successfully registered. Please verify your account by clicking on the link you received in the email.`,
- ),
- duration: 5000,
- });
-
- analytics.capture('App: User Claim Account', {
- email,
- timestamp: new Date().toISOString(),
- });
- } catch (err) {
- const error = AppError.parseError(err);
-
- const errorMessage = signupErrorMessages[error.code] ?? signupErrorMessages.INVALID_REQUEST;
-
- toast({
- title: _(msg`An error occurred`),
- description: _(errorMessage),
- variant: 'destructive',
- });
- }
- };
-
- return (
-
- );
-};
diff --git a/apps/web/src/app/(signing)/sign/[token]/complete/document-preview-button.tsx b/apps/web/src/app/(signing)/sign/[token]/complete/document-preview-button.tsx
deleted file mode 100644
index 0fd17b4fb..000000000
--- a/apps/web/src/app/(signing)/sign/[token]/complete/document-preview-button.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-'use client';
-
-import { useState } from 'react';
-
-import { Trans } from '@lingui/macro';
-import { FileSearch } from 'lucide-react';
-
-import type { DocumentData } from '@documenso/prisma/client';
-import DocumentDialog from '@documenso/ui/components/document/document-dialog';
-import type { ButtonProps } from '@documenso/ui/primitives/button';
-import { Button } from '@documenso/ui/primitives/button';
-
-export type DocumentPreviewButtonProps = {
- className?: string;
- documentData: DocumentData;
-} & ButtonProps;
-
-export const DocumentPreviewButton = ({
- className,
- documentData,
- ...props
-}: DocumentPreviewButtonProps) => {
- const [showDialog, setShowDialog] = useState(false);
-
- return (
- <>
- setShowDialog((visible) => !visible)}
- {...props}
- >
-
- View Original Document
-
-
-
- >
- );
-};
diff --git a/apps/web/src/app/(signing)/sign/[token]/complete/layout.tsx b/apps/web/src/app/(signing)/sign/[token]/complete/layout.tsx
deleted file mode 100644
index 23a5f1278..000000000
--- a/apps/web/src/app/(signing)/sign/[token]/complete/layout.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import React from 'react';
-
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-
-import { RefreshOnFocus } from '~/components/(dashboard)/refresh-on-focus/refresh-on-focus';
-
-export type SigningLayoutProps = {
- children: React.ReactNode;
-};
-
-export default async function SigningLayout({ children }: SigningLayoutProps) {
- await setupI18nSSR();
-
- return (
-
- {children}
-
-
-
- );
-}
diff --git a/apps/web/src/app/(signing)/sign/[token]/complete/page.tsx b/apps/web/src/app/(signing)/sign/[token]/complete/page.tsx
deleted file mode 100644
index b1ca1c9c6..000000000
--- a/apps/web/src/app/(signing)/sign/[token]/complete/page.tsx
+++ /dev/null
@@ -1,233 +0,0 @@
-import Link from 'next/link';
-import { notFound } from 'next/navigation';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { CheckCircle2, Clock8 } from 'lucide-react';
-import { getServerSession } from 'next-auth';
-import { env } from 'next-runtime-env';
-import { match } from 'ts-pattern';
-
-import signingCelebration from '@documenso/assets/images/signing-celebration.png';
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
-import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
-import { isRecipientAuthorized } from '@documenso/lib/server-only/document/is-recipient-authorized';
-import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
-import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
-import { getRecipientSignatures } from '@documenso/lib/server-only/recipient/get-recipient-signatures';
-import { getUserByEmail } from '@documenso/lib/server-only/user/get-user-by-email';
-import { DocumentStatus, FieldType, RecipientRole } from '@documenso/prisma/client';
-import { DocumentDownloadButton } from '@documenso/ui/components/document/document-download-button';
-import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button';
-import { SigningCard3D } from '@documenso/ui/components/signing-card';
-import { cn } from '@documenso/ui/lib/utils';
-import { Badge } from '@documenso/ui/primitives/badge';
-
-import { SigningAuthPageView } from '../signing-auth-page';
-import { ClaimAccount } from './claim-account';
-import { DocumentPreviewButton } from './document-preview-button';
-import { PollUntilDocumentCompleted } from './poll-until-document-completed';
-
-export type CompletedSigningPageProps = {
- params: {
- token?: string;
- };
-};
-
-export default async function CompletedSigningPage({
- params: { token },
-}: CompletedSigningPageProps) {
- await setupI18nSSR();
-
- const { _ } = useLingui();
-
- const NEXT_PUBLIC_DISABLE_SIGNUP = env('NEXT_PUBLIC_DISABLE_SIGNUP');
-
- if (!token) {
- return notFound();
- }
-
- const { user } = await getServerComponentSession();
-
- const document = await getDocumentAndSenderByToken({
- token,
- requireAccessAuth: false,
- }).catch(() => null);
-
- if (!document || !document.documentData) {
- return notFound();
- }
-
- const { documentData } = document;
-
- const [fields, recipient] = await Promise.all([
- getFieldsForToken({ token }),
- getRecipientByToken({ token }).catch(() => null),
- ]);
-
- if (!recipient) {
- return notFound();
- }
-
- const isDocumentAccessValid = await isRecipientAuthorized({
- type: 'ACCESS',
- documentAuthOptions: document.authOptions,
- recipient,
- userId: user?.id,
- });
-
- if (!isDocumentAccessValid) {
- return ;
- }
-
- const signatures = await getRecipientSignatures({ recipientId: recipient.id });
- const isExistingUser = await getUserByEmail({ email: recipient.email })
- .then((u) => !!u)
- .catch(() => false);
-
- const recipientName =
- recipient.name ||
- fields.find((field) => field.type === FieldType.NAME)?.customText ||
- recipient.email;
-
- const sessionData = await getServerSession();
- const isLoggedIn = !!sessionData?.user;
- const canSignUp = !isExistingUser && NEXT_PUBLIC_DISABLE_SIGNUP !== 'true';
-
- return (
-
-
-
-
-
- {document.title}
-
-
-
- {/* Card with recipient */}
-
-
-
- {recipient.role === RecipientRole.SIGNER && Document Signed }
- {recipient.role === RecipientRole.VIEWER && Document Viewed }
- {recipient.role === RecipientRole.APPROVER && Document Approved }
-
-
- {match({ status: document.status, deletedAt: document.deletedAt })
- .with({ status: DocumentStatus.COMPLETED }, () => (
-
-
-
- Everyone has signed
-
-
- ))
- .with({ deletedAt: null }, () => (
-
-
-
- Waiting for others to sign
-
-
- ))
- .otherwise(() => (
-
-
-
- Document no longer available to sign
-
-
- ))}
-
- {match({ status: document.status, deletedAt: document.deletedAt })
- .with({ status: DocumentStatus.COMPLETED }, () => (
-
-
- Everyone has signed! You will receive an Email copy of the signed document.
-
-
- ))
- .with({ deletedAt: null }, () => (
-
-
- You will receive an Email copy of the signed document once everyone has signed.
-
-
- ))
- .otherwise(() => (
-
-
- This document has been cancelled by the owner and is no longer available for
- others to sign.
-
-
- ))}
-
-
-
-
- {document.status === DocumentStatus.COMPLETED ? (
-
- ) : (
-
- )}
-
-
-
-
- {canSignUp && (
-
-
- Need to sign documents?
-
-
-
-
- Create your account and start using state-of-the-art document signing.
-
-
-
-
-
- )}
-
- {isLoggedIn && (
-
-
Go Back Home
-
- )}
-
-
-
-
-
- );
-}
diff --git a/apps/web/src/app/(signing)/sign/[token]/complete/poll-until-document-completed.tsx b/apps/web/src/app/(signing)/sign/[token]/complete/poll-until-document-completed.tsx
deleted file mode 100644
index ffc46d8ee..000000000
--- a/apps/web/src/app/(signing)/sign/[token]/complete/poll-until-document-completed.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-'use client';
-
-import { useEffect } from 'react';
-
-import { useRouter } from 'next/navigation';
-
-import type { Document } from '@documenso/prisma/client';
-import { DocumentStatus } from '@documenso/prisma/client';
-
-export type PollUntilDocumentCompletedProps = {
- document: Pick;
-};
-
-export const PollUntilDocumentCompleted = ({ document }: PollUntilDocumentCompletedProps) => {
- const router = useRouter();
-
- useEffect(() => {
- if (document.status === DocumentStatus.COMPLETED) {
- return;
- }
-
- const interval = setInterval(() => {
- if (window.document.hasFocus()) {
- router.refresh();
- }
- }, 5000);
-
- return () => clearInterval(interval);
- }, [router, document.status]);
-
- return <>>;
-};
diff --git a/apps/web/src/app/(signing)/sign/[token]/date-field.tsx b/apps/web/src/app/(signing)/sign/[token]/date-field.tsx
deleted file mode 100644
index b62eaf652..000000000
--- a/apps/web/src/app/(signing)/sign/[token]/date-field.tsx
+++ /dev/null
@@ -1,159 +0,0 @@
-'use client';
-
-import { useTransition } from 'react';
-
-import { useRouter } from 'next/navigation';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { Loader } from 'lucide-react';
-
-import {
- DEFAULT_DOCUMENT_DATE_FORMAT,
- convertToLocalSystemFormat,
-} from '@documenso/lib/constants/date-formats';
-import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones';
-import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
-import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
-import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
-import type { Recipient } from '@documenso/prisma/client';
-import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
-import { trpc } from '@documenso/trpc/react';
-import type {
- TRemovedSignedFieldWithTokenMutationSchema,
- TSignFieldWithTokenMutationSchema,
-} from '@documenso/trpc/server/field-router/schema';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-import { SigningFieldContainer } from './signing-field-container';
-
-export type DateFieldProps = {
- field: FieldWithSignature;
- recipient: Recipient;
- dateFormat?: string | null;
- timezone?: string | null;
- onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise | void;
- onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise | void;
-};
-
-export const DateField = ({
- field,
- recipient,
- dateFormat = DEFAULT_DOCUMENT_DATE_FORMAT,
- timezone = DEFAULT_DOCUMENT_TIME_ZONE,
- onSignField,
- onUnsignField,
-}: DateFieldProps) => {
- const router = useRouter();
-
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const [isPending, startTransition] = useTransition();
-
- const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
- trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
-
- const {
- mutateAsync: removeSignedFieldWithToken,
- isPending: isRemoveSignedFieldWithTokenLoading,
- } = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
-
- const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;
-
- const localDateString = convertToLocalSystemFormat(field.customText, dateFormat, timezone);
-
- const isDifferentTime = field.inserted && localDateString !== field.customText;
-
- const tooltipText = _(
- msg`"${field.customText}" will appear on the document as it has a timezone of "${timezone}".`,
- );
-
- const onSign = async (authOptions?: TRecipientActionAuth) => {
- try {
- const payload: TSignFieldWithTokenMutationSchema = {
- token: recipient.token,
- fieldId: field.id,
- value: dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT,
- authOptions,
- };
-
- if (onSignField) {
- await onSignField(payload);
- return;
- }
-
- await signFieldWithToken(payload);
-
- startTransition(() => router.refresh());
- } catch (err) {
- const error = AppError.parseError(err);
-
- if (error.code === AppErrorCode.UNAUTHORIZED) {
- throw error;
- }
-
- console.error(err);
-
- toast({
- title: _(msg`Error`),
- description: _(msg`An error occurred while signing the document.`),
- variant: 'destructive',
- });
- }
- };
-
- const onRemove = async () => {
- try {
- const payload: TRemovedSignedFieldWithTokenMutationSchema = {
- token: recipient.token,
- fieldId: field.id,
- };
-
- if (onUnsignField) {
- await onUnsignField(payload);
- return;
- }
-
- await removeSignedFieldWithToken(payload);
-
- startTransition(() => router.refresh());
- } catch (err) {
- console.error(err);
-
- toast({
- title: _(msg`Error`),
- description: _(msg`An error occurred while removing the signature.`),
- variant: 'destructive',
- });
- }
- };
-
- return (
-
- {isLoading && (
-
-
-
- )}
-
- {!field.inserted && (
-
- Date
-
- )}
-
- {field.inserted && (
-
- {localDateString}
-
- )}
-
- );
-};
diff --git a/apps/web/src/app/(signing)/sign/[token]/document-action-auth-2fa.tsx b/apps/web/src/app/(signing)/sign/[token]/document-action-auth-2fa.tsx
deleted file mode 100644
index 9666b4235..000000000
--- a/apps/web/src/app/(signing)/sign/[token]/document-action-auth-2fa.tsx
+++ /dev/null
@@ -1,190 +0,0 @@
-import { useEffect, useState } from 'react';
-
-import { zodResolver } from '@hookform/resolvers/zod';
-import { Trans } from '@lingui/macro';
-import { useForm } from 'react-hook-form';
-import { z } from 'zod';
-
-import { AppError } from '@documenso/lib/errors/app-error';
-import { DocumentAuth, type TRecipientActionAuth } from '@documenso/lib/types/document-auth';
-import { RecipientRole } from '@documenso/prisma/client';
-import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
-import { Button } from '@documenso/ui/primitives/button';
-import { DialogFooter } from '@documenso/ui/primitives/dialog';
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from '@documenso/ui/primitives/form/form';
-import { PinInput, PinInputGroup, PinInputSlot } from '@documenso/ui/primitives/pin-input';
-
-import { EnableAuthenticatorAppDialog } from '~/components/forms/2fa/enable-authenticator-app-dialog';
-
-import { useRequiredDocumentAuthContext } from './document-auth-provider';
-
-export type DocumentActionAuth2FAProps = {
- actionTarget?: 'FIELD' | 'DOCUMENT';
- actionVerb?: string;
- open: boolean;
- onOpenChange: (value: boolean) => void;
- onReauthFormSubmit: (values?: TRecipientActionAuth) => Promise | void;
-};
-
-const Z2FAAuthFormSchema = z.object({
- token: z
- .string()
- .min(4, { message: 'Token must at least 4 characters long' })
- .max(10, { message: 'Token must be at most 10 characters long' }),
-});
-
-type T2FAAuthFormSchema = z.infer;
-
-export const DocumentActionAuth2FA = ({
- actionTarget = 'FIELD',
- actionVerb = 'sign',
- onReauthFormSubmit,
- open,
- onOpenChange,
-}: DocumentActionAuth2FAProps) => {
- const { recipient, user, isCurrentlyAuthenticating, setIsCurrentlyAuthenticating } =
- useRequiredDocumentAuthContext();
-
- const form = useForm({
- resolver: zodResolver(Z2FAAuthFormSchema),
- defaultValues: {
- token: '',
- },
- });
-
- const [is2FASetupSuccessful, setIs2FASetupSuccessful] = useState(false);
- const [formErrorCode, setFormErrorCode] = useState(null);
-
- const onFormSubmit = async ({ token }: T2FAAuthFormSchema) => {
- try {
- setIsCurrentlyAuthenticating(true);
-
- await onReauthFormSubmit({
- type: DocumentAuth.TWO_FACTOR_AUTH,
- token,
- });
-
- setIsCurrentlyAuthenticating(false);
-
- onOpenChange(false);
- } catch (err) {
- setIsCurrentlyAuthenticating(false);
-
- const error = AppError.parseError(err);
- setFormErrorCode(error.code);
-
- // Todo: Alert.
- }
- };
-
- useEffect(() => {
- form.reset({
- token: '',
- });
-
- setIs2FASetupSuccessful(false);
- setFormErrorCode(null);
-
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [open]);
-
- if (!user?.twoFactorEnabled && !is2FASetupSuccessful) {
- return (
-
-
-
-
- {recipient.role === RecipientRole.VIEWER && actionTarget === 'DOCUMENT' ? (
- You need to setup 2FA to mark this document as viewed.
- ) : (
- // Todo: Translate
- `You need to setup 2FA to ${actionVerb.toLowerCase()} this ${actionTarget.toLowerCase()}.`
- )}
-
-
- {user?.identityProvider === 'DOCUMENSO' && (
-
-
- By enabling 2FA, you will be required to enter a code from your authenticator app
- every time you sign in.
-
-
- )}
-
-
-
-
- onOpenChange(false)}>
- Close
-
-
- setIs2FASetupSuccessful(true)} />
-
-
- );
- }
-
- return (
-
-
-
-
-
(
-
- 2FA token
-
-
-
- {Array(6)
- .fill(null)
- .map((_, i) => (
-
-
-
- ))}
-
-
-
-
-
- )}
- />
-
- {formErrorCode && (
-
-
- Unauthorized
-
-
-
- We were unable to verify your details. Please try again or contact support
-
-
-
- )}
-
-
- onOpenChange(false)}>
- Cancel
-
-
-
- Sign
-
-
-
-
-
-
- );
-};
diff --git a/apps/web/src/app/(signing)/sign/[token]/document-action-auth-account.tsx b/apps/web/src/app/(signing)/sign/[token]/document-action-auth-account.tsx
deleted file mode 100644
index 5fb40394d..000000000
--- a/apps/web/src/app/(signing)/sign/[token]/document-action-auth-account.tsx
+++ /dev/null
@@ -1,80 +0,0 @@
-import { useState } from 'react';
-
-import { useRouter } from 'next/navigation';
-
-import { Trans } from '@lingui/macro';
-import { signOut } from 'next-auth/react';
-
-import { RecipientRole } from '@documenso/prisma/client';
-import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
-import { Button } from '@documenso/ui/primitives/button';
-import { DialogFooter } from '@documenso/ui/primitives/dialog';
-
-import { useRequiredDocumentAuthContext } from './document-auth-provider';
-
-export type DocumentActionAuthAccountProps = {
- actionTarget?: 'FIELD' | 'DOCUMENT';
- actionVerb?: string;
- onOpenChange: (value: boolean) => void;
-};
-
-export const DocumentActionAuthAccount = ({
- actionTarget = 'FIELD',
- actionVerb = 'sign',
- onOpenChange,
-}: DocumentActionAuthAccountProps) => {
- const { recipient } = useRequiredDocumentAuthContext();
-
- const router = useRouter();
-
- const [isSigningOut, setIsSigningOut] = useState(false);
-
- const handleChangeAccount = async (email: string) => {
- try {
- setIsSigningOut(true);
-
- await signOut({
- redirect: false,
- });
-
- router.push(`/signin#email=${email}`);
- } catch {
- setIsSigningOut(false);
-
- // Todo: Alert.
- }
- };
-
- return (
-
-
-
- {actionTarget === 'DOCUMENT' && recipient.role === RecipientRole.VIEWER ? (
-
-
- To mark this document as viewed, you need to be logged in as{' '}
- {recipient.email}
-
-
- ) : (
-
- {/* Todo: Translate */}
- To {actionVerb.toLowerCase()} this {actionTarget.toLowerCase()}, you need to be logged
- in as {recipient.email}
-
- )}
-
-
-
-
- onOpenChange(false)}>
- Cancel
-
-
- handleChangeAccount(recipient.email)} loading={isSigningOut}>
- Login
-
-
-
- );
-};
diff --git a/apps/web/src/app/(signing)/sign/[token]/document-action-auth-dialog.tsx b/apps/web/src/app/(signing)/sign/[token]/document-action-auth-dialog.tsx
deleted file mode 100644
index 5cb618a0e..000000000
--- a/apps/web/src/app/(signing)/sign/[token]/document-action-auth-dialog.tsx
+++ /dev/null
@@ -1,91 +0,0 @@
-import { Trans } from '@lingui/macro';
-import { P, match } from 'ts-pattern';
-
-import {
- DocumentAuth,
- type TRecipientActionAuth,
- type TRecipientActionAuthTypes,
-} from '@documenso/lib/types/document-auth';
-import type { FieldType } from '@documenso/prisma/client';
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogHeader,
- DialogTitle,
-} from '@documenso/ui/primitives/dialog';
-
-import { DocumentActionAuth2FA } from './document-action-auth-2fa';
-import { DocumentActionAuthAccount } from './document-action-auth-account';
-import { DocumentActionAuthPasskey } from './document-action-auth-passkey';
-import { useRequiredDocumentAuthContext } from './document-auth-provider';
-
-export type DocumentActionAuthDialogProps = {
- title?: string;
- documentAuthType: TRecipientActionAuthTypes;
- description?: string;
- actionTarget: FieldType | 'DOCUMENT';
- open: boolean;
- onOpenChange: (value: boolean) => void;
-
- /**
- * The callback to run when the reauth form is filled out.
- */
- onReauthFormSubmit: (values?: TRecipientActionAuth) => Promise | void;
-};
-
-export const DocumentActionAuthDialog = ({
- title,
- description,
- documentAuthType,
- open,
- onOpenChange,
- onReauthFormSubmit,
-}: DocumentActionAuthDialogProps) => {
- const { recipient, user, isCurrentlyAuthenticating } = useRequiredDocumentAuthContext();
-
- const handleOnOpenChange = (value: boolean) => {
- if (isCurrentlyAuthenticating) {
- return;
- }
-
- onOpenChange(value);
- };
-
- return (
-
-
-
- {title || Sign field }
-
-
- {description || Reauthentication is required to sign this field }
-
-
-
- {match({ documentAuthType, user })
- .with(
- { documentAuthType: DocumentAuth.ACCOUNT },
- { user: P.when((user) => !user || user.email !== recipient.email) }, // Assume all current auth methods requires them to be logged in.
- () => ,
- )
- .with({ documentAuthType: DocumentAuth.PASSKEY }, () => (
-
- ))
- .with({ documentAuthType: DocumentAuth.TWO_FACTOR_AUTH }, () => (
-
- ))
- .with({ documentAuthType: DocumentAuth.EXPLICIT_NONE }, () => null)
- .exhaustive()}
-
-
- );
-};
diff --git a/apps/web/src/app/(signing)/sign/[token]/document-action-auth-passkey.tsx b/apps/web/src/app/(signing)/sign/[token]/document-action-auth-passkey.tsx
deleted file mode 100644
index fbdc6947d..000000000
--- a/apps/web/src/app/(signing)/sign/[token]/document-action-auth-passkey.tsx
+++ /dev/null
@@ -1,264 +0,0 @@
-import { useEffect, useState } from 'react';
-
-import { zodResolver } from '@hookform/resolvers/zod';
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { browserSupportsWebAuthn, startAuthentication } from '@simplewebauthn/browser';
-import { Loader } from 'lucide-react';
-import { useForm } from 'react-hook-form';
-import { z } from 'zod';
-
-import { AppError } from '@documenso/lib/errors/app-error';
-import { DocumentAuth, type TRecipientActionAuth } from '@documenso/lib/types/document-auth';
-import { RecipientRole } from '@documenso/prisma/client';
-import { trpc } from '@documenso/trpc/react';
-import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
-import { Button } from '@documenso/ui/primitives/button';
-import { DialogFooter } from '@documenso/ui/primitives/dialog';
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from '@documenso/ui/primitives/form/form';
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from '@documenso/ui/primitives/select';
-
-import { CreatePasskeyDialog } from '~/app/(dashboard)/settings/security/passkeys/create-passkey-dialog';
-
-import { useRequiredDocumentAuthContext } from './document-auth-provider';
-
-export type DocumentActionAuthPasskeyProps = {
- actionTarget?: 'FIELD' | 'DOCUMENT';
- actionVerb?: string;
- open: boolean;
- onOpenChange: (value: boolean) => void;
- onReauthFormSubmit: (values?: TRecipientActionAuth) => Promise | void;
-};
-
-const ZPasskeyAuthFormSchema = z.object({
- passkeyId: z.string(),
-});
-
-type TPasskeyAuthFormSchema = z.infer;
-
-export const DocumentActionAuthPasskey = ({
- actionTarget = 'FIELD',
- actionVerb = 'sign',
- onReauthFormSubmit,
- open,
- onOpenChange,
-}: DocumentActionAuthPasskeyProps) => {
- const { _ } = useLingui();
-
- const {
- recipient,
- passkeyData,
- preferredPasskeyId,
- setPreferredPasskeyId,
- isCurrentlyAuthenticating,
- setIsCurrentlyAuthenticating,
- refetchPasskeys,
- } = useRequiredDocumentAuthContext();
-
- const form = useForm({
- resolver: zodResolver(ZPasskeyAuthFormSchema),
- defaultValues: {
- passkeyId: preferredPasskeyId || '',
- },
- });
-
- const { mutateAsync: createPasskeyAuthenticationOptions } =
- trpc.auth.createPasskeyAuthenticationOptions.useMutation();
-
- const [formErrorCode, setFormErrorCode] = useState(null);
-
- const onFormSubmit = async ({ passkeyId }: TPasskeyAuthFormSchema) => {
- try {
- setPreferredPasskeyId(passkeyId);
- setIsCurrentlyAuthenticating(true);
-
- const { options, tokenReference } = await createPasskeyAuthenticationOptions({
- preferredPasskeyId: passkeyId,
- });
-
- const authenticationResponse = await startAuthentication(options);
-
- await onReauthFormSubmit({
- type: DocumentAuth.PASSKEY,
- authenticationResponse,
- tokenReference,
- });
-
- setIsCurrentlyAuthenticating(false);
-
- onOpenChange(false);
- } catch (err) {
- setIsCurrentlyAuthenticating(false);
-
- if (err.name === 'NotAllowedError') {
- return;
- }
-
- const error = AppError.parseError(err);
- setFormErrorCode(error.code);
-
- // Todo: Alert.
- }
- };
-
- useEffect(() => {
- form.reset({
- passkeyId: preferredPasskeyId || '',
- });
-
- setFormErrorCode(null);
- }, [open, form, preferredPasskeyId]);
-
- if (!browserSupportsWebAuthn()) {
- return (
-
-
-
- {/* Todo: Translate */}
- Your browser does not support passkeys, which is required to {actionVerb.toLowerCase()}{' '}
- this {actionTarget.toLowerCase()}.
-
-
-
-
- onOpenChange(false)}>
- Close
-
-
-
- );
- }
-
- if (passkeyData.isInitialLoading || (passkeyData.isError && passkeyData.passkeys.length === 0)) {
- return (
-
-
-
- );
- }
-
- if (passkeyData.isError) {
- return (
-
-
-
- Something went wrong while loading your passkeys.
-
-
-
-
- onOpenChange(false)}>
- Cancel
-
-
- void refetchPasskeys()}>
- Retry
-
-
-
- );
- }
-
- if (passkeyData.passkeys.length === 0) {
- return (
-
-
-
- {/* 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()}.`}
-
-
-
-
- onOpenChange(false)}>
- Cancel
-
-
- refetchPasskeys()}
- trigger={
-
- Setup
-
- }
- />
-
-
- );
- }
-
- return (
-
-
-
-
-
(
-
- Passkey
-
-
-
-
-
-
-
-
- {passkeyData.passkeys.map((passkey) => (
-
- {passkey.name}
-
- ))}
-
-
-
-
-
-
- )}
- />
-
- {formErrorCode && (
-
- Unauthorized
-
- We were unable to verify your details. Please try again or contact support
-
-
- )}
-
-
- onOpenChange(false)}>
- Cancel
-
-
-
- Sign
-
-
-
-
-
-
- );
-};
diff --git a/apps/web/src/app/(signing)/sign/[token]/document-auth-provider.tsx b/apps/web/src/app/(signing)/sign/[token]/document-auth-provider.tsx
deleted file mode 100644
index 90e5adcc2..000000000
--- a/apps/web/src/app/(signing)/sign/[token]/document-auth-provider.tsx
+++ /dev/null
@@ -1,230 +0,0 @@
-'use client';
-
-import { createContext, useContext, useEffect, useMemo, useState } from 'react';
-
-import { match } from 'ts-pattern';
-
-import { MAXIMUM_PASSKEYS } from '@documenso/lib/constants/auth';
-import type {
- TDocumentAuthOptions,
- TRecipientAccessAuthTypes,
- TRecipientActionAuthTypes,
- TRecipientAuthOptions,
-} from '@documenso/lib/types/document-auth';
-import { DocumentAuth } from '@documenso/lib/types/document-auth';
-import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
-import {
- type Document,
- FieldType,
- type Passkey,
- type Recipient,
- type User,
-} from '@documenso/prisma/client';
-import { trpc } from '@documenso/trpc/react';
-
-import type { DocumentActionAuthDialogProps } from './document-action-auth-dialog';
-import { DocumentActionAuthDialog } from './document-action-auth-dialog';
-
-type PasskeyData = {
- passkeys: Omit[];
- isInitialLoading: boolean;
- isRefetching: boolean;
- isError: boolean;
-};
-
-export type DocumentAuthContextValue = {
- executeActionAuthProcedure: (_value: ExecuteActionAuthProcedureOptions) => Promise;
- documentAuthOptions: Document['authOptions'];
- documentAuthOption: TDocumentAuthOptions;
- setDocumentAuthOptions: (_value: Document['authOptions']) => void;
- recipient: Recipient;
- recipientAuthOption: TRecipientAuthOptions;
- setRecipient: (_value: Recipient) => void;
- derivedRecipientAccessAuth: TRecipientAccessAuthTypes | null;
- derivedRecipientActionAuth: TRecipientActionAuthTypes | null;
- isAuthRedirectRequired: boolean;
- isCurrentlyAuthenticating: boolean;
- setIsCurrentlyAuthenticating: (_value: boolean) => void;
- passkeyData: PasskeyData;
- preferredPasskeyId: string | null;
- setPreferredPasskeyId: (_value: string | null) => void;
- user?: User | null;
- refetchPasskeys: () => Promise;
-};
-
-const DocumentAuthContext = createContext(null);
-
-export const useDocumentAuthContext = () => {
- return useContext(DocumentAuthContext);
-};
-
-export const useRequiredDocumentAuthContext = () => {
- const context = useDocumentAuthContext();
-
- if (!context) {
- throw new Error('Document auth context is required');
- }
-
- return context;
-};
-
-export interface DocumentAuthProviderProps {
- documentAuthOptions: Document['authOptions'];
- recipient: Recipient;
- user?: User | null;
- children: React.ReactNode;
-}
-
-export const DocumentAuthProvider = ({
- documentAuthOptions: initialDocumentAuthOptions,
- recipient: initialRecipient,
- user,
- children,
-}: DocumentAuthProviderProps) => {
- const [documentAuthOptions, setDocumentAuthOptions] = useState(initialDocumentAuthOptions);
- const [recipient, setRecipient] = useState(initialRecipient);
-
- const [isCurrentlyAuthenticating, setIsCurrentlyAuthenticating] = useState(false);
- const [preferredPasskeyId, setPreferredPasskeyId] = useState(null);
-
- const {
- documentAuthOption,
- recipientAuthOption,
- derivedRecipientAccessAuth,
- derivedRecipientActionAuth,
- } = useMemo(
- () =>
- extractDocumentAuthMethods({
- documentAuth: documentAuthOptions,
- recipientAuth: recipient.authOptions,
- }),
- [documentAuthOptions, recipient],
- );
-
- const passkeyQuery = trpc.auth.findPasskeys.useQuery(
- {
- perPage: MAXIMUM_PASSKEYS,
- },
- {
- placeholderData: (previousData) => previousData,
- enabled: derivedRecipientActionAuth === DocumentAuth.PASSKEY,
- },
- );
-
- const passkeyData: PasskeyData = {
- passkeys: passkeyQuery.data?.data || [],
- isInitialLoading: passkeyQuery.isInitialLoading,
- isRefetching: passkeyQuery.isRefetching,
- isError: passkeyQuery.isError,
- };
-
- const [documentAuthDialogPayload, setDocumentAuthDialogPayload] =
- useState(null);
-
- /**
- * The pre calculated auth payload if the current user is authenticated correctly
- * for the `derivedRecipientActionAuth`.
- *
- * Will be `null` if the user still requires authentication, or if they don't need
- * authentication.
- */
- const preCalculatedActionAuthOptions = match(derivedRecipientActionAuth)
- .with(DocumentAuth.ACCOUNT, () => {
- if (recipient.email !== user?.email) {
- return null;
- }
-
- return {
- type: DocumentAuth.ACCOUNT,
- };
- })
- .with(DocumentAuth.EXPLICIT_NONE, () => ({
- type: DocumentAuth.EXPLICIT_NONE,
- }))
- .with(DocumentAuth.PASSKEY, DocumentAuth.TWO_FACTOR_AUTH, null, () => null)
- .exhaustive();
-
- const executeActionAuthProcedure = async (options: ExecuteActionAuthProcedureOptions) => {
- // Directly run callback if no auth required.
- if (!derivedRecipientActionAuth || options.actionTarget !== FieldType.SIGNATURE) {
- await options.onReauthFormSubmit();
- return;
- }
-
- // Run callback with precalculated auth options if available.
- if (preCalculatedActionAuthOptions) {
- setDocumentAuthDialogPayload(null);
- await options.onReauthFormSubmit(preCalculatedActionAuthOptions);
- return;
- }
-
- // Request the required auth from the user.
- setDocumentAuthDialogPayload({
- ...options,
- });
- };
-
- useEffect(() => {
- const { passkeys } = passkeyData;
-
- if (!preferredPasskeyId && passkeys.length > 0) {
- setPreferredPasskeyId(passkeys[0].id);
- }
-
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [passkeyData.passkeys]);
-
- // Assume that a user must be logged in for any auth requirements.
- const isAuthRedirectRequired = Boolean(
- derivedRecipientActionAuth &&
- derivedRecipientActionAuth !== DocumentAuth.EXPLICIT_NONE &&
- user?.email !== recipient.email,
- );
-
- const refetchPasskeys = async () => {
- await passkeyQuery.refetch();
- };
-
- return (
-
- {children}
-
- {documentAuthDialogPayload && derivedRecipientActionAuth && (
- setDocumentAuthDialogPayload(null)}
- onReauthFormSubmit={documentAuthDialogPayload.onReauthFormSubmit}
- actionTarget={documentAuthDialogPayload.actionTarget}
- documentAuthType={derivedRecipientActionAuth}
- />
- )}
-
- );
-};
-
-type ExecuteActionAuthProcedureOptions = Omit<
- DocumentActionAuthDialogProps,
- 'open' | 'onOpenChange' | 'documentAuthType' | 'recipientRole'
->;
-
-DocumentAuthProvider.displayName = 'DocumentAuthProvider';
diff --git a/apps/web/src/app/(signing)/sign/[token]/dropdown-field.tsx b/apps/web/src/app/(signing)/sign/[token]/dropdown-field.tsx
deleted file mode 100644
index 5f4e1a444..000000000
--- a/apps/web/src/app/(signing)/sign/[token]/dropdown-field.tsx
+++ /dev/null
@@ -1,216 +0,0 @@
-'use client';
-
-import { useEffect, useState, useTransition } from 'react';
-
-import { useRouter } from 'next/navigation';
-
-import { msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { Loader } from 'lucide-react';
-
-import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
-import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
-import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
-import { ZDropdownFieldMeta } from '@documenso/lib/types/field-meta';
-import type { Recipient } from '@documenso/prisma/client';
-import type { FieldWithSignatureAndFieldMeta } from '@documenso/prisma/types/field-with-signature-and-fieldmeta';
-import { trpc } from '@documenso/trpc/react';
-import type {
- TRemovedSignedFieldWithTokenMutationSchema,
- TSignFieldWithTokenMutationSchema,
-} from '@documenso/trpc/server/field-router/schema';
-import { cn } from '@documenso/ui/lib/utils';
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from '@documenso/ui/primitives/select';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-import { useRequiredDocumentAuthContext } from './document-auth-provider';
-import { SigningFieldContainer } from './signing-field-container';
-
-export type DropdownFieldProps = {
- field: FieldWithSignatureAndFieldMeta;
- recipient: Recipient;
- onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise | void;
- onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise | void;
-};
-
-export const DropdownField = ({
- field,
- recipient,
- onSignField,
- onUnsignField,
-}: DropdownFieldProps) => {
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const router = useRouter();
- const [isPending, startTransition] = useTransition();
-
- const { executeActionAuthProcedure } = useRequiredDocumentAuthContext();
-
- const parsedFieldMeta = ZDropdownFieldMeta.parse(field.fieldMeta);
- const isReadOnly = parsedFieldMeta?.readOnly;
- const defaultValue = parsedFieldMeta?.defaultValue;
- const [localChoice, setLocalChoice] = useState(parsedFieldMeta.defaultValue ?? '');
-
- const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
- trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
-
- const {
- mutateAsync: removeSignedFieldWithToken,
- isPending: isRemoveSignedFieldWithTokenLoading,
- } = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
-
- const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;
- const shouldAutoSignField =
- (!field.inserted && localChoice) || (!field.inserted && isReadOnly && defaultValue);
-
- const onSign = async (authOptions?: TRecipientActionAuth) => {
- try {
- if (!localChoice) {
- return;
- }
-
- const payload: TSignFieldWithTokenMutationSchema = {
- token: recipient.token,
- fieldId: field.id,
- value: localChoice,
- isBase64: true,
- authOptions,
- };
-
- if (onSignField) {
- await onSignField(payload);
- } else {
- await signFieldWithToken(payload);
- }
-
- setLocalChoice('');
- startTransition(() => router.refresh());
- } catch (err) {
- const error = AppError.parseError(err);
-
- if (error.code === AppErrorCode.UNAUTHORIZED) {
- throw error;
- }
-
- console.error(err);
-
- toast({
- title: _(msg`Error`),
- description: _(msg`An error occurred while signing the document.`),
- variant: 'destructive',
- });
- }
- };
-
- const onPreSign = () => {
- return true;
- };
-
- const onRemove = async () => {
- try {
- const payload: TRemovedSignedFieldWithTokenMutationSchema = {
- token: recipient.token,
- fieldId: field.id,
- };
-
- if (onUnsignField) {
- await onUnsignField(payload);
- return;
- } else {
- await removeSignedFieldWithToken(payload);
- }
-
- setLocalChoice('');
- startTransition(() => router.refresh());
- } catch (err) {
- console.error(err);
-
- toast({
- title: _(msg`Error`),
- description: _(msg`An error occurred while removing the signature.`),
- variant: 'destructive',
- });
- }
- };
-
- const handleSelectItem = (val: string) => {
- setLocalChoice(val);
- };
-
- useEffect(() => {
- if (!field.inserted && localChoice) {
- void executeActionAuthProcedure({
- onReauthFormSubmit: async (authOptions) => await onSign(authOptions),
- actionTarget: field.type,
- });
- }
- }, [localChoice]);
-
- useEffect(() => {
- if (shouldAutoSignField) {
- void executeActionAuthProcedure({
- onReauthFormSubmit: async (authOptions) => await onSign(authOptions),
- actionTarget: field.type,
- });
- }
- }, []);
-
- return (
-
-
- {isLoading && (
-
-
-
- )}
-
- {!field.inserted && (
-
-
-
-
-
-
- {parsedFieldMeta?.values?.map((item, index) => (
-
- {item.value}
-
- ))}
-
-
-
- )}
-
- {field.inserted && (
-
- {field.customText}
-
- )}
-
-
- );
-};
diff --git a/apps/web/src/app/(signing)/sign/[token]/email-field.tsx b/apps/web/src/app/(signing)/sign/[token]/email-field.tsx
deleted file mode 100644
index 9300aef63..000000000
--- a/apps/web/src/app/(signing)/sign/[token]/email-field.tsx
+++ /dev/null
@@ -1,137 +0,0 @@
-'use client';
-
-import { useTransition } from 'react';
-
-import { useRouter } from 'next/navigation';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { Loader } from 'lucide-react';
-
-import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
-import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
-import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
-import type { Recipient } from '@documenso/prisma/client';
-import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
-import { trpc } from '@documenso/trpc/react';
-import type {
- TRemovedSignedFieldWithTokenMutationSchema,
- TSignFieldWithTokenMutationSchema,
-} from '@documenso/trpc/server/field-router/schema';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-import { useRequiredSigningContext } from './provider';
-import { SigningFieldContainer } from './signing-field-container';
-
-export type EmailFieldProps = {
- field: FieldWithSignature;
- recipient: Recipient;
- onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise | void;
- onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise | void;
-};
-
-export const EmailField = ({ field, recipient, onSignField, onUnsignField }: EmailFieldProps) => {
- const router = useRouter();
-
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const { email: providedEmail } = useRequiredSigningContext();
-
- const [isPending, startTransition] = useTransition();
-
- const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
- trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
-
- const {
- mutateAsync: removeSignedFieldWithToken,
- isPending: isRemoveSignedFieldWithTokenLoading,
- } = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
-
- const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;
-
- const onSign = async (authOptions?: TRecipientActionAuth) => {
- try {
- const value = providedEmail ?? '';
-
- const payload: TSignFieldWithTokenMutationSchema = {
- token: recipient.token,
- fieldId: field.id,
- value,
- isBase64: false,
- authOptions,
- };
-
- if (onSignField) {
- await onSignField(payload);
- return;
- }
-
- await signFieldWithToken(payload);
-
- startTransition(() => router.refresh());
- } catch (err) {
- const error = AppError.parseError(err);
-
- if (error.code === AppErrorCode.UNAUTHORIZED) {
- throw error;
- }
-
- console.error(err);
-
- toast({
- title: _(msg`Error`),
- description: _(msg`An error occurred while signing the document.`),
- variant: 'destructive',
- });
- }
- };
-
- const onRemove = async () => {
- try {
- const payload: TRemovedSignedFieldWithTokenMutationSchema = {
- token: recipient.token,
- fieldId: field.id,
- };
-
- if (onUnsignField) {
- await onUnsignField(payload);
- return;
- }
-
- await removeSignedFieldWithToken(payload);
-
- startTransition(() => router.refresh());
- } catch (err) {
- console.error(err);
-
- toast({
- title: _(msg`Error`),
- description: _(msg`An error occurred while removing the signature.`),
- variant: 'destructive',
- });
- }
- };
-
- return (
-
- {isLoading && (
-
-
-
- )}
-
- {!field.inserted && (
-
- Email
-
- )}
-
- {field.inserted && (
-
- {field.customText}
-
- )}
-
- );
-};
diff --git a/apps/web/src/app/(signing)/sign/[token]/form.tsx b/apps/web/src/app/(signing)/sign/[token]/form.tsx
deleted file mode 100644
index b69280c71..000000000
--- a/apps/web/src/app/(signing)/sign/[token]/form.tsx
+++ /dev/null
@@ -1,266 +0,0 @@
-'use client';
-
-import { useMemo, useState } from 'react';
-
-import { useRouter } from 'next/navigation';
-
-import { Trans } from '@lingui/macro';
-import { useSession } from 'next-auth/react';
-import { useForm } from 'react-hook-form';
-
-import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
-import type { DocumentAndSender } from '@documenso/lib/server-only/document/get-document-by-token';
-import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
-import { isFieldUnsignedAndRequired } from '@documenso/lib/utils/advanced-fields-helpers';
-import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields';
-import { type Field, FieldType, type Recipient, RecipientRole } from '@documenso/prisma/client';
-import { trpc } from '@documenso/trpc/react';
-import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
-import { cn } from '@documenso/ui/lib/utils';
-import { Button } from '@documenso/ui/primitives/button';
-import { Card, CardContent } from '@documenso/ui/primitives/card';
-import { Input } from '@documenso/ui/primitives/input';
-import { Label } from '@documenso/ui/primitives/label';
-import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
-
-import { useRequiredSigningContext } from './provider';
-import { SignDialog } from './sign-dialog';
-
-export type SigningFormProps = {
- document: DocumentAndSender;
- recipient: Recipient;
- fields: Field[];
- redirectUrl?: string | null;
- isRecipientsTurn: boolean;
-};
-
-export const SigningForm = ({
- document,
- recipient,
- fields,
- redirectUrl,
- isRecipientsTurn,
-}: SigningFormProps) => {
- const router = useRouter();
- const analytics = useAnalytics();
- const { data: session } = useSession();
-
- const { fullName, signature, setFullName, setSignature, signatureValid, setSignatureValid } =
- useRequiredSigningContext();
-
- const [validateUninsertedFields, setValidateUninsertedFields] = useState(false);
-
- const { mutateAsync: completeDocumentWithToken } =
- trpc.recipient.completeDocumentWithToken.useMutation();
-
- const { handleSubmit, formState } = useForm();
-
- // Keep the loading state going if successful since the redirect may take some time.
- const isSubmitting = formState.isSubmitting || formState.isSubmitSuccessful;
-
- const fieldsRequiringValidation = useMemo(
- () => fields.filter(isFieldUnsignedAndRequired),
- [fields],
- );
-
- const hasSignatureField = fields.some((field) => field.type === FieldType.SIGNATURE);
-
- const uninsertedFields = useMemo(() => {
- return sortFieldsByPosition(fieldsRequiringValidation.filter((field) => !field.inserted));
- }, [fields]);
-
- const fieldsValidated = () => {
- setValidateUninsertedFields(true);
- validateFieldsInserted(fieldsRequiringValidation);
- };
-
- const onFormSubmit = async () => {
- setValidateUninsertedFields(true);
-
- const isFieldsValid = validateFieldsInserted(fieldsRequiringValidation);
-
- if (hasSignatureField && !signatureValid) {
- return;
- }
-
- if (!isFieldsValid) {
- return;
- }
-
- await completeDocument();
-
- // Reauth is currently not required for completing the document.
- // await executeActionAuthProcedure({
- // onReauthFormSubmit: completeDocument,
- // actionTarget: 'DOCUMENT',
- // });
- };
-
- const completeDocument = async (authOptions?: TRecipientActionAuth) => {
- await completeDocumentWithToken({
- token: recipient.token,
- documentId: document.id,
- authOptions,
- });
-
- analytics.capture('App: Recipient has completed signing', {
- signerId: recipient.id,
- documentId: document.id,
- timestamp: new Date().toISOString(),
- });
-
- redirectUrl ? router.push(redirectUrl) : router.push(`/sign/${recipient.token}/complete`);
- };
-
- return (
-
- {validateUninsertedFields && uninsertedFields[0] && (
-
- Click to insert field
-
- )}
-
-
-
-
- {recipient.role === RecipientRole.VIEWER && View Document }
- {recipient.role === RecipientRole.SIGNER && Sign Document }
- {recipient.role === RecipientRole.APPROVER && Approve Document }
-
-
- {recipient.role === RecipientRole.VIEWER ? (
- <>
-
- Please mark as viewed to complete
-
-
-
-
-
-
-
- router.back()}
- >
- Cancel
-
-
-
-
-
- >
- ) : (
- <>
-
- Please review the document before signing.
-
-
-
-
-
-
-
-
- Full Name
-
-
- setFullName(e.target.value.trimStart())}
- />
-
-
-
-
- Signature
-
-
-
-
- {
- setSignatureValid(isValid);
- }}
- onChange={(value) => {
- if (signatureValid) {
- setSignature(value);
- }
- }}
- allowTypedSignature={document.documentMeta?.typedSignatureEnabled}
- />
-
-
-
- {hasSignatureField && !signatureValid && (
-
-
- Signature is too small. Please provide a more complete signature.
-
-
- )}
-
-
-
-
- router.back()}
- >
- Cancel
-
-
-
-
-
- >
- )}
-
-
-
- );
-};
diff --git a/apps/web/src/app/(signing)/sign/[token]/initials-field.tsx b/apps/web/src/app/(signing)/sign/[token]/initials-field.tsx
deleted file mode 100644
index b63418076..000000000
--- a/apps/web/src/app/(signing)/sign/[token]/initials-field.tsx
+++ /dev/null
@@ -1,143 +0,0 @@
-'use client';
-
-import { useTransition } from 'react';
-
-import { useRouter } from 'next/navigation';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { Loader } from 'lucide-react';
-
-import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
-import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
-import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
-import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
-import type { Recipient } from '@documenso/prisma/client';
-import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
-import { trpc } from '@documenso/trpc/react';
-import type {
- TRemovedSignedFieldWithTokenMutationSchema,
- TSignFieldWithTokenMutationSchema,
-} from '@documenso/trpc/server/field-router/schema';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-import { useRequiredSigningContext } from './provider';
-import { SigningFieldContainer } from './signing-field-container';
-
-export type InitialsFieldProps = {
- field: FieldWithSignature;
- recipient: Recipient;
- onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise | void;
- onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise | void;
-};
-
-export const InitialsField = ({
- field,
- recipient,
- onSignField,
- onUnsignField,
-}: InitialsFieldProps) => {
- const router = useRouter();
- const { toast } = useToast();
- const { _ } = useLingui();
-
- const { fullName } = useRequiredSigningContext();
- const initials = extractInitials(fullName);
-
- const [isPending, startTransition] = useTransition();
-
- const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
- trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
-
- const {
- mutateAsync: removeSignedFieldWithToken,
- isPending: isRemoveSignedFieldWithTokenLoading,
- } = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
-
- const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;
-
- const onSign = async (authOptions?: TRecipientActionAuth) => {
- try {
- const value = initials ?? '';
-
- const payload: TSignFieldWithTokenMutationSchema = {
- token: recipient.token,
- fieldId: field.id,
- value,
- isBase64: false,
- authOptions,
- };
-
- if (onSignField) {
- await onSignField(payload);
- return;
- }
-
- await signFieldWithToken(payload);
-
- startTransition(() => router.refresh());
- } catch (err) {
- const error = AppError.parseError(err);
-
- if (error.code === AppErrorCode.UNAUTHORIZED) {
- throw error;
- }
-
- console.error(err);
-
- toast({
- title: _(msg`Error`),
- description: _(msg`An error occurred while signing the document.`),
- variant: 'destructive',
- });
- }
- };
-
- const onRemove = async () => {
- try {
- const payload: TRemovedSignedFieldWithTokenMutationSchema = {
- token: recipient.token,
- fieldId: field.id,
- };
-
- if (onUnsignField) {
- await onUnsignField(payload);
- return;
- }
-
- await removeSignedFieldWithToken(payload);
-
- startTransition(() => router.refresh());
- } catch (err) {
- console.error(err);
-
- toast({
- title: _(msg`Error`),
- description: _(msg`An error occurred while removing the field.`),
- variant: 'destructive',
- });
- }
- };
-
- return (
-
- {isLoading && (
-
-
-
- )}
-
- {!field.inserted && (
-
- Initials
-
- )}
-
- {field.inserted && (
-
- {field.customText}
-
- )}
-
- );
-};
diff --git a/apps/web/src/app/(signing)/sign/[token]/layout.tsx b/apps/web/src/app/(signing)/sign/[token]/layout.tsx
deleted file mode 100644
index 33b6d2bf0..000000000
--- a/apps/web/src/app/(signing)/sign/[token]/layout.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import React from 'react';
-
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
-import type { TGetTeamsResponse } from '@documenso/lib/server-only/team/get-teams';
-import { getTeams } from '@documenso/lib/server-only/team/get-teams';
-
-import { Header as AuthenticatedHeader } from '~/components/(dashboard)/layout/header';
-import { NextAuthProvider } from '~/providers/next-auth';
-
-export type SigningLayoutProps = {
- children: React.ReactNode;
-};
-
-export default async function SigningLayout({ children }: SigningLayoutProps) {
- await setupI18nSSR();
-
- const { user, session } = await getServerComponentSession();
-
- let teams: TGetTeamsResponse = [];
-
- if (user && session) {
- teams = await getTeams({ userId: user.id });
- }
-
- return (
-
-
- {user &&
}
-
-
{children}
-
-
- );
-}
diff --git a/apps/web/src/app/(signing)/sign/[token]/name-field.tsx b/apps/web/src/app/(signing)/sign/[token]/name-field.tsx
deleted file mode 100644
index bc83e5a49..000000000
--- a/apps/web/src/app/(signing)/sign/[token]/name-field.tsx
+++ /dev/null
@@ -1,232 +0,0 @@
-'use client';
-
-import { useState, useTransition } from 'react';
-
-import { useRouter } from 'next/navigation';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { Loader } from 'lucide-react';
-
-import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
-import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
-import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
-import { type Recipient } from '@documenso/prisma/client';
-import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
-import { trpc } from '@documenso/trpc/react';
-import type {
- TRemovedSignedFieldWithTokenMutationSchema,
- TSignFieldWithTokenMutationSchema,
-} from '@documenso/trpc/server/field-router/schema';
-import { Button } from '@documenso/ui/primitives/button';
-import { Dialog, DialogContent, DialogFooter, DialogTitle } from '@documenso/ui/primitives/dialog';
-import { Input } from '@documenso/ui/primitives/input';
-import { Label } from '@documenso/ui/primitives/label';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-import { useRequiredDocumentAuthContext } from './document-auth-provider';
-import { useRequiredSigningContext } from './provider';
-import { SigningFieldContainer } from './signing-field-container';
-
-export type NameFieldProps = {
- field: FieldWithSignature;
- recipient: Recipient;
- onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise | void;
- onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise | void;
-};
-
-export const NameField = ({ field, recipient, onSignField, onUnsignField }: NameFieldProps) => {
- const router = useRouter();
-
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const { fullName: providedFullName, setFullName: setProvidedFullName } =
- useRequiredSigningContext();
-
- const { executeActionAuthProcedure } = useRequiredDocumentAuthContext();
-
- const [isPending, startTransition] = useTransition();
-
- const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
- trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
-
- const {
- mutateAsync: removeSignedFieldWithToken,
- isPending: isRemoveSignedFieldWithTokenLoading,
- } = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
-
- const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;
-
- const [showFullNameModal, setShowFullNameModal] = useState(false);
- const [localFullName, setLocalFullName] = useState('');
-
- const onPreSign = () => {
- if (!providedFullName) {
- setShowFullNameModal(true);
- return false;
- }
-
- return true;
- };
-
- /**
- * When the user clicks the sign button in the dialog where they enter their full name.
- */
- const onDialogSignClick = () => {
- setShowFullNameModal(false);
- setProvidedFullName(localFullName);
-
- void executeActionAuthProcedure({
- onReauthFormSubmit: async (authOptions) => await onSign(authOptions, localFullName),
- actionTarget: field.type,
- });
- };
-
- const onSign = async (authOptions?: TRecipientActionAuth, name?: string) => {
- try {
- const value = name || providedFullName;
-
- if (!value) {
- setShowFullNameModal(true);
- return;
- }
-
- const payload: TSignFieldWithTokenMutationSchema = {
- token: recipient.token,
- fieldId: field.id,
- value,
- isBase64: false,
- authOptions,
- };
-
- if (onSignField) {
- await onSignField(payload);
- return;
- }
-
- await signFieldWithToken(payload);
-
- startTransition(() => router.refresh());
- } catch (err) {
- const error = AppError.parseError(err);
-
- if (error.code === AppErrorCode.UNAUTHORIZED) {
- throw error;
- }
-
- console.error(err);
-
- toast({
- title: _(msg`Error`),
- description: _(msg`An error occurred while signing the document.`),
- variant: 'destructive',
- });
- }
- };
-
- const onRemove = async () => {
- try {
- const payload: TRemovedSignedFieldWithTokenMutationSchema = {
- token: recipient.token,
- fieldId: field.id,
- };
-
- if (onUnsignField) {
- await onUnsignField(payload);
- return;
- }
-
- await removeSignedFieldWithToken(payload);
-
- startTransition(() => router.refresh());
- } catch (err) {
- console.error(err);
-
- toast({
- title: _(msg`Error`),
- description: _(msg`An error occurred while removing the signature.`),
- variant: 'destructive',
- });
- }
- };
-
- return (
-
- {isLoading && (
-
-
-
- )}
-
- {!field.inserted && (
-
- Name
-
- )}
-
- {field.inserted && (
-
- {field.customText}
-
- )}
-
-
-
-
-
- Sign as
-
- {recipient.name}
({recipient.email})
-
-
-
-
-
-
- Full Name
-
-
- setLocalFullName(e.target.value.trimStart())}
- />
-
-
-
-
- {
- setShowFullNameModal(false);
- setLocalFullName('');
- }}
- >
- Cancel
-
-
- onDialogSignClick()}
- >
- Sign
-
-
-
-
-
-
- );
-};
diff --git a/apps/web/src/app/(signing)/sign/[token]/no-longer-available.tsx b/apps/web/src/app/(signing)/sign/[token]/no-longer-available.tsx
deleted file mode 100644
index 3c608e6af..000000000
--- a/apps/web/src/app/(signing)/sign/[token]/no-longer-available.tsx
+++ /dev/null
@@ -1,75 +0,0 @@
-'use client';
-
-import React from 'react';
-
-import Link from 'next/link';
-
-import { Trans } from '@lingui/macro';
-import { Clock8 } from 'lucide-react';
-import { useSession } from 'next-auth/react';
-
-import signingCelebration from '@documenso/assets/images/signing-celebration.png';
-import type { Document, Signature } from '@documenso/prisma/client';
-import { SigningCard3D } from '@documenso/ui/components/signing-card';
-
-type NoLongerAvailableProps = {
- document: Document;
- recipientName: string;
- recipientSignature: Signature;
-};
-
-export const NoLongerAvailable = ({
- document,
- recipientName,
- recipientSignature,
-}: NoLongerAvailableProps) => {
- const { data: session } = useSession();
-
- return (
-
-
-
-
-
-
-
- Document Cancelled
-
-
-
-
-
- "{document.title}"
- is no longer available to sign
-
-
-
-
- This document has been cancelled by the owner.
-
-
- {session?.user ? (
-
-
Go Back Home
-
- ) : (
-
-
- Want to send slick signing links like this one?{' '}
-
- Check out Documenso.
-
-
-
- )}
-
-
- );
-};
diff --git a/apps/web/src/app/(signing)/sign/[token]/number-field.tsx b/apps/web/src/app/(signing)/sign/[token]/number-field.tsx
deleted file mode 100644
index ffd90df64..000000000
--- a/apps/web/src/app/(signing)/sign/[token]/number-field.tsx
+++ /dev/null
@@ -1,345 +0,0 @@
-'use client';
-
-import { useEffect, useState, useTransition } from 'react';
-
-import { useRouter } from 'next/navigation';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { Hash, Loader } from 'lucide-react';
-
-import { validateNumberField } from '@documenso/lib/advanced-fields-validation/validate-number';
-import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
-import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
-import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
-import { ZNumberFieldMeta } from '@documenso/lib/types/field-meta';
-import type { Recipient } from '@documenso/prisma/client';
-import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
-import { trpc } from '@documenso/trpc/react';
-import type {
- TRemovedSignedFieldWithTokenMutationSchema,
- TSignFieldWithTokenMutationSchema,
-} from '@documenso/trpc/server/field-router/schema';
-import { cn } from '@documenso/ui/lib/utils';
-import { Button } from '@documenso/ui/primitives/button';
-import { Dialog, DialogContent, DialogFooter, DialogTitle } from '@documenso/ui/primitives/dialog';
-import { Input } from '@documenso/ui/primitives/input';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-import { useRequiredDocumentAuthContext } from './document-auth-provider';
-import { SigningFieldContainer } from './signing-field-container';
-
-type ValidationErrors = {
- isNumber: string[];
- required: string[];
- minValue: string[];
- maxValue: string[];
- numberFormat: string[];
-};
-
-export type NumberFieldProps = {
- field: FieldWithSignature;
- recipient: Recipient;
- onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise | void;
- onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise | void;
-};
-
-export const NumberField = ({ field, recipient, onSignField, onUnsignField }: NumberFieldProps) => {
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const router = useRouter();
- const [isPending, startTransition] = useTransition();
- const [showRadioModal, setShowRadioModal] = useState(false);
-
- const parsedFieldMeta = field.fieldMeta ? ZNumberFieldMeta.parse(field.fieldMeta) : null;
- const isReadOnly = parsedFieldMeta?.readOnly;
- const defaultValue = parsedFieldMeta?.value;
- const [localNumber, setLocalNumber] = useState(
- parsedFieldMeta?.value ? String(parsedFieldMeta.value) : '0',
- );
-
- const initialErrors: ValidationErrors = {
- isNumber: [],
- required: [],
- minValue: [],
- maxValue: [],
- numberFormat: [],
- };
-
- const [errors, setErrors] = useState(initialErrors);
-
- const { executeActionAuthProcedure } = useRequiredDocumentAuthContext();
-
- const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
- trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
-
- const {
- mutateAsync: removeSignedFieldWithToken,
- isPending: isRemoveSignedFieldWithTokenLoading,
- } = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
-
- const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;
-
- const handleNumberChange = (e: React.ChangeEvent) => {
- const text = e.target.value;
- setLocalNumber(text);
-
- if (parsedFieldMeta) {
- const validationErrors = validateNumberField(text, parsedFieldMeta, true);
- setErrors({
- isNumber: validationErrors.filter((error) => error.includes('valid number')),
- required: validationErrors.filter((error) => error.includes('required')),
- minValue: validationErrors.filter((error) => error.includes('minimum value')),
- maxValue: validationErrors.filter((error) => error.includes('maximum value')),
- numberFormat: validationErrors.filter((error) => error.includes('number format')),
- });
- } else {
- const validationErrors = validateNumberField(text);
- setErrors((prevErrors) => ({
- ...prevErrors,
- isNumber: validationErrors.filter((error) => error.includes('valid number')),
- }));
- }
- };
-
- const onDialogSignClick = () => {
- setShowRadioModal(false);
-
- void executeActionAuthProcedure({
- onReauthFormSubmit: async (authOptions) => await onSign(authOptions),
- actionTarget: field.type,
- });
- };
-
- const onSign = async (authOptions?: TRecipientActionAuth) => {
- try {
- if (!localNumber || Object.values(errors).some((error) => error.length > 0)) {
- return;
- }
-
- const payload: TSignFieldWithTokenMutationSchema = {
- token: recipient.token,
- fieldId: field.id,
- value: localNumber,
- isBase64: true,
- authOptions,
- };
-
- if (onSignField) {
- await onSignField(payload);
- return;
- }
-
- await signFieldWithToken(payload);
-
- setLocalNumber('');
-
- startTransition(() => router.refresh());
- } catch (err) {
- const error = AppError.parseError(err);
-
- if (error.code === AppErrorCode.UNAUTHORIZED) {
- throw error;
- }
-
- console.error(err);
-
- toast({
- title: _(msg`Error`),
- description: _(msg`An error occurred while signing the document.`),
- variant: 'destructive',
- });
- }
- };
-
- const onPreSign = () => {
- setShowRadioModal(true);
-
- if (localNumber && parsedFieldMeta) {
- const validationErrors = validateNumberField(localNumber, parsedFieldMeta, true);
- setErrors({
- isNumber: validationErrors.filter((error) => error.includes('valid number')),
- required: validationErrors.filter((error) => error.includes('required')),
- minValue: validationErrors.filter((error) => error.includes('minimum value')),
- maxValue: validationErrors.filter((error) => error.includes('maximum value')),
- numberFormat: validationErrors.filter((error) => error.includes('number format')),
- });
- }
-
- return false;
- };
-
- const onRemove = async () => {
- try {
- const payload: TRemovedSignedFieldWithTokenMutationSchema = {
- token: recipient.token,
- fieldId: field.id,
- };
-
- if (onUnsignField) {
- await onUnsignField(payload);
- return;
- }
-
- await removeSignedFieldWithToken(payload);
-
- setLocalNumber(parsedFieldMeta?.value ? String(parsedFieldMeta?.value) : '');
-
- startTransition(() => router.refresh());
- } catch (err) {
- console.error(err);
-
- toast({
- title: _(msg`Error`),
- description: _(msg`An error occurred while removing the signature.`),
- variant: 'destructive',
- });
- }
- };
-
- useEffect(() => {
- if (!showRadioModal) {
- setLocalNumber(parsedFieldMeta?.value ? String(parsedFieldMeta.value) : '0');
- setErrors(initialErrors);
- }
- }, [showRadioModal]);
-
- useEffect(() => {
- if (
- (!field.inserted && defaultValue && localNumber) ||
- (!field.inserted && isReadOnly && defaultValue)
- ) {
- void executeActionAuthProcedure({
- onReauthFormSubmit: async (authOptions) => await onSign(authOptions),
- actionTarget: field.type,
- });
- }
- }, []);
-
- let fieldDisplayName = 'Number';
-
- if (parsedFieldMeta?.label) {
- fieldDisplayName =
- parsedFieldMeta.label.length > 10
- ? parsedFieldMeta.label.substring(0, 10) + '...'
- : parsedFieldMeta.label;
- }
-
- const userInputHasErrors = Object.values(errors).some((error) => error.length > 0);
-
- return (
-
- {isLoading && (
-
-
-
- )}
-
- {!field.inserted && (
-
-
- {' '}
- {fieldDisplayName}
-
-
- )}
-
- {field.inserted && (
-
- {field.customText}
-
- )}
-
-
-
-
- {parsedFieldMeta?.label ? parsedFieldMeta?.label : Number }
-
-
-
-
-
-
- {userInputHasErrors && (
-
- {errors.isNumber?.map((error, index) => (
-
- {error}
-
- ))}
- {errors.required?.map((error, index) => (
-
- {error}
-
- ))}
- {errors.minValue?.map((error, index) => (
-
- {error}
-
- ))}
- {errors.maxValue?.map((error, index) => (
-
- {error}
-
- ))}
- {errors.numberFormat?.map((error, index) => (
-
- {error}
-
- ))}
-
- )}
-
-
-
- {
- setShowRadioModal(false);
- setLocalNumber('');
- }}
- >
- Cancel
-
-
- onDialogSignClick()}
- >
- Save
-
-
-
-
-
-
- );
-};
diff --git a/apps/web/src/app/(signing)/sign/[token]/page.tsx b/apps/web/src/app/(signing)/sign/[token]/page.tsx
deleted file mode 100644
index ec32082db..000000000
--- a/apps/web/src/app/(signing)/sign/[token]/page.tsx
+++ /dev/null
@@ -1,165 +0,0 @@
-import { headers } from 'next/headers';
-import { notFound, redirect } from 'next/navigation';
-
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-import { DOCUMENSO_ENCRYPTION_KEY } from '@documenso/lib/constants/crypto';
-import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
-import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
-import { isRecipientAuthorized } from '@documenso/lib/server-only/document/is-recipient-authorized';
-import { viewedDocument } from '@documenso/lib/server-only/document/viewed-document';
-import { getCompletedFieldsForToken } from '@documenso/lib/server-only/field/get-completed-fields-for-token';
-import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
-import { getIsRecipientsTurnToSign } from '@documenso/lib/server-only/recipient/get-is-recipient-turn';
-import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
-import { getRecipientSignatures } from '@documenso/lib/server-only/recipient/get-recipient-signatures';
-import { getUserByEmail } from '@documenso/lib/server-only/user/get-user-by-email';
-import { symmetricDecrypt } from '@documenso/lib/universal/crypto';
-import { extractNextHeaderRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
-import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
-import { DocumentStatus, SigningStatus } from '@documenso/prisma/client';
-
-import { DocumentAuthProvider } from './document-auth-provider';
-import { NoLongerAvailable } from './no-longer-available';
-import { SigningProvider } from './provider';
-import { SigningAuthPageView } from './signing-auth-page';
-import { SigningPageView } from './signing-page-view';
-
-export type SigningPageProps = {
- params: {
- token?: string;
- };
-};
-
-export default async function SigningPage({ params: { token } }: SigningPageProps) {
- await setupI18nSSR();
-
- if (!token) {
- return notFound();
- }
-
- const { user } = await getServerComponentSession();
-
- const requestHeaders = Object.fromEntries(headers().entries());
-
- const requestMetadata = extractNextHeaderRequestMetadata(requestHeaders);
-
- const [document, fields, recipient, completedFields] = await Promise.all([
- getDocumentAndSenderByToken({
- token,
- userId: user?.id,
- requireAccessAuth: false,
- }).catch(() => null),
- getFieldsForToken({ token }),
- getRecipientByToken({ token }).catch(() => null),
- getCompletedFieldsForToken({ token }),
- ]);
-
- if (
- !document ||
- !document.documentData ||
- !recipient ||
- document.status === DocumentStatus.DRAFT
- ) {
- return notFound();
- }
-
- const isRecipientsTurn = await getIsRecipientsTurnToSign({ token });
-
- if (!isRecipientsTurn) {
- return redirect(`/sign/${token}/waiting`);
- }
-
- const { derivedRecipientAccessAuth } = extractDocumentAuthMethods({
- documentAuth: document.authOptions,
- recipientAuth: recipient.authOptions,
- });
-
- const isDocumentAccessValid = await isRecipientAuthorized({
- type: 'ACCESS',
- documentAuthOptions: document.authOptions,
- recipient,
- userId: user?.id,
- });
-
- let recipientHasAccount: boolean | null = null;
-
- if (!isDocumentAccessValid) {
- recipientHasAccount = await getUserByEmail({ email: recipient?.email })
- .then((user) => !!user)
- .catch(() => false);
-
- return ;
- }
-
- await viewedDocument({
- token,
- requestMetadata,
- recipientAccessAuth: derivedRecipientAccessAuth,
- }).catch(() => null);
-
- const { documentMeta } = document;
-
- if (recipient.signingStatus === SigningStatus.REJECTED) {
- return redirect(`/sign/${token}/rejected`);
- }
-
- if (
- document.status === DocumentStatus.COMPLETED ||
- recipient.signingStatus === SigningStatus.SIGNED
- ) {
- documentMeta?.redirectUrl
- ? redirect(documentMeta.redirectUrl)
- : redirect(`/sign/${token}/complete`);
- }
-
- if (documentMeta?.password) {
- const key = DOCUMENSO_ENCRYPTION_KEY;
-
- if (!key) {
- throw new Error('Missing DOCUMENSO_ENCRYPTION_KEY');
- }
-
- const securePassword = Buffer.from(
- symmetricDecrypt({
- key,
- data: documentMeta.password,
- }),
- ).toString('utf-8');
-
- documentMeta.password = securePassword;
- }
-
- const [recipientSignature] = await getRecipientSignatures({ recipientId: recipient.id });
-
- if (document.deletedAt) {
- return (
-
- );
- }
-
- return (
-
-
-
-
-
- );
-}
diff --git a/apps/web/src/app/(signing)/sign/[token]/provider.tsx b/apps/web/src/app/(signing)/sign/[token]/provider.tsx
deleted file mode 100644
index 3e491bf32..000000000
--- a/apps/web/src/app/(signing)/sign/[token]/provider.tsx
+++ /dev/null
@@ -1,74 +0,0 @@
-'use client';
-
-import { createContext, useContext, useEffect, useState } from 'react';
-
-export type SigningContextValue = {
- fullName: string;
- setFullName: (_value: string) => void;
- email: string;
- setEmail: (_value: string) => void;
- signature: string | null;
- setSignature: (_value: string | null) => void;
- signatureValid: boolean;
- setSignatureValid: (_valid: boolean) => void;
-};
-
-const SigningContext = createContext(null);
-
-export const useSigningContext = () => {
- return useContext(SigningContext);
-};
-
-export const useRequiredSigningContext = () => {
- const context = useSigningContext();
-
- if (!context) {
- throw new Error('Signing context is required');
- }
-
- return context;
-};
-
-export interface SigningProviderProps {
- fullName?: string | null;
- email?: string | null;
- signature?: string | null;
- children: React.ReactNode;
-}
-
-export const SigningProvider = ({
- fullName: initialFullName,
- email: initialEmail,
- signature: initialSignature,
- children,
-}: SigningProviderProps) => {
- const [fullName, setFullName] = useState(initialFullName || '');
- const [email, setEmail] = useState(initialEmail || '');
- const [signature, setSignature] = useState(initialSignature || null);
- const [signatureValid, setSignatureValid] = useState(true);
-
- useEffect(() => {
- if (initialSignature) {
- setSignature(initialSignature);
- }
- }, [initialSignature]);
-
- return (
-
- {children}
-
- );
-};
-
-SigningProvider.displayName = 'SigningProvider';
diff --git a/apps/web/src/app/(signing)/sign/[token]/radio-field.tsx b/apps/web/src/app/(signing)/sign/[token]/radio-field.tsx
deleted file mode 100644
index 398181ec1..000000000
--- a/apps/web/src/app/(signing)/sign/[token]/radio-field.tsx
+++ /dev/null
@@ -1,194 +0,0 @@
-'use client';
-
-import { useEffect, useState, useTransition } from 'react';
-
-import { useRouter } from 'next/navigation';
-
-import { msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { Loader } from 'lucide-react';
-
-import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
-import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
-import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
-import { ZRadioFieldMeta } from '@documenso/lib/types/field-meta';
-import type { Recipient } from '@documenso/prisma/client';
-import type { FieldWithSignatureAndFieldMeta } from '@documenso/prisma/types/field-with-signature-and-fieldmeta';
-import { trpc } from '@documenso/trpc/react';
-import type {
- TRemovedSignedFieldWithTokenMutationSchema,
- TSignFieldWithTokenMutationSchema,
-} from '@documenso/trpc/server/field-router/schema';
-import { Label } from '@documenso/ui/primitives/label';
-import { RadioGroup, RadioGroupItem } from '@documenso/ui/primitives/radio-group';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-import { useRequiredDocumentAuthContext } from './document-auth-provider';
-import { SigningFieldContainer } from './signing-field-container';
-
-export type RadioFieldProps = {
- field: FieldWithSignatureAndFieldMeta;
- recipient: Recipient;
- onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise | void;
- onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise | void;
-};
-
-export const RadioField = ({ field, recipient, onSignField, onUnsignField }: RadioFieldProps) => {
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const router = useRouter();
- const [isPending, startTransition] = useTransition();
-
- const parsedFieldMeta = ZRadioFieldMeta.parse(field.fieldMeta);
- const values = parsedFieldMeta.values?.map((item) => ({
- ...item,
- value: item.value.length > 0 ? item.value : `empty-value-${item.id}`,
- }));
- const checkedItem = values?.find((item) => item.checked);
- const defaultValue = !field.inserted && !!checkedItem ? checkedItem.value : '';
-
- const [selectedOption, setSelectedOption] = useState(defaultValue);
-
- const { executeActionAuthProcedure } = useRequiredDocumentAuthContext();
-
- const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
- trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
-
- const {
- mutateAsync: removeSignedFieldWithToken,
- isPending: isRemoveSignedFieldWithTokenLoading,
- } = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
-
- const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;
- const shouldAutoSignField =
- (!field.inserted && selectedOption) ||
- (!field.inserted && defaultValue) ||
- (!field.inserted && parsedFieldMeta.readOnly && defaultValue);
-
- const onSign = async (authOptions?: TRecipientActionAuth) => {
- try {
- if (!selectedOption) {
- return;
- }
-
- const payload: TSignFieldWithTokenMutationSchema = {
- token: recipient.token,
- fieldId: field.id,
- value: selectedOption,
- isBase64: true,
- authOptions,
- };
-
- if (onSignField) {
- await onSignField(payload);
- } else {
- await signFieldWithToken(payload);
- }
-
- setSelectedOption('');
- startTransition(() => router.refresh());
- } catch (err) {
- const error = AppError.parseError(err);
-
- if (error.code === AppErrorCode.UNAUTHORIZED) {
- throw error;
- }
-
- console.error(err);
-
- toast({
- title: _(msg`Error`),
- description: _(msg`An error occurred while signing the document.`),
- variant: 'destructive',
- });
- }
- };
-
- const onRemove = async () => {
- try {
- const payload: TRemovedSignedFieldWithTokenMutationSchema = {
- token: recipient.token,
- fieldId: field.id,
- };
-
- if (onUnsignField) {
- await onUnsignField(payload);
- } else {
- await removeSignedFieldWithToken(payload);
- }
-
- setSelectedOption('');
-
- startTransition(() => router.refresh());
- } catch (err) {
- console.error(err);
-
- toast({
- title: _(msg`Error`),
- description: _(msg`An error occurred while removing the signature.`),
- variant: 'destructive',
- });
- }
- };
-
- const handleSelectItem = (selectedOption: string) => {
- setSelectedOption(selectedOption);
- };
-
- useEffect(() => {
- if (shouldAutoSignField) {
- void executeActionAuthProcedure({
- onReauthFormSubmit: async (authOptions) => await onSign(authOptions),
- actionTarget: field.type,
- });
- }
- }, [selectedOption, field]);
-
- return (
-
- {isLoading && (
-
-
-
- )}
-
- {!field.inserted && (
- handleSelectItem(value)} className="z-10">
- {values?.map((item, index) => (
-
-
-
-
- {item.value.includes('empty-value-') ? '' : item.value}
-
-
- ))}
-
- )}
-
- {field.inserted && (
-
- {values?.map((item, index) => (
-
-
-
- {item.value.includes('empty-value-') ? '' : item.value}
-
-
- ))}
-
- )}
-
- );
-};
diff --git a/apps/web/src/app/(signing)/sign/[token]/reject-document-dialog.tsx b/apps/web/src/app/(signing)/sign/[token]/reject-document-dialog.tsx
deleted file mode 100644
index 547a346d8..000000000
--- a/apps/web/src/app/(signing)/sign/[token]/reject-document-dialog.tsx
+++ /dev/null
@@ -1,170 +0,0 @@
-'use client';
-
-import { useEffect, useState } from 'react';
-
-import { useRouter, useSearchParams } from 'next/navigation';
-
-import { zodResolver } from '@hookform/resolvers/zod';
-import { Trans, msg } from '@lingui/macro';
-import { useForm } from 'react-hook-form';
-import { z } from 'zod';
-
-import type { Document } from '@documenso/prisma/client';
-import { trpc } from '@documenso/trpc/react';
-import { Button } from '@documenso/ui/primitives/button';
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from '@documenso/ui/primitives/dialog';
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormMessage,
-} from '@documenso/ui/primitives/form/form';
-import { Textarea } from '@documenso/ui/primitives/textarea';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-const ZRejectDocumentFormSchema = z.object({
- reason: z
- .string()
- .min(5, msg`Please provide a reason`)
- .max(500, msg`Reason must be less than 500 characters`),
-});
-
-type TRejectDocumentFormSchema = z.infer;
-
-export interface RejectDocumentDialogProps {
- document: Pick;
- token: string;
-}
-
-export function RejectDocumentDialog({ document, token }: RejectDocumentDialogProps) {
- const { toast } = useToast();
- const router = useRouter();
- const searchParams = useSearchParams();
-
- const [isOpen, setIsOpen] = useState(false);
-
- const { mutateAsync: rejectDocumentWithToken } =
- trpc.recipient.rejectDocumentWithToken.useMutation();
-
- const form = useForm({
- resolver: zodResolver(ZRejectDocumentFormSchema),
- defaultValues: {
- reason: '',
- },
- });
-
- const onRejectDocument = async ({ reason }: TRejectDocumentFormSchema) => {
- try {
- // TODO: Add trpc mutation here
- await rejectDocumentWithToken({
- documentId: document.id,
- token,
- reason,
- });
-
- toast({
- title: 'Document rejected',
- description: 'The document has been successfully rejected.',
- duration: 5000,
- });
-
- setIsOpen(false);
-
- router.push(`/sign/${token}/rejected`);
- } catch (err) {
- toast({
- title: 'Error',
- description: 'An error occurred while rejecting the document. Please try again.',
- variant: 'destructive',
- duration: 5000,
- });
- }
- };
-
- useEffect(() => {
- if (searchParams?.get('reject') === 'true') {
- setIsOpen(true);
- }
- }, []);
-
- useEffect(() => {
- if (!isOpen) {
- form.reset();
- }
- }, [isOpen]);
-
- return (
-
-
-
- Reject Document
-
-
-
-
-
-
- Reject Document
-
-
-
-
- Are you sure you want to reject this document? This action cannot be undone.
-
-
-
-
-
-
- (
-
-
-
-
-
-
- )}
- />
-
-
- setIsOpen(false)}
- disabled={form.formState.isSubmitting}
- >
- Cancel
-
-
-
- Reject Document
-
-
-
-
-
-
- );
-}
diff --git a/apps/web/src/app/(signing)/sign/[token]/rejected/page.tsx b/apps/web/src/app/(signing)/sign/[token]/rejected/page.tsx
deleted file mode 100644
index e3e630d8c..000000000
--- a/apps/web/src/app/(signing)/sign/[token]/rejected/page.tsx
+++ /dev/null
@@ -1,110 +0,0 @@
-import Link from 'next/link';
-import { notFound } from 'next/navigation';
-
-import { Trans } from '@lingui/macro';
-import { XCircle } from 'lucide-react';
-
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
-import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
-import { isRecipientAuthorized } from '@documenso/lib/server-only/document/is-recipient-authorized';
-import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
-import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
-import { FieldType } from '@documenso/prisma/client';
-import { Badge } from '@documenso/ui/primitives/badge';
-import { Button } from '@documenso/ui/primitives/button';
-
-import { truncateTitle } from '~/helpers/truncate-title';
-
-import { SigningAuthPageView } from '../signing-auth-page';
-
-export type RejectedSigningPageProps = {
- params: {
- token?: string;
- };
-};
-
-export default async function RejectedSigningPage({ params: { token } }: RejectedSigningPageProps) {
- await setupI18nSSR();
-
- if (!token) {
- return notFound();
- }
-
- const { user } = await getServerComponentSession();
-
- const document = await getDocumentAndSenderByToken({
- token,
- requireAccessAuth: false,
- }).catch(() => null);
-
- if (!document) {
- return notFound();
- }
-
- const truncatedTitle = truncateTitle(document.title);
-
- const [fields, recipient] = await Promise.all([
- getFieldsForToken({ token }),
- getRecipientByToken({ token }).catch(() => null),
- ]);
-
- if (!recipient) {
- return notFound();
- }
-
- const isDocumentAccessValid = await isRecipientAuthorized({
- type: 'ACCESS',
- documentAuthOptions: document.authOptions,
- recipient,
- userId: user?.id,
- });
-
- if (!isDocumentAccessValid) {
- return ;
- }
-
- const recipientName =
- recipient.name ||
- fields.find((field) => field.type === FieldType.NAME)?.customText ||
- recipient.email;
-
- return (
-
-
- {truncatedTitle}
-
-
-
-
-
-
-
- Document Rejected
-
-
-
-
- You have rejected this document
-
-
-
-
- The document owner has been notified of your decision. They may contact you with further
- instructions if necessary.
-
-
-
-
- No further action is required from you at this time.
-
-
- {user && (
-
- Return Home
-
- )}
-
-
- );
-}
diff --git a/apps/web/src/app/(signing)/sign/[token]/sign-dialog.tsx b/apps/web/src/app/(signing)/sign/[token]/sign-dialog.tsx
deleted file mode 100644
index 1bbffa9e6..000000000
--- a/apps/web/src/app/(signing)/sign/[token]/sign-dialog.tsx
+++ /dev/null
@@ -1,150 +0,0 @@
-import { useMemo, useState } from 'react';
-
-import { Trans } from '@lingui/macro';
-
-import { fieldsContainUnsignedRequiredField } from '@documenso/lib/utils/advanced-fields-helpers';
-import type { Field } from '@documenso/prisma/client';
-import { RecipientRole } from '@documenso/prisma/client';
-import { Button } from '@documenso/ui/primitives/button';
-import {
- Dialog,
- DialogContent,
- DialogFooter,
- DialogTitle,
- DialogTrigger,
-} from '@documenso/ui/primitives/dialog';
-
-import { SigningDisclosure } from '~/components/general/signing-disclosure';
-
-export type SignDialogProps = {
- isSubmitting: boolean;
- documentTitle: string;
- fields: Field[];
- fieldsValidated: () => void | Promise;
- onSignatureComplete: () => void | Promise;
- role: RecipientRole;
- disabled?: boolean;
-};
-
-export const SignDialog = ({
- isSubmitting,
- documentTitle,
- fields,
- fieldsValidated,
- onSignatureComplete,
- role,
- disabled = false,
-}: SignDialogProps) => {
- const [showDialog, setShowDialog] = useState(false);
-
- const isComplete = useMemo(() => !fieldsContainUnsignedRequiredField(fields), [fields]);
-
- const handleOpenChange = (open: boolean) => {
- if (isSubmitting || !isComplete) {
- return;
- }
-
- setShowDialog(open);
- };
-
- return (
-
-
-
- {isComplete ? Complete : Next field }
-
-
-
-
-
-
- {role === RecipientRole.VIEWER && Complete Viewing }
- {role === RecipientRole.SIGNER && Complete Signing }
- {role === RecipientRole.APPROVER && Complete Approval }
-
-
-
-
- {role === RecipientRole.VIEWER && (
-
-
-
- You are about to complete viewing "
-
- {documentTitle}
-
- ".
-
- Are you sure?
-
-
- )}
- {role === RecipientRole.SIGNER && (
-
-
-
- You are about to complete signing "
-
- {documentTitle}
-
- ".
-
- Are you sure?
-
-
- )}
- {role === RecipientRole.APPROVER && (
-
-
-
- You are about to complete approving{' '}
-
- "{documentTitle}"
-
- .
-
- Are you sure?
-
-
- )}
-
-
-
-
-
-
- {
- setShowDialog(false);
- }}
- >
- Cancel
-
-
-
- {role === RecipientRole.VIEWER && Mark as Viewed }
- {role === RecipientRole.SIGNER && Sign }
- {role === RecipientRole.APPROVER && Approve }
-
-
-
-
-
- );
-};
diff --git a/apps/web/src/app/(signing)/sign/[token]/signature-field.tsx b/apps/web/src/app/(signing)/sign/[token]/signature-field.tsx
deleted file mode 100644
index bba784975..000000000
--- a/apps/web/src/app/(signing)/sign/[token]/signature-field.tsx
+++ /dev/null
@@ -1,336 +0,0 @@
-'use client';
-
-import { useLayoutEffect, useMemo, useRef, useState, useTransition } from 'react';
-
-import { useRouter } from 'next/navigation';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { Loader } from 'lucide-react';
-
-import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
-import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
-import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
-import { type Recipient } from '@documenso/prisma/client';
-import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
-import { trpc } from '@documenso/trpc/react';
-import type {
- TRemovedSignedFieldWithTokenMutationSchema,
- TSignFieldWithTokenMutationSchema,
-} from '@documenso/trpc/server/field-router/schema';
-import { Button } from '@documenso/ui/primitives/button';
-import { Dialog, DialogContent, DialogFooter, DialogTitle } from '@documenso/ui/primitives/dialog';
-import { Label } from '@documenso/ui/primitives/label';
-import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-import { SigningDisclosure } from '~/components/general/signing-disclosure';
-
-import { useRequiredDocumentAuthContext } from './document-auth-provider';
-import { useRequiredSigningContext } from './provider';
-import { SigningFieldContainer } from './signing-field-container';
-
-type SignatureFieldState = 'empty' | 'signed-image' | 'signed-text';
-export type SignatureFieldProps = {
- field: FieldWithSignature;
- recipient: Recipient;
- onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise | void;
- onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise | void;
- typedSignatureEnabled?: boolean;
-};
-
-export const SignatureField = ({
- field,
- recipient,
- onSignField,
- onUnsignField,
- typedSignatureEnabled,
-}: SignatureFieldProps) => {
- const router = useRouter();
-
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const signatureRef = useRef(null);
- const containerRef = useRef(null);
- const [fontSize, setFontSize] = useState(2);
-
- const {
- signature: providedSignature,
- setSignature: setProvidedSignature,
- signatureValid,
- setSignatureValid,
- } = useRequiredSigningContext();
-
- const { executeActionAuthProcedure } = useRequiredDocumentAuthContext();
-
- const [isPending, startTransition] = useTransition();
-
- const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
- trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
-
- const {
- mutateAsync: removeSignedFieldWithToken,
- isPending: isRemoveSignedFieldWithTokenLoading,
- } = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
-
- const { signature } = field;
-
- const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;
-
- const [showSignatureModal, setShowSignatureModal] = useState(false);
- const [localSignature, setLocalSignature] = useState(null);
-
- const state = useMemo(() => {
- if (!field.inserted) {
- return 'empty';
- }
-
- if (signature?.signatureImageAsBase64) {
- return 'signed-image';
- }
-
- return 'signed-text';
- }, [field.inserted, signature?.signatureImageAsBase64]);
-
- const onPreSign = () => {
- if (!providedSignature || !signatureValid) {
- setShowSignatureModal(true);
- return false;
- }
-
- return true;
- };
- /**
- * When the user clicks the sign button in the dialog where they enter their signature.
- */
- const onDialogSignClick = () => {
- setShowSignatureModal(false);
- setProvidedSignature(localSignature);
- if (!localSignature) {
- return;
- }
-
- void executeActionAuthProcedure({
- onReauthFormSubmit: async (authOptions) => await onSign(authOptions, localSignature),
- actionTarget: field.type,
- });
- };
-
- const onSign = async (authOptions?: TRecipientActionAuth, signature?: string) => {
- try {
- const value = signature || providedSignature;
-
- if (!value || (signature && !signatureValid)) {
- setShowSignatureModal(true);
- return;
- }
-
- const isTypedSignature = !value.startsWith('data:image');
-
- if (isTypedSignature && !typedSignatureEnabled) {
- toast({
- title: _(msg`Error`),
- description: _(msg`Typed signatures are not allowed. Please draw your signature.`),
- variant: 'destructive',
- });
-
- return;
- }
-
- const payload: TSignFieldWithTokenMutationSchema = {
- token: recipient.token,
- fieldId: field.id,
- value,
- isBase64: !isTypedSignature,
- authOptions,
- };
-
- if (onSignField) {
- await onSignField(payload);
- return;
- }
-
- await signFieldWithToken(payload);
-
- startTransition(() => router.refresh());
- } catch (err) {
- const error = AppError.parseError(err);
-
- if (error.code === AppErrorCode.UNAUTHORIZED) {
- throw error;
- }
-
- console.error(err);
-
- toast({
- title: _(msg`Error`),
- description: _(msg`An error occurred while signing the document.`),
- variant: 'destructive',
- });
- }
- };
-
- const onRemove = async () => {
- try {
- const payload: TRemovedSignedFieldWithTokenMutationSchema = {
- token: recipient.token,
- fieldId: field.id,
- };
-
- if (onUnsignField) {
- await onUnsignField(payload);
- return;
- }
-
- await removeSignedFieldWithToken(payload);
-
- startTransition(() => router.refresh());
- } catch (err) {
- console.error(err);
-
- toast({
- title: _(msg`Error`),
- description: _(msg`An error occurred while removing the signature.`),
- variant: 'destructive',
- });
- }
- };
-
- useLayoutEffect(() => {
- if (!signatureRef.current || !containerRef.current || !signature?.typedSignature) {
- return;
- }
-
- const adjustTextSize = () => {
- const container = containerRef.current;
- const text = signatureRef.current;
-
- if (!container || !text) {
- return;
- }
-
- let size = 2;
- text.style.fontSize = `${size}rem`;
-
- while (
- (text.scrollWidth > container.clientWidth || text.scrollHeight > container.clientHeight) &&
- size > 0.8
- ) {
- size -= 0.1;
- text.style.fontSize = `${size}rem`;
- }
-
- setFontSize(size);
- };
-
- const resizeObserver = new ResizeObserver(adjustTextSize);
- resizeObserver.observe(containerRef.current);
-
- adjustTextSize();
-
- return () => resizeObserver.disconnect();
- }, [signature?.typedSignature]);
-
- return (
-
- {isLoading && (
-
-
-
- )}
-
- {state === 'empty' && (
-
- Signature
-
- )}
-
- {state === 'signed-image' && signature?.signatureImageAsBase64 && (
-
- )}
-
- {state === 'signed-text' && (
-
-
- {signature?.typedSignature}
-
-
- )}
-
-
-
-
-
- Sign as {recipient.name}{' '}
- ({recipient.email})
-
-
-
-
-
- Signature
-
-
-
- setLocalSignature(value)}
- allowTypedSignature={typedSignatureEnabled}
- onValidityChange={(isValid) => {
- setSignatureValid(isValid);
- }}
- />
-
-
- {!signatureValid && (
-
- Signature is too small. Please provide a more complete signature.
-
- )}
-
-
-
-
-
- {
- setShowSignatureModal(false);
- setLocalSignature(null);
- }}
- >
- Cancel
-
- onDialogSignClick()}
- >
- Sign
-
-
-
-
-
-
- );
-};
diff --git a/apps/web/src/app/(signing)/sign/[token]/signing-auth-page.tsx b/apps/web/src/app/(signing)/sign/[token]/signing-auth-page.tsx
deleted file mode 100644
index 4fde9ef50..000000000
--- a/apps/web/src/app/(signing)/sign/[token]/signing-auth-page.tsx
+++ /dev/null
@@ -1,72 +0,0 @@
-'use client';
-
-import { useState } from 'react';
-
-import { useRouter } from 'next/navigation';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { signOut } from 'next-auth/react';
-
-import { Button } from '@documenso/ui/primitives/button';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-export type SigningAuthPageViewProps = {
- email: string;
- emailHasAccount?: boolean;
-};
-
-export const SigningAuthPageView = ({ email, emailHasAccount }: SigningAuthPageViewProps) => {
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const router = useRouter();
-
- const [isSigningOut, setIsSigningOut] = useState(false);
-
- const handleChangeAccount = async (email: string) => {
- try {
- setIsSigningOut(true);
-
- await signOut({
- redirect: false,
- });
-
- router.push(emailHasAccount ? `/signin#email=${email}` : `/signup#email=${email}`);
- } catch {
- toast({
- title: _(msg`Something went wrong`),
- description: _(msg`We were unable to log you out at this time.`),
- duration: 10000,
- variant: 'destructive',
- });
- }
-
- setIsSigningOut(false);
- };
-
- return (
-
-
-
- Authentication required
-
-
-
-
- You need to be logged in as {email} to view this page.
-
-
-
-
handleChangeAccount(email)}
- loading={isSigningOut}
- >
- {emailHasAccount ? Login : Sign up }
-
-
-
- );
-};
diff --git a/apps/web/src/app/(signing)/sign/[token]/signing-field-container.tsx b/apps/web/src/app/(signing)/sign/[token]/signing-field-container.tsx
deleted file mode 100644
index cf8403696..000000000
--- a/apps/web/src/app/(signing)/sign/[token]/signing-field-container.tsx
+++ /dev/null
@@ -1,188 +0,0 @@
-'use client';
-
-import React from 'react';
-
-import { Trans } from '@lingui/macro';
-import { X } from 'lucide-react';
-
-import { type TRecipientActionAuth } from '@documenso/lib/types/document-auth';
-import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
-import { FieldType } from '@documenso/prisma/client';
-import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
-import { FieldRootContainer } from '@documenso/ui/components/field/field';
-import { cn } from '@documenso/ui/lib/utils';
-import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
-
-import { useRequiredDocumentAuthContext } from './document-auth-provider';
-
-export type SignatureFieldProps = {
- field: FieldWithSignature;
- loading?: boolean;
- children: React.ReactNode;
-
- /**
- * A function that is called before the field requires to be signed, or reauthed.
- *
- * Example, you may want to show a dialog prior to signing where they can enter a value.
- *
- * Once that action is complete, you will need to call `executeActionAuthProcedure` to proceed
- * regardless if it requires reauth or not.
- *
- * If the function returns true, we will proceed with the signing process. Otherwise if
- * false is returned we will not proceed.
- */
- onPreSign?: () => Promise | boolean;
-
- /**
- * The function required to be executed to insert the field.
- *
- * The auth values will be passed in if available.
- */
- onSign?: (documentAuthValue?: TRecipientActionAuth) => Promise | void;
- onRemove?: (fieldType?: string) => Promise | void;
- type?:
- | 'Date'
- | 'Initials'
- | 'Email'
- | 'Name'
- | 'Signature'
- | 'Radio'
- | 'Dropdown'
- | 'Number'
- | 'Checkbox';
- tooltipText?: string | null;
-};
-
-export const SigningFieldContainer = ({
- field,
- loading,
- onPreSign,
- onSign,
- onRemove,
- children,
- type,
- tooltipText,
-}: SignatureFieldProps) => {
- const { executeActionAuthProcedure, isAuthRedirectRequired } = useRequiredDocumentAuthContext();
-
- const parsedFieldMeta = field.fieldMeta ? ZFieldMetaSchema.parse(field.fieldMeta) : undefined;
- const readOnlyField = parsedFieldMeta?.readOnly || false;
-
- const handleInsertField = async () => {
- if (field.inserted || !onSign) {
- return;
- }
-
- // Bypass reauth for non signature fields.
- if (field.type !== FieldType.SIGNATURE) {
- const presignResult = await onPreSign?.();
-
- if (presignResult === false) {
- return;
- }
-
- await onSign();
- return;
- }
-
- if (isAuthRedirectRequired) {
- await executeActionAuthProcedure({
- onReauthFormSubmit: () => {
- // Do nothing since the user should be redirected.
- },
- actionTarget: field.type,
- });
-
- return;
- }
-
- // Handle any presign requirements, and halt if required.
- if (onPreSign) {
- const preSignResult = await onPreSign();
-
- if (preSignResult === false) {
- return;
- }
- }
-
- await executeActionAuthProcedure({
- onReauthFormSubmit: onSign,
- actionTarget: field.type,
- });
- };
-
- const onRemoveSignedFieldClick = async () => {
- if (!field.inserted) {
- return;
- }
-
- await onRemove?.();
- };
-
- const onClearCheckBoxValues = async (fieldType?: string) => {
- if (!field.inserted) {
- return;
- }
-
- await onRemove?.(fieldType);
- };
-
- return (
-
-
- {!field.inserted && !loading && !readOnlyField && (
- handleInsertField()}
- />
- )}
-
- {readOnlyField && (
-
-
- Read only field
-
-
- )}
-
- {type === 'Date' && field.inserted && !loading && !readOnlyField && (
-
-
-
- Remove
-
-
-
- {tooltipText && {tooltipText} }
-
- )}
-
- {type === 'Checkbox' && field.inserted && !loading && !readOnlyField && (
- void onClearCheckBoxValues(type)}
- >
-
-
-
-
- )}
-
- {type !== 'Date' && type !== 'Checkbox' && field.inserted && !loading && !readOnlyField && (
-
- Remove
-
- )}
-
- {children}
-
-
- );
-};
diff --git a/apps/web/src/app/(signing)/sign/[token]/signing-page-view.tsx b/apps/web/src/app/(signing)/sign/[token]/signing-page-view.tsx
deleted file mode 100644
index 019f3e9c3..000000000
--- a/apps/web/src/app/(signing)/sign/[token]/signing-page-view.tsx
+++ /dev/null
@@ -1,216 +0,0 @@
-import { Trans } from '@lingui/macro';
-import { match } from 'ts-pattern';
-
-import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
-import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
-import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones';
-import type { DocumentAndSender } from '@documenso/lib/server-only/document/get-document-by-token';
-import {
- ZCheckboxFieldMeta,
- ZDropdownFieldMeta,
- ZNumberFieldMeta,
- ZRadioFieldMeta,
- ZTextFieldMeta,
-} from '@documenso/lib/types/field-meta';
-import type { CompletedField } from '@documenso/lib/types/fields';
-import type { Field, Recipient } from '@documenso/prisma/client';
-import { FieldType, RecipientRole } from '@documenso/prisma/client';
-import type { FieldWithSignatureAndFieldMeta } from '@documenso/prisma/types/field-with-signature-and-fieldmeta';
-import { Card, CardContent } from '@documenso/ui/primitives/card';
-import { ElementVisible } from '@documenso/ui/primitives/element-visible';
-import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
-
-import { DocumentReadOnlyFields } from '~/components/document/document-read-only-fields';
-
-import { AutoSign } from './auto-sign';
-import { CheckboxField } from './checkbox-field';
-import { DateField } from './date-field';
-import { DropdownField } from './dropdown-field';
-import { EmailField } from './email-field';
-import { SigningForm } from './form';
-import { InitialsField } from './initials-field';
-import { NameField } from './name-field';
-import { NumberField } from './number-field';
-import { RadioField } from './radio-field';
-import { RejectDocumentDialog } from './reject-document-dialog';
-import { SignatureField } from './signature-field';
-import { TextField } from './text-field';
-
-export type SigningPageViewProps = {
- document: DocumentAndSender;
- recipient: Recipient;
- fields: Field[];
- completedFields: CompletedField[];
- isRecipientsTurn: boolean;
-};
-
-export const SigningPageView = ({
- document,
- recipient,
- fields,
- completedFields,
- isRecipientsTurn,
-}: SigningPageViewProps) => {
- const { documentData, documentMeta } = document;
-
- const shouldUseTeamDetails =
- document.teamId && document.team?.teamGlobalSettings?.includeSenderDetails === false;
-
- let senderName = document.user.name ?? '';
- let senderEmail = `(${document.user.email})`;
-
- if (shouldUseTeamDetails) {
- senderName = document.team?.name ?? '';
- senderEmail = document.team?.teamEmail?.email ? `(${document.team.teamEmail.email})` : '';
- }
-
- return (
-
-
- {document.title}
-
-
-
-
-
- {senderName} {senderEmail}
- {' '}
-
- {match(recipient.role)
- .with(RecipientRole.VIEWER, () =>
- document.teamId && !shouldUseTeamDetails ? (
-
- on behalf of "{document.team?.name}" has invited you to view this document
-
- ) : (
- has invited you to view this document
- ),
- )
- .with(RecipientRole.SIGNER, () =>
- document.teamId && !shouldUseTeamDetails ? (
-
- on behalf of "{document.team?.name}" has invited you to sign this document
-
- ) : (
- has invited you to sign this document
- ),
- )
- .with(RecipientRole.APPROVER, () =>
- document.teamId && !shouldUseTeamDetails ? (
-
- on behalf of "{document.team?.name}" has invited you to approve this document
-
- ) : (
- has invited you to approve this document
- ),
- )
- .otherwise(() => null)}
-
-
-
-
-
-
-
-
-
-
-
-
-
- {fields.map((field) =>
- match(field.type)
- .with(FieldType.SIGNATURE, () => (
-
- ))
- .with(FieldType.INITIALS, () => (
-
- ))
- .with(FieldType.NAME, () => (
-
- ))
- .with(FieldType.DATE, () => (
-
- ))
- .with(FieldType.EMAIL, () => (
-
- ))
- .with(FieldType.TEXT, () => {
- const fieldWithMeta: FieldWithSignatureAndFieldMeta = {
- ...field,
- fieldMeta: field.fieldMeta ? ZTextFieldMeta.parse(field.fieldMeta) : null,
- };
- return ;
- })
- .with(FieldType.NUMBER, () => {
- const fieldWithMeta: FieldWithSignatureAndFieldMeta = {
- ...field,
- fieldMeta: field.fieldMeta ? ZNumberFieldMeta.parse(field.fieldMeta) : null,
- };
- return ;
- })
- .with(FieldType.RADIO, () => {
- const fieldWithMeta: FieldWithSignatureAndFieldMeta = {
- ...field,
- fieldMeta: field.fieldMeta ? ZRadioFieldMeta.parse(field.fieldMeta) : null,
- };
- return ;
- })
- .with(FieldType.CHECKBOX, () => {
- const fieldWithMeta: FieldWithSignatureAndFieldMeta = {
- ...field,
- fieldMeta: field.fieldMeta ? ZCheckboxFieldMeta.parse(field.fieldMeta) : null,
- };
- return ;
- })
- .with(FieldType.DROPDOWN, () => {
- const fieldWithMeta: FieldWithSignatureAndFieldMeta = {
- ...field,
- fieldMeta: field.fieldMeta ? ZDropdownFieldMeta.parse(field.fieldMeta) : null,
- };
- return ;
- })
- .otherwise(() => null),
- )}
-
-
- );
-};
diff --git a/apps/web/src/app/(signing)/sign/[token]/text-field.tsx b/apps/web/src/app/(signing)/sign/[token]/text-field.tsx
deleted file mode 100644
index 0c4088d75..000000000
--- a/apps/web/src/app/(signing)/sign/[token]/text-field.tsx
+++ /dev/null
@@ -1,352 +0,0 @@
-'use client';
-
-import { useEffect, useState, useTransition } from 'react';
-
-import { useRouter } from 'next/navigation';
-
-import { Plural, Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { Loader, Type } from 'lucide-react';
-
-import { validateTextField } from '@documenso/lib/advanced-fields-validation/validate-text';
-import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
-import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
-import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
-import { ZTextFieldMeta } from '@documenso/lib/types/field-meta';
-import type { Recipient } from '@documenso/prisma/client';
-import type { FieldWithSignatureAndFieldMeta } from '@documenso/prisma/types/field-with-signature-and-fieldmeta';
-import { trpc } from '@documenso/trpc/react';
-import type {
- TRemovedSignedFieldWithTokenMutationSchema,
- TSignFieldWithTokenMutationSchema,
-} from '@documenso/trpc/server/field-router/schema';
-import { cn } from '@documenso/ui/lib/utils';
-import { Button } from '@documenso/ui/primitives/button';
-import { Dialog, DialogContent, DialogFooter, DialogTitle } from '@documenso/ui/primitives/dialog';
-import { Textarea } from '@documenso/ui/primitives/textarea';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-import { useRequiredDocumentAuthContext } from './document-auth-provider';
-import { SigningFieldContainer } from './signing-field-container';
-
-export type TextFieldProps = {
- field: FieldWithSignatureAndFieldMeta;
- recipient: Recipient;
- onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise | void;
- onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise | void;
-};
-
-export const TextField = ({ field, recipient, onSignField, onUnsignField }: TextFieldProps) => {
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const router = useRouter();
-
- const initialErrors: Record = {
- required: [],
- characterLimit: [],
- };
-
- const [errors, setErrors] = useState(initialErrors);
- const userInputHasErrors = Object.values(errors).some((error) => error.length > 0);
-
- const { executeActionAuthProcedure } = useRequiredDocumentAuthContext();
-
- const [isPending, startTransition] = useTransition();
-
- const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
- trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
-
- const {
- mutateAsync: removeSignedFieldWithToken,
- isPending: isRemoveSignedFieldWithTokenLoading,
- } = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
-
- const parsedFieldMeta = field.fieldMeta ? ZTextFieldMeta.parse(field.fieldMeta) : null;
-
- const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;
- const shouldAutoSignField =
- (!field.inserted && parsedFieldMeta?.text) ||
- (!field.inserted && parsedFieldMeta?.text && parsedFieldMeta?.readOnly);
-
- const [showCustomTextModal, setShowCustomTextModal] = useState(false);
- const [localText, setLocalCustomText] = useState(parsedFieldMeta?.text ?? '');
-
- useEffect(() => {
- if (!showCustomTextModal) {
- setLocalCustomText(parsedFieldMeta?.text ?? '');
- setErrors(initialErrors);
- }
- }, [showCustomTextModal]);
-
- const handleTextChange = (e: React.ChangeEvent) => {
- const text = e.target.value;
- setLocalCustomText(text);
-
- if (parsedFieldMeta) {
- const validationErrors = validateTextField(text, parsedFieldMeta, true);
- setErrors({
- required: validationErrors.filter((error) => error.includes('required')),
- characterLimit: validationErrors.filter((error) => error.includes('character limit')),
- });
- }
- };
-
- /**
- * When the user clicks the sign button in the dialog where they enter the text field.
- */
- const onDialogSignClick = () => {
- if (parsedFieldMeta) {
- const validationErrors = validateTextField(localText, parsedFieldMeta, true);
-
- if (validationErrors.length > 0) {
- setErrors({
- required: validationErrors.filter((error) => error.includes('required')),
- characterLimit: validationErrors.filter((error) => error.includes('character limit')),
- });
- return;
- }
- }
-
- setShowCustomTextModal(false);
-
- void executeActionAuthProcedure({
- onReauthFormSubmit: async (authOptions) => await onSign(authOptions),
- actionTarget: field.type,
- });
- };
-
- const onPreSign = () => {
- setShowCustomTextModal(true);
-
- if (localText && parsedFieldMeta) {
- const validationErrors = validateTextField(localText, parsedFieldMeta, true);
- setErrors({
- required: validationErrors.filter((error) => error.includes('required')),
- characterLimit: validationErrors.filter((error) => error.includes('character limit')),
- });
- }
-
- return false;
- };
-
- const onSign = async (authOptions?: TRecipientActionAuth) => {
- try {
- if (!localText || userInputHasErrors) {
- return;
- }
-
- const payload: TSignFieldWithTokenMutationSchema = {
- token: recipient.token,
- fieldId: field.id,
- value: localText,
- isBase64: true,
- authOptions,
- };
-
- if (onSignField) {
- await onSignField(payload);
- return;
- }
-
- await signFieldWithToken(payload);
-
- setLocalCustomText('');
-
- startTransition(() => router.refresh());
- } catch (err) {
- const error = AppError.parseError(err);
-
- if (error.code === AppErrorCode.UNAUTHORIZED) {
- throw error;
- }
-
- console.error(err);
-
- toast({
- title: _(msg`Error`),
- description: _(msg`An error occurred while signing the document.`),
- variant: 'destructive',
- });
- }
- };
-
- const onRemove = async () => {
- try {
- const payload: TRemovedSignedFieldWithTokenMutationSchema = {
- token: recipient.token,
- fieldId: field.id,
- };
-
- if (onUnsignField) {
- await onUnsignField(payload);
- return;
- }
-
- await removeSignedFieldWithToken(payload);
-
- setLocalCustomText(parsedFieldMeta?.text ?? '');
-
- startTransition(() => router.refresh());
- } catch (err) {
- console.error(err);
-
- toast({
- title: _(msg`Error`),
- description: _(msg`An error occurred while removing the text.`),
- variant: 'destructive',
- });
- }
- };
-
- useEffect(() => {
- if (shouldAutoSignField) {
- void executeActionAuthProcedure({
- onReauthFormSubmit: async (authOptions) => await onSign(authOptions),
- actionTarget: field.type,
- });
- }
- }, []);
-
- const parsedField = field.fieldMeta ? ZTextFieldMeta.parse(field.fieldMeta) : undefined;
-
- const labelDisplay =
- parsedField?.label && parsedField.label.length < 20
- ? parsedField.label
- : parsedField?.label
- ? parsedField?.label.substring(0, 20) + '...'
- : undefined;
-
- const textDisplay =
- parsedField?.text && parsedField.text.length < 20
- ? parsedField.text
- : parsedField?.text
- ? parsedField?.text.substring(0, 20) + '...'
- : undefined;
-
- const fieldDisplayName = labelDisplay ? labelDisplay : textDisplay;
- const charactersRemaining = (parsedFieldMeta?.characterLimit ?? 0) - (localText.length ?? 0);
-
- return (
-
- {isLoading && (
-
-
-
- )}
-
- {!field.inserted && (
-
-
-
-
- {fieldDisplayName || Text }
-
-
-
- )}
-
- {field.inserted && (
-
- {field.customText.length < 20
- ? field.customText
- : field.customText.substring(0, 15) + '...'}
-
- )}
-
-
-
-
- {parsedFieldMeta?.label ? parsedFieldMeta?.label : Text }
-
-
-
-
-
-
- {parsedFieldMeta?.characterLimit !== undefined &&
- parsedFieldMeta?.characterLimit > 0 &&
- !userInputHasErrors && (
-
- )}
-
- {userInputHasErrors && (
-
- {errors.required.map((error, index) => (
-
- {error}
-
- ))}
- {errors.characterLimit.map((error, index) => (
-
- {error}{' '}
- {charactersRemaining < 0 && (
-
- )}
-
- ))}
-
- )}
-
-
-
- {
- setShowCustomTextModal(false);
- setLocalCustomText('');
- }}
- >
- Cancel
-
-
- onDialogSignClick()}
- >
- Save
-
-
-
-
-
-
- );
-};
diff --git a/apps/web/src/app/(signing)/sign/[token]/waiting/page.tsx b/apps/web/src/app/(signing)/sign/[token]/waiting/page.tsx
deleted file mode 100644
index 4288d52be..000000000
--- a/apps/web/src/app/(signing)/sign/[token]/waiting/page.tsx
+++ /dev/null
@@ -1,100 +0,0 @@
-import Link from 'next/link';
-import { notFound, redirect } from 'next/navigation';
-
-import { Trans } from '@lingui/macro';
-
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
-import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
-import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
-import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
-import { getTeamById } from '@documenso/lib/server-only/team/get-team';
-import { formatDocumentsPath } from '@documenso/lib/utils/teams';
-import type { Team } from '@documenso/prisma/client';
-import { DocumentStatus } from '@documenso/prisma/client';
-import { Button } from '@documenso/ui/primitives/button';
-
-type WaitingForTurnToSignPageProps = {
- params: { token?: string };
-};
-
-export default async function WaitingForTurnToSignPage({
- params: { token },
-}: WaitingForTurnToSignPageProps) {
- await setupI18nSSR();
-
- if (!token) {
- return notFound();
- }
-
- const { user } = await getServerComponentSession();
-
- const [document, recipient] = await Promise.all([
- getDocumentAndSenderByToken({ token }).catch(() => null),
- getRecipientByToken({ token }).catch(() => null),
- ]);
-
- if (!document || !recipient) {
- return notFound();
- }
-
- if (document.status === DocumentStatus.COMPLETED) {
- return redirect(`/sign/${token}/complete`);
- }
-
- let isOwnerOrTeamMember = false;
-
- let team: Team | null = null;
-
- if (user) {
- isOwnerOrTeamMember = await getDocumentById({
- documentId: document.id,
- userId: user.id,
- teamId: document.teamId ?? undefined,
- })
- .then((document) => !!document)
- .catch(() => false);
-
- if (document.teamId) {
- team = await getTeamById({
- userId: user.id,
- teamId: document.teamId,
- });
- }
- }
-
- return (
-
-
-
- Waiting for Your Turn
-
-
-
-
- It's currently not your turn to sign. You will receive an email with instructions once
- it's your turn to sign the document.
-
-
-
-
- Please check your email for updates.
-
-
-
- {isOwnerOrTeamMember ? (
-
-
- Were you trying to edit this document instead?
-
-
- ) : (
-
- Return Home
-
- )}
-
-
-
- );
-}
diff --git a/apps/web/src/app/(teams)/t/[teamUrl]/documents/[id]/edit/page.tsx b/apps/web/src/app/(teams)/t/[teamUrl]/documents/[id]/edit/page.tsx
deleted file mode 100644
index ea6351ad9..000000000
--- a/apps/web/src/app/(teams)/t/[teamUrl]/documents/[id]/edit/page.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
-import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
-
-import { DocumentEditPageView } from '~/app/(dashboard)/documents/[id]/edit/document-edit-page-view';
-
-export type DocumentPageProps = {
- params: {
- id: string;
- teamUrl: string;
- };
-};
-
-export default async function TeamsDocumentEditPage({ params }: DocumentPageProps) {
- await setupI18nSSR();
-
- const { teamUrl } = params;
-
- const { user } = await getRequiredServerComponentSession();
-
- const team = await getTeamByUrl({ userId: user.id, teamUrl });
-
- return ;
-}
diff --git a/apps/web/src/app/(teams)/t/[teamUrl]/documents/[id]/logs/page.tsx b/apps/web/src/app/(teams)/t/[teamUrl]/documents/[id]/logs/page.tsx
deleted file mode 100644
index 9f83273e4..000000000
--- a/apps/web/src/app/(teams)/t/[teamUrl]/documents/[id]/logs/page.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
-import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
-
-import { DocumentLogsPageView } from '~/app/(dashboard)/documents/[id]/logs/document-logs-page-view';
-
-export type TeamDocumentsLogsPageProps = {
- params: {
- id: string;
- teamUrl: string;
- };
-};
-
-export default async function TeamsDocumentsLogsPage({ params }: TeamDocumentsLogsPageProps) {
- await setupI18nSSR();
-
- const { teamUrl } = params;
-
- const { user } = await getRequiredServerComponentSession();
- const team = await getTeamByUrl({ userId: user.id, teamUrl });
-
- return ;
-}
diff --git a/apps/web/src/app/(teams)/t/[teamUrl]/documents/[id]/page.tsx b/apps/web/src/app/(teams)/t/[teamUrl]/documents/[id]/page.tsx
deleted file mode 100644
index 30bd93555..000000000
--- a/apps/web/src/app/(teams)/t/[teamUrl]/documents/[id]/page.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
-import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
-
-import { DocumentPageView } from '~/app/(dashboard)/documents/[id]/document-page-view';
-
-export type DocumentPageProps = {
- params: {
- id: string;
- teamUrl: string;
- };
-};
-
-export default async function DocumentPage({ params }: DocumentPageProps) {
- await setupI18nSSR();
-
- const { teamUrl } = params;
-
- const { user } = await getRequiredServerComponentSession();
- const team = await getTeamByUrl({ userId: user.id, teamUrl });
-
- return ;
-}
diff --git a/apps/web/src/app/(teams)/t/[teamUrl]/documents/page.tsx b/apps/web/src/app/(teams)/t/[teamUrl]/documents/page.tsx
deleted file mode 100644
index 0d5b9692f..000000000
--- a/apps/web/src/app/(teams)/t/[teamUrl]/documents/page.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
-import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
-
-import type { DocumentsPageViewProps } from '~/app/(dashboard)/documents/documents-page-view';
-import { DocumentsPageView } from '~/app/(dashboard)/documents/documents-page-view';
-
-export type TeamsDocumentPageProps = {
- params: {
- teamUrl: string;
- };
- searchParams?: DocumentsPageViewProps['searchParams'];
-};
-
-export default async function TeamsDocumentPage({
- params,
- searchParams = {},
-}: TeamsDocumentPageProps) {
- await setupI18nSSR();
-
- const { teamUrl } = params;
-
- const { user } = await getRequiredServerComponentSession();
-
- const team = await getTeamByUrl({ userId: user.id, teamUrl });
-
- return ;
-}
diff --git a/apps/web/src/app/(teams)/t/[teamUrl]/error.tsx b/apps/web/src/app/(teams)/t/[teamUrl]/error.tsx
deleted file mode 100644
index 73f9510b7..000000000
--- a/apps/web/src/app/(teams)/t/[teamUrl]/error.tsx
+++ /dev/null
@@ -1,63 +0,0 @@
-'use client';
-
-import Link from 'next/link';
-import { useRouter } from 'next/navigation';
-
-import type { MessageDescriptor } from '@lingui/core';
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { ChevronLeft } from 'lucide-react';
-
-import { AppErrorCode } from '@documenso/lib/errors/app-error';
-import { Button } from '@documenso/ui/primitives/button';
-
-type ErrorProps = {
- error: Error & { digest?: string };
-};
-
-export default function ErrorPage({ error }: ErrorProps) {
- const { _ } = useLingui();
-
- const router = useRouter();
-
- let errorMessage = msg`Unknown error`;
- let errorDetails: MessageDescriptor | null = null;
-
- if (error.message === AppErrorCode.UNAUTHORIZED) {
- errorMessage = msg`Unauthorized`;
- errorDetails = msg`You are not authorized to view this page.`;
- }
-
- return (
-
-
-
{_(errorMessage)}
-
-
- Oops! Something went wrong.
-
-
-
{errorDetails ? _(errorDetails) : ''}
-
-
- {
- void router.back();
- }}
- >
-
- Go Back
-
-
-
-
- View teams
-
-
-
-
-
- );
-}
diff --git a/apps/web/src/app/(teams)/t/[teamUrl]/layout-billing-banner.tsx b/apps/web/src/app/(teams)/t/[teamUrl]/layout-billing-banner.tsx
deleted file mode 100644
index d29b8abfb..000000000
--- a/apps/web/src/app/(teams)/t/[teamUrl]/layout-billing-banner.tsx
+++ /dev/null
@@ -1,140 +0,0 @@
-'use client';
-
-import { useState } from 'react';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { AlertTriangle } from 'lucide-react';
-import { match } from 'ts-pattern';
-
-import { canExecuteTeamAction } from '@documenso/lib/utils/teams';
-import type { TeamMemberRole } from '@documenso/prisma/client';
-import { type Subscription, SubscriptionStatus } from '@documenso/prisma/client';
-import { trpc } from '@documenso/trpc/react';
-import { cn } from '@documenso/ui/lib/utils';
-import { Button } from '@documenso/ui/primitives/button';
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogTitle,
-} from '@documenso/ui/primitives/dialog';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-export type LayoutBillingBannerProps = {
- subscription: Subscription;
- teamId: number;
- userRole: TeamMemberRole;
-};
-
-export const LayoutBillingBanner = ({
- subscription,
- teamId,
- userRole,
-}: LayoutBillingBannerProps) => {
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const [isOpen, setIsOpen] = useState(false);
-
- const { mutateAsync: createBillingPortal, isPending } =
- trpc.team.createBillingPortal.useMutation();
-
- const handleCreatePortal = async () => {
- try {
- const sessionUrl = await createBillingPortal({ teamId });
-
- window.open(sessionUrl, '_blank');
-
- setIsOpen(false);
- } catch (err) {
- toast({
- title: _(msg`Something went wrong`),
- description: _(
- msg`We are unable to proceed to the billing portal at this time. Please try again, or contact support.`,
- ),
- variant: 'destructive',
- duration: 10000,
- });
- }
- };
-
- if (subscription.status === SubscriptionStatus.ACTIVE) {
- return null;
- }
-
- return (
- <>
-
-
-
-
-
- {match(subscription.status)
- .with(SubscriptionStatus.PAST_DUE, () =>
Payment overdue )
- .with(SubscriptionStatus.INACTIVE, () =>
Teams restricted )
- .exhaustive()}
-
-
-
setIsOpen(true)}
- size="sm"
- >
- Resolve
-
-
-
-
- !isPending && setIsOpen(value)}>
-
-
- Payment overdue
-
-
- {match(subscription.status)
- .with(SubscriptionStatus.PAST_DUE, () => (
-
-
- Your payment for teams is overdue. Please settle the payment to avoid any service
- disruptions.
-
-
- ))
- .with(SubscriptionStatus.INACTIVE, () => (
-
-
- Due to an unpaid invoice, your team has been restricted. Please settle the payment
- to restore full access to your team.
-
-
- ))
- .otherwise(() => null)}
-
- {canExecuteTeamAction('MANAGE_BILLING', userRole) && (
-
-
- Resolve payment
-
-
- )}
-
-
- >
- );
-};
diff --git a/apps/web/src/app/(teams)/t/[teamUrl]/layout.tsx b/apps/web/src/app/(teams)/t/[teamUrl]/layout.tsx
deleted file mode 100644
index 47a9bda70..000000000
--- a/apps/web/src/app/(teams)/t/[teamUrl]/layout.tsx
+++ /dev/null
@@ -1,78 +0,0 @@
-import React from 'react';
-
-import { RedirectType, redirect } from 'next/navigation';
-
-import { LimitsProvider } from '@documenso/ee/server-only/limits/provider/server';
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
-import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
-import { getTeams } from '@documenso/lib/server-only/team/get-teams';
-import { SubscriptionStatus } from '@documenso/prisma/client';
-import { TrpcProvider } from '@documenso/trpc/react';
-
-import { Header } from '~/components/(dashboard)/layout/header';
-import { RefreshOnFocus } from '~/components/(dashboard)/refresh-on-focus/refresh-on-focus';
-import { NextAuthProvider } from '~/providers/next-auth';
-import { TeamProvider } from '~/providers/team';
-
-import { LayoutBillingBanner } from './layout-billing-banner';
-
-export type AuthenticatedTeamsLayoutProps = {
- children: React.ReactNode;
- params: {
- teamUrl: string;
- };
-};
-
-export default async function AuthenticatedTeamsLayout({
- children,
- params,
-}: AuthenticatedTeamsLayoutProps) {
- await setupI18nSSR();
-
- const { session, user } = await getServerComponentSession();
-
- if (!session || !user) {
- redirect('/signin');
- }
-
- const [getTeamsPromise, getTeamPromise] = await Promise.allSettled([
- getTeams({ userId: user.id }),
- getTeamByUrl({ userId: user.id, teamUrl: params.teamUrl }),
- ]);
-
- if (getTeamPromise.status === 'rejected') {
- redirect('/documents', RedirectType.replace);
- }
-
- const team = getTeamPromise.value;
- const teams = getTeamsPromise.status === 'fulfilled' ? getTeamsPromise.value : [];
-
- const trpcHeaders = {
- 'x-team-Id': team.id.toString(),
- };
-
- return (
-
-
- {team.subscription && team.subscription.status !== SubscriptionStatus.ACTIVE && (
-
- )}
-
-
-
-
-
- {children}
-
-
-
-
-
-
- );
-}
diff --git a/apps/web/src/app/(teams)/t/[teamUrl]/not-found.tsx b/apps/web/src/app/(teams)/t/[teamUrl]/not-found.tsx
deleted file mode 100644
index 0175c568e..000000000
--- a/apps/web/src/app/(teams)/t/[teamUrl]/not-found.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-'use client';
-
-import Link from 'next/link';
-
-import { Trans } from '@lingui/macro';
-import { ChevronLeft } from 'lucide-react';
-
-import { Button } from '@documenso/ui/primitives/button';
-
-export default function NotFound() {
- return (
-
-
-
- 404 Team not found
-
-
-
- Oops! Something went wrong.
-
-
-
-
- The team you are looking for may have been removed, renamed or may have never existed.
-
-
-
-
-
-
-
- Go Back
-
-
-
-
-
- );
-}
diff --git a/apps/web/src/app/(teams)/t/[teamUrl]/settings/billing/page.tsx b/apps/web/src/app/(teams)/t/[teamUrl]/settings/billing/page.tsx
deleted file mode 100644
index d3cba21eb..000000000
--- a/apps/web/src/app/(teams)/t/[teamUrl]/settings/billing/page.tsx
+++ /dev/null
@@ -1,102 +0,0 @@
-import { Plural, Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { DateTime } from 'luxon';
-import type Stripe from 'stripe';
-import { match } from 'ts-pattern';
-
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
-import { stripe } from '@documenso/lib/server-only/stripe';
-import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
-import { canExecuteTeamAction } from '@documenso/lib/utils/teams';
-import { Card, CardContent } from '@documenso/ui/primitives/card';
-
-import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
-import { TeamBillingInvoicesDataTable } from '~/components/(teams)/tables/team-billing-invoices-data-table';
-import { TeamBillingPortalButton } from '~/components/(teams)/team-billing-portal-button';
-
-export type TeamsSettingsBillingPageProps = {
- params: {
- teamUrl: string;
- };
-};
-
-export default async function TeamsSettingBillingPage({ params }: TeamsSettingsBillingPageProps) {
- await setupI18nSSR();
-
- const { _ } = useLingui();
-
- const session = await getRequiredServerComponentSession();
-
- const team = await getTeamByUrl({ userId: session.user.id, teamUrl: params.teamUrl });
-
- const canManageBilling = canExecuteTeamAction('MANAGE_BILLING', team.currentTeamMember.role);
-
- let teamSubscription: Stripe.Subscription | null = null;
-
- if (team.subscription) {
- teamSubscription = await stripe.subscriptions.retrieve(team.subscription.planId);
- }
-
- const formatTeamSubscriptionDetails = (subscription: Stripe.Subscription | null) => {
- if (!subscription) {
- return No payment required ;
- }
-
- const numberOfSeats = subscription.items.data[0].quantity ?? 0;
-
- const formattedDate = DateTime.fromSeconds(subscription.current_period_end).toFormat(
- 'LLL dd, yyyy',
- );
-
- const subscriptionInterval = match(subscription?.items.data[0].plan.interval)
- .with('year', () => _(msg`Yearly`))
- .with('month', () => _(msg`Monthly`))
- .otherwise(() => _(msg`Unknown`));
-
- return (
-
-
- {' • '}
- {subscriptionInterval}
- {' • '}
- Renews: {formattedDate}
-
- );
- };
-
- return (
-
-
-
-
-
-
-
- {formatTeamSubscriptionDetails(teamSubscription)}
-
-
-
- {teamSubscription && (
-
-
-
- )}
-
-
-
-
-
- );
-}
diff --git a/apps/web/src/app/(teams)/t/[teamUrl]/settings/layout.tsx b/apps/web/src/app/(teams)/t/[teamUrl]/settings/layout.tsx
deleted file mode 100644
index c2fc3c39e..000000000
--- a/apps/web/src/app/(teams)/t/[teamUrl]/settings/layout.tsx
+++ /dev/null
@@ -1,61 +0,0 @@
-import React from 'react';
-
-import { notFound } from 'next/navigation';
-
-import { Trans } from '@lingui/macro';
-
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
-import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
-import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
-import { canExecuteTeamAction } from '@documenso/lib/utils/teams';
-
-import { DesktopNav } from '~/components/(teams)/settings/layout/desktop-nav';
-import { MobileNav } from '~/components/(teams)/settings/layout/mobile-nav';
-
-export type TeamSettingsLayoutProps = {
- children: React.ReactNode;
- params: {
- teamUrl: string;
- };
-};
-
-export default async function TeamsSettingsLayout({
- children,
- params: { teamUrl },
-}: TeamSettingsLayoutProps) {
- await setupI18nSSR();
-
- const session = await getRequiredServerComponentSession();
-
- try {
- const team = await getTeamByUrl({ userId: session.user.id, teamUrl });
-
- if (!canExecuteTeamAction('MANAGE_TEAM', team.currentTeamMember.role)) {
- throw new Error(AppErrorCode.UNAUTHORIZED);
- }
- } catch (e) {
- const error = AppError.parseError(e);
-
- if (error.code === 'P2025') {
- notFound();
- }
-
- throw e;
- }
-
- return (
-
-
- Team Settings
-
-
-
-
- );
-}
diff --git a/apps/web/src/app/(teams)/t/[teamUrl]/settings/members/page.tsx b/apps/web/src/app/(teams)/t/[teamUrl]/settings/members/page.tsx
deleted file mode 100644
index cda20f9c0..000000000
--- a/apps/web/src/app/(teams)/t/[teamUrl]/settings/members/page.tsx
+++ /dev/null
@@ -1,48 +0,0 @@
-import { msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
-import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
-
-import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
-import { InviteTeamMembersDialog } from '~/components/(teams)/dialogs/invite-team-member-dialog';
-import { TeamsMemberPageDataTable } from '~/components/(teams)/tables/teams-member-page-data-table';
-
-export type TeamsSettingsMembersPageProps = {
- params: {
- teamUrl: string;
- };
-};
-
-export default async function TeamsSettingsMembersPage({ params }: TeamsSettingsMembersPageProps) {
- await setupI18nSSR();
-
- const { _ } = useLingui();
- const { teamUrl } = params;
-
- const session = await getRequiredServerComponentSession();
-
- const team = await getTeamByUrl({ userId: session.user.id, teamUrl });
-
- return (
-
-
-
-
-
-
-
- );
-}
diff --git a/apps/web/src/app/(teams)/t/[teamUrl]/settings/page.tsx b/apps/web/src/app/(teams)/t/[teamUrl]/settings/page.tsx
deleted file mode 100644
index 3a72cb255..000000000
--- a/apps/web/src/app/(teams)/t/[teamUrl]/settings/page.tsx
+++ /dev/null
@@ -1,209 +0,0 @@
-import { Trans } from '@lingui/macro';
-import { CheckCircle2, Clock } from 'lucide-react';
-import { P, match } from 'ts-pattern';
-
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
-import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
-import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
-import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
-import { isTokenExpired } from '@documenso/lib/utils/token-verification';
-import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
-import { AvatarWithText } from '@documenso/ui/primitives/avatar';
-
-import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
-import { AddTeamEmailDialog } from '~/components/(teams)/dialogs/add-team-email-dialog';
-import { DeleteTeamDialog } from '~/components/(teams)/dialogs/delete-team-dialog';
-import { TransferTeamDialog } from '~/components/(teams)/dialogs/transfer-team-dialog';
-import { UpdateTeamForm } from '~/components/(teams)/forms/update-team-form';
-import { AvatarImageForm } from '~/components/forms/avatar-image';
-
-import { TeamEmailDropdown } from './team-email-dropdown';
-import { TeamTransferStatus } from './team-transfer-status';
-
-export type TeamsSettingsPageProps = {
- params: {
- teamUrl: string;
- };
-};
-
-export default async function TeamsSettingsPage({ params }: TeamsSettingsPageProps) {
- await setupI18nSSR();
-
- const { teamUrl } = params;
-
- const session = await getRequiredServerComponentSession();
-
- const team = await getTeamByUrl({ userId: session.user.id, teamUrl });
-
- const isTransferVerificationExpired =
- !team.transferVerification || isTokenExpired(team.transferVerification.expiresAt);
-
- return (
-
-
-
-
-
-
-
-
-
-
- {(team.teamEmail || team.emailVerification) && (
-
-
- Team email
-
-
-
-
- You can view documents associated with this email and use this identity when sending
- documents.
-
-
-
-
-
-
-
- {team.teamEmail?.name || team.emailVerification?.name}
-
- }
- secondaryText={
-
- {team.teamEmail?.email || team.emailVerification?.email}
-
- }
- />
-
-
-
- {match({
- teamEmail: team.teamEmail,
- emailVerification: team.emailVerification,
- })
- .with({ teamEmail: P.not(null) }, () => (
- <>
-
- Active
- >
- ))
- .with(
- {
- emailVerification: P.when(
- (emailVerification) =>
- emailVerification && emailVerification?.expiresAt < new Date(),
- ),
- },
- () => (
- <>
-
- Expired
- >
- ),
- )
- .with({ emailVerification: P.not(null) }, () => (
- <>
-
- Awaiting email confirmation
- >
- ))
- .otherwise(() => null)}
-
-
-
-
-
-
- )}
-
- {!team.teamEmail && !team.emailVerification && (
-
-
-
- Team email
-
-
-
-
- {/* Feature not available yet. */}
- {/* Display this name and email when sending documents */}
- {/* View documents associated with this email */}
-
-
- View documents associated with this email
-
-
-
-
-
-
-
- )}
-
- {team.ownerUserId === session.user.id && (
- <>
- {isTransferVerificationExpired && (
-
-
-
- Transfer team
-
-
-
- Transfer the ownership of the team to another team member.
-
-
-
-
-
- )}
-
-
-
-
- Delete team
-
-
-
-
- This team, and any associated data excluding billing invoices will be
- permanently deleted.
-
-
-
-
-
-
- >
- )}
-
-
- );
-}
diff --git a/apps/web/src/app/(teams)/t/[teamUrl]/settings/preferences/branding-preferences.tsx b/apps/web/src/app/(teams)/t/[teamUrl]/settings/preferences/branding-preferences.tsx
deleted file mode 100644
index 3f937a0b8..000000000
--- a/apps/web/src/app/(teams)/t/[teamUrl]/settings/preferences/branding-preferences.tsx
+++ /dev/null
@@ -1,319 +0,0 @@
-'use client';
-
-import { useEffect, useState } from 'react';
-
-import { zodResolver } from '@hookform/resolvers/zod';
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { Loader } from 'lucide-react';
-import { useForm } from 'react-hook-form';
-import { z } from 'zod';
-
-import { getFile } from '@documenso/lib/universal/upload/get-file';
-import { putFile } from '@documenso/lib/universal/upload/put-file';
-import type { Team, TeamGlobalSettings } from '@documenso/prisma/client';
-import { trpc } from '@documenso/trpc/react';
-import { cn } from '@documenso/ui/lib/utils';
-import { Button } from '@documenso/ui/primitives/button';
-import {
- Form,
- FormControl,
- FormDescription,
- FormField,
- FormItem,
- FormLabel,
-} from '@documenso/ui/primitives/form/form';
-import { Input } from '@documenso/ui/primitives/input';
-import { Switch } from '@documenso/ui/primitives/switch';
-import { Textarea } from '@documenso/ui/primitives/textarea';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
-const ACCEPTED_FILE_TYPES = ['image/jpeg', 'image/png', 'image/webp'];
-
-const ZTeamBrandingPreferencesFormSchema = z.object({
- brandingEnabled: z.boolean(),
- brandingLogo: z
- .instanceof(File)
- .refine((file) => file.size <= MAX_FILE_SIZE, 'File size must be less than 5MB')
- .refine(
- (file) => ACCEPTED_FILE_TYPES.includes(file.type),
- 'Only .jpg, .png, and .webp files are accepted',
- )
- .nullish(),
- brandingUrl: z.string().url().optional().or(z.literal('')),
- brandingCompanyDetails: z.string().max(500).optional(),
-});
-
-type TTeamBrandingPreferencesFormSchema = z.infer;
-
-export type TeamBrandingPreferencesFormProps = {
- team: Team;
- settings?: TeamGlobalSettings | null;
-};
-
-export function TeamBrandingPreferencesForm({ team, settings }: TeamBrandingPreferencesFormProps) {
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const [previewUrl, setPreviewUrl] = useState('');
- const [hasLoadedPreview, setHasLoadedPreview] = useState(false);
-
- const { mutateAsync: updateTeamBrandingSettings } =
- trpc.team.updateTeamBrandingSettings.useMutation();
-
- const form = useForm({
- defaultValues: {
- brandingEnabled: settings?.brandingEnabled ?? false,
- brandingUrl: settings?.brandingUrl ?? '',
- brandingLogo: undefined,
- brandingCompanyDetails: settings?.brandingCompanyDetails ?? '',
- },
- resolver: zodResolver(ZTeamBrandingPreferencesFormSchema),
- });
-
- const isBrandingEnabled = form.watch('brandingEnabled');
-
- const onSubmit = async (data: TTeamBrandingPreferencesFormSchema) => {
- try {
- const { brandingEnabled, brandingLogo, brandingUrl, brandingCompanyDetails } = data;
-
- let uploadedBrandingLogo = settings?.brandingLogo;
-
- if (brandingLogo) {
- uploadedBrandingLogo = JSON.stringify(await putFile(brandingLogo));
- }
-
- if (brandingLogo === null) {
- uploadedBrandingLogo = '';
- }
-
- await updateTeamBrandingSettings({
- teamId: team.id,
- settings: {
- brandingEnabled,
- brandingLogo: uploadedBrandingLogo,
- brandingUrl,
- brandingCompanyDetails,
- },
- });
-
- toast({
- title: _(msg`Branding preferences updated`),
- description: _(msg`Your branding preferences have been updated`),
- });
- } catch (err) {
- toast({
- title: _(msg`Something went wrong`),
- description: _(
- msg`We were unable to update your branding preferences at this time, please try again later`,
- ),
- variant: 'destructive',
- });
- }
- };
-
- useEffect(() => {
- if (settings?.brandingLogo) {
- const file = JSON.parse(settings.brandingLogo);
-
- if ('type' in file && 'data' in file) {
- void getFile(file).then((binaryData) => {
- const objectUrl = URL.createObjectURL(new Blob([binaryData]));
-
- setPreviewUrl(objectUrl);
- setHasLoadedPreview(true);
- });
-
- return;
- }
- }
-
- setHasLoadedPreview(true);
- }, [settings?.brandingLogo]);
-
- // Cleanup ObjectURL on unmount or when previewUrl changes
- useEffect(() => {
- return () => {
- if (previewUrl.startsWith('blob:')) {
- URL.revokeObjectURL(previewUrl);
- }
- };
- }, [previewUrl]);
-
- return (
-
-
-
- (
-
- Enable Custom Branding
-
-
-
-
-
-
-
-
- Enable custom branding for all documents in this team.
-
-
- )}
- />
-
-
- {!isBrandingEnabled &&
}
-
-
(
-
- Branding Logo
-
-
-
- {previewUrl ? (
-
- ) : (
-
- Please upload a logo
- {!hasLoadedPreview && (
-
-
-
- )}
-
- )}
-
-
-
-
- {
- const file = e.target.files?.[0];
-
- if (file) {
- if (previewUrl.startsWith('blob:')) {
- URL.revokeObjectURL(previewUrl);
- }
-
- const objectUrl = URL.createObjectURL(file);
-
- setPreviewUrl(objectUrl);
-
- onChange(file);
- }
- }}
- className={cn(
- 'h-auto p-2',
- 'file:text-primary hover:file:bg-primary/90',
- 'file:mr-4 file:cursor-pointer file:rounded-md file:border-0',
- 'file:p-2 file:py-2 file:font-medium',
- 'file:bg-primary file:text-primary-foreground',
- !isBrandingEnabled && 'cursor-not-allowed',
- )}
- {...field}
- />
-
-
-
- {
- setPreviewUrl('');
- onChange(null);
- }}
- >
- Remove
-
-
-
-
-
- Upload your brand logo (max 5MB, JPG, PNG, or WebP)
-
-
-
- )}
- />
-
- (
-
- Brand Website
-
-
-
-
-
-
- Your brand website URL
-
-
- )}
- />
-
- (
-
- Brand Details
-
-
-
-
-
-
- Additional brand information to display at the bottom of emails
-
-
- )}
- />
-
-
-
-
- Save
-
-
-
-
-
- );
-}
diff --git a/apps/web/src/app/(teams)/t/[teamUrl]/settings/preferences/document-preferences.tsx b/apps/web/src/app/(teams)/t/[teamUrl]/settings/preferences/document-preferences.tsx
deleted file mode 100644
index ed7875ab0..000000000
--- a/apps/web/src/app/(teams)/t/[teamUrl]/settings/preferences/document-preferences.tsx
+++ /dev/null
@@ -1,312 +0,0 @@
-'use client';
-
-import { zodResolver } from '@hookform/resolvers/zod';
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { useSession } from 'next-auth/react';
-import { useForm } from 'react-hook-form';
-import { z } from 'zod';
-
-import {
- SUPPORTED_LANGUAGES,
- SUPPORTED_LANGUAGE_CODES,
- isValidLanguageCode,
-} from '@documenso/lib/constants/i18n';
-import type { Team, TeamGlobalSettings } from '@documenso/prisma/client';
-import { DocumentVisibility } from '@documenso/prisma/client';
-import { trpc } from '@documenso/trpc/react';
-import { Alert } from '@documenso/ui/primitives/alert';
-import { Button } from '@documenso/ui/primitives/button';
-import {
- Form,
- FormControl,
- FormDescription,
- FormField,
- FormItem,
- FormLabel,
-} from '@documenso/ui/primitives/form/form';
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from '@documenso/ui/primitives/select';
-import { Switch } from '@documenso/ui/primitives/switch';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-const ZTeamDocumentPreferencesFormSchema = z.object({
- documentVisibility: z.nativeEnum(DocumentVisibility),
- documentLanguage: z.enum(SUPPORTED_LANGUAGE_CODES),
- includeSenderDetails: z.boolean(),
- typedSignatureEnabled: z.boolean(),
- includeSigningCertificate: z.boolean(),
-});
-
-type TTeamDocumentPreferencesFormSchema = z.infer;
-
-export type TeamDocumentPreferencesFormProps = {
- team: Team;
- settings?: TeamGlobalSettings | null;
-};
-
-export const TeamDocumentPreferencesForm = ({
- team,
- settings,
-}: TeamDocumentPreferencesFormProps) => {
- const { _ } = useLingui();
- const { toast } = useToast();
- const { data } = useSession();
-
- const placeholderEmail = data?.user.email ?? 'user@example.com';
-
- const { mutateAsync: updateTeamDocumentPreferences } =
- trpc.team.updateTeamDocumentSettings.useMutation();
-
- const form = useForm({
- defaultValues: {
- documentVisibility: settings?.documentVisibility ?? 'EVERYONE',
- documentLanguage: isValidLanguageCode(settings?.documentLanguage)
- ? settings?.documentLanguage
- : 'en',
- includeSenderDetails: settings?.includeSenderDetails ?? false,
- typedSignatureEnabled: settings?.typedSignatureEnabled ?? true,
- includeSigningCertificate: settings?.includeSigningCertificate ?? true,
- },
- resolver: zodResolver(ZTeamDocumentPreferencesFormSchema),
- });
-
- const includeSenderDetails = form.watch('includeSenderDetails');
-
- const onSubmit = async (data: TTeamDocumentPreferencesFormSchema) => {
- try {
- const {
- documentVisibility,
- documentLanguage,
- includeSenderDetails,
- includeSigningCertificate,
- typedSignatureEnabled,
- } = data;
-
- await updateTeamDocumentPreferences({
- teamId: team.id,
- settings: {
- documentVisibility,
- documentLanguage,
- includeSenderDetails,
- typedSignatureEnabled,
- includeSigningCertificate,
- },
- });
-
- toast({
- title: _(msg`Document preferences updated`),
- description: _(msg`Your document preferences have been updated`),
- });
- } catch (err) {
- toast({
- title: _(msg`Something went wrong!`),
- description: _(
- msg`We were unable to update your document preferences at this time, please try again later`,
- ),
- });
- }
- };
-
- return (
-
-
-
- (
-
-
- Default Document Visibility
-
-
-
-
-
-
-
-
-
-
- Everyone can access and view the document
-
-
- Only managers and above can access and view the document
-
-
- Only admins can access and view the document
-
-
-
-
-
-
- Controls the default visibility of an uploaded document.
-
-
- )}
- />
-
- (
-
-
- Default Document Language
-
-
-
-
-
-
-
-
-
- {Object.entries(SUPPORTED_LANGUAGES).map(([code, language]) => (
-
- {language.full}
-
- ))}
-
-
-
-
-
-
- Controls the default language of an uploaded document. This will be used as the
- language in email communications with the recipients.
-
-
-
- )}
- />
-
- (
-
-
- Send on Behalf of Team
-
-
-
-
-
-
-
-
-
-
- Preview
-
-
-
- {includeSenderDetails ? (
-
- "{placeholderEmail}" on behalf of "{team.name}" has invited you to sign
- "example document".
-
- ) : (
- "{team.name}" has invited you to sign "example document".
- )}
-
-
-
-
-
- Controls the formatting of the message that will be sent when inviting a
- recipient to sign a document. If a custom message has been provided while
- configuring the document, it will be used instead.
-
-
-
- )}
- />
-
- (
-
-
- Enable Typed Signature
-
-
-
-
-
-
-
-
-
-
- Controls whether the recipients can sign the documents using a typed signature.
- Enable or disable the typed signature globally.
-
-
-
- )}
- />
-
- (
-
-
- Include the Signing Certificate in the Document
-
-
-
-
-
-
-
-
-
-
- Controls whether the signing certificate will be included in the document when
- it is downloaded. The signing certificate can still be downloaded from the logs
- page separately.
-
-
-
- )}
- />
-
-
-
- Save
-
-
-
-
-
- );
-};
diff --git a/apps/web/src/app/(teams)/t/[teamUrl]/settings/preferences/page.tsx b/apps/web/src/app/(teams)/t/[teamUrl]/settings/preferences/page.tsx
deleted file mode 100644
index 51d65301a..000000000
--- a/apps/web/src/app/(teams)/t/[teamUrl]/settings/preferences/page.tsx
+++ /dev/null
@@ -1,52 +0,0 @@
-import { msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
-import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
-
-import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
-
-import { TeamBrandingPreferencesForm } from './branding-preferences';
-import { TeamDocumentPreferencesForm } from './document-preferences';
-
-export type TeamsSettingsPageProps = {
- params: {
- teamUrl: string;
- };
-};
-
-export default async function TeamsSettingsPage({ params }: TeamsSettingsPageProps) {
- await setupI18nSSR();
-
- const { _ } = useLingui();
-
- const { teamUrl } = params;
-
- const session = await getRequiredServerComponentSession();
-
- const team = await getTeamByUrl({ userId: session.user.id, teamUrl });
-
- return (
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/apps/web/src/app/(teams)/t/[teamUrl]/settings/public-profile/page.tsx b/apps/web/src/app/(teams)/t/[teamUrl]/settings/public-profile/page.tsx
deleted file mode 100644
index d184f2845..000000000
--- a/apps/web/src/app/(teams)/t/[teamUrl]/settings/public-profile/page.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
-import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
-import { getTeamPublicProfile } from '@documenso/lib/server-only/team/get-team-public-profile';
-
-import { PublicProfilePageView } from '~/app/(dashboard)/settings/public-profile/public-profile-page-view';
-
-export type TeamsSettingsPublicProfilePageProps = {
- params: {
- teamUrl: string;
- };
-};
-
-export default async function TeamsSettingsPublicProfilePage({
- params,
-}: TeamsSettingsPublicProfilePageProps) {
- await setupI18nSSR();
-
- const { teamUrl } = params;
-
- const { user } = await getRequiredServerComponentSession();
-
- const team = await getTeamByUrl({ userId: user.id, teamUrl });
-
- const { profile } = await getTeamPublicProfile({
- userId: user.id,
- teamId: team.id,
- });
-
- return ;
-}
diff --git a/apps/web/src/app/(teams)/t/[teamUrl]/settings/team-email-dropdown.tsx b/apps/web/src/app/(teams)/t/[teamUrl]/settings/team-email-dropdown.tsx
deleted file mode 100644
index c1ae53a12..000000000
--- a/apps/web/src/app/(teams)/t/[teamUrl]/settings/team-email-dropdown.tsx
+++ /dev/null
@@ -1,96 +0,0 @@
-'use client';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { Edit, Loader, Mail, MoreHorizontal, X } from 'lucide-react';
-
-import type { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
-import { trpc } from '@documenso/trpc/react';
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuTrigger,
-} from '@documenso/ui/primitives/dropdown-menu';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-import { RemoveTeamEmailDialog } from '~/components/(teams)/dialogs/remove-team-email-dialog';
-import { UpdateTeamEmailDialog } from '~/components/(teams)/dialogs/update-team-email-dialog';
-
-export type TeamsSettingsPageProps = {
- team: Awaited>;
-};
-
-export const TeamEmailDropdown = ({ team }: TeamsSettingsPageProps) => {
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const { mutateAsync: resendEmailVerification, isPending: isResendingEmailVerification } =
- trpc.team.resendTeamEmailVerification.useMutation({
- onSuccess: () => {
- toast({
- title: _(msg`Success`),
- description: _(msg`Email verification has been resent`),
- duration: 5000,
- });
- },
- onError: () => {
- toast({
- title: _(msg`Something went wrong`),
- description: _(msg`Unable to resend verification at this time. Please try again.`),
- variant: 'destructive',
- duration: 10000,
- });
- },
- });
-
- return (
-
-
-
-
-
-
- {!team.teamEmail && team.emailVerification && (
- {
- e.preventDefault();
- void resendEmailVerification({ teamId: team.id });
- }}
- >
- {isResendingEmailVerification ? (
-
- ) : (
-
- )}
- Resend verification
-
- )}
-
- {team.teamEmail && (
- e.preventDefault()}>
-
- Edit
-
- }
- />
- )}
-
- e.preventDefault()}>
-
- Remove
-
- }
- />
-
-
- );
-};
diff --git a/apps/web/src/app/(teams)/t/[teamUrl]/settings/team-transfer-status.tsx b/apps/web/src/app/(teams)/t/[teamUrl]/settings/team-transfer-status.tsx
deleted file mode 100644
index ccfea8983..000000000
--- a/apps/web/src/app/(teams)/t/[teamUrl]/settings/team-transfer-status.tsx
+++ /dev/null
@@ -1,129 +0,0 @@
-'use client';
-
-import { useRouter } from 'next/navigation';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { AnimatePresence } from 'framer-motion';
-
-import { canExecuteTeamAction } from '@documenso/lib/utils/teams';
-import { isTokenExpired } from '@documenso/lib/utils/token-verification';
-import type { TeamMemberRole, TeamTransferVerification } from '@documenso/prisma/client';
-import { trpc } from '@documenso/trpc/react';
-import { AnimateGenericFadeInOut } from '@documenso/ui/components/animate/animate-generic-fade-in-out';
-import { cn } from '@documenso/ui/lib/utils';
-import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
-import { Button } from '@documenso/ui/primitives/button';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-export type TeamTransferStatusProps = {
- className?: string;
- currentUserTeamRole: TeamMemberRole;
- teamId: number;
- transferVerification: Pick | null;
-};
-
-export const TeamTransferStatus = ({
- className,
- currentUserTeamRole,
- teamId,
- transferVerification,
-}: TeamTransferStatusProps) => {
- const router = useRouter();
-
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const isExpired = transferVerification && isTokenExpired(transferVerification.expiresAt);
-
- const { mutateAsync: deleteTeamTransferRequest, isPending } =
- trpc.team.deleteTeamTransferRequest.useMutation({
- onSuccess: () => {
- if (!isExpired) {
- toast({
- title: _(msg`Success`),
- description: _(msg`The team transfer invitation has been successfully deleted.`),
- duration: 5000,
- });
- }
-
- router.refresh();
- },
- onError: () => {
- toast({
- title: _(msg`An unknown error occurred`),
- description: _(
- msg`We encountered an unknown error while attempting to remove this transfer. Please try again or contact support.`,
- ),
- variant: 'destructive',
- });
- },
- });
-
- return (
-
- {transferVerification && (
-
-
-
-
- {isExpired ? (
- Team transfer request expired
- ) : (
- Team transfer in progress
- )}
-
-
-
- {isExpired ? (
-
-
- The team transfer request to {transferVerification.name} has
- expired.
-
-
- ) : (
-
-
-
- A request to transfer the ownership of this team has been sent to{' '}
-
- {transferVerification.name} ({transferVerification.email})
-
-
-
-
-
-
- If they accept this request, the team will be transferred to their account.
-
-
-
- )}
-
-
-
- {canExecuteTeamAction('DELETE_TEAM_TRANSFER_REQUEST', currentUserTeamRole) && (
- deleteTeamTransferRequest({ teamId })}
- loading={isPending}
- variant={isExpired ? 'destructive' : 'ghost'}
- className={cn('ml-auto', {
- 'hover:bg-transparent hover:text-blue-800': !isExpired,
- })}
- >
- {isExpired ? Close : Cancel }
-
- )}
-
-
- )}
-
- );
-};
diff --git a/apps/web/src/app/(teams)/t/[teamUrl]/settings/tokens/page.tsx b/apps/web/src/app/(teams)/t/[teamUrl]/settings/tokens/page.tsx
deleted file mode 100644
index d3dd91b2c..000000000
--- a/apps/web/src/app/(teams)/t/[teamUrl]/settings/tokens/page.tsx
+++ /dev/null
@@ -1,127 +0,0 @@
-import { Trans } from '@lingui/macro';
-import { DateTime } from 'luxon';
-import { match } from 'ts-pattern';
-
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
-import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
-import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
-import type { GetTeamTokensResponse } from '@documenso/lib/server-only/public-api/get-all-team-tokens';
-import { getTeamTokens } from '@documenso/lib/server-only/public-api/get-all-team-tokens';
-import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
-import { Button } from '@documenso/ui/primitives/button';
-
-import DeleteTokenDialog from '~/components/(dashboard)/settings/token/delete-token-dialog';
-import { ApiTokenForm } from '~/components/forms/token';
-
-type ApiTokensPageProps = {
- params: {
- teamUrl: string;
- };
-};
-
-export default async function ApiTokensPage({ params }: ApiTokensPageProps) {
- const { i18n } = await setupI18nSSR();
-
- const { teamUrl } = params;
-
- const { user } = await getRequiredServerComponentSession();
-
- const team = await getTeamByUrl({ userId: user.id, teamUrl });
-
- let tokens: GetTeamTokensResponse | undefined = undefined;
-
- try {
- tokens = await getTeamTokens({ userId: user.id, teamId: team.id });
- } catch (err) {
- const error = AppError.parseError(err);
-
- return (
-
-
- API Tokens
-
-
- {match(error.code)
- .with(AppErrorCode.UNAUTHORIZED, () => error.message)
- .otherwise(() => 'Something went wrong.')}
-
-
- );
- }
-
- return (
-
-
- API Tokens
-
-
-
-
- On this page, you can create new API tokens and manage the existing ones.
- You can view our swagger docs{' '}
-
- here
-
-
-
-
-
-
-
-
-
-
-
- Your existing tokens
-
-
- {tokens.length === 0 && (
-
-
- Your tokens will be shown here once you create them.
-
-
- )}
-
- {tokens.length > 0 && (
-
- {tokens.map((token) => (
-
-
-
-
{token.name}
-
-
- Created on {i18n.date(token.createdAt, DateTime.DATETIME_FULL)}
-
- {token.expires ? (
-
- Expires on {i18n.date(token.expires, DateTime.DATETIME_FULL)}
-
- ) : (
-
- Token doesn't have an expiration date
-
- )}
-
-
-
-
-
- Delete
-
-
-
-
-
- ))}
-
- )}
-
- );
-}
diff --git a/apps/web/src/app/(teams)/t/[teamUrl]/settings/webhooks/[id]/page.tsx b/apps/web/src/app/(teams)/t/[teamUrl]/settings/webhooks/[id]/page.tsx
deleted file mode 100644
index 29c694c2b..000000000
--- a/apps/web/src/app/(teams)/t/[teamUrl]/settings/webhooks/[id]/page.tsx
+++ /dev/null
@@ -1,218 +0,0 @@
-'use client';
-
-import { useRouter } from 'next/navigation';
-
-import { zodResolver } from '@hookform/resolvers/zod';
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { Loader } from 'lucide-react';
-import { useForm } from 'react-hook-form';
-import type { z } from 'zod';
-
-import { trpc } from '@documenso/trpc/react';
-import { ZEditWebhookMutationSchema } from '@documenso/trpc/server/webhook-router/schema';
-import { Button } from '@documenso/ui/primitives/button';
-import {
- Form,
- FormControl,
- FormDescription,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from '@documenso/ui/primitives/form/form';
-import { Input } from '@documenso/ui/primitives/input';
-import { PasswordInput } from '@documenso/ui/primitives/password-input';
-import { Switch } from '@documenso/ui/primitives/switch';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
-import { TriggerMultiSelectCombobox } from '~/components/(dashboard)/settings/webhooks/trigger-multiselect-combobox';
-import { useCurrentTeam } from '~/providers/team';
-
-const ZEditWebhookFormSchema = ZEditWebhookMutationSchema.omit({ id: true });
-
-type TEditWebhookFormSchema = z.infer;
-
-export type WebhookPageOptions = {
- params: {
- id: string;
- };
-};
-
-export default function WebhookPage({ params }: WebhookPageOptions) {
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const router = useRouter();
-
- const team = useCurrentTeam();
-
- const { data: webhook, isLoading } = trpc.webhook.getWebhookById.useQuery(
- {
- id: params.id,
- teamId: team.id,
- },
- { enabled: !!params.id && !!team.id },
- );
-
- const { mutateAsync: updateWebhook } = trpc.webhook.editWebhook.useMutation();
-
- const form = useForm({
- resolver: zodResolver(ZEditWebhookFormSchema),
- values: {
- webhookUrl: webhook?.webhookUrl ?? '',
- eventTriggers: webhook?.eventTriggers ?? [],
- secret: webhook?.secret ?? '',
- enabled: webhook?.enabled ?? true,
- },
- });
-
- const onSubmit = async (data: TEditWebhookFormSchema) => {
- try {
- await updateWebhook({
- id: params.id,
- teamId: team.id,
- ...data,
- });
-
- toast({
- title: _(msg`Webhook updated`),
- description: _(msg`The webhook has been updated successfully.`),
- duration: 5000,
- });
-
- router.refresh();
- } catch (err) {
- toast({
- title: _(msg`Failed to update webhook`),
- description: _(
- msg`We encountered an error while updating the webhook. Please try again later.`,
- ),
- variant: 'destructive',
- });
- }
- };
-
- return (
-
-
-
- {isLoading && (
-
-
-
- )}
-
-
-
-
-
-
- (
-
-
- Triggers
-
-
- {
- onChange(values);
- }}
- />
-
-
-
- The events that will trigger a webhook to be sent to your URL.
-
-
-
-
- )}
- />
-
- (
-
- Secret
-
-
-
-
-
-
- A secret that will be sent to your URL so you can verify that the request has
- been sent by Documenso.
-
-
-
-
- )}
- />
-
-
-
- Update webhook
-
-
-
-
-
-
- );
-}
diff --git a/apps/web/src/app/(teams)/t/[teamUrl]/settings/webhooks/page.tsx b/apps/web/src/app/(teams)/t/[teamUrl]/settings/webhooks/page.tsx
deleted file mode 100644
index a3cda07f9..000000000
--- a/apps/web/src/app/(teams)/t/[teamUrl]/settings/webhooks/page.tsx
+++ /dev/null
@@ -1,116 +0,0 @@
-'use client';
-
-import Link from 'next/link';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { Loader } from 'lucide-react';
-import { DateTime } from 'luxon';
-
-import { toFriendlyWebhookEventName } from '@documenso/lib/universal/webhook/to-friendly-webhook-event-name';
-import { trpc } from '@documenso/trpc/react';
-import { cn } from '@documenso/ui/lib/utils';
-import { Badge } from '@documenso/ui/primitives/badge';
-import { Button } from '@documenso/ui/primitives/button';
-
-import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
-import { CreateWebhookDialog } from '~/components/(dashboard)/settings/webhooks/create-webhook-dialog';
-import { DeleteWebhookDialog } from '~/components/(dashboard)/settings/webhooks/delete-webhook-dialog';
-import { useCurrentTeam } from '~/providers/team';
-
-export default function WebhookPage() {
- const { _, i18n } = useLingui();
-
- const team = useCurrentTeam();
-
- const { data: webhooks, isLoading } = trpc.webhook.getTeamWebhooks.useQuery({
- teamId: team.id,
- });
-
- return (
-
-
-
-
-
- {isLoading && (
-
-
-
- )}
-
- {webhooks && webhooks.length === 0 && (
- // TODO: Perhaps add some illustrations here to make the page more engaging
-
-
-
- You have no webhooks yet. Your webhooks will be shown here once you create them.
-
-
-
- )}
-
- {webhooks && webhooks.length > 0 && (
-
- {webhooks?.map((webhook) => (
-
-
-
-
{webhook.id}
-
-
-
- {webhook.webhookUrl}
-
-
-
- {webhook.enabled ? Enabled : Disabled }
-
-
-
-
-
- Listening to{' '}
- {webhook.eventTriggers
- .map((trigger) => toFriendlyWebhookEventName(trigger))
- .join(', ')}
-
-
-
-
- Created on {i18n.date(webhook.createdAt, DateTime.DATETIME_FULL)}
-
-
-
-
-
-
- Edit
-
-
-
-
- Delete
-
-
-
-
-
- ))}
-
- )}
-
- );
-}
diff --git a/apps/web/src/app/(teams)/t/[teamUrl]/templates/[id]/edit/page.tsx b/apps/web/src/app/(teams)/t/[teamUrl]/templates/[id]/edit/page.tsx
deleted file mode 100644
index 2ae081ba4..000000000
--- a/apps/web/src/app/(teams)/t/[teamUrl]/templates/[id]/edit/page.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
-import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
-
-import type { TemplateEditPageViewProps } from '~/app/(dashboard)/templates/[id]/edit/template-edit-page-view';
-import { TemplateEditPageView } from '~/app/(dashboard)/templates/[id]/edit/template-edit-page-view';
-
-export type TeamsTemplateEditPageProps = {
- params: TemplateEditPageViewProps['params'] & {
- teamUrl: string;
- };
-};
-
-export default async function TeamsTemplateEditPage({ params }: TeamsTemplateEditPageProps) {
- await setupI18nSSR();
-
- const { teamUrl } = params;
-
- const { user } = await getRequiredServerComponentSession();
-
- const team = await getTeamByUrl({ userId: user.id, teamUrl });
-
- return ;
-}
diff --git a/apps/web/src/app/(teams)/t/[teamUrl]/templates/[id]/page.tsx b/apps/web/src/app/(teams)/t/[teamUrl]/templates/[id]/page.tsx
deleted file mode 100644
index 2efcf7c5e..000000000
--- a/apps/web/src/app/(teams)/t/[teamUrl]/templates/[id]/page.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import React from 'react';
-
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
-import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
-
-import type { TemplatePageViewProps } from '~/app/(dashboard)/templates/[id]/template-page-view';
-import { TemplatePageView } from '~/app/(dashboard)/templates/[id]/template-page-view';
-
-type TeamTemplatePageProps = {
- params: TemplatePageViewProps['params'] & {
- teamUrl: string;
- };
-};
-
-export default async function TeamTemplatePage({ params }: TeamTemplatePageProps) {
- await setupI18nSSR();
-
- const { teamUrl } = params;
-
- const { user } = await getRequiredServerComponentSession();
- const team = await getTeamByUrl({ userId: user.id, teamUrl });
-
- return ;
-}
diff --git a/apps/web/src/app/(teams)/t/[teamUrl]/templates/page.tsx b/apps/web/src/app/(teams)/t/[teamUrl]/templates/page.tsx
deleted file mode 100644
index 784866148..000000000
--- a/apps/web/src/app/(teams)/t/[teamUrl]/templates/page.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import React from 'react';
-
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
-import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
-
-import type { TemplatesPageViewProps } from '~/app/(dashboard)/templates/templates-page-view';
-import { TemplatesPageView } from '~/app/(dashboard)/templates/templates-page-view';
-
-type TeamTemplatesPageProps = {
- searchParams?: TemplatesPageViewProps['searchParams'];
- params: {
- teamUrl: string;
- };
-};
-
-export default async function TeamTemplatesPage({
- searchParams = {},
- params,
-}: TeamTemplatesPageProps) {
- await setupI18nSSR();
-
- const { teamUrl } = params;
-
- const { user } = await getRequiredServerComponentSession();
- const team = await getTeamByUrl({ userId: user.id, teamUrl });
-
- return ;
-}
diff --git a/apps/web/src/app/(unauthenticated)/articles/signature-disclosure/page.tsx b/apps/web/src/app/(unauthenticated)/articles/signature-disclosure/page.tsx
deleted file mode 100644
index 160215df8..000000000
--- a/apps/web/src/app/(unauthenticated)/articles/signature-disclosure/page.tsx
+++ /dev/null
@@ -1,170 +0,0 @@
-import Link from 'next/link';
-
-import { Trans } from '@lingui/macro';
-
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-import { Button } from '@documenso/ui/primitives/button';
-
-const SUPPORT_EMAIL = 'support@documenso.com';
-
-export default async function SignatureDisclosure() {
- await setupI18nSSR();
-
- return (
-
-
-
- Electronic Signature Disclosure
-
-
-
- Welcome
-
-
-
- Thank you for using Documenso to perform your electronic document signing. The purpose
- of this disclosure is to inform you about the process, legality, and your rights
- regarding the use of electronic signatures on our platform. By opting to use an
- electronic signature, you are agreeing to the terms and conditions outlined below.
-
-
-
-
- Acceptance and Consent
-
-
-
- When you use our platform to affix your electronic signature to documents, you are
- consenting to do so under the Electronic Signatures in Global and National Commerce Act
- (E-Sign Act) and other applicable laws. This action indicates your agreement to use
- electronic means to sign documents and receive notifications.
-
-
-
-
- Legality of Electronic Signatures
-
-
-
- An electronic signature provided by you on our platform, achieved through clicking
- through to a document and entering your name, or any other electronic signing method we
- provide, is legally binding. It carries the same weight and enforceability as a manual
- signature written with ink on paper.
-
-
-
-
- System Requirements
-
-
- To use our electronic signature service, you must have access to:
-
-
-
- A stable internet connection
-
-
- An email account
-
-
- A device capable of accessing, opening, and reading documents
-
-
- A means to print or download documents for your records
-
-
-
-
- Electronic Delivery of Documents
-
-
-
- All documents related to the electronic signing process will be provided to you
- electronically through our platform or via email. It is your responsibility to ensure
- that your email address is current and that you can receive and open our emails.
-
-
-
-
- Consent to Electronic Transactions
-
-
-
- By using the electronic signature feature, you are consenting to conduct transactions
- and receive disclosures electronically. You acknowledge that your electronic signature
- on documents is binding and that you accept the terms outlined in the documents you are
- signing.
-
-
-
-
- Withdrawing Consent
-
-
-
- You have the right to withdraw your consent to use electronic signatures at any time
- before completing the signing process. To withdraw your consent, please contact the
- sender of the document. In failing to contact the sender you may reach out to{' '}
- {SUPPORT_EMAIL} for assistance. Be aware that
- withdrawing consent may delay or halt the completion of the related transaction or
- service.
-
-
-
-
- Updating Your Information
-
-
-
- It is crucial to keep your contact information, especially your email address, up to
- date with us. Please notify us immediately of any changes to ensure that you continue to
- receive all necessary communications.
-
-
-
-
- Retention of Documents
-
-
-
- After signing a document electronically, you will be provided the opportunity to view,
- download, and print the document for your records. It is highly recommended that you
- retain a copy of all electronically signed documents for your personal records. We will
- also retain a copy of the signed document for our records however we may not be able to
- provide you with a copy of the signed document after a certain period of time.
-
-
-
-
- Acknowledgment
-
-
-
- By proceeding to use the electronic signature service provided by Documenso, you affirm
- that you have read and understood this disclosure. You agree to all terms and conditions
- related to the use of electronic signatures and electronic transactions as outlined
- herein.
-
-
-
-
- Contact Information
-
-
-
- For any questions regarding this disclosure, electronic signatures, or any related
- process, please contact us at: {SUPPORT_EMAIL}
-
-
-
-
-
-
-
- Back to Documents
-
-
-
-
- );
-}
diff --git a/apps/web/src/app/(unauthenticated)/check-email/page.tsx b/apps/web/src/app/(unauthenticated)/check-email/page.tsx
deleted file mode 100644
index a0aa50fae..000000000
--- a/apps/web/src/app/(unauthenticated)/check-email/page.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import type { Metadata } from 'next';
-import Link from 'next/link';
-
-import { Trans } from '@lingui/macro';
-
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-import { Button } from '@documenso/ui/primitives/button';
-
-export const metadata: Metadata = {
- title: 'Forgot password',
-};
-
-export default async function ForgotPasswordPage() {
- await setupI18nSSR();
-
- return (
-
-
-
- Email sent!
-
-
-
-
- A password reset email has been sent, if you have an account you should see it in your
- inbox shortly.
-
-
-
-
-
- Return to sign in
-
-
-
-
- );
-}
diff --git a/apps/web/src/app/(unauthenticated)/forgot-password/page.tsx b/apps/web/src/app/(unauthenticated)/forgot-password/page.tsx
deleted file mode 100644
index 0bb05587a..000000000
--- a/apps/web/src/app/(unauthenticated)/forgot-password/page.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-import type { Metadata } from 'next';
-import Link from 'next/link';
-
-import { Trans } from '@lingui/macro';
-
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-
-import { ForgotPasswordForm } from '~/components/forms/forgot-password';
-
-export const metadata: Metadata = {
- title: 'Forgot Password',
-};
-
-export default async function ForgotPasswordPage() {
- await setupI18nSSR();
-
- return (
-
-
-
- Forgot your password?
-
-
-
-
- No worries, it happens! Enter your email and we'll email you a special link to reset
- your password.
-
-
-
-
-
-
-
- Remembered your password?{' '}
-
- Sign In
-
-
-
-
-
- );
-}
diff --git a/apps/web/src/app/(unauthenticated)/layout.tsx b/apps/web/src/app/(unauthenticated)/layout.tsx
deleted file mode 100644
index 2b70d2610..000000000
--- a/apps/web/src/app/(unauthenticated)/layout.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-import React from 'react';
-
-import Image from 'next/image';
-
-import backgroundPattern from '@documenso/assets/images/background-pattern.png';
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-
-type UnauthenticatedLayoutProps = {
- children: React.ReactNode;
-};
-
-export default async function UnauthenticatedLayout({ children }: UnauthenticatedLayoutProps) {
- await setupI18nSSR();
-
- return (
-
-
-
- );
-}
diff --git a/apps/web/src/app/(unauthenticated)/reset-password/[token]/page.tsx b/apps/web/src/app/(unauthenticated)/reset-password/[token]/page.tsx
deleted file mode 100644
index 1bbca3cfe..000000000
--- a/apps/web/src/app/(unauthenticated)/reset-password/[token]/page.tsx
+++ /dev/null
@@ -1,50 +0,0 @@
-import Link from 'next/link';
-import { redirect } from 'next/navigation';
-
-import { Trans } from '@lingui/macro';
-
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-import { getResetTokenValidity } from '@documenso/lib/server-only/user/get-reset-token-validity';
-
-import { ResetPasswordForm } from '~/components/forms/reset-password';
-
-type ResetPasswordPageProps = {
- params: {
- token: string;
- };
-};
-
-export default async function ResetPasswordPage({ params: { token } }: ResetPasswordPageProps) {
- await setupI18nSSR();
-
- const isValid = await getResetTokenValidity({ token });
-
- if (!isValid) {
- redirect('/reset-password');
- }
-
- return (
-
-
-
- Reset Password
-
-
-
- Please choose your new password
-
-
-
-
-
-
- Don't have an account?{' '}
-
- Sign up
-
-
-
-
-
- );
-}
diff --git a/apps/web/src/app/(unauthenticated)/reset-password/page.tsx b/apps/web/src/app/(unauthenticated)/reset-password/page.tsx
deleted file mode 100644
index fc54aceec..000000000
--- a/apps/web/src/app/(unauthenticated)/reset-password/page.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import type { Metadata } from 'next';
-import Link from 'next/link';
-
-import { Trans } from '@lingui/macro';
-
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-import { Button } from '@documenso/ui/primitives/button';
-
-export const metadata: Metadata = {
- title: 'Reset Password',
-};
-
-export default async function ResetPasswordPage() {
- await setupI18nSSR();
-
- return (
-
-
-
- Unable to reset password
-
-
-
-
- The token you have used to reset your password is either expired or it never existed. If
- you have still forgotten your password, please request a new reset link.
-
-
-
-
-
- Return to sign in
-
-
-
-
- );
-}
diff --git a/apps/web/src/app/(unauthenticated)/signin/page.tsx b/apps/web/src/app/(unauthenticated)/signin/page.tsx
deleted file mode 100644
index 9b041e22b..000000000
--- a/apps/web/src/app/(unauthenticated)/signin/page.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-import type { Metadata } from 'next';
-import Link from 'next/link';
-
-import { Trans } from '@lingui/macro';
-import { env } from 'next-runtime-env';
-
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-import {
- IS_GOOGLE_SSO_ENABLED,
- IS_OIDC_SSO_ENABLED,
- OIDC_PROVIDER_LABEL,
-} from '@documenso/lib/constants/auth';
-
-import { SignInForm } from '~/components/forms/signin';
-
-export const metadata: Metadata = {
- title: 'Sign In',
-};
-
-export default async function SignInPage() {
- await setupI18nSSR();
-
- const NEXT_PUBLIC_DISABLE_SIGNUP = env('NEXT_PUBLIC_DISABLE_SIGNUP');
-
- return (
-
-
-
- Sign in to your account
-
-
-
- Welcome back, we are lucky to have you.
-
-
-
-
-
- {NEXT_PUBLIC_DISABLE_SIGNUP !== 'true' && (
-
-
- Don't have an account?{' '}
-
- Sign up
-
-
-
- )}
-
-
- );
-}
diff --git a/apps/web/src/app/(unauthenticated)/signup/page.tsx b/apps/web/src/app/(unauthenticated)/signup/page.tsx
deleted file mode 100644
index 7686d2f70..000000000
--- a/apps/web/src/app/(unauthenticated)/signup/page.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import type { Metadata } from 'next';
-import { redirect } from 'next/navigation';
-
-import { env } from 'next-runtime-env';
-
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-import { IS_GOOGLE_SSO_ENABLED, IS_OIDC_SSO_ENABLED } from '@documenso/lib/constants/auth';
-
-import { SignUpFormV2 } from '~/components/forms/v2/signup';
-
-export const metadata: Metadata = {
- title: 'Sign Up',
-};
-
-export default async function SignUpPage() {
- await setupI18nSSR();
-
- const NEXT_PUBLIC_DISABLE_SIGNUP = env('NEXT_PUBLIC_DISABLE_SIGNUP');
-
- if (NEXT_PUBLIC_DISABLE_SIGNUP === 'true') {
- redirect('/signin');
- }
-
- return (
-
- );
-}
diff --git a/apps/web/src/app/(unauthenticated)/team/decline/[token]/page.tsx b/apps/web/src/app/(unauthenticated)/team/decline/[token]/page.tsx
deleted file mode 100644
index 06c7dadc9..000000000
--- a/apps/web/src/app/(unauthenticated)/team/decline/[token]/page.tsx
+++ /dev/null
@@ -1,142 +0,0 @@
-import Link from 'next/link';
-
-import { Trans } from '@lingui/macro';
-import { DateTime } from 'luxon';
-
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
-import { encryptSecondaryData } from '@documenso/lib/server-only/crypto/encrypt';
-import { declineTeamInvitation } from '@documenso/lib/server-only/team/decline-team-invitation';
-import { getTeamById } from '@documenso/lib/server-only/team/get-team';
-import { prisma } from '@documenso/prisma';
-import { TeamMemberInviteStatus } from '@documenso/prisma/client';
-import { Button } from '@documenso/ui/primitives/button';
-
-type DeclineInvitationPageProps = {
- params: {
- token: string;
- };
-};
-
-export default async function DeclineInvitationPage({
- params: { token },
-}: DeclineInvitationPageProps) {
- await setupI18nSSR();
-
- const session = await getServerComponentSession();
-
- const teamMemberInvite = await prisma.teamMemberInvite.findUnique({
- where: {
- token,
- },
- });
-
- if (!teamMemberInvite) {
- return (
-
-
-
- Invalid token
-
-
-
- This token is invalid or has expired. No action is needed.
-
-
-
-
- Return
-
-
-
-
- );
- }
-
- const team = await getTeamById({ teamId: teamMemberInvite.teamId });
-
- const user = await prisma.user.findFirst({
- where: {
- email: {
- equals: teamMemberInvite.email,
- mode: 'insensitive',
- },
- },
- });
-
- if (user) {
- await declineTeamInvitation({ userId: user.id, teamId: team.id });
- }
-
- if (!user && teamMemberInvite.status !== TeamMemberInviteStatus.DECLINED) {
- await prisma.teamMemberInvite.update({
- where: {
- id: teamMemberInvite.id,
- },
- data: {
- status: TeamMemberInviteStatus.DECLINED,
- },
- });
- }
-
- const email = encryptSecondaryData({
- data: teamMemberInvite.email,
- expiresAt: DateTime.now().plus({ days: 1 }).toMillis(),
- });
-
- if (!user) {
- return (
-
-
- Team invitation
-
-
-
-
- You have been invited by {team.name} to join their team.
-
-
-
-
- To decline this invitation you must create an account.
-
-
-
-
- Create account
-
-
-
- );
- }
-
- const isSessionUserTheInvitedUser = user?.id === session.user?.id;
-
- return (
-
-
- Invitation declined
-
-
-
-
- You have declined the invitation from {team.name} to join their team.
-
-
-
- {isSessionUserTheInvitedUser ? (
-
-
- Return to Dashboard
-
-
- ) : (
-
-
- Return to Home
-
-
- )}
-
- );
-}
diff --git a/apps/web/src/app/(unauthenticated)/team/invite/[token]/page.tsx b/apps/web/src/app/(unauthenticated)/team/invite/[token]/page.tsx
deleted file mode 100644
index 3441dbed7..000000000
--- a/apps/web/src/app/(unauthenticated)/team/invite/[token]/page.tsx
+++ /dev/null
@@ -1,147 +0,0 @@
-import Link from 'next/link';
-
-import { Trans } from '@lingui/macro';
-import { DateTime } from 'luxon';
-
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
-import { encryptSecondaryData } from '@documenso/lib/server-only/crypto/encrypt';
-import { acceptTeamInvitation } from '@documenso/lib/server-only/team/accept-team-invitation';
-import { getTeamById } from '@documenso/lib/server-only/team/get-team';
-import { prisma } from '@documenso/prisma';
-import { TeamMemberInviteStatus } from '@documenso/prisma/client';
-import { Button } from '@documenso/ui/primitives/button';
-
-type AcceptInvitationPageProps = {
- params: {
- token: string;
- };
-};
-
-export default async function AcceptInvitationPage({
- params: { token },
-}: AcceptInvitationPageProps) {
- await setupI18nSSR();
-
- const session = await getServerComponentSession();
-
- const teamMemberInvite = await prisma.teamMemberInvite.findUnique({
- where: {
- token,
- },
- });
-
- if (!teamMemberInvite) {
- return (
-
-
-
- Invalid token
-
-
-
-
- This token is invalid or has expired. Please contact your team for a new invitation.
-
-
-
-
-
- Return
-
-
-
-
- );
- }
-
- const team = await getTeamById({ teamId: teamMemberInvite.teamId });
-
- const user = await prisma.user.findFirst({
- where: {
- email: {
- equals: teamMemberInvite.email,
- mode: 'insensitive',
- },
- },
- });
-
- // Directly convert the team member invite to a team member if they already have an account.
- if (user) {
- await acceptTeamInvitation({ userId: user.id, teamId: team.id });
- }
-
- // For users who do not exist yet, set the team invite status to accepted, which is checked during
- // user creation to determine if we should add the user to the team at that time.
- if (!user && teamMemberInvite.status !== TeamMemberInviteStatus.ACCEPTED) {
- await prisma.teamMemberInvite.update({
- where: {
- id: teamMemberInvite.id,
- },
- data: {
- status: TeamMemberInviteStatus.ACCEPTED,
- },
- });
- }
-
- const email = encryptSecondaryData({
- data: teamMemberInvite.email,
- expiresAt: DateTime.now().plus({ days: 1 }).toMillis(),
- });
-
- if (!user) {
- return (
-
-
- Team invitation
-
-
-
-
- You have been invited by {team.name} to join their team.
-
-
-
-
- To accept this invitation you must create an account.
-
-
-
-
- Create account
-
-
-
- );
- }
-
- const isSessionUserTheInvitedUser = user.id === session.user?.id;
-
- return (
-
-
- Invitation accepted!
-
-
-
-
- You have accepted an invitation from {team.name} to join their team.
-
-
-
- {isSessionUserTheInvitedUser ? (
-
-
- Continue
-
-
- ) : (
-
-
- Continue to login
-
-
- )}
-
- );
-}
diff --git a/apps/web/src/app/(unauthenticated)/team/verify/email/[token]/page.tsx b/apps/web/src/app/(unauthenticated)/team/verify/email/[token]/page.tsx
deleted file mode 100644
index b53fb5f71..000000000
--- a/apps/web/src/app/(unauthenticated)/team/verify/email/[token]/page.tsx
+++ /dev/null
@@ -1,148 +0,0 @@
-import Link from 'next/link';
-
-import { Trans } from '@lingui/macro';
-
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-import { isTokenExpired } from '@documenso/lib/utils/token-verification';
-import { prisma } from '@documenso/prisma';
-import { Button } from '@documenso/ui/primitives/button';
-
-type VerifyTeamEmailPageProps = {
- params: {
- token: string;
- };
-};
-
-export default async function VerifyTeamEmailPage({ params: { token } }: VerifyTeamEmailPageProps) {
- await setupI18nSSR();
-
- const teamEmailVerification = await prisma.teamEmailVerification.findUnique({
- where: {
- token,
- },
- include: {
- team: true,
- },
- });
-
- if (!teamEmailVerification || isTokenExpired(teamEmailVerification.expiresAt)) {
- return (
-
-
-
- Invalid link
-
-
-
-
- This link is invalid or has expired. Please contact your team to resend a
- verification.
-
-
-
-
-
- Return
-
-
-
-
- );
- }
-
- if (teamEmailVerification.completed) {
- return (
-
-
- Team email already verified!
-
-
-
-
- You have already verified your email address for{' '}
- {teamEmailVerification.team.name} .
-
-
-
-
-
- Continue
-
-
-
- );
- }
-
- const { team } = teamEmailVerification;
-
- let isTeamEmailVerificationError = false;
-
- try {
- await prisma.$transaction([
- prisma.teamEmailVerification.updateMany({
- where: {
- teamId: team.id,
- email: teamEmailVerification.email,
- },
- data: {
- completed: true,
- },
- }),
- prisma.teamEmailVerification.deleteMany({
- where: {
- teamId: team.id,
- expiresAt: {
- lt: new Date(),
- },
- },
- }),
- prisma.teamEmail.create({
- data: {
- teamId: team.id,
- email: teamEmailVerification.email,
- name: teamEmailVerification.name,
- },
- }),
- ]);
- } catch (e) {
- console.error(e);
- isTeamEmailVerificationError = true;
- }
-
- if (isTeamEmailVerificationError) {
- return (
-
-
- Team email verification
-
-
-
-
- Something went wrong while attempting to verify your email address for{' '}
- {team.name} . Please try again later.
-
-
-
- );
- }
-
- return (
-
-
- Team email verified!
-
-
-
-
- You have verified your email address for {team.name} .
-
-
-
-
-
- Continue
-
-
-
- );
-}
diff --git a/apps/web/src/app/(unauthenticated)/team/verify/transfer/[token]/page.tsx b/apps/web/src/app/(unauthenticated)/team/verify/transfer/[token]/page.tsx
deleted file mode 100644
index 8713aeecd..000000000
--- a/apps/web/src/app/(unauthenticated)/team/verify/transfer/[token]/page.tsx
+++ /dev/null
@@ -1,127 +0,0 @@
-import Link from 'next/link';
-
-import { Trans } from '@lingui/macro';
-
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-import { transferTeamOwnership } from '@documenso/lib/server-only/team/transfer-team-ownership';
-import { isTokenExpired } from '@documenso/lib/utils/token-verification';
-import { prisma } from '@documenso/prisma';
-import { Button } from '@documenso/ui/primitives/button';
-
-type VerifyTeamTransferPage = {
- params: {
- token: string;
- };
-};
-
-export default async function VerifyTeamTransferPage({
- params: { token },
-}: VerifyTeamTransferPage) {
- await setupI18nSSR();
-
- const teamTransferVerification = await prisma.teamTransferVerification.findUnique({
- where: {
- token,
- },
- include: {
- team: true,
- },
- });
-
- if (!teamTransferVerification || isTokenExpired(teamTransferVerification.expiresAt)) {
- return (
-
-
-
- Invalid link
-
-
-
-
- This link is invalid or has expired. Please contact your team to resend a transfer
- request.
-
-
-
-
-
- Return
-
-
-
-
- );
- }
-
- if (teamTransferVerification.completed) {
- return (
-
-
- Team ownership transfer already completed!
-
-
-
-
- You have already completed the ownership transfer for{' '}
- {teamTransferVerification.team.name} .
-
-
-
-
-
- Continue
-
-
-
- );
- }
-
- const { team } = teamTransferVerification;
-
- let isTransferError = false;
-
- try {
- await transferTeamOwnership({ token });
- } catch (e) {
- console.error(e);
- isTransferError = true;
- }
-
- if (isTransferError) {
- return (
-
-
- Team ownership transfer
-
-
-
-
- Something went wrong while attempting to transfer the ownership of team{' '}
- {team.name} to your. Please try again later or contact support.
-
-
-
- );
- }
-
- return (
-
-
- Team ownership transferred!
-
-
-
-
- The ownership of team {team.name} has been successfully transferred to
- you.
-
-
-
-
-
- Continue
-
-
-
- );
-}
diff --git a/apps/web/src/app/(unauthenticated)/unverified-account/page.tsx b/apps/web/src/app/(unauthenticated)/unverified-account/page.tsx
deleted file mode 100644
index 3c3588c1e..000000000
--- a/apps/web/src/app/(unauthenticated)/unverified-account/page.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import { Trans } from '@lingui/macro';
-import { Mails } from 'lucide-react';
-
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-
-import { SendConfirmationEmailForm } from '~/components/forms/send-confirmation-email';
-
-export default async function UnverifiedAccount() {
- await setupI18nSSR();
-
- return (
-
-
-
-
-
-
-
- Confirm email
-
-
-
-
- To gain access to your account, please confirm your email address by clicking on the
- confirmation link from your inbox.
-
-
-
-
-
- If you don't find the confirmation link in your inbox, you can request a new one
- below.
-
-
-
-
-
-
-
- );
-}
diff --git a/apps/web/src/app/(unauthenticated)/verify-email/[token]/client.tsx b/apps/web/src/app/(unauthenticated)/verify-email/[token]/client.tsx
deleted file mode 100644
index d7c2a936a..000000000
--- a/apps/web/src/app/(unauthenticated)/verify-email/[token]/client.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-'use client';
-
-import { useEffect } from 'react';
-
-import Link from 'next/link';
-
-import { Trans } from '@lingui/macro';
-import { CheckCircle2 } from 'lucide-react';
-import { signIn } from 'next-auth/react';
-
-import { Button } from '@documenso/ui/primitives/button';
-
-export type VerifyEmailPageClientProps = {
- signInData?: string;
-};
-
-export const VerifyEmailPageClient = ({ signInData }: VerifyEmailPageClientProps) => {
- useEffect(() => {
- if (signInData) {
- void signIn('manual', {
- credential: signInData,
- callbackUrl: '/documents',
- });
- }
- }, [signInData]);
-
- return (
-
-
-
-
-
-
-
-
- Email Confirmed!
-
-
-
-
- Your email has been successfully confirmed! You can now use all features of Documenso.
-
-
-
- {!signInData && (
-
-
- Go back home
-
-
- )}
-
-
-
- );
-};
diff --git a/apps/web/src/app/(unauthenticated)/verify-email/[token]/page.tsx b/apps/web/src/app/(unauthenticated)/verify-email/[token]/page.tsx
deleted file mode 100644
index eb88538c4..000000000
--- a/apps/web/src/app/(unauthenticated)/verify-email/[token]/page.tsx
+++ /dev/null
@@ -1,130 +0,0 @@
-import Link from 'next/link';
-
-import { Trans } from '@lingui/macro';
-import { AlertTriangle, XCircle, XOctagon } from 'lucide-react';
-import { DateTime } from 'luxon';
-import { match } from 'ts-pattern';
-
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-import { encryptSecondaryData } from '@documenso/lib/server-only/crypto/encrypt';
-import {
- EMAIL_VERIFICATION_STATE,
- verifyEmail,
-} from '@documenso/lib/server-only/user/verify-email';
-import { prisma } from '@documenso/prisma';
-import { Button } from '@documenso/ui/primitives/button';
-
-import { VerifyEmailPageClient } from './client';
-
-export type PageProps = {
- params: {
- token: string;
- };
-};
-
-export default async function VerifyEmailPage({ params: { token } }: PageProps) {
- await setupI18nSSR();
-
- if (!token) {
- return (
-
-
-
-
-
-
-
- No token provided
-
-
-
- It seems that there is no token provided. Please check your email and try again.
-
-
-
-
- );
- }
-
- const verified = await verifyEmail({ token });
-
- return await match(verified)
- .with(EMAIL_VERIFICATION_STATE.NOT_FOUND, () => (
-
-
-
-
-
-
- Something went wrong
-
-
-
-
- We were unable to verify your email. If your email is not verified already, please
- try again.
-
-
-
-
-
- Go back home
-
-
-
-
-
- ))
- .with(EMAIL_VERIFICATION_STATE.EXPIRED, () => (
-
-
-
-
-
-
-
-
- Your token has expired!
-
-
-
-
- It seems that the provided token has expired. We've just sent you another token,
- please check your email and try again.
-
-
-
-
-
- Go back home
-
-
-
-
-
- ))
- .with(EMAIL_VERIFICATION_STATE.VERIFIED, async () => {
- const { user } = await prisma.verificationToken.findFirstOrThrow({
- where: {
- token,
- },
- include: {
- user: true,
- },
- });
-
- const data = encryptSecondaryData({
- data: JSON.stringify({
- userId: user.id,
- email: user.email,
- }),
- expiresAt: DateTime.now().plus({ minutes: 5 }).toMillis(),
- });
-
- return ;
- })
- .with(EMAIL_VERIFICATION_STATE.ALREADY_VERIFIED, () => )
- .exhaustive();
-}
diff --git a/apps/web/src/app/(unauthenticated)/verify-email/page.tsx b/apps/web/src/app/(unauthenticated)/verify-email/page.tsx
deleted file mode 100644
index cd518a913..000000000
--- a/apps/web/src/app/(unauthenticated)/verify-email/page.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-import type { Metadata } from 'next';
-import Link from 'next/link';
-
-import { Trans } from '@lingui/macro';
-import { XCircle } from 'lucide-react';
-
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-import { Button } from '@documenso/ui/primitives/button';
-
-export const metadata: Metadata = {
- title: 'Verify Email',
-};
-
-export default async function EmailVerificationWithoutTokenPage() {
- await setupI18nSSR();
-
- return (
-
-
-
-
-
-
-
-
- Uh oh! Looks like you're missing a token
-
-
-
-
- It seems that there is no token provided, if you are trying to verify your email
- please follow the link in your email.
-
-
-
-
-
- Go back home
-
-
-
-
-
- );
-}
diff --git a/apps/web/src/app/api/v1/openapi/page.tsx b/apps/web/src/app/api/v1/openapi/page.tsx
deleted file mode 100644
index 24e14c958..000000000
--- a/apps/web/src/app/api/v1/openapi/page.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-'use client';
-
-import dynamic from 'next/dynamic';
-
-const Docs = dynamic(async () => import('@documenso/api/v1/api-documentation'), {
- ssr: false,
-});
-
-export default function OpenApiDocsPage() {
- return ;
-}
diff --git a/apps/web/src/app/embed/authenticate.tsx b/apps/web/src/app/embed/authenticate.tsx
deleted file mode 100644
index b7260aa5e..000000000
--- a/apps/web/src/app/embed/authenticate.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import { Trans } from '@lingui/macro';
-
-import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
-
-import { Logo } from '~/components/branding/logo';
-import { SignInForm } from '~/components/forms/signin';
-
-export type EmbedAuthenticateViewProps = {
- email?: string;
- returnTo: string;
-};
-
-export const EmbedAuthenticateView = ({ email, returnTo }: EmbedAuthenticateViewProps) => {
- return (
-
-
-
-
-
-
-
- To view this document you need to be signed into your account, please sign in to
- continue.
-
-
-
-
-
-
-
- );
-};
diff --git a/apps/web/src/app/embed/base-schema.ts b/apps/web/src/app/embed/base-schema.ts
deleted file mode 100644
index 003553301..000000000
--- a/apps/web/src/app/embed/base-schema.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { z } from 'zod';
-
-import { ZCssVarsSchema } from './css-vars';
-
-export const ZBaseEmbedDataSchema = z.object({
- darkModeDisabled: z.boolean().optional().default(false),
- css: z
- .string()
- .optional()
- .transform((value) => value || undefined),
- cssVars: ZCssVarsSchema.optional().default({}),
-});
diff --git a/apps/web/src/app/embed/client-loading.tsx b/apps/web/src/app/embed/client-loading.tsx
deleted file mode 100644
index d67af37a2..000000000
--- a/apps/web/src/app/embed/client-loading.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-export const EmbedClientLoading = () => {
- return (
-
- Loading...
-
- );
-};
diff --git a/apps/web/src/app/embed/completed.tsx b/apps/web/src/app/embed/completed.tsx
deleted file mode 100644
index 1cfc07d3b..000000000
--- a/apps/web/src/app/embed/completed.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import { Trans } from '@lingui/macro';
-
-import signingCelebration from '@documenso/assets/images/signing-celebration.png';
-import type { Signature } from '@documenso/prisma/client';
-import { SigningCard3D } from '@documenso/ui/components/signing-card';
-
-export type EmbedDocumentCompletedPageProps = {
- name?: string;
- signature?: Signature;
-};
-
-export const EmbedDocumentCompleted = ({ name, signature }: EmbedDocumentCompletedPageProps) => {
- console.log({ signature });
- return (
-
-
- Document Completed!
-
-
-
-
-
-
-
-
- The document is now completed, please follow any instructions provided within the parent
- application.
-
-
-
- );
-};
diff --git a/apps/web/src/app/embed/css-vars.ts b/apps/web/src/app/embed/css-vars.ts
deleted file mode 100644
index 65bb3174b..000000000
--- a/apps/web/src/app/embed/css-vars.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-import { colord } from 'colord';
-import { toKebabCase } from 'remeda';
-import { z } from 'zod';
-
-export const ZCssVarsSchema = z
- .object({
- background: z.string().optional().describe('Base background color'),
- foreground: z.string().optional().describe('Base text color'),
- muted: z.string().optional().describe('Muted/subtle background color'),
- mutedForeground: z.string().optional().describe('Muted/subtle text color'),
- popover: z.string().optional().describe('Popover/dropdown background color'),
- popoverForeground: z.string().optional().describe('Popover/dropdown text color'),
- card: z.string().optional().describe('Card background color'),
- cardBorder: z.string().optional().describe('Card border color'),
- cardBorderTint: z.string().optional().describe('Card border tint/highlight color'),
- cardForeground: z.string().optional().describe('Card text color'),
- fieldCard: z.string().optional().describe('Field card background color'),
- fieldCardBorder: z.string().optional().describe('Field card border color'),
- fieldCardForeground: z.string().optional().describe('Field card text color'),
- widget: z.string().optional().describe('Widget background color'),
- widgetForeground: z.string().optional().describe('Widget text color'),
- border: z.string().optional().describe('Default border color'),
- input: z.string().optional().describe('Input field border color'),
- primary: z.string().optional().describe('Primary action/button color'),
- primaryForeground: z.string().optional().describe('Primary action/button text color'),
- secondary: z.string().optional().describe('Secondary action/button color'),
- secondaryForeground: z.string().optional().describe('Secondary action/button text color'),
- accent: z.string().optional().describe('Accent/highlight color'),
- accentForeground: z.string().optional().describe('Accent/highlight text color'),
- destructive: z.string().optional().describe('Destructive/danger action color'),
- destructiveForeground: z.string().optional().describe('Destructive/danger text color'),
- ring: z.string().optional().describe('Focus ring color'),
- radius: z.string().optional().describe('Border radius size in REM units'),
- warning: z.string().optional().describe('Warning/alert color'),
- })
- .describe('Custom CSS variables for theming');
-
-export type TCssVarsSchema = z.infer;
-
-export const toNativeCssVars = (vars: TCssVarsSchema) => {
- const cssVars: Record = {};
-
- const { radius, ...colorVars } = vars;
-
- for (const [key, value] of Object.entries(colorVars)) {
- if (value) {
- const color = colord(value);
- const { h, s, l } = color.toHsl();
-
- cssVars[`--${toKebabCase(key)}`] = `${h} ${s} ${l}`;
- }
- }
-
- if (radius) {
- cssVars[`--radius`] = `${radius}`;
- }
-
- return cssVars;
-};
diff --git a/apps/web/src/app/embed/direct/[[...url]]/client.tsx b/apps/web/src/app/embed/direct/[[...url]]/client.tsx
deleted file mode 100644
index 266672209..000000000
--- a/apps/web/src/app/embed/direct/[[...url]]/client.tsx
+++ /dev/null
@@ -1,504 +0,0 @@
-'use client';
-
-import { useEffect, useLayoutEffect, useState } from 'react';
-
-import { useSearchParams } from 'next/navigation';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { LucideChevronDown, LucideChevronUp } from 'lucide-react';
-import { DateTime } from 'luxon';
-
-import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn';
-import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
-import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
-import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones';
-import { validateFieldsInserted } from '@documenso/lib/utils/fields';
-import type { DocumentMeta, Recipient, Signature, TemplateMeta } from '@documenso/prisma/client';
-import { type DocumentData, type Field, FieldType } from '@documenso/prisma/client';
-import { trpc } from '@documenso/trpc/react';
-import type {
- TRemovedSignedFieldWithTokenMutationSchema,
- TSignFieldWithTokenMutationSchema,
-} from '@documenso/trpc/server/field-router/schema';
-import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
-import { Button } from '@documenso/ui/primitives/button';
-import { Card, CardContent } from '@documenso/ui/primitives/card';
-import { ElementVisible } from '@documenso/ui/primitives/element-visible';
-import { Input } from '@documenso/ui/primitives/input';
-import { Label } from '@documenso/ui/primitives/label';
-import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
-import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-import type { DirectTemplateLocalField } from '~/app/(recipient)/d/[token]/sign-direct-template';
-import { useRequiredSigningContext } from '~/app/(signing)/sign/[token]/provider';
-import { Logo } from '~/components/branding/logo';
-
-import { EmbedClientLoading } from '../../client-loading';
-import { EmbedDocumentCompleted } from '../../completed';
-import { EmbedDocumentFields } from '../../document-fields';
-import { injectCss } from '../../util';
-import { ZDirectTemplateEmbedDataSchema } from './schema';
-
-export type EmbedDirectTemplateClientPageProps = {
- token: string;
- updatedAt: Date;
- documentData: DocumentData;
- recipient: Recipient;
- fields: Field[];
- metadata?: DocumentMeta | TemplateMeta | null;
- hidePoweredBy?: boolean;
- isPlatformOrEnterprise?: boolean;
-};
-
-export const EmbedDirectTemplateClientPage = ({
- token,
- updatedAt,
- documentData,
- recipient,
- fields,
- metadata,
- hidePoweredBy = false,
- isPlatformOrEnterprise = false,
-}: EmbedDirectTemplateClientPageProps) => {
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const searchParams = useSearchParams();
-
- const {
- fullName,
- email,
- signature,
- signatureValid,
- setFullName,
- setEmail,
- setSignature,
- setSignatureValid,
- } = useRequiredSigningContext();
-
- const [hasFinishedInit, setHasFinishedInit] = useState(false);
- const [hasDocumentLoaded, setHasDocumentLoaded] = useState(false);
- const [hasCompletedDocument, setHasCompletedDocument] = useState(false);
-
- const [isExpanded, setIsExpanded] = useState(false);
-
- const [isEmailLocked, setIsEmailLocked] = useState(false);
- const [isNameLocked, setIsNameLocked] = useState(false);
-
- const [showPendingFieldTooltip, setShowPendingFieldTooltip] = useState(false);
-
- const [throttledOnCompleteClick, isThrottled] = useThrottleFn(() => void onCompleteClick(), 500);
-
- const [localFields, setLocalFields] = useState(() => fields);
-
- const [pendingFields, _completedFields] = [
- localFields.filter((field) => !field.inserted),
- localFields.filter((field) => field.inserted),
- ];
-
- const hasSignatureField = localFields.some((field) => field.type === FieldType.SIGNATURE);
-
- const { mutateAsync: createDocumentFromDirectTemplate, isPending: isSubmitting } =
- trpc.template.createDocumentFromDirectTemplate.useMutation();
-
- const onSignField = (payload: TSignFieldWithTokenMutationSchema) => {
- setLocalFields((fields) =>
- fields.map((field) => {
- if (field.id !== payload.fieldId) {
- return field;
- }
-
- const newField: DirectTemplateLocalField = structuredClone({
- ...field,
- customText: payload.value,
- inserted: true,
- signedValue: payload,
- });
-
- if (field.type === FieldType.SIGNATURE) {
- newField.signature = {
- id: 1,
- created: new Date(),
- recipientId: 1,
- fieldId: 1,
- signatureImageAsBase64: payload.value.startsWith('data:') ? payload.value : null,
- typedSignature: payload.value.startsWith('data:') ? null : payload.value,
- } satisfies Signature;
- }
-
- if (field.type === FieldType.DATE) {
- newField.customText = DateTime.now()
- .setZone(metadata?.timezone ?? DEFAULT_DOCUMENT_TIME_ZONE)
- .toFormat(metadata?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT);
- }
-
- return newField;
- }),
- );
-
- if (window.parent) {
- window.parent.postMessage(
- {
- action: 'field-signed',
- data: null,
- },
- '*',
- );
- }
-
- setShowPendingFieldTooltip(false);
- };
-
- const onUnsignField = (payload: TRemovedSignedFieldWithTokenMutationSchema) => {
- setLocalFields((fields) =>
- fields.map((field) => {
- if (field.id !== payload.fieldId) {
- return field;
- }
-
- return structuredClone({
- ...field,
- customText: '',
- inserted: false,
- signedValue: undefined,
- signature: undefined,
- });
- }),
- );
-
- if (window.parent) {
- window.parent.postMessage(
- {
- action: 'field-unsigned',
- data: null,
- },
- '*',
- );
- }
-
- setShowPendingFieldTooltip(false);
- };
-
- const onNextFieldClick = () => {
- validateFieldsInserted(localFields);
-
- setShowPendingFieldTooltip(true);
- setIsExpanded(false);
- };
-
- const onCompleteClick = async () => {
- try {
- if (hasSignatureField && !signatureValid) {
- return;
- }
-
- const valid = validateFieldsInserted(localFields);
-
- if (!valid) {
- setShowPendingFieldTooltip(true);
- return;
- }
-
- let directTemplateExternalId = searchParams?.get('externalId') || undefined;
-
- if (directTemplateExternalId) {
- directTemplateExternalId = decodeURIComponent(directTemplateExternalId);
- }
-
- localFields.forEach((field) => {
- if (!field.signedValue) {
- throw new Error('Invalid configuration');
- }
- });
-
- const {
- documentId,
- token: documentToken,
- recipientId,
- } = await createDocumentFromDirectTemplate({
- directTemplateToken: token,
- directTemplateExternalId,
- directRecipientName: fullName,
- directRecipientEmail: email,
- templateUpdatedAt: updatedAt,
- signedFieldValues: localFields.map((field) => {
- if (!field.signedValue) {
- throw new Error('Invalid configuration');
- }
-
- return field.signedValue;
- }),
- });
-
- if (window.parent) {
- window.parent.postMessage(
- {
- action: 'document-completed',
- data: {
- token: documentToken,
- documentId,
- recipientId,
- },
- },
- '*',
- );
- }
-
- setHasCompletedDocument(true);
- } catch (err) {
- if (window.parent) {
- window.parent.postMessage(
- {
- action: 'document-error',
- data: String(err),
- },
- '*',
- );
- }
-
- toast({
- title: _(msg`Something went wrong`),
- description: _(
- msg`We were unable to submit this document at this time. Please try again later.`,
- ),
- variant: 'destructive',
- });
- }
- };
-
- useLayoutEffect(() => {
- const hash = window.location.hash.slice(1);
-
- try {
- const data = ZDirectTemplateEmbedDataSchema.parse(JSON.parse(decodeURIComponent(atob(hash))));
-
- if (data.email) {
- setEmail(data.email);
- setIsEmailLocked(!!data.lockEmail);
- }
-
- if (data.name) {
- setFullName(data.name);
- setIsNameLocked(!!data.lockName);
- }
-
- if (data.darkModeDisabled) {
- document.documentElement.classList.add('dark-mode-disabled');
- }
-
- if (isPlatformOrEnterprise) {
- injectCss({
- css: data.css,
- cssVars: data.cssVars,
- });
- }
- } catch (err) {
- console.error(err);
- }
-
- setHasFinishedInit(true);
-
- // !: While the two setters are stable we still want to ensure we're avoiding
- // !: re-renders.
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
-
- useEffect(() => {
- if (hasFinishedInit && hasDocumentLoaded && window.parent) {
- window.parent.postMessage(
- {
- action: 'document-ready',
- data: null,
- },
- '*',
- );
- }
- }, [hasFinishedInit, hasDocumentLoaded]);
-
- if (hasCompletedDocument) {
- return (
-
- );
- }
-
- return (
-
- {(!hasFinishedInit || !hasDocumentLoaded) &&
}
-
-
- {/* Viewer */}
-
- setHasDocumentLoaded(true)}
- />
-
-
- {/* Widget */}
-
-
- {/* Header */}
-
-
-
- Sign document
-
-
-
- {isExpanded ? (
- setIsExpanded(false)}
- />
- ) : (
- setIsExpanded(true)}
- />
- )}
-
-
-
-
-
-
- Sign the document to complete the process.
-
-
-
-
-
- {/* Form */}
-
-
-
-
- Full Name
-
-
- !isNameLocked && setFullName(e.target.value)}
- />
-
-
-
-
- Email
-
-
- !isEmailLocked && setEmail(e.target.value.trim())}
- />
-
-
-
-
- Signature
-
-
-
-
- {
- setSignature(value);
- }}
- onValidityChange={(isValid) => {
- setSignatureValid(isValid);
- }}
- allowTypedSignature={Boolean(
- metadata &&
- 'typedSignatureEnabled' in metadata &&
- metadata.typedSignatureEnabled,
- )}
- />
-
-
-
- {hasSignatureField && !signatureValid && (
-
-
- Signature is too small. Please provide a more complete signature.
-
-
- )}
-
-
-
-
-
-
-
- {pendingFields.length > 0 ? (
- onNextFieldClick()}>
- Next
-
- ) : (
- throttledOnCompleteClick()}
- >
- Complete
-
- )}
-
-
-
-
-
- {showPendingFieldTooltip && pendingFields.length > 0 && (
-
- Click to insert field
-
- )}
-
-
- {/* Fields */}
-
-
-
- {!hidePoweredBy && (
-
- Powered by
-
-
- )}
-
- );
-};
diff --git a/apps/web/src/app/embed/direct/[[...url]]/not-found.tsx b/apps/web/src/app/embed/direct/[[...url]]/not-found.tsx
deleted file mode 100644
index f4e331929..000000000
--- a/apps/web/src/app/embed/direct/[[...url]]/not-found.tsx
+++ /dev/null
@@ -1,3 +0,0 @@
-export default function EmbedDirectTemplateNotFound() {
- return Not Found
;
-}
diff --git a/apps/web/src/app/embed/direct/[[...url]]/page.tsx b/apps/web/src/app/embed/direct/[[...url]]/page.tsx
deleted file mode 100644
index bca80d0b8..000000000
--- a/apps/web/src/app/embed/direct/[[...url]]/page.tsx
+++ /dev/null
@@ -1,112 +0,0 @@
-import { notFound } from 'next/navigation';
-
-import { match } from 'ts-pattern';
-
-import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
-import { isDocumentPlatform } from '@documenso/ee/server-only/util/is-document-platform';
-import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
-import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
-import { getTeamById } from '@documenso/lib/server-only/team/get-team';
-import { getTemplateByDirectLinkToken } from '@documenso/lib/server-only/template/get-template-by-direct-link-token';
-import { DocumentAccessAuth } from '@documenso/lib/types/document-auth';
-import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
-
-import { DocumentAuthProvider } from '~/app/(signing)/sign/[token]/document-auth-provider';
-import { SigningProvider } from '~/app/(signing)/sign/[token]/provider';
-
-import { EmbedAuthenticateView } from '../../authenticate';
-import { EmbedPaywall } from '../../paywall';
-import { EmbedDirectTemplateClientPage } from './client';
-
-export type EmbedDirectTemplatePageProps = {
- params: {
- url?: string[];
- };
-};
-
-export default async function EmbedDirectTemplatePage({ params }: EmbedDirectTemplatePageProps) {
- if (params.url?.length !== 1) {
- return notFound();
- }
-
- const [token] = params.url;
-
- const template = await getTemplateByDirectLinkToken({
- token,
- }).catch(() => null);
-
- // `template.directLink` is always available but we're doing this to
- // satisfy the type checker.
- if (!template || !template.directLink) {
- return notFound();
- }
-
- // TODO: Make this more robust, we need to ensure the owner is either
- // TODO: the member of a team that has an active subscription, is an early
- // TODO: adopter or is an enterprise user.
- if (IS_BILLING_ENABLED() && !template.teamId) {
- return ;
- }
-
- const { user } = await getServerComponentSession();
-
- const { derivedRecipientAccessAuth } = extractDocumentAuthMethods({
- documentAuth: template.authOptions,
- });
-
- const [isPlatformDocument, isEnterpriseDocument] = await Promise.all([
- isDocumentPlatform(template),
- isUserEnterprise({
- userId: template.userId,
- teamId: template.teamId ?? undefined,
- }),
- ]);
-
- const isAccessAuthValid = match(derivedRecipientAccessAuth)
- .with(DocumentAccessAuth.ACCOUNT, () => user !== null)
- .with(null, () => true)
- .exhaustive();
-
- if (!isAccessAuthValid) {
- return ;
- }
-
- const { directTemplateRecipientId } = template.directLink;
-
- const recipient = template.recipients.find(
- (recipient) => recipient.id === directTemplateRecipientId,
- );
-
- if (!recipient) {
- return notFound();
- }
-
- const fields = template.fields.filter((field) => field.recipientId === directTemplateRecipientId);
-
- const team = template.teamId
- ? await getTeamById({ teamId: template.teamId, userId: template.userId }).catch(() => null)
- : null;
-
- const hidePoweredBy = team?.teamGlobalSettings?.brandingHidePoweredBy ?? false;
-
- return (
-
-
-
-
-
- );
-}
diff --git a/apps/web/src/app/embed/direct/[[...url]]/schema.ts b/apps/web/src/app/embed/direct/[[...url]]/schema.ts
deleted file mode 100644
index 078e6cdb1..000000000
--- a/apps/web/src/app/embed/direct/[[...url]]/schema.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { z } from 'zod';
-
-import { ZBaseEmbedDataSchema } from '../../base-schema';
-
-export const ZDirectTemplateEmbedDataSchema = ZBaseEmbedDataSchema.extend({
- email: z
- .union([z.literal(''), z.string().email()])
- .optional()
- .transform((value) => value || undefined),
- lockEmail: z.boolean().optional().default(false),
- name: z
- .string()
- .optional()
- .transform((value) => value || undefined),
- lockName: z.boolean().optional().default(false),
-});
-
-export type TDirectTemplateEmbedDataSchema = z.infer;
-
-export type TDirectTemplateEmbedDataInputSchema = z.input;
diff --git a/apps/web/src/app/embed/document-fields.tsx b/apps/web/src/app/embed/document-fields.tsx
deleted file mode 100644
index 79256b07e..000000000
--- a/apps/web/src/app/embed/document-fields.tsx
+++ /dev/null
@@ -1,186 +0,0 @@
-'use client';
-
-import { match } from 'ts-pattern';
-
-import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
-import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
-import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones';
-import {
- ZCheckboxFieldMeta,
- ZDropdownFieldMeta,
- ZNumberFieldMeta,
- ZRadioFieldMeta,
- ZTextFieldMeta,
-} from '@documenso/lib/types/field-meta';
-import type { DocumentMeta, Recipient, TemplateMeta } from '@documenso/prisma/client';
-import { type Field, FieldType } from '@documenso/prisma/client';
-import type { FieldWithSignatureAndFieldMeta } from '@documenso/prisma/types/field-with-signature-and-fieldmeta';
-import type {
- TRemovedSignedFieldWithTokenMutationSchema,
- TSignFieldWithTokenMutationSchema,
-} from '@documenso/trpc/server/field-router/schema';
-import { ElementVisible } from '@documenso/ui/primitives/element-visible';
-
-import { CheckboxField } from '~/app/(signing)/sign/[token]/checkbox-field';
-import { DateField } from '~/app/(signing)/sign/[token]/date-field';
-import { DropdownField } from '~/app/(signing)/sign/[token]/dropdown-field';
-import { EmailField } from '~/app/(signing)/sign/[token]/email-field';
-import { InitialsField } from '~/app/(signing)/sign/[token]/initials-field';
-import { NameField } from '~/app/(signing)/sign/[token]/name-field';
-import { NumberField } from '~/app/(signing)/sign/[token]/number-field';
-import { RadioField } from '~/app/(signing)/sign/[token]/radio-field';
-import { SignatureField } from '~/app/(signing)/sign/[token]/signature-field';
-import { TextField } from '~/app/(signing)/sign/[token]/text-field';
-
-export type EmbedDocumentFieldsProps = {
- recipient: Recipient;
- fields: Field[];
- metadata?: DocumentMeta | TemplateMeta | null;
- onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise | void;
- onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise | void;
-};
-
-export const EmbedDocumentFields = ({
- recipient,
- fields,
- metadata,
- onSignField,
- onUnsignField,
-}: EmbedDocumentFieldsProps) => {
- return (
-
- {fields.map((field) =>
- match(field.type)
- .with(FieldType.SIGNATURE, () => (
-
- ))
- .with(FieldType.INITIALS, () => (
-
- ))
- .with(FieldType.NAME, () => (
-
- ))
- .with(FieldType.DATE, () => (
-
- ))
- .with(FieldType.EMAIL, () => (
-
- ))
- .with(FieldType.TEXT, () => {
- const fieldWithMeta: FieldWithSignatureAndFieldMeta = {
- ...field,
- fieldMeta: field.fieldMeta ? ZTextFieldMeta.parse(field.fieldMeta) : null,
- };
-
- return (
-
- );
- })
- .with(FieldType.NUMBER, () => {
- const fieldWithMeta: FieldWithSignatureAndFieldMeta = {
- ...field,
- fieldMeta: field.fieldMeta ? ZNumberFieldMeta.parse(field.fieldMeta) : null,
- };
-
- return (
-
- );
- })
- .with(FieldType.RADIO, () => {
- const fieldWithMeta: FieldWithSignatureAndFieldMeta = {
- ...field,
- fieldMeta: field.fieldMeta ? ZRadioFieldMeta.parse(field.fieldMeta) : null,
- };
-
- return (
-
- );
- })
- .with(FieldType.CHECKBOX, () => {
- const fieldWithMeta: FieldWithSignatureAndFieldMeta = {
- ...field,
- fieldMeta: field.fieldMeta ? ZCheckboxFieldMeta.parse(field.fieldMeta) : null,
- };
-
- return (
-
- );
- })
- .with(FieldType.DROPDOWN, () => {
- const fieldWithMeta: FieldWithSignatureAndFieldMeta = {
- ...field,
- fieldMeta: field.fieldMeta ? ZDropdownFieldMeta.parse(field.fieldMeta) : null,
- };
-
- return (
-
- );
- })
- .otherwise(() => null),
- )}
-
- );
-};
diff --git a/apps/web/src/app/embed/paywall.tsx b/apps/web/src/app/embed/paywall.tsx
deleted file mode 100644
index aa3af647f..000000000
--- a/apps/web/src/app/embed/paywall.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-export const EmbedPaywall = () => {
- return (
-
-
Paywall
-
- );
-};
diff --git a/apps/web/src/app/embed/sign/[[...url]]/client.tsx b/apps/web/src/app/embed/sign/[[...url]]/client.tsx
deleted file mode 100644
index bfaeaeec5..000000000
--- a/apps/web/src/app/embed/sign/[[...url]]/client.tsx
+++ /dev/null
@@ -1,377 +0,0 @@
-'use client';
-
-import { useEffect, useLayoutEffect, useState } from 'react';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { LucideChevronDown, LucideChevronUp } from 'lucide-react';
-
-import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn';
-import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
-import { validateFieldsInserted } from '@documenso/lib/utils/fields';
-import type { DocumentMeta, Recipient, TemplateMeta } from '@documenso/prisma/client';
-import { type DocumentData, type Field, FieldType } from '@documenso/prisma/client';
-import { trpc } from '@documenso/trpc/react';
-import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
-import { Button } from '@documenso/ui/primitives/button';
-import { Card, CardContent } from '@documenso/ui/primitives/card';
-import { ElementVisible } from '@documenso/ui/primitives/element-visible';
-import { Input } from '@documenso/ui/primitives/input';
-import { Label } from '@documenso/ui/primitives/label';
-import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
-import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-import { useRequiredSigningContext } from '~/app/(signing)/sign/[token]/provider';
-import { Logo } from '~/components/branding/logo';
-
-import { EmbedClientLoading } from '../../client-loading';
-import { EmbedDocumentCompleted } from '../../completed';
-import { EmbedDocumentFields } from '../../document-fields';
-import { injectCss } from '../../util';
-import { ZSignDocumentEmbedDataSchema } from './schema';
-
-export type EmbedSignDocumentClientPageProps = {
- token: string;
- documentId: number;
- documentData: DocumentData;
- recipient: Recipient;
- fields: Field[];
- metadata?: DocumentMeta | TemplateMeta | null;
- isCompleted?: boolean;
- hidePoweredBy?: boolean;
- isPlatformOrEnterprise?: boolean;
-};
-
-export const EmbedSignDocumentClientPage = ({
- token,
- documentId,
- documentData,
- recipient,
- fields,
- metadata,
- isCompleted,
- hidePoweredBy = false,
- isPlatformOrEnterprise = false,
-}: EmbedSignDocumentClientPageProps) => {
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const {
- fullName,
- email,
- signature,
- signatureValid,
- setFullName,
- setSignature,
- setSignatureValid,
- } = useRequiredSigningContext();
-
- const [hasFinishedInit, setHasFinishedInit] = useState(false);
- const [hasDocumentLoaded, setHasDocumentLoaded] = useState(false);
- const [hasCompletedDocument, setHasCompletedDocument] = useState(isCompleted);
-
- const [isExpanded, setIsExpanded] = useState(false);
-
- const [isNameLocked, setIsNameLocked] = useState(false);
-
- const [showPendingFieldTooltip, setShowPendingFieldTooltip] = useState(false);
-
- const [throttledOnCompleteClick, isThrottled] = useThrottleFn(() => void onCompleteClick(), 500);
-
- const [pendingFields, _completedFields] = [
- fields.filter((field) => !field.inserted),
- fields.filter((field) => field.inserted),
- ];
-
- const { mutateAsync: completeDocumentWithToken, isPending: isSubmitting } =
- trpc.recipient.completeDocumentWithToken.useMutation();
-
- const hasSignatureField = fields.some((field) => field.type === FieldType.SIGNATURE);
-
- const onNextFieldClick = () => {
- validateFieldsInserted(fields);
-
- setShowPendingFieldTooltip(true);
- setIsExpanded(false);
- };
-
- const onCompleteClick = async () => {
- try {
- if (hasSignatureField && !signatureValid) {
- return;
- }
-
- const valid = validateFieldsInserted(fields);
-
- if (!valid) {
- setShowPendingFieldTooltip(true);
- return;
- }
-
- await completeDocumentWithToken({
- documentId,
- token,
- });
-
- if (window.parent) {
- window.parent.postMessage(
- {
- action: 'document-completed',
- data: {
- token,
- documentId,
- recipientId: recipient.id,
- },
- },
- '*',
- );
- }
-
- setHasCompletedDocument(true);
- } catch (err) {
- if (window.parent) {
- window.parent.postMessage(
- {
- action: 'document-error',
- data: null,
- },
- '*',
- );
- }
-
- toast({
- title: _(msg`Something went wrong`),
- description: _(
- msg`We were unable to submit this document at this time. Please try again later.`,
- ),
- variant: 'destructive',
- });
- }
- };
-
- useLayoutEffect(() => {
- const hash = window.location.hash.slice(1);
-
- try {
- const data = ZSignDocumentEmbedDataSchema.parse(JSON.parse(decodeURIComponent(atob(hash))));
-
- if (!isCompleted && data.name) {
- setFullName(data.name);
- }
-
- // Since a recipient can be provided a name we can lock it without requiring
- // a to be provided by the parent application, unlike direct templates.
- setIsNameLocked(!!data.lockName);
-
- if (data.darkModeDisabled) {
- document.documentElement.classList.add('dark-mode-disabled');
- }
-
- if (isPlatformOrEnterprise) {
- injectCss({
- css: data.css,
- cssVars: data.cssVars,
- });
- }
- } catch (err) {
- console.error(err);
- }
-
- setHasFinishedInit(true);
-
- // !: While the two setters are stable we still want to ensure we're avoiding
- // !: re-renders.
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
-
- useEffect(() => {
- if (hasFinishedInit && hasDocumentLoaded && window.parent) {
- window.parent.postMessage(
- {
- action: 'document-ready',
- data: null,
- },
- '*',
- );
- }
- }, [hasFinishedInit, hasDocumentLoaded]);
-
- if (hasCompletedDocument) {
- return (
-
- );
- }
-
- return (
-
- {(!hasFinishedInit || !hasDocumentLoaded) &&
}
-
-
- {/* Viewer */}
-
- setHasDocumentLoaded(true)}
- />
-
-
- {/* Widget */}
-
-
- {/* Header */}
-
-
-
- Sign document
-
-
-
- {isExpanded ? (
- setIsExpanded(false)}
- />
- ) : (
- setIsExpanded(true)}
- />
- )}
-
-
-
-
-
-
- Sign the document to complete the process.
-
-
-
-
-
- {/* Form */}
-
-
-
-
- Full Name
-
-
- !isNameLocked && setFullName(e.target.value)}
- />
-
-
-
-
- Email
-
-
-
-
-
-
-
- Signature
-
-
-
-
- {
- setSignature(value);
- }}
- onValidityChange={(isValid) => {
- setSignatureValid(isValid);
- }}
- allowTypedSignature={Boolean(
- metadata &&
- 'typedSignatureEnabled' in metadata &&
- metadata.typedSignatureEnabled,
- )}
- />
-
-
-
- {hasSignatureField && !signatureValid && (
-
-
- Signature is too small. Please provide a more complete signature.
-
-
- )}
-
-
-
-
-
-
-
- {pendingFields.length > 0 ? (
- onNextFieldClick()}>
- Next
-
- ) : (
- throttledOnCompleteClick()}
- >
- Complete
-
- )}
-
-
-
-
-
- {showPendingFieldTooltip && pendingFields.length > 0 && (
-
- Click to insert field
-
- )}
-
-
- {/* Fields */}
-
-
-
- {!hidePoweredBy && (
-
- Powered by
-
-
- )}
-
- );
-};
diff --git a/apps/web/src/app/embed/sign/[[...url]]/not-found.tsx b/apps/web/src/app/embed/sign/[[...url]]/not-found.tsx
deleted file mode 100644
index f4e331929..000000000
--- a/apps/web/src/app/embed/sign/[[...url]]/not-found.tsx
+++ /dev/null
@@ -1,3 +0,0 @@
-export default function EmbedDirectTemplateNotFound() {
- return Not Found
;
-}
diff --git a/apps/web/src/app/embed/sign/[[...url]]/page.tsx b/apps/web/src/app/embed/sign/[[...url]]/page.tsx
deleted file mode 100644
index c07cd0be3..000000000
--- a/apps/web/src/app/embed/sign/[[...url]]/page.tsx
+++ /dev/null
@@ -1,119 +0,0 @@
-import { notFound } from 'next/navigation';
-
-import { match } from 'ts-pattern';
-
-import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
-import { isDocumentPlatform } from '@documenso/ee/server-only/util/is-document-platform';
-import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
-import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
-import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
-import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
-import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
-import { getTeamById } from '@documenso/lib/server-only/team/get-team';
-import { DocumentAccessAuth } from '@documenso/lib/types/document-auth';
-import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
-import { DocumentStatus } from '@documenso/prisma/client';
-
-import { DocumentAuthProvider } from '~/app/(signing)/sign/[token]/document-auth-provider';
-import { SigningProvider } from '~/app/(signing)/sign/[token]/provider';
-
-import { EmbedAuthenticateView } from '../../authenticate';
-import { EmbedPaywall } from '../../paywall';
-import { EmbedSignDocumentClientPage } from './client';
-
-export type EmbedSignDocumentPageProps = {
- params: {
- url?: string[];
- };
-};
-
-export default async function EmbedSignDocumentPage({ params }: EmbedSignDocumentPageProps) {
- if (params.url?.length !== 1) {
- return notFound();
- }
-
- const [token] = params.url;
-
- const { user } = await getServerComponentSession();
-
- const [document, fields, recipient] = await Promise.all([
- getDocumentAndSenderByToken({
- token,
- userId: user?.id,
- requireAccessAuth: false,
- }).catch(() => null),
- getFieldsForToken({ token }),
- getRecipientByToken({ token }).catch(() => null),
- ]);
-
- // `document.directLink` is always available but we're doing this to
- // satisfy the type checker.
- if (!document || !recipient) {
- return notFound();
- }
-
- // TODO: Make this more robust, we need to ensure the owner is either
- // TODO: the member of a team that has an active subscription, is an early
- // TODO: adopter or is an enterprise user.
- if (IS_BILLING_ENABLED() && !document.teamId) {
- return ;
- }
-
- const [isPlatformDocument, isEnterpriseDocument] = await Promise.all([
- isDocumentPlatform(document),
- isUserEnterprise({
- userId: document.userId,
- teamId: document.teamId ?? undefined,
- }),
- ]);
-
- const { derivedRecipientAccessAuth } = extractDocumentAuthMethods({
- documentAuth: document.authOptions,
- });
-
- const isAccessAuthValid = match(derivedRecipientAccessAuth)
- .with(DocumentAccessAuth.ACCOUNT, () => user !== null)
- .with(null, () => true)
- .exhaustive();
-
- if (!isAccessAuthValid) {
- return (
-
- );
- }
-
- const team = document.teamId
- ? await getTeamById({ teamId: document.teamId, userId: document.userId }).catch(() => null)
- : null;
-
- const hidePoweredBy = team?.teamGlobalSettings?.brandingHidePoweredBy ?? false;
-
- return (
-
-
-
-
-
- );
-}
diff --git a/apps/web/src/app/embed/sign/[[...url]]/schema.ts b/apps/web/src/app/embed/sign/[[...url]]/schema.ts
deleted file mode 100644
index 78e499d5a..000000000
--- a/apps/web/src/app/embed/sign/[[...url]]/schema.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { z } from 'zod';
-
-import { ZBaseEmbedDataSchema } from '../../base-schema';
-
-export const ZSignDocumentEmbedDataSchema = ZBaseEmbedDataSchema.extend({
- email: z
- .union([z.literal(''), z.string().email()])
- .optional()
- .transform((value) => value || undefined),
- lockEmail: z.boolean().optional().default(false),
- name: z
- .string()
- .optional()
- .transform((value) => value || undefined),
- lockName: z.boolean().optional().default(false),
-});
diff --git a/apps/web/src/app/embed/util.ts b/apps/web/src/app/embed/util.ts
deleted file mode 100644
index 099ecb9f8..000000000
--- a/apps/web/src/app/embed/util.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { type TCssVarsSchema, toNativeCssVars } from './css-vars';
-
-export const injectCss = (options: { css?: string; cssVars?: TCssVarsSchema }) => {
- const { css, cssVars } = options;
-
- if (css) {
- const style = document.createElement('style');
- style.innerHTML = css;
-
- document.head.appendChild(style);
- }
-
- if (cssVars) {
- const nativeVars = toNativeCssVars(cssVars);
-
- for (const [key, value] of Object.entries(nativeVars)) {
- document.documentElement.style.setProperty(key, value);
- }
- }
-};
diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css
deleted file mode 100644
index 044c9763f..000000000
--- a/apps/web/src/app/globals.css
+++ /dev/null
@@ -1 +0,0 @@
-@import '@documenso/ui/styles/theme.css';
diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx
deleted file mode 100644
index 87328e378..000000000
--- a/apps/web/src/app/layout.tsx
+++ /dev/null
@@ -1,102 +0,0 @@
-import { Suspense } from 'react';
-
-import { Caveat, Inter } from 'next/font/google';
-
-import { AxiomWebVitals } from 'next-axiom';
-import { PublicEnvScript } from 'next-runtime-env';
-
-import { FeatureFlagProvider } from '@documenso/lib/client-only/providers/feature-flag';
-import { I18nClientProvider } from '@documenso/lib/client-only/providers/i18n.client';
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
-import { getServerComponentAllFlags } from '@documenso/lib/server-only/feature-flags/get-server-component-feature-flag';
-import { TrpcProvider } from '@documenso/trpc/react';
-import { cn } from '@documenso/ui/lib/utils';
-import { Toaster } from '@documenso/ui/primitives/toaster';
-import { TooltipProvider } from '@documenso/ui/primitives/tooltip';
-
-import { ThemeProvider } from '~/providers/next-theme';
-import { PostHogPageview } from '~/providers/posthog';
-
-import './globals.css';
-
-const fontInter = Inter({ subsets: ['latin'], variable: '--font-sans' });
-const fontCaveat = Caveat({ subsets: ['latin'], variable: '--font-signature' });
-
-export function generateMetadata() {
- return {
- title: {
- template: '%s - Documenso',
- default: 'Documenso',
- },
- description:
- 'Join Documenso, the open signing infrastructure, and get a 10x better signing experience. Pricing starts at $30/mo. forever! Sign in now and enjoy a faster, smarter, and more beautiful document signing process. Integrates with your favorite tools, customizable, and expandable. Support our mission and become a part of our open-source community.',
- keywords:
- 'Documenso, open source, DocuSign alternative, document signing, open signing infrastructure, open-source community, fast signing, beautiful signing, smart templates',
- authors: { name: 'Documenso, Inc.' },
- robots: 'index, follow',
- metadataBase: new URL(NEXT_PUBLIC_WEBAPP_URL() ?? 'http://localhost:3000'),
- openGraph: {
- title: 'Documenso - The Open Source DocuSign Alternative',
- description:
- 'Join Documenso, the open signing infrastructure, and get a 10x better signing experience. Pricing starts at $30/mo. forever! Sign in now and enjoy a faster, smarter, and more beautiful document signing process. Integrates with your favorite tools, customizable, and expandable. Support our mission and become a part of our open-source community.',
- type: 'website',
- images: ['/opengraph-image.jpg'],
- },
- twitter: {
- site: '@documenso',
- card: 'summary_large_image',
- images: ['/opengraph-image.jpg'],
- description:
- 'Join Documenso, the open signing infrastructure, and get a 10x better signing experience. Pricing starts at $30/mo. forever! Sign in now and enjoy a faster, smarter, and more beautiful document signing process. Integrates with your favorite tools, customizable, and expandable. Support our mission and become a part of our open-source community.',
- },
- };
-}
-
-export default async function RootLayout({ children }: { children: React.ReactNode }) {
- const flags = await getServerComponentAllFlags();
-
- const { i18n, lang, locales } = await setupI18nSSR();
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {children}
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/apps/web/src/app/not-found.tsx b/apps/web/src/app/not-found.tsx
deleted file mode 100644
index 522a31c10..000000000
--- a/apps/web/src/app/not-found.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import Link from 'next/link';
-
-import { Trans } from '@lingui/macro';
-
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
-import { Button } from '@documenso/ui/primitives/button';
-
-import NotFoundPartial from '~/components/partials/not-found';
-
-export default async function NotFound() {
- await setupI18nSSR();
-
- const { session } = await getServerComponentSession();
-
- return (
-
- {session && (
-
-
- Documents
-
-
- )}
-
- {!session && (
-
-
- Sign In
-
-
- )}
-
- );
-}
diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx
deleted file mode 100644
index 270a6bb75..000000000
--- a/apps/web/src/app/page.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
-
-export default async function DashboardPage() {
- await setupI18nSSR();
-
- return Documenso
;
-}
diff --git a/apps/web/src/components/(dashboard)/avatar/avatar-with-recipient.tsx b/apps/web/src/components/(dashboard)/avatar/avatar-with-recipient.tsx
deleted file mode 100644
index 9f29bb06a..000000000
--- a/apps/web/src/components/(dashboard)/avatar/avatar-with-recipient.tsx
+++ /dev/null
@@ -1,75 +0,0 @@
-'use client';
-
-import React from 'react';
-
-import { msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-
-import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard';
-import { getRecipientType } from '@documenso/lib/client-only/recipient-type';
-import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
-import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
-import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter';
-import type { Recipient } from '@documenso/prisma/client';
-import { DocumentStatus } from '@documenso/prisma/client';
-import { cn } from '@documenso/ui/lib/utils';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-import { StackAvatar } from './stack-avatar';
-
-export type AvatarWithRecipientProps = {
- recipient: Recipient;
- documentStatus: DocumentStatus;
-};
-
-export function AvatarWithRecipient({ recipient, documentStatus }: AvatarWithRecipientProps) {
- const [, copy] = useCopyToClipboard();
-
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const signingToken = documentStatus === DocumentStatus.PENDING ? recipient.token : null;
-
- const onRecipientClick = () => {
- if (!signingToken) {
- return;
- }
-
- void copy(`${NEXT_PUBLIC_WEBAPP_URL()}/sign/${signingToken}`).then(() => {
- toast({
- title: _(msg`Copied to clipboard`),
- description: _(msg`The signing link has been copied to your clipboard.`),
- });
- });
- };
-
- return (
-
-
-
-
-
{recipient.email}
-
- {_(RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName)}
-
-
-
- );
-}
diff --git a/apps/web/src/components/(dashboard)/avatar/stack-avatar.tsx b/apps/web/src/components/(dashboard)/avatar/stack-avatar.tsx
deleted file mode 100644
index beafbebd5..000000000
--- a/apps/web/src/components/(dashboard)/avatar/stack-avatar.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-import { RecipientStatusType } from '@documenso/lib/client-only/recipient-type';
-import { Avatar, AvatarFallback } from '@documenso/ui/primitives/avatar';
-
-const ZIndexes: { [key: string]: string } = {
- '10': 'z-10',
- '20': 'z-20',
- '30': 'z-30',
- '40': 'z-40',
- '50': 'z-50',
-};
-
-export type StackAvatarProps = {
- first?: boolean;
- zIndex?: string;
- fallbackText?: string;
- type: RecipientStatusType;
-};
-
-export const StackAvatar = ({ first, zIndex, fallbackText = '', type }: StackAvatarProps) => {
- let classes = '';
- let zIndexClass = '';
- const firstClass = first ? '' : '-ml-3';
-
- if (zIndex) {
- zIndexClass = ZIndexes[zIndex] ?? '';
- }
-
- switch (type) {
- case RecipientStatusType.UNSIGNED:
- classes = 'bg-dawn-200 text-dawn-900';
- break;
- case RecipientStatusType.OPENED:
- classes = 'bg-yellow-200 text-yellow-700';
- break;
- case RecipientStatusType.WAITING:
- classes = 'bg-water text-water-700';
- break;
- case RecipientStatusType.COMPLETED:
- classes = 'bg-documenso-200 text-documenso-800';
- break;
- case RecipientStatusType.REJECTED:
- classes = 'bg-red-200 text-red-800';
- break;
- default:
- break;
- }
-
- return (
-
- {fallbackText}
-
- );
-};
diff --git a/apps/web/src/components/(dashboard)/avatar/stack-avatars-with-tooltip.tsx b/apps/web/src/components/(dashboard)/avatar/stack-avatars-with-tooltip.tsx
deleted file mode 100644
index bccee558e..000000000
--- a/apps/web/src/components/(dashboard)/avatar/stack-avatars-with-tooltip.tsx
+++ /dev/null
@@ -1,168 +0,0 @@
-'use client';
-
-import { useMemo } from 'react';
-
-import { Trans } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-
-import { RecipientStatusType, getRecipientType } from '@documenso/lib/client-only/recipient-type';
-import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
-import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter';
-import { type DocumentStatus, type Recipient } from '@documenso/prisma/client';
-import { PopoverHover } from '@documenso/ui/primitives/popover';
-
-import { AvatarWithRecipient } from './avatar-with-recipient';
-import { StackAvatar } from './stack-avatar';
-import { StackAvatars } from './stack-avatars';
-
-export type StackAvatarsWithTooltipProps = {
- documentStatus: DocumentStatus;
- recipients: Recipient[];
- position?: 'top' | 'bottom';
- children?: React.ReactNode;
-};
-
-export const StackAvatarsWithTooltip = ({
- documentStatus,
- recipients,
- position,
- children,
-}: StackAvatarsWithTooltipProps) => {
- const { _ } = useLingui();
-
- const waitingRecipients = recipients.filter(
- (recipient) => getRecipientType(recipient) === RecipientStatusType.WAITING,
- );
-
- const openedRecipients = recipients.filter(
- (recipient) => getRecipientType(recipient) === RecipientStatusType.OPENED,
- );
-
- const completedRecipients = recipients.filter(
- (recipient) => getRecipientType(recipient) === RecipientStatusType.COMPLETED,
- );
-
- const uncompletedRecipients = recipients.filter(
- (recipient) => getRecipientType(recipient) === RecipientStatusType.UNSIGNED,
- );
-
- const rejectedRecipients = recipients.filter(
- (recipient) => getRecipientType(recipient) === RecipientStatusType.REJECTED,
- );
-
- const sortedRecipients = useMemo(() => {
- const otherRecipients = recipients.filter(
- (recipient) => getRecipientType(recipient) !== RecipientStatusType.REJECTED,
- );
-
- return [
- ...rejectedRecipients.sort((a, b) => a.id - b.id),
- ...otherRecipients.sort((a, b) => {
- return a.id - b.id;
- }),
- ];
- }, [recipients]);
-
- return (
- }
- contentProps={{
- className: 'flex flex-col gap-y-5 py-2',
- side: position,
- }}
- >
- {completedRecipients.length > 0 && (
-
-
- Completed
-
- {completedRecipients.map((recipient: Recipient) => (
-
-
-
-
{recipient.email}
-
- {_(RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName)}
-
-
-
- ))}
-
- )}
-
- {rejectedRecipients.length > 0 && (
-
-
- Rejected
-
- {rejectedRecipients.map((recipient: Recipient) => (
-
-
-
-
{recipient.email}
-
- {_(RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName)}
-
-
-
- ))}
-
- )}
-
- {waitingRecipients.length > 0 && (
-
-
- Waiting
-
- {waitingRecipients.map((recipient: Recipient) => (
-
- ))}
-
- )}
-
- {openedRecipients.length > 0 && (
-
-
- Opened
-
- {openedRecipients.map((recipient: Recipient) => (
-
- ))}
-
- )}
-
- {uncompletedRecipients.length > 0 && (
-
-
- Uncompleted
-
- {uncompletedRecipients.map((recipient: Recipient) => (
-
- ))}
-
- )}
-
- );
-};
diff --git a/apps/web/src/components/(dashboard)/avatar/stack-avatars.tsx b/apps/web/src/components/(dashboard)/avatar/stack-avatars.tsx
deleted file mode 100644
index 95621c760..000000000
--- a/apps/web/src/components/(dashboard)/avatar/stack-avatars.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import React from 'react';
-
-import {
- getExtraRecipientsType,
- getRecipientType,
-} from '@documenso/lib/client-only/recipient-type';
-import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter';
-import type { Recipient } from '@documenso/prisma/client';
-
-import { StackAvatar } from './stack-avatar';
-
-export function StackAvatars({ recipients }: { recipients: Recipient[] }) {
- const renderStackAvatars = (recipients: Recipient[]) => {
- const zIndex = 50;
- const itemsToRender = recipients.slice(0, 5);
- const remainingItems = recipients.length - itemsToRender.length;
-
- return itemsToRender.map((recipient: Recipient, index: number) => {
- const first = index === 0;
-
- if (index === 4 && remainingItems > 0) {
- return (
-
- );
- }
-
- return (
-
- );
- });
- };
-
- return <>{renderStackAvatars(recipients)}>;
-}
diff --git a/apps/web/src/components/(dashboard)/common/command-menu.tsx b/apps/web/src/components/(dashboard)/common/command-menu.tsx
deleted file mode 100644
index e07ed9f1b..000000000
--- a/apps/web/src/components/(dashboard)/common/command-menu.tsx
+++ /dev/null
@@ -1,324 +0,0 @@
-'use client';
-
-import { useCallback, useMemo, useState } from 'react';
-
-import { useRouter } from 'next/navigation';
-
-import type { MessageDescriptor } from '@lingui/core';
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { CheckIcon, Loader, Monitor, Moon, Sun } from 'lucide-react';
-import { useTheme } from 'next-themes';
-import { useHotkeys } from 'react-hotkeys-hook';
-
-import { SUPPORTED_LANGUAGES } from '@documenso/lib/constants/i18n';
-import {
- DOCUMENTS_PAGE_SHORTCUT,
- SETTINGS_PAGE_SHORTCUT,
- TEMPLATES_PAGE_SHORTCUT,
-} from '@documenso/lib/constants/keyboard-shortcuts';
-import {
- DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
- SKIP_QUERY_BATCH_META,
-} from '@documenso/lib/constants/trpc';
-import { switchI18NLanguage } from '@documenso/lib/server-only/i18n/switch-i18n-language';
-import { dynamicActivate } from '@documenso/lib/utils/i18n';
-import { trpc as trpcReact } from '@documenso/trpc/react';
-import { cn } from '@documenso/ui/lib/utils';
-import {
- CommandDialog,
- CommandEmpty,
- CommandGroup,
- CommandInput,
- CommandItem,
- CommandList,
- CommandShortcut,
-} from '@documenso/ui/primitives/command';
-import { THEMES_TYPE } from '@documenso/ui/primitives/constants';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-const DOCUMENTS_PAGES = [
- {
- label: msg`All documents`,
- path: '/documents?status=ALL',
- shortcut: DOCUMENTS_PAGE_SHORTCUT.replace('+', ''),
- },
- { label: msg`Draft documents`, path: '/documents?status=DRAFT' },
- {
- label: msg`Completed documents`,
- path: '/documents?status=COMPLETED',
- },
- { label: msg`Pending documents`, path: '/documents?status=PENDING' },
- { label: msg`Inbox documents`, path: '/documents?status=INBOX' },
-];
-
-const TEMPLATES_PAGES = [
- {
- label: msg`All templates`,
- path: '/templates',
- shortcut: TEMPLATES_PAGE_SHORTCUT.replace('+', ''),
- },
-];
-
-const SETTINGS_PAGES = [
- {
- label: msg`Settings`,
- path: '/settings',
- shortcut: SETTINGS_PAGE_SHORTCUT.replace('+', ''),
- },
- { label: msg`Profile`, path: '/settings/profile' },
- { label: msg`Password`, path: '/settings/password' },
-];
-
-export type CommandMenuProps = {
- open?: boolean;
- onOpenChange?: (_open: boolean) => void;
-};
-
-export function CommandMenu({ open, onOpenChange }: CommandMenuProps) {
- const { _ } = useLingui();
- const { setTheme } = useTheme();
-
- const router = useRouter();
-
- const [isOpen, setIsOpen] = useState(() => open ?? false);
- const [search, setSearch] = useState('');
- const [pages, setPages] = useState([]);
-
- const { data: searchDocumentsData, isLoading: isSearchingDocuments } =
- trpcReact.document.searchDocuments.useQuery(
- {
- query: search,
- },
- {
- placeholderData: (previousData) => previousData,
- // Do not batch this due to relatively long request time compared to
- // other queries which are generally batched with this.
- ...SKIP_QUERY_BATCH_META,
- ...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
- },
- );
-
- const searchResults = useMemo(() => {
- if (!searchDocumentsData) {
- return [];
- }
-
- return searchDocumentsData.map((document) => ({
- label: document.title,
- path: document.path,
- value: document.value,
- }));
- }, [searchDocumentsData]);
-
- const currentPage = pages[pages.length - 1];
-
- const toggleOpen = () => {
- setIsOpen((isOpen) => !isOpen);
- onOpenChange?.(!isOpen);
-
- if (isOpen) {
- setPages([]);
- setSearch('');
- }
- };
-
- const setOpen = useCallback(
- (open: boolean) => {
- setIsOpen(open);
- onOpenChange?.(open);
-
- if (!open) {
- setPages([]);
- setSearch('');
- }
- },
- [onOpenChange],
- );
-
- const push = useCallback(
- (path: string) => {
- router.push(path);
- setOpen(false);
- },
- [router, setOpen],
- );
-
- const addPage = (page: string) => {
- setPages((pages) => [...pages, page]);
- setSearch('');
- };
-
- const goToSettings = useCallback(() => push(SETTINGS_PAGES[0].path), [push]);
- const goToDocuments = useCallback(() => push(DOCUMENTS_PAGES[0].path), [push]);
- const goToTemplates = useCallback(() => push(TEMPLATES_PAGES[0].path), [push]);
-
- useHotkeys(['ctrl+k', 'meta+k'], toggleOpen, { preventDefault: true });
- useHotkeys(SETTINGS_PAGE_SHORTCUT, goToSettings);
- useHotkeys(DOCUMENTS_PAGE_SHORTCUT, goToDocuments);
- useHotkeys(TEMPLATES_PAGE_SHORTCUT, goToTemplates);
-
- const handleKeyDown = (e: React.KeyboardEvent) => {
- // Escape goes to previous page
- // Backspace goes to previous page when search is empty
- if (e.key === 'Escape' || (e.key === 'Backspace' && !search)) {
- e.preventDefault();
-
- if (currentPage === undefined) {
- setOpen(false);
- }
-
- setPages((pages) => pages.slice(0, -1));
- }
- };
-
- return (
-
-
-
-
- {isSearchingDocuments ? (
-
-
-
-
-
-
-
- ) : (
-
- No results found.
-
- )}
- {!currentPage && (
- <>
-
-
-
-
-
-
-
-
-
-
- addPage('language')}>
- Change language
-
- addPage('theme')}>
- Change theme
-
-
- {searchResults.length > 0 && (
-
-
-
- )}
- >
- )}
-
- {currentPage === 'theme' && }
- {currentPage === 'language' && }
-
-
- );
-}
-
-const Commands = ({
- push,
- pages,
-}: {
- push: (_path: string) => void;
- pages: { label: MessageDescriptor | string; path: string; shortcut?: string; value?: string }[];
-}) => {
- const { _ } = useLingui();
-
- return pages.map((page, idx) => (
- push(page.path)}
- >
- {typeof page.label === 'string' ? page.label : _(page.label)}
- {page.shortcut && {page.shortcut} }
-
- ));
-};
-
-const ThemeCommands = ({ setTheme }: { setTheme: (_theme: string) => void }) => {
- const { _ } = useLingui();
-
- const THEMES = useMemo(
- () => [
- { label: msg`Light Mode`, theme: THEMES_TYPE.LIGHT, icon: Sun },
- { label: msg`Dark Mode`, theme: THEMES_TYPE.DARK, icon: Moon },
- { label: msg`System Theme`, theme: THEMES_TYPE.SYSTEM, icon: Monitor },
- ],
- [],
- );
-
- return THEMES.map((theme) => (
- setTheme(theme.theme)}
- className="-my-1 mx-2 rounded-lg first:mt-2 last:mb-2"
- >
-
- {_(theme.label)}
-
- ));
-};
-
-const LanguageCommands = () => {
- const { i18n, _ } = useLingui();
- const { toast } = useToast();
-
- const [isLoading, setIsLoading] = useState(false);
-
- const setLanguage = async (lang: string) => {
- if (isLoading || lang === i18n.locale) {
- return;
- }
-
- setIsLoading(true);
-
- try {
- await dynamicActivate(i18n, lang);
- await switchI18NLanguage(lang);
- } catch (err) {
- toast({
- title: _(msg`An unknown error occurred`),
- variant: 'destructive',
- description: _(msg`Unable to change the language at this time. Please try again later.`),
- });
- }
-
- setIsLoading(false);
- };
-
- return Object.values(SUPPORTED_LANGUAGES).map((language) => (
- setLanguage(language.short)}
- className="-my-1 mx-2 rounded-lg first:mt-2 last:mb-2"
- >
-
-
- {language.full}
-
- ));
-};
diff --git a/apps/web/src/components/(dashboard)/document-search/document-search.tsx b/apps/web/src/components/(dashboard)/document-search/document-search.tsx
deleted file mode 100644
index 966452152..000000000
--- a/apps/web/src/components/(dashboard)/document-search/document-search.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-'use client';
-
-import { useCallback, useEffect, useState } from 'react';
-
-import { useRouter, useSearchParams } from 'next/navigation';
-
-import { msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-
-import { useDebouncedValue } from '@documenso/lib/client-only/hooks/use-debounced-value';
-import { Input } from '@documenso/ui/primitives/input';
-
-export const DocumentSearch = ({ initialValue = '' }: { initialValue?: string }) => {
- const { _ } = useLingui();
-
- const router = useRouter();
- const searchParams = useSearchParams();
-
- const [searchTerm, setSearchTerm] = useState(initialValue);
- const debouncedSearchTerm = useDebouncedValue(searchTerm, 500);
-
- const handleSearch = useCallback(
- (term: string) => {
- const params = new URLSearchParams(searchParams?.toString() ?? '');
- if (term) {
- params.set('search', term);
- } else {
- params.delete('search');
- }
- router.push(`?${params.toString()}`);
- },
- [router, searchParams],
- );
-
- useEffect(() => {
- handleSearch(searchTerm);
- }, [debouncedSearchTerm]);
-
- return (
- setSearchTerm(e.target.value)}
- />
- );
-};
diff --git a/apps/web/src/components/(dashboard)/layout/banner.tsx b/apps/web/src/components/(dashboard)/layout/banner.tsx
deleted file mode 100644
index 95a0de3dd..000000000
--- a/apps/web/src/components/(dashboard)/layout/banner.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import { getSiteSettings } from '@documenso/lib/server-only/site-settings/get-site-settings';
-import { SITE_SETTINGS_BANNER_ID } from '@documenso/lib/server-only/site-settings/schemas/banner';
-
-export const Banner = async () => {
- const banner = await getSiteSettings().then((settings) =>
- settings.find((setting) => setting.id === SITE_SETTINGS_BANNER_ID),
- );
-
- return (
- <>
- {banner && banner.enabled && (
-
- )}
- >
- );
-};
-
-// Banner
-// Custom Text
-// Custom Text with Custom Icon
diff --git a/apps/web/src/components/(dashboard)/layout/desktop-nav.tsx b/apps/web/src/components/(dashboard)/layout/desktop-nav.tsx
deleted file mode 100644
index 0eea958b7..000000000
--- a/apps/web/src/components/(dashboard)/layout/desktop-nav.tsx
+++ /dev/null
@@ -1,92 +0,0 @@
-import type { HTMLAttributes } from 'react';
-import { useEffect, useState } from 'react';
-
-import Link from 'next/link';
-import { useParams, usePathname } from 'next/navigation';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { Search } from 'lucide-react';
-
-import { getRootHref } from '@documenso/lib/utils/params';
-import { cn } from '@documenso/ui/lib/utils';
-import { Button } from '@documenso/ui/primitives/button';
-
-const navigationLinks = [
- {
- href: '/documents',
- label: msg`Documents`,
- },
- {
- href: '/templates',
- label: msg`Templates`,
- },
-];
-
-export type DesktopNavProps = HTMLAttributes & {
- setIsCommandMenuOpen: (value: boolean) => void;
-};
-
-export const DesktopNav = ({ className, setIsCommandMenuOpen, ...props }: DesktopNavProps) => {
- const { _ } = useLingui();
-
- const pathname = usePathname();
- const params = useParams();
-
- const [modifierKey, setModifierKey] = useState(() => 'Ctrl');
-
- const rootHref = getRootHref(params, { returnEmptyRootString: true });
-
- useEffect(() => {
- const userAgent = typeof navigator !== 'undefined' ? navigator.userAgent : 'unknown';
- const isMacOS = /Macintosh|Mac\s+OS\s+X/i.test(userAgent);
-
- setModifierKey(isMacOS ? '⌘' : 'Ctrl');
- }, []);
-
- return (
-
-
- {navigationLinks.map(({ href, label }) => (
-
- {_(label)}
-
- ))}
-
-
-
setIsCommandMenuOpen(true)}
- >
-
-
- Search
-
-
-
-
-
- );
-};
diff --git a/apps/web/src/components/(dashboard)/layout/header.tsx b/apps/web/src/components/(dashboard)/layout/header.tsx
deleted file mode 100644
index ac50d1145..000000000
--- a/apps/web/src/components/(dashboard)/layout/header.tsx
+++ /dev/null
@@ -1,102 +0,0 @@
-'use client';
-
-import { type HTMLAttributes, useEffect, useState } from 'react';
-
-import Link from 'next/link';
-import { useParams } from 'next/navigation';
-import { usePathname } from 'next/navigation';
-
-import { MenuIcon, SearchIcon } from 'lucide-react';
-
-import type { TGetTeamsResponse } from '@documenso/lib/server-only/team/get-teams';
-import { getRootHref } from '@documenso/lib/utils/params';
-import type { User } from '@documenso/prisma/client';
-import { cn } from '@documenso/ui/lib/utils';
-
-import { Logo } from '~/components/branding/logo';
-
-import { CommandMenu } from '../common/command-menu';
-import { DesktopNav } from './desktop-nav';
-import { MenuSwitcher } from './menu-switcher';
-import { MobileNavigation } from './mobile-navigation';
-
-export type HeaderProps = HTMLAttributes & {
- user: User;
- teams: TGetTeamsResponse;
-};
-
-export const Header = ({ className, user, teams, ...props }: HeaderProps) => {
- const params = useParams();
-
- const [isCommandMenuOpen, setIsCommandMenuOpen] = useState(false);
- const [isHamburgerMenuOpen, setIsHamburgerMenuOpen] = useState(false);
- const [scrollY, setScrollY] = useState(0);
-
- useEffect(() => {
- const onScroll = () => {
- setScrollY(window.scrollY);
- };
-
- window.addEventListener('scroll', onScroll);
-
- return () => window.removeEventListener('scroll', onScroll);
- }, []);
-
- const pathname = usePathname();
-
- const isPathTeamUrl = (teamUrl: string) => {
- if (!pathname || !pathname.startsWith(`/t/`)) {
- return false;
- }
-
- return pathname.split('/')[2] === teamUrl;
- };
-
- const selectedTeam = teams?.find((team) => isPathTeamUrl(team.url));
-
- return (
-
- );
-};
diff --git a/apps/web/src/components/(dashboard)/layout/menu-switcher.tsx b/apps/web/src/components/(dashboard)/layout/menu-switcher.tsx
deleted file mode 100644
index 6731845fc..000000000
--- a/apps/web/src/components/(dashboard)/layout/menu-switcher.tsx
+++ /dev/null
@@ -1,304 +0,0 @@
-'use client';
-
-import { useState } from 'react';
-
-import Link from 'next/link';
-import { usePathname } from 'next/navigation';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { motion } from 'framer-motion';
-import { CheckCircle2, ChevronsUpDown, Plus, Settings2 } from 'lucide-react';
-import { signOut } from 'next-auth/react';
-
-import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
-import { TEAM_MEMBER_ROLE_MAP, TEAM_URL_REGEX } from '@documenso/lib/constants/teams';
-import { isAdmin } from '@documenso/lib/next-auth/guards/is-admin';
-import type { TGetTeamsResponse } from '@documenso/lib/server-only/team/get-teams';
-import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
-import { canExecuteTeamAction } from '@documenso/lib/utils/teams';
-import type { User } from '@documenso/prisma/client';
-import { trpc } from '@documenso/trpc/react';
-import { LanguageSwitcherDialog } from '@documenso/ui/components/common/language-switcher-dialog';
-import { cn } from '@documenso/ui/lib/utils';
-import { AvatarWithText } from '@documenso/ui/primitives/avatar';
-import { Button } from '@documenso/ui/primitives/button';
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuLabel,
- DropdownMenuSeparator,
- DropdownMenuTrigger,
-} from '@documenso/ui/primitives/dropdown-menu';
-
-const MotionLink = motion(Link);
-
-export type MenuSwitcherProps = {
- user: User;
- teams: TGetTeamsResponse;
-};
-
-export const MenuSwitcher = ({ user, teams: initialTeamsData }: MenuSwitcherProps) => {
- const { _ } = useLingui();
-
- const pathname = usePathname();
-
- const [languageSwitcherOpen, setLanguageSwitcherOpen] = useState(false);
-
- const isUserAdmin = isAdmin(user);
-
- const { data: teamsQueryResult } = trpc.team.getTeams.useQuery(undefined, {
- initialData: initialTeamsData,
- });
-
- const teams = teamsQueryResult && teamsQueryResult.length > 0 ? teamsQueryResult : null;
-
- const isPathTeamUrl = (teamUrl: string) => {
- if (!pathname || !pathname.startsWith(`/t/`)) {
- return false;
- }
-
- return pathname.split('/')[2] === teamUrl;
- };
-
- const selectedTeam = teams?.find((team) => isPathTeamUrl(team.url));
-
- const formatAvatarFallback = (teamName?: string) => {
- if (teamName !== undefined) {
- return teamName.slice(0, 1).toUpperCase();
- }
-
- return user.name ? extractInitials(user.name) : user.email.slice(0, 1).toUpperCase();
- };
-
- const formatSecondaryAvatarText = (team?: typeof selectedTeam) => {
- if (!team) {
- return _(msg`Personal Account`);
- }
-
- if (team.ownerUserId === user.id) {
- return _(msg`Owner`);
- }
-
- return _(TEAM_MEMBER_ROLE_MAP[team.currentTeamMember.role]);
- };
-
- /**
- * Formats the redirect URL so we can switch between documents and templates page
- * seemlessly between teams and personal accounts.
- */
- const formatRedirectUrlOnSwitch = (teamUrl?: string) => {
- const baseUrl = teamUrl ? `/t/${teamUrl}/` : '/';
-
- const currentPathname = (pathname ?? '/').replace(TEAM_URL_REGEX, '');
-
- if (currentPathname === '/templates') {
- return `${baseUrl}templates`;
- }
-
- return baseUrl;
- };
-
- return (
-
-
-
-
- }
- textSectionClassName="hidden lg:flex"
- />
-
-
-
-
- {teams ? (
- <>
-
- Personal
-
-
-
-
-
- )
- }
- />
-
-
-
-
-
-
-
-
- Teams
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {teams.map((team) => (
-
-
-
-
- {formatSecondaryAvatarText(team)}
-
-
- {`/t/${team.url}`}
-
- }
- rightSideComponent={
- isPathTeamUrl(team.url) && (
-
- )
- }
- />
-
-
- ))}
-
- >
- ) : (
-
-
- Create team
-
-
-
- )}
-
-
-
- {isUserAdmin && (
-
-
- Admin panel
-
-
- )}
-
-
-
- User settings
-
-
-
- {selectedTeam &&
- canExecuteTeamAction('MANAGE_TEAM', selectedTeam.currentTeamMember.role) && (
-
-
- Team settings
-
-
- )}
-
- setLanguageSwitcherOpen(true)}
- >
- Language
-
-
-
- signOut({
- callbackUrl: '/',
- })
- }
- >
- Sign Out
-
-
-
-
-
- );
-};
diff --git a/apps/web/src/components/(dashboard)/layout/mobile-navigation.tsx b/apps/web/src/components/(dashboard)/layout/mobile-navigation.tsx
deleted file mode 100644
index a118233ff..000000000
--- a/apps/web/src/components/(dashboard)/layout/mobile-navigation.tsx
+++ /dev/null
@@ -1,100 +0,0 @@
-'use client';
-
-import Image from 'next/image';
-import Link from 'next/link';
-import { useParams } from 'next/navigation';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { signOut } from 'next-auth/react';
-
-import LogoImage from '@documenso/assets/logo.png';
-import { getRootHref } from '@documenso/lib/utils/params';
-import { Sheet, SheetContent } from '@documenso/ui/primitives/sheet';
-import { ThemeSwitcher } from '@documenso/ui/primitives/theme-switcher';
-
-export type MobileNavigationProps = {
- isMenuOpen: boolean;
- onMenuOpenChange?: (_value: boolean) => void;
-};
-
-export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigationProps) => {
- const { _ } = useLingui();
-
- const params = useParams();
-
- const handleMenuItemClick = () => {
- onMenuOpenChange?.(false);
- };
-
- const rootHref = getRootHref(params, { returnEmptyRootString: true });
-
- const menuNavigationLinks = [
- {
- href: `${rootHref}/documents`,
- text: msg`Documents`,
- },
- {
- href: `${rootHref}/templates`,
- text: msg`Templates`,
- },
- {
- href: '/settings/teams',
- text: msg`Teams`,
- },
- {
- href: '/settings/profile',
- text: msg`Settings`,
- },
- ];
-
- return (
-
-
-
-
-
-
-
- {menuNavigationLinks.map(({ href, text }) => (
- handleMenuItemClick()}
- >
- {_(text)}
-
- ))}
-
-
- signOut({
- callbackUrl: '/',
- })
- }
- >
- Sign Out
-
-
-
-
-
-
-
-
-
- © {new Date().getFullYear()} Documenso, Inc. All rights reserved.
-
-
-
-
- );
-};
diff --git a/apps/web/src/components/(dashboard)/layout/verify-email-banner.tsx b/apps/web/src/components/(dashboard)/layout/verify-email-banner.tsx
deleted file mode 100644
index 4e16e7a3e..000000000
--- a/apps/web/src/components/(dashboard)/layout/verify-email-banner.tsx
+++ /dev/null
@@ -1,135 +0,0 @@
-'use client';
-
-import { useEffect, useState } from 'react';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { AlertTriangle } from 'lucide-react';
-
-import { ONE_DAY, ONE_SECOND } from '@documenso/lib/constants/time';
-import { trpc } from '@documenso/trpc/react';
-import { Button } from '@documenso/ui/primitives/button';
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogTitle,
-} from '@documenso/ui/primitives/dialog';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-export type VerifyEmailBannerProps = {
- email: string;
-};
-
-const RESEND_CONFIRMATION_EMAIL_TIMEOUT = 20 * ONE_SECOND;
-
-export const VerifyEmailBanner = ({ email }: VerifyEmailBannerProps) => {
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const [isOpen, setIsOpen] = useState(false);
-
- const [isButtonDisabled, setIsButtonDisabled] = useState(false);
-
- const { mutateAsync: sendConfirmationEmail, isPending } =
- trpc.profile.sendConfirmationEmail.useMutation();
-
- const onResendConfirmationEmail = async () => {
- try {
- setIsButtonDisabled(true);
-
- await sendConfirmationEmail({ email: email });
-
- toast({
- title: _(msg`Success`),
- description: _(msg`Verification email sent successfully.`),
- });
-
- setIsOpen(false);
- setTimeout(() => setIsButtonDisabled(false), RESEND_CONFIRMATION_EMAIL_TIMEOUT);
- } catch (err) {
- setIsButtonDisabled(false);
-
- toast({
- title: _(msg`Error`),
- description: _(msg`Something went wrong while sending the confirmation email.`),
- variant: 'destructive',
- });
- }
- };
-
- useEffect(() => {
- // Check localStorage to see if we've recently automatically displayed the dialog
- // if it was within the past 24 hours, don't show it again
- // otherwise, show it again and update the localStorage timestamp
- const emailVerificationDialogLastShown = localStorage.getItem(
- 'emailVerificationDialogLastShown',
- );
-
- if (emailVerificationDialogLastShown) {
- const lastShownTimestamp = parseInt(emailVerificationDialogLastShown);
-
- if (Date.now() - lastShownTimestamp < ONE_DAY) {
- return;
- }
- }
-
- setIsOpen(true);
-
- localStorage.setItem('emailVerificationDialogLastShown', Date.now().toString());
- }, []);
-
- return (
- <>
-
-
-
-
-
Verify your email address to unlock all features.
-
-
-
- setIsOpen(true)}
- size="sm"
- >
- {isButtonDisabled ? (
- Verification Email Sent
- ) : (
- Verify Now
- )}
-
-
-
-
-
-
-
-
- Verify your email address
-
-
-
-
- We've sent a confirmation email to {email} . Please check your inbox
- and click the link in the email to verify your account.
-
-
-
-
-
- {isPending ? Sending... : Resend Confirmation Email }
-
-
-
-
- >
- );
-};
diff --git a/apps/web/src/components/(dashboard)/metric-card/metric-card.tsx b/apps/web/src/components/(dashboard)/metric-card/metric-card.tsx
deleted file mode 100644
index 67ecf17aa..000000000
--- a/apps/web/src/components/(dashboard)/metric-card/metric-card.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import type { LucideIcon } from 'lucide-react/dist/lucide-react';
-
-import { cn } from '@documenso/ui/lib/utils';
-
-export type CardMetricProps = {
- icon?: LucideIcon;
- title: string;
- value: string | number;
- className?: string;
-};
-
-export const CardMetric = ({ icon: Icon, title, value, className }: CardMetricProps) => {
- return (
-
-
-
- {Icon && (
-
-
-
- )}
-
-
- {title}
-
-
-
-
- {typeof value === 'number' ? value.toLocaleString('en-US') : value}
-
-
-
- );
-};
diff --git a/apps/web/src/components/(dashboard)/period-selector/period-selector.tsx b/apps/web/src/components/(dashboard)/period-selector/period-selector.tsx
deleted file mode 100644
index 94285c138..000000000
--- a/apps/web/src/components/(dashboard)/period-selector/period-selector.tsx
+++ /dev/null
@@ -1,69 +0,0 @@
-'use client';
-
-import { useMemo } from 'react';
-
-import { usePathname, useRouter, useSearchParams } from 'next/navigation';
-
-import { Trans } from '@lingui/macro';
-
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from '@documenso/ui/primitives/select';
-
-import { isPeriodSelectorValue } from './types';
-
-export const PeriodSelector = () => {
- const pathname = usePathname();
- const searchParams = useSearchParams();
-
- const router = useRouter();
-
- const period = useMemo(() => {
- const p = searchParams?.get('period') ?? 'all';
-
- return isPeriodSelectorValue(p) ? p : 'all';
- }, [searchParams]);
-
- const onPeriodChange = (newPeriod: string) => {
- if (!pathname) {
- return;
- }
-
- const params = new URLSearchParams(searchParams?.toString());
-
- params.set('period', newPeriod);
-
- if (newPeriod === '' || newPeriod === 'all') {
- params.delete('period');
- }
-
- router.push(`${pathname}?${params.toString()}`, { scroll: false });
- };
-
- return (
-
-
-
-
-
-
-
- All Time
-
-
- Last 7 days
-
-
- Last 14 days
-
-
- Last 30 days
-
-
-
- );
-};
diff --git a/apps/web/src/components/(dashboard)/period-selector/types.ts b/apps/web/src/components/(dashboard)/period-selector/types.ts
deleted file mode 100644
index 8ae1c5fbe..000000000
--- a/apps/web/src/components/(dashboard)/period-selector/types.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import type { PeriodSelectorValue } from '@documenso/lib/server-only/document/find-documents';
-
-export const isPeriodSelectorValue = (value: unknown): value is PeriodSelectorValue => {
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
- return ['', '7d', '14d', '30d'].includes(value as string);
-};
diff --git a/apps/web/src/components/(dashboard)/refresh-on-focus/refresh-on-focus.tsx b/apps/web/src/components/(dashboard)/refresh-on-focus/refresh-on-focus.tsx
deleted file mode 100644
index 1b2f529b8..000000000
--- a/apps/web/src/components/(dashboard)/refresh-on-focus/refresh-on-focus.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-'use client';
-
-import { useCallback, useEffect } from 'react';
-
-import { useRouter } from 'next/navigation';
-
-export const RefreshOnFocus = () => {
- const { refresh } = useRouter();
-
- const onFocus = useCallback(() => {
- refresh();
- }, [refresh]);
-
- useEffect(() => {
- window.addEventListener('focus', onFocus);
-
- return () => {
- window.removeEventListener('focus', onFocus);
- };
- }, [onFocus]);
-
- return null;
-};
diff --git a/apps/web/src/components/(dashboard)/settings/layout/activity-back.tsx b/apps/web/src/components/(dashboard)/settings/layout/activity-back.tsx
deleted file mode 100644
index a7be4d72e..000000000
--- a/apps/web/src/components/(dashboard)/settings/layout/activity-back.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-'use client';
-
-import { useRouter } from 'next/navigation';
-
-import { Trans } from '@lingui/macro';
-
-import { Button } from '@documenso/ui/primitives/button';
-
-export default function ActivityPageBackButton() {
- const router = useRouter();
- return (
-
- {
- void router.back();
- }}
- >
- Back
-
-
- );
-}
diff --git a/apps/web/src/components/(dashboard)/settings/layout/desktop-nav.tsx b/apps/web/src/components/(dashboard)/settings/layout/desktop-nav.tsx
deleted file mode 100644
index 43b1ef988..000000000
--- a/apps/web/src/components/(dashboard)/settings/layout/desktop-nav.tsx
+++ /dev/null
@@ -1,123 +0,0 @@
-'use client';
-
-import type { HTMLAttributes } from 'react';
-
-import Link from 'next/link';
-import { usePathname } from 'next/navigation';
-
-import { Trans } from '@lingui/macro';
-import { Braces, CreditCard, Globe2Icon, Lock, User, Users, Webhook } from 'lucide-react';
-
-import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
-import { cn } from '@documenso/ui/lib/utils';
-import { Button } from '@documenso/ui/primitives/button';
-
-export type DesktopNavProps = HTMLAttributes;
-
-export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
- const pathname = usePathname();
-
- const { getFlag } = useFeatureFlags();
-
- const isBillingEnabled = getFlag('app_billing');
- const isPublicProfileEnabled = getFlag('app_public_profile');
-
- return (
-
-
-
-
- Profile
-
-
-
- {isPublicProfileEnabled && (
-
-
-
- Public Profile
-
-
- )}
-
-
-
-
- Teams
-
-
-
-
-
-
- Security
-
-
-
-
-
-
- API Tokens
-
-
-
-
-
-
- Webhooks
-
-
-
- {isBillingEnabled && (
-
-
-
- Billing
-
-
- )}
-
- );
-};
diff --git a/apps/web/src/components/(dashboard)/settings/layout/header.tsx b/apps/web/src/components/(dashboard)/settings/layout/header.tsx
deleted file mode 100644
index 6f5ae28bc..000000000
--- a/apps/web/src/components/(dashboard)/settings/layout/header.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import React from 'react';
-
-import { cn } from '@documenso/ui/lib/utils';
-
-export type SettingsHeaderProps = {
- title: string;
- subtitle: string;
- hideDivider?: boolean;
- children?: React.ReactNode;
- className?: string;
-};
-
-export const SettingsHeader = ({
- children,
- title,
- subtitle,
- className,
- hideDivider,
-}: SettingsHeaderProps) => {
- return (
- <>
-
-
-
{title}
-
-
{subtitle}
-
-
- {children}
-
-
- {!hideDivider && }
- >
- );
-};
diff --git a/apps/web/src/components/(dashboard)/settings/layout/mobile-nav.tsx b/apps/web/src/components/(dashboard)/settings/layout/mobile-nav.tsx
deleted file mode 100644
index e87d01ba4..000000000
--- a/apps/web/src/components/(dashboard)/settings/layout/mobile-nav.tsx
+++ /dev/null
@@ -1,126 +0,0 @@
-'use client';
-
-import type { HTMLAttributes } from 'react';
-
-import Link from 'next/link';
-import { usePathname } from 'next/navigation';
-
-import { Trans } from '@lingui/macro';
-import { Braces, CreditCard, Globe2Icon, Lock, User, Users, Webhook } from 'lucide-react';
-
-import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
-import { cn } from '@documenso/ui/lib/utils';
-import { Button } from '@documenso/ui/primitives/button';
-
-export type MobileNavProps = HTMLAttributes;
-
-export const MobileNav = ({ className, ...props }: MobileNavProps) => {
- const pathname = usePathname();
-
- const { getFlag } = useFeatureFlags();
-
- const isBillingEnabled = getFlag('app_billing');
- const isPublicProfileEnabled = getFlag('app_public_profile');
-
- return (
-
-
-
-
- Profile
-
-
-
- {isPublicProfileEnabled && (
-
-
-
- Public Profile
-
-
- )}
-
-
-
-
- Teams
-
-
-
-
-
-
- Security
-
-
-
-
-
-
- API Tokens
-
-
-
-
-
-
- Webhooks
-
-
-
- {isBillingEnabled && (
-
-
-
- Billing
-
-
- )}
-
- );
-};
diff --git a/apps/web/src/components/(dashboard)/settings/token/contants.ts b/apps/web/src/components/(dashboard)/settings/token/contants.ts
deleted file mode 100644
index 414425b25..000000000
--- a/apps/web/src/components/(dashboard)/settings/token/contants.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { msg } from '@lingui/macro';
-
-export const EXPIRATION_DATES = {
- ONE_WEEK: msg`7 days`,
- ONE_MONTH: msg`1 month`,
- THREE_MONTHS: msg`3 months`,
- SIX_MONTHS: msg`6 months`,
- ONE_YEAR: msg`12 months`,
-} as const;
diff --git a/apps/web/src/components/(dashboard)/settings/token/delete-token-dialog.tsx b/apps/web/src/components/(dashboard)/settings/token/delete-token-dialog.tsx
deleted file mode 100644
index adaac05b0..000000000
--- a/apps/web/src/components/(dashboard)/settings/token/delete-token-dialog.tsx
+++ /dev/null
@@ -1,196 +0,0 @@
-'use client';
-
-import { useEffect, useState } from 'react';
-
-import { useRouter } from 'next/navigation';
-
-import { zodResolver } from '@hookform/resolvers/zod';
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { useForm } from 'react-hook-form';
-import { z } from 'zod';
-
-import type { ApiToken } from '@documenso/prisma/client';
-import { trpc } from '@documenso/trpc/react';
-import { Button } from '@documenso/ui/primitives/button';
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from '@documenso/ui/primitives/dialog';
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from '@documenso/ui/primitives/form/form';
-import { Input } from '@documenso/ui/primitives/input';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-export type DeleteTokenDialogProps = {
- teamId?: number;
- token: Pick;
- onDelete?: () => void;
- children?: React.ReactNode;
-};
-
-export default function DeleteTokenDialog({
- teamId,
- token,
- onDelete,
- children,
-}: DeleteTokenDialogProps) {
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const router = useRouter();
-
- const [isOpen, setIsOpen] = useState(false);
-
- const deleteMessage = _(msg`delete ${token.name}`);
-
- const ZDeleteTokenDialogSchema = z.object({
- tokenName: z.literal(deleteMessage, {
- errorMap: () => ({ message: _(msg`You must enter '${deleteMessage}' to proceed`) }),
- }),
- });
-
- type TDeleteTokenByIdMutationSchema = z.infer;
-
- const { mutateAsync: deleteTokenMutation } = trpc.apiToken.deleteTokenById.useMutation({
- onSuccess() {
- onDelete?.();
- },
- });
-
- const form = useForm({
- resolver: zodResolver(ZDeleteTokenDialogSchema),
- values: {
- tokenName: '',
- },
- });
-
- const onSubmit = async () => {
- try {
- await deleteTokenMutation({
- id: token.id,
- teamId,
- });
-
- toast({
- title: _(msg`Token deleted`),
- description: _(msg`The token was deleted successfully.`),
- duration: 5000,
- });
-
- setIsOpen(false);
-
- router.refresh();
- } catch (error) {
- toast({
- title: _(msg`An unknown error occurred`),
- description: _(
- msg`We encountered an unknown error while attempting to delete this token. Please try again later.`,
- ),
- variant: 'destructive',
- duration: 5000,
- });
- }
- };
-
- useEffect(() => {
- if (!isOpen) {
- form.reset();
- }
- }, [isOpen, form]);
-
- return (
- !form.formState.isSubmitting && setIsOpen(value)}
- >
-
- {children ?? (
-
- Delete
-
- )}
-
-
-
-
-
- Are you sure you want to delete this token?
-
-
-
-
- Please note that this action is irreversible. Once confirmed, your token will be
- permanently deleted.
-
-
-
-
-
-
-
- (
-
-
-
- Confirm by typing:{' '}
-
- {deleteMessage}
-
-
-
-
-
-
-
-
-
- )}
- />
-
-
-
- setIsOpen(false)}
- >
- Cancel
-
-
-
- I'm sure! Delete it
-
-
-
-
-
-
-
-
- );
-}
diff --git a/apps/web/src/components/(dashboard)/settings/webhooks/create-webhook-dialog.tsx b/apps/web/src/components/(dashboard)/settings/webhooks/create-webhook-dialog.tsx
deleted file mode 100644
index 0d3afd52e..000000000
--- a/apps/web/src/components/(dashboard)/settings/webhooks/create-webhook-dialog.tsx
+++ /dev/null
@@ -1,255 +0,0 @@
-'use client';
-
-import { useState } from 'react';
-
-import { useRouter } from 'next/navigation';
-
-import { zodResolver } from '@hookform/resolvers/zod';
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import type * as DialogPrimitive from '@radix-ui/react-dialog';
-import { useForm } from 'react-hook-form';
-import type { z } from 'zod';
-
-import { trpc } from '@documenso/trpc/react';
-import { ZCreateWebhookMutationSchema } from '@documenso/trpc/server/webhook-router/schema';
-import { Button } from '@documenso/ui/primitives/button';
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from '@documenso/ui/primitives/dialog';
-import {
- Form,
- FormControl,
- FormDescription,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from '@documenso/ui/primitives/form/form';
-import { Input } from '@documenso/ui/primitives/input';
-import { PasswordInput } from '@documenso/ui/primitives/password-input';
-import { Switch } from '@documenso/ui/primitives/switch';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-import { useOptionalCurrentTeam } from '~/providers/team';
-
-import { TriggerMultiSelectCombobox } from './trigger-multiselect-combobox';
-
-const ZCreateWebhookFormSchema = ZCreateWebhookMutationSchema.omit({ teamId: true });
-
-type TCreateWebhookFormSchema = z.infer;
-
-export type CreateWebhookDialogProps = {
- trigger?: React.ReactNode;
-} & Omit;
-
-export const CreateWebhookDialog = ({ trigger, ...props }: CreateWebhookDialogProps) => {
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const router = useRouter();
-
- const team = useOptionalCurrentTeam();
-
- const [open, setOpen] = useState(false);
-
- const form = useForm({
- resolver: zodResolver(ZCreateWebhookFormSchema),
- values: {
- webhookUrl: '',
- eventTriggers: [],
- secret: '',
- enabled: true,
- },
- });
-
- const { mutateAsync: createWebhook } = trpc.webhook.createWebhook.useMutation();
-
- const onSubmit = async ({
- enabled,
- eventTriggers,
- secret,
- webhookUrl,
- }: TCreateWebhookFormSchema) => {
- try {
- await createWebhook({
- enabled,
- eventTriggers,
- secret,
- webhookUrl,
- teamId: team?.id,
- });
-
- setOpen(false);
-
- toast({
- title: _(msg`Webhook created`),
- description: _(msg`The webhook was successfully created.`),
- });
-
- form.reset();
-
- router.refresh();
- } catch (err) {
- toast({
- title: _(msg`Error`),
- description: _(msg`An error occurred while creating the webhook. Please try again.`),
- variant: 'destructive',
- });
- }
- };
-
- return (
- !form.formState.isSubmitting && setOpen(value)}
- {...props}
- >
- e.stopPropagation()} asChild>
- {trigger ?? (
-
- Create Webhook
-
- )}
-
-
-
-
-
- Create webhook
-
-
- On this page, you can create a new webhook.
-
-
-
-
-
-
-
-
- (
-
-
- Triggers
-
-
- {
- onChange(values);
- }}
- />
-
-
-
- The events that will trigger a webhook to be sent to your URL.
-
-
-
-
- )}
- />
-
- (
-
-
- Secret
-
-
-
-
-
-
-
- A secret that will be sent to your URL so you can verify that the request
- has been sent by Documenso
-
- .
-
-
-
- )}
- />
-
-
-
- setOpen(false)}>
- Cancel
-
-
- Create
-
-
-
-
-
-
-
-
- );
-};
diff --git a/apps/web/src/components/(dashboard)/settings/webhooks/delete-webhook-dialog.tsx b/apps/web/src/components/(dashboard)/settings/webhooks/delete-webhook-dialog.tsx
deleted file mode 100644
index 62d9df9bc..000000000
--- a/apps/web/src/components/(dashboard)/settings/webhooks/delete-webhook-dialog.tsx
+++ /dev/null
@@ -1,183 +0,0 @@
-'use effect';
-
-import { useEffect, useState } from 'react';
-
-import { useRouter } from 'next/navigation';
-
-import { zodResolver } from '@hookform/resolvers/zod';
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { useForm } from 'react-hook-form';
-import { z } from 'zod';
-
-import type { Webhook } from '@documenso/prisma/client';
-import { trpc } from '@documenso/trpc/react';
-import { Button } from '@documenso/ui/primitives/button';
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from '@documenso/ui/primitives/dialog';
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from '@documenso/ui/primitives/form/form';
-import { Input } from '@documenso/ui/primitives/input';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-import { useOptionalCurrentTeam } from '~/providers/team';
-
-export type DeleteWebhookDialogProps = {
- webhook: Pick;
- onDelete?: () => void;
- children: React.ReactNode;
-};
-
-export const DeleteWebhookDialog = ({ webhook, children }: DeleteWebhookDialogProps) => {
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const router = useRouter();
-
- const team = useOptionalCurrentTeam();
-
- const [open, setOpen] = useState(false);
-
- const deleteMessage = _(msg`delete ${webhook.webhookUrl}`);
-
- const ZDeleteWebhookFormSchema = z.object({
- webhookUrl: z.literal(deleteMessage, {
- errorMap: () => ({ message: _(msg`You must enter '${deleteMessage}' to proceed`) }),
- }),
- });
-
- type TDeleteWebhookFormSchema = z.infer;
-
- const { mutateAsync: deleteWebhook } = trpc.webhook.deleteWebhook.useMutation();
-
- const form = useForm({
- resolver: zodResolver(ZDeleteWebhookFormSchema),
- values: {
- webhookUrl: '',
- },
- });
-
- const onSubmit = async () => {
- try {
- await deleteWebhook({ id: webhook.id, teamId: team?.id });
-
- toast({
- title: _(msg`Webhook deleted`),
- description: _(msg`The webhook has been successfully deleted.`),
- duration: 5000,
- });
-
- setOpen(false);
-
- router.refresh();
- } catch (error) {
- toast({
- title: _(msg`An unknown error occurred`),
- description: _(
- msg`We encountered an unknown error while attempting to delete it. Please try again later.`,
- ),
- variant: 'destructive',
- duration: 5000,
- });
- }
- };
-
- useEffect(() => {
- if (!open) {
- form.reset();
- }
- }, [open, form]);
-
- return (
- !form.formState.isSubmitting && setOpen(value)}>
-
- {children ?? (
-
- Delete
-
- )}
-
-
-
-
-
- Delete Webhook
-
-
-
-
- Please note that this action is irreversible. Once confirmed, your webhook will be
- permanently deleted.
-
-
-
-
-
-
-
- (
-
-
-
- Confirm by typing:{' '}
-
- {deleteMessage}
-
-
-
-
-
-
-
-
- )}
- />
-
-
-
- setOpen(false)}
- >
- Cancel
-
-
-
- I'm sure! Delete it
-
-
-
-
-
-
-
-
- );
-};
diff --git a/apps/web/src/components/(dashboard)/settings/webhooks/trigger-multiselect-combobox.tsx b/apps/web/src/components/(dashboard)/settings/webhooks/trigger-multiselect-combobox.tsx
deleted file mode 100644
index 5d5f2f682..000000000
--- a/apps/web/src/components/(dashboard)/settings/webhooks/trigger-multiselect-combobox.tsx
+++ /dev/null
@@ -1,96 +0,0 @@
-import { useEffect, useState } from 'react';
-
-import { Plural, Trans } from '@lingui/macro';
-import { WebhookTriggerEvents } from '@prisma/client/';
-import { Check, ChevronsUpDown } from 'lucide-react';
-
-import { toFriendlyWebhookEventName } from '@documenso/lib/universal/webhook/to-friendly-webhook-event-name';
-import { cn } from '@documenso/ui/lib/utils';
-import { Button } from '@documenso/ui/primitives/button';
-import {
- Command,
- CommandEmpty,
- CommandGroup,
- CommandInput,
- CommandItem,
-} from '@documenso/ui/primitives/command';
-import { Popover, PopoverContent, PopoverTrigger } from '@documenso/ui/primitives/popover';
-
-import { truncateTitle } from '~/helpers/truncate-title';
-
-type TriggerMultiSelectComboboxProps = {
- listValues: string[];
- onChange: (_values: string[]) => void;
-};
-
-export const TriggerMultiSelectCombobox = ({
- listValues,
- onChange,
-}: TriggerMultiSelectComboboxProps) => {
- const [isOpen, setIsOpen] = useState(false);
- const [selectedValues, setSelectedValues] = useState([]);
-
- const triggerEvents = Object.values(WebhookTriggerEvents);
-
- useEffect(() => {
- setSelectedValues(listValues);
- }, [listValues]);
-
- const allEvents = [...new Set([...triggerEvents, ...selectedValues])];
-
- const handleSelect = (currentValue: string) => {
- let newSelectedValues;
-
- if (selectedValues.includes(currentValue)) {
- newSelectedValues = selectedValues.filter((value) => value !== currentValue);
- } else {
- newSelectedValues = [...selectedValues, currentValue];
- }
-
- setSelectedValues(newSelectedValues);
- onChange(newSelectedValues);
- setIsOpen(false);
- };
-
- return (
-
-
-
-
-
-
-
-
-
- toFriendlyWebhookEventName(v)).join(', '),
- 15,
- )}
- />
-
- No value found.
-
-
- {allEvents.map((value: string, i: number) => (
- handleSelect(value)}>
-
- {toFriendlyWebhookEventName(value)}
-
- ))}
-
-
-
-
- );
-};
diff --git a/apps/web/src/components/(teams)/dialogs/add-team-email-dialog.tsx b/apps/web/src/components/(teams)/dialogs/add-team-email-dialog.tsx
deleted file mode 100644
index 23c23f751..000000000
--- a/apps/web/src/components/(teams)/dialogs/add-team-email-dialog.tsx
+++ /dev/null
@@ -1,198 +0,0 @@
-'use client';
-
-import { useEffect, useState } from 'react';
-
-import { useRouter } from 'next/navigation';
-
-import { zodResolver } from '@hookform/resolvers/zod';
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import type * as DialogPrimitive from '@radix-ui/react-dialog';
-import { Plus } from 'lucide-react';
-import { useForm } from 'react-hook-form';
-import type { z } from 'zod';
-
-import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
-import { trpc } from '@documenso/trpc/react';
-import { ZCreateTeamEmailVerificationMutationSchema } from '@documenso/trpc/server/team-router/schema';
-import { Button } from '@documenso/ui/primitives/button';
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from '@documenso/ui/primitives/dialog';
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from '@documenso/ui/primitives/form/form';
-import { Input } from '@documenso/ui/primitives/input';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-export type AddTeamEmailDialogProps = {
- teamId: number;
- trigger?: React.ReactNode;
-} & Omit;
-
-const ZCreateTeamEmailFormSchema = ZCreateTeamEmailVerificationMutationSchema.pick({
- name: true,
- email: true,
-});
-
-type TCreateTeamEmailFormSchema = z.infer;
-
-export const AddTeamEmailDialog = ({ teamId, trigger, ...props }: AddTeamEmailDialogProps) => {
- const router = useRouter();
-
- const [open, setOpen] = useState(false);
-
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const form = useForm({
- resolver: zodResolver(ZCreateTeamEmailFormSchema),
- defaultValues: {
- name: '',
- email: '',
- },
- });
-
- const { mutateAsync: createTeamEmailVerification, isPending } =
- trpc.team.createTeamEmailVerification.useMutation();
-
- const onFormSubmit = async ({ name, email }: TCreateTeamEmailFormSchema) => {
- try {
- await createTeamEmailVerification({
- teamId,
- name,
- email,
- });
-
- toast({
- title: _(msg`Success`),
- description: _(msg`We have sent a confirmation email for verification.`),
- duration: 5000,
- });
-
- router.refresh();
-
- setOpen(false);
- } catch (err) {
- const error = AppError.parseError(err);
-
- if (error.code === AppErrorCode.ALREADY_EXISTS) {
- form.setError('email', {
- type: 'manual',
- message: _(msg`This email is already being used by another team.`),
- });
-
- return;
- }
-
- toast({
- title: _(msg`An unknown error occurred`),
- description: _(
- msg`We encountered an unknown error while attempting to add this email. Please try again later.`,
- ),
- variant: 'destructive',
- });
- }
- };
-
- useEffect(() => {
- if (!open) {
- form.reset();
- }
- }, [open, form]);
-
- return (
- !form.formState.isSubmitting && setOpen(value)}
- >
- e.stopPropagation()} asChild={true}>
- {trigger ?? (
-
-
- Add email
-
- )}
-
-
-
-
-
- Add team email
-
-
-
- A verification email will be sent to the provided email.
-
-
-
-
-
-
- (
-
-
- Name
-
-
-
-
-
-
- )}
- />
-
- (
-
-
- Email
-
-
-
-
-
-
- )}
- />
-
-
- setOpen(false)}>
- Cancel
-
-
-
- Add
-
-
-
-
-
-
-
- );
-};
diff --git a/apps/web/src/components/(teams)/dialogs/create-team-checkout-dialog.tsx b/apps/web/src/components/(teams)/dialogs/create-team-checkout-dialog.tsx
deleted file mode 100644
index 9a66c7073..000000000
--- a/apps/web/src/components/(teams)/dialogs/create-team-checkout-dialog.tsx
+++ /dev/null
@@ -1,187 +0,0 @@
-import { useMemo, useState } from 'react';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import type * as DialogPrimitive from '@radix-ui/react-dialog';
-import { AnimatePresence, motion } from 'framer-motion';
-import { Loader, TagIcon } from 'lucide-react';
-
-import { trpc } from '@documenso/trpc/react';
-import { Button } from '@documenso/ui/primitives/button';
-import { Card, CardContent } from '@documenso/ui/primitives/card';
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from '@documenso/ui/primitives/dialog';
-import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-export type CreateTeamCheckoutDialogProps = {
- pendingTeamId: number | null;
- onClose: () => void;
-} & Omit;
-
-const MotionCard = motion(Card);
-
-export const CreateTeamCheckoutDialog = ({
- pendingTeamId,
- onClose,
- ...props
-}: CreateTeamCheckoutDialogProps) => {
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const [interval, setInterval] = useState<'monthly' | 'yearly'>('monthly');
-
- const { data, isLoading } = trpc.team.getTeamPrices.useQuery();
-
- const { mutateAsync: createCheckout, isPending: isCreatingCheckout } =
- trpc.team.createTeamPendingCheckout.useMutation({
- onSuccess: (checkoutUrl) => {
- window.open(checkoutUrl, '_blank');
- onClose();
- },
- onError: () =>
- toast({
- title: _(msg`Something went wrong`),
- description: _(
- msg`We were unable to create a checkout session. Please try again, or contact support`,
- ),
- variant: 'destructive',
- }),
- });
-
- const selectedPrice = useMemo(() => {
- if (!data) {
- return null;
- }
-
- return data[interval];
- }, [data, interval]);
-
- const handleOnOpenChange = (open: boolean) => {
- if (pendingTeamId === null) {
- return;
- }
-
- if (!open) {
- onClose();
- }
- };
-
- if (pendingTeamId === null) {
- return null;
- }
-
- return (
-
-
-
-
- Team checkout
-
-
-
- Payment is required to finalise the creation of your team.
-
-
-
- {(isLoading || !data) && (
-
- {isLoading ? (
-
- ) : (
-
- Something went wrong
-
- )}
-
- )}
-
- {data && selectedPrice && !isLoading && (
-
-
setInterval(value as 'monthly' | 'yearly')}
- value={interval}
- className="mb-4"
- >
-
- {[data.monthly, data.yearly].map((price) => (
-
- {price.friendlyInterval}
-
- ))}
-
-
-
-
-
-
- {selectedPrice.interval === 'monthly' ? (
-
- $50 USD per month
-
- ) : (
-
-
- $480 USD per year
-
-
-
- 20% off
-
-
- )}
-
-
-
- This price includes minimum 5 seats.
-
-
-
- Adding and removing seats will adjust your invoice accordingly.
-
-
-
-
-
-
-
- onClose()}
- >
- Cancel
-
-
-
- createCheckout({
- interval: selectedPrice.interval,
- pendingTeamId,
- })
- }
- >
- Checkout
-
-
-
- )}
-
-
- );
-};
diff --git a/apps/web/src/components/(teams)/dialogs/create-team-dialog.tsx b/apps/web/src/components/(teams)/dialogs/create-team-dialog.tsx
deleted file mode 100644
index b80234a96..000000000
--- a/apps/web/src/components/(teams)/dialogs/create-team-dialog.tsx
+++ /dev/null
@@ -1,235 +0,0 @@
-'use client';
-
-import { useEffect, useState } from 'react';
-
-import { useRouter, useSearchParams } from 'next/navigation';
-
-import { zodResolver } from '@hookform/resolvers/zod';
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import type * as DialogPrimitive from '@radix-ui/react-dialog';
-import { useForm } from 'react-hook-form';
-import type { z } from 'zod';
-
-import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
-import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
-import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
-import { trpc } from '@documenso/trpc/react';
-import { ZCreateTeamMutationSchema } from '@documenso/trpc/server/team-router/schema';
-import { Button } from '@documenso/ui/primitives/button';
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from '@documenso/ui/primitives/dialog';
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from '@documenso/ui/primitives/form/form';
-import { Input } from '@documenso/ui/primitives/input';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-export type CreateTeamDialogProps = {
- trigger?: React.ReactNode;
-} & Omit;
-
-const ZCreateTeamFormSchema = ZCreateTeamMutationSchema.pick({
- teamName: true,
- teamUrl: true,
-});
-
-type TCreateTeamFormSchema = z.infer;
-
-export const CreateTeamDialog = ({ trigger, ...props }: CreateTeamDialogProps) => {
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const router = useRouter();
- const searchParams = useSearchParams();
- const updateSearchParams = useUpdateSearchParams();
-
- const [open, setOpen] = useState(false);
-
- const actionSearchParam = searchParams?.get('action');
-
- const form = useForm({
- resolver: zodResolver(ZCreateTeamFormSchema),
- defaultValues: {
- teamName: '',
- teamUrl: '',
- },
- });
-
- const { mutateAsync: createTeam } = trpc.team.createTeam.useMutation();
-
- const onFormSubmit = async ({ teamName, teamUrl }: TCreateTeamFormSchema) => {
- try {
- const response = await createTeam({
- teamName,
- teamUrl,
- });
-
- setOpen(false);
-
- if (response.paymentRequired) {
- router.push(`/settings/teams?tab=pending&checkout=${response.pendingTeamId}`);
- return;
- }
-
- toast({
- title: _(msg`Success`),
- description: _(msg`Your team has been created.`),
- duration: 5000,
- });
- } catch (err) {
- const error = AppError.parseError(err);
-
- if (error.code === AppErrorCode.ALREADY_EXISTS) {
- form.setError('teamUrl', {
- type: 'manual',
- message: _(msg`This URL is already in use.`),
- });
-
- return;
- }
-
- toast({
- title: _(msg`An unknown error occurred`),
- description: _(
- msg`We encountered an unknown error while attempting to create a team. Please try again later.`,
- ),
- variant: 'destructive',
- });
- }
- };
-
- const mapTextToUrl = (text: string) => {
- return text.toLowerCase().replace(/\s+/g, '-');
- };
-
- useEffect(() => {
- if (actionSearchParam === 'add-team') {
- setOpen(true);
- updateSearchParams({ action: null });
- }
- }, [actionSearchParam, open, setOpen, updateSearchParams]);
-
- useEffect(() => {
- form.reset();
- }, [open, form]);
-
- return (
- !form.formState.isSubmitting && setOpen(value)}
- >
- e.stopPropagation()} asChild={true}>
- {trigger ?? (
-
- Create team
-
- )}
-
-
-
-
-
- Create team
-
-
-
- Create a team to collaborate with your team members.
-
-
-
-
-
-
- (
-
-
- Team Name
-
-
- {
- const oldGeneratedUrl = mapTextToUrl(field.value);
- const newGeneratedUrl = mapTextToUrl(event.target.value);
-
- const urlField = form.getValues('teamUrl');
- if (urlField === oldGeneratedUrl) {
- form.setValue('teamUrl', newGeneratedUrl);
- }
-
- field.onChange(event);
- }}
- />
-
-
-
- )}
- />
-
- (
-
-
- Team URL
-
-
-
-
- {!form.formState.errors.teamUrl && (
-
- {field.value ? (
- `${NEXT_PUBLIC_WEBAPP_URL()}/t/${field.value}`
- ) : (
- A unique URL to identify your team
- )}
-
- )}
-
-
-
- )}
- />
-
-
- setOpen(false)}>
- Cancel
-
-
-
- Create Team
-
-
-
-
-
-
-
- );
-};
diff --git a/apps/web/src/components/(teams)/dialogs/delete-team-dialog.tsx b/apps/web/src/components/(teams)/dialogs/delete-team-dialog.tsx
deleted file mode 100644
index 3377bc989..000000000
--- a/apps/web/src/components/(teams)/dialogs/delete-team-dialog.tsx
+++ /dev/null
@@ -1,176 +0,0 @@
-'use client';
-
-import { useEffect, useState } from 'react';
-
-import { useRouter } from 'next/navigation';
-
-import { zodResolver } from '@hookform/resolvers/zod';
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { useForm } from 'react-hook-form';
-import { z } from 'zod';
-
-import { AppError } from '@documenso/lib/errors/app-error';
-import { trpc } from '@documenso/trpc/react';
-import { Button } from '@documenso/ui/primitives/button';
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from '@documenso/ui/primitives/dialog';
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from '@documenso/ui/primitives/form/form';
-import { Input } from '@documenso/ui/primitives/input';
-import type { Toast } from '@documenso/ui/primitives/use-toast';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-export type DeleteTeamDialogProps = {
- teamId: number;
- teamName: string;
- trigger?: React.ReactNode;
-};
-
-export const DeleteTeamDialog = ({ trigger, teamId, teamName }: DeleteTeamDialogProps) => {
- const router = useRouter();
- const [open, setOpen] = useState(false);
-
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const deleteMessage = _(msg`delete ${teamName}`);
-
- const ZDeleteTeamFormSchema = z.object({
- teamName: z.literal(deleteMessage, {
- errorMap: () => ({ message: _(msg`You must enter '${deleteMessage}' to proceed`) }),
- }),
- });
-
- const form = useForm({
- resolver: zodResolver(ZDeleteTeamFormSchema),
- defaultValues: {
- teamName: '',
- },
- });
-
- const { mutateAsync: deleteTeam } = trpc.team.deleteTeam.useMutation();
-
- const onFormSubmit = async () => {
- try {
- await deleteTeam({ teamId });
-
- toast({
- title: _(msg`Success`),
- description: _(msg`Your team has been successfully deleted.`),
- duration: 5000,
- });
-
- setOpen(false);
-
- router.push('/settings/teams');
- } catch (err) {
- const error = AppError.parseError(err);
-
- let toastError: Toast = {
- title: _(msg`An unknown error occurred`),
- description: _(
- msg`We encountered an unknown error while attempting to delete this team. Please try again later.`,
- ),
- variant: 'destructive',
- duration: 10000,
- };
-
- if (error.code === 'resource_missing') {
- toastError = {
- title: _(msg`Unable to delete team`),
- description: _(
- msg`Something went wrong while updating the team billing subscription, please contact support.`,
- ),
- variant: 'destructive',
- duration: 15000,
- };
- }
-
- toast(toastError);
- }
- };
-
- useEffect(() => {
- if (!open) {
- form.reset();
- }
- }, [open, form]);
-
- return (
- !form.formState.isSubmitting && setOpen(value)}>
-
- {trigger ?? (
-
- Delete team
-
- )}
-
-
-
-
-
- Are you sure you wish to delete this team?
-
-
-
-
- Please note that you will lose access to all documents associated with this team & all
- the members will be removed and notified
-
-
-
-
-
-
-
- (
-
-
-
- Confirm by typing {deleteMessage}
-
-
-
-
-
-
-
- )}
- />
-
-
- setOpen(false)}>
- Cancel
-
-
-
- Delete
-
-
-
-
-
-
-
- );
-};
diff --git a/apps/web/src/components/(teams)/dialogs/delete-team-member-dialog.tsx b/apps/web/src/components/(teams)/dialogs/delete-team-member-dialog.tsx
deleted file mode 100644
index 0036f7386..000000000
--- a/apps/web/src/components/(teams)/dialogs/delete-team-member-dialog.tsx
+++ /dev/null
@@ -1,120 +0,0 @@
-'use client';
-
-import { useState } from 'react';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-
-import { trpc } from '@documenso/trpc/react';
-import { Alert } from '@documenso/ui/primitives/alert';
-import { AvatarWithText } from '@documenso/ui/primitives/avatar';
-import { Button } from '@documenso/ui/primitives/button';
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from '@documenso/ui/primitives/dialog';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-export type DeleteTeamMemberDialogProps = {
- teamId: number;
- teamName: string;
- teamMemberId: number;
- teamMemberName: string;
- teamMemberEmail: string;
- trigger?: React.ReactNode;
-};
-
-export const DeleteTeamMemberDialog = ({
- trigger,
- teamId,
- teamName,
- teamMemberId,
- teamMemberName,
- teamMemberEmail,
-}: DeleteTeamMemberDialogProps) => {
- const [open, setOpen] = useState(false);
-
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const { mutateAsync: deleteTeamMembers, isPending: isDeletingTeamMember } =
- trpc.team.deleteTeamMembers.useMutation({
- onSuccess: () => {
- toast({
- title: _(msg`Success`),
- description: _(msg`You have successfully removed this user from the team.`),
- duration: 5000,
- });
-
- setOpen(false);
- },
- onError: () => {
- toast({
- title: _(msg`An unknown error occurred`),
- description: _(
- msg`We encountered an unknown error while attempting to remove this user. Please try again later.`,
- ),
- variant: 'destructive',
- duration: 10000,
- });
- },
- });
-
- return (
- !isDeletingTeamMember && setOpen(value)}>
-
- {trigger ?? (
-
- Delete team member
-
- )}
-
-
-
-
-
- Are you sure?
-
-
-
-
- You are about to remove the following user from{' '}
- {teamName} .
-
-
-
-
-
- {teamMemberName}}
- secondaryText={teamMemberEmail}
- />
-
-
-
-
- setOpen(false)}>
- Cancel
-
-
- deleteTeamMembers({ teamId, teamMemberIds: [teamMemberId] })}
- >
- Delete
-
-
-
-
-
- );
-};
diff --git a/apps/web/src/components/(teams)/dialogs/invite-team-member-dialog.tsx b/apps/web/src/components/(teams)/dialogs/invite-team-member-dialog.tsx
deleted file mode 100644
index db17a23a7..000000000
--- a/apps/web/src/components/(teams)/dialogs/invite-team-member-dialog.tsx
+++ /dev/null
@@ -1,417 +0,0 @@
-'use client';
-
-import { useEffect, useRef, useState } from 'react';
-
-import { zodResolver } from '@hookform/resolvers/zod';
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import type * as DialogPrimitive from '@radix-ui/react-dialog';
-import { Download, Mail, MailIcon, PlusCircle, Trash, Upload, UsersIcon } from 'lucide-react';
-import Papa, { type ParseResult } from 'papaparse';
-import { useFieldArray, useForm } from 'react-hook-form';
-import { z } from 'zod';
-
-import { downloadFile } from '@documenso/lib/client-only/download-file';
-import { TEAM_MEMBER_ROLE_HIERARCHY, TEAM_MEMBER_ROLE_MAP } from '@documenso/lib/constants/teams';
-import { TeamMemberRole } from '@documenso/prisma/client';
-import { trpc } from '@documenso/trpc/react';
-import { ZCreateTeamMemberInvitesMutationSchema } from '@documenso/trpc/server/team-router/schema';
-import { cn } from '@documenso/ui/lib/utils';
-import { Button } from '@documenso/ui/primitives/button';
-import { Card, CardContent } from '@documenso/ui/primitives/card';
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from '@documenso/ui/primitives/dialog';
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from '@documenso/ui/primitives/form/form';
-import { Input } from '@documenso/ui/primitives/input';
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from '@documenso/ui/primitives/select';
-import { Tabs, TabsContent, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-export type InviteTeamMembersDialogProps = {
- currentUserTeamRole: TeamMemberRole;
- teamId: number;
- trigger?: React.ReactNode;
-} & Omit;
-
-const ZInviteTeamMembersFormSchema = z
- .object({
- invitations: ZCreateTeamMemberInvitesMutationSchema.shape.invitations,
- })
- // Display exactly which rows are duplicates.
- .superRefine((items, ctx) => {
- const uniqueEmails = new Map();
-
- for (const [index, invitation] of items.invitations.entries()) {
- const email = invitation.email.toLowerCase();
-
- const firstFoundIndex = uniqueEmails.get(email);
-
- if (firstFoundIndex === undefined) {
- uniqueEmails.set(email, index);
- continue;
- }
-
- ctx.addIssue({
- code: z.ZodIssueCode.custom,
- message: 'Emails must be unique',
- path: ['invitations', index, 'email'],
- });
-
- ctx.addIssue({
- code: z.ZodIssueCode.custom,
- message: 'Emails must be unique',
- path: ['invitations', firstFoundIndex, 'email'],
- });
- }
- });
-
-type TInviteTeamMembersFormSchema = z.infer;
-
-type TabTypes = 'INDIVIDUAL' | 'BULK';
-
-const ZImportTeamMemberSchema = z.array(
- z.object({
- email: z.string().email(),
- role: z.nativeEnum(TeamMemberRole),
- }),
-);
-
-export const InviteTeamMembersDialog = ({
- currentUserTeamRole,
- teamId,
- trigger,
- ...props
-}: InviteTeamMembersDialogProps) => {
- const [open, setOpen] = useState(false);
- const fileInputRef = useRef(null);
- const [invitationType, setInvitationType] = useState('INDIVIDUAL');
-
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const form = useForm({
- resolver: zodResolver(ZInviteTeamMembersFormSchema),
- defaultValues: {
- invitations: [
- {
- email: '',
- role: TeamMemberRole.MEMBER,
- },
- ],
- },
- });
-
- const {
- append: appendTeamMemberInvite,
- fields: teamMemberInvites,
- remove: removeTeamMemberInvite,
- } = useFieldArray({
- control: form.control,
- name: 'invitations',
- });
-
- const { mutateAsync: createTeamMemberInvites } = trpc.team.createTeamMemberInvites.useMutation();
-
- const onAddTeamMemberInvite = () => {
- appendTeamMemberInvite({
- email: '',
- role: TeamMemberRole.MEMBER,
- });
- };
-
- const onFormSubmit = async ({ invitations }: TInviteTeamMembersFormSchema) => {
- try {
- await createTeamMemberInvites({
- teamId,
- invitations,
- });
-
- toast({
- title: _(msg`Success`),
- description: _(msg`Team invitations have been sent.`),
- duration: 5000,
- });
-
- setOpen(false);
- } catch {
- toast({
- title: _(msg`An unknown error occurred`),
- description: _(
- msg`We encountered an unknown error while attempting to invite team members. Please try again later.`,
- ),
- variant: 'destructive',
- });
- }
- };
-
- useEffect(() => {
- if (!open) {
- form.reset();
- setInvitationType('INDIVIDUAL');
- }
- }, [open, form]);
-
- const onFileInputChange = (e: React.ChangeEvent) => {
- if (!e.target.files?.length) {
- return;
- }
-
- const csvFile = e.target.files[0];
-
- Papa.parse(csvFile, {
- skipEmptyLines: true,
- comments: 'Work email,Job title',
- complete: (results: ParseResult) => {
- const members = results.data.map((row) => {
- const [email, role] = row;
-
- return {
- email: email.trim(),
- role: role.trim().toUpperCase(),
- };
- });
-
- // Remove the first row if it contains the headers.
- if (members.length > 1 && members[0].role.toUpperCase() === 'ROLE') {
- members.shift();
- }
-
- try {
- const importedInvitations = ZImportTeamMemberSchema.parse(members);
-
- form.setValue('invitations', importedInvitations);
- form.clearErrors('invitations');
-
- setInvitationType('INDIVIDUAL');
- } catch (err) {
- console.error(err.message);
-
- toast({
- title: _(msg`Something went wrong`),
- description: _(
- msg`Please check the CSV file and make sure it is according to our format`,
- ),
- variant: 'destructive',
- });
- }
- },
- });
- };
-
- const downloadTemplate = () => {
- const data = [
- { email: 'admin@documenso.com', role: 'Admin' },
- { email: 'manager@documenso.com', role: 'Manager' },
- { email: 'member@documenso.com', role: 'Member' },
- ];
-
- const csvContent =
- 'Email address,Role\n' + data.map((row) => `${row.email},${row.role}`).join('\n');
-
- const blob = new Blob([csvContent], {
- type: 'text/csv',
- });
-
- downloadFile({
- filename: 'documenso-team-member-invites-template.csv',
- data: blob,
- });
- };
-
- return (
- !form.formState.isSubmitting && setOpen(value)}
- >
- e.stopPropagation()} asChild>
- {trigger ?? (
-
- Invite member
-
- )}
-
-
-
-
-
- Invite team members
-
-
-
- An email containing an invitation will be sent to each member.
-
-
-
- setInvitationType(value as TabTypes)}
- >
-
-
-
- Invite Members
-
-
-
- Bulk Import
-
-
-
-
-
-
-
-
- {teamMemberInvites.map((teamMemberInvite, index) => (
-
- (
-
- {index === 0 && (
-
- Email address
-
- )}
-
-
-
-
-
- )}
- />
-
- (
-
- {index === 0 && (
-
- Role
-
- )}
-
-
-
-
-
-
-
- {TEAM_MEMBER_ROLE_HIERARCHY[currentUserTeamRole].map((role) => (
-
- {_(TEAM_MEMBER_ROLE_MAP[role]) ?? role}
-
- ))}
-
-
-
-
-
- )}
- />
-
- removeTeamMemberInvite(index)}
- >
-
-
-
- ))}
-
-
- onAddTeamMemberInvite()}
- >
-
- Add more
-
-
-
- setOpen(false)}>
- Cancel
-
-
-
- {!form.formState.isSubmitting && }
- Invite
-
-
-
-
-
-
-
-
-
-
- fileInputRef.current?.click()}
- >
-
-
-
- Click here to upload
-
-
-
-
-
-
-
-
-
- Template
-
-
-
-
-
-
-
- );
-};
diff --git a/apps/web/src/components/(teams)/dialogs/leave-team-dialog.tsx b/apps/web/src/components/(teams)/dialogs/leave-team-dialog.tsx
deleted file mode 100644
index 3689d5e92..000000000
--- a/apps/web/src/components/(teams)/dialogs/leave-team-dialog.tsx
+++ /dev/null
@@ -1,118 +0,0 @@
-'use client';
-
-import { useState } from 'react';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-
-import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
-import { TEAM_MEMBER_ROLE_MAP } from '@documenso/lib/constants/teams';
-import type { TeamMemberRole } from '@documenso/prisma/client';
-import { trpc } from '@documenso/trpc/react';
-import { Alert } from '@documenso/ui/primitives/alert';
-import { AvatarWithText } from '@documenso/ui/primitives/avatar';
-import { Button } from '@documenso/ui/primitives/button';
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from '@documenso/ui/primitives/dialog';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-export type LeaveTeamDialogProps = {
- teamId: number;
- teamName: string;
- teamAvatarImageId?: string | null;
- role: TeamMemberRole;
- trigger?: React.ReactNode;
-};
-
-export const LeaveTeamDialog = ({
- trigger,
- teamId,
- teamName,
- teamAvatarImageId,
- role,
-}: LeaveTeamDialogProps) => {
- const [open, setOpen] = useState(false);
-
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const { mutateAsync: leaveTeam, isPending: isLeavingTeam } = trpc.team.leaveTeam.useMutation({
- onSuccess: () => {
- toast({
- title: _(msg`Success`),
- description: _(msg`You have successfully left this team.`),
- duration: 5000,
- });
-
- setOpen(false);
- },
- onError: () => {
- toast({
- title: _(msg`An unknown error occurred`),
- description: _(
- msg`We encountered an unknown error while attempting to leave this team. Please try again later.`,
- ),
- variant: 'destructive',
- duration: 10000,
- });
- },
- });
-
- return (
- !isLeavingTeam && setOpen(value)}>
-
- {trigger ?? (
-
- Leave team
-
- )}
-
-
-
-
-
- Are you sure?
-
-
-
- You are about to leave the following team.
-
-
-
-
-
-
-
-
-
- setOpen(false)}>
- Cancel
-
-
- leaveTeam({ teamId })}
- >
- Leave
-
-
-
-
-
- );
-};
diff --git a/apps/web/src/components/(teams)/dialogs/remove-team-email-dialog.tsx b/apps/web/src/components/(teams)/dialogs/remove-team-email-dialog.tsx
deleted file mode 100644
index 0496f923a..000000000
--- a/apps/web/src/components/(teams)/dialogs/remove-team-email-dialog.tsx
+++ /dev/null
@@ -1,166 +0,0 @@
-'use client';
-
-import { useState } from 'react';
-
-import { useRouter } from 'next/navigation';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-
-import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
-import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
-import type { Prisma } from '@documenso/prisma/client';
-import { trpc } from '@documenso/trpc/react';
-import { Alert } from '@documenso/ui/primitives/alert';
-import { AvatarWithText } from '@documenso/ui/primitives/avatar';
-import { Button } from '@documenso/ui/primitives/button';
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from '@documenso/ui/primitives/dialog';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-export type RemoveTeamEmailDialogProps = {
- trigger?: React.ReactNode;
- teamName: string;
- team: Prisma.TeamGetPayload<{
- include: {
- teamEmail: true;
- emailVerification: {
- select: {
- expiresAt: true;
- name: true;
- email: true;
- };
- };
- };
- }>;
-};
-
-export const RemoveTeamEmailDialog = ({ trigger, teamName, team }: RemoveTeamEmailDialogProps) => {
- const [open, setOpen] = useState(false);
-
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const router = useRouter();
-
- const { mutateAsync: deleteTeamEmail, isPending: isDeletingTeamEmail } =
- trpc.team.deleteTeamEmail.useMutation({
- onSuccess: () => {
- toast({
- title: _(msg`Success`),
- description: _(msg`Team email has been removed`),
- duration: 5000,
- });
- },
- onError: () => {
- toast({
- title: _(msg`Something went wrong`),
- description: _(msg`Unable to remove team email at this time. Please try again.`),
- variant: 'destructive',
- duration: 10000,
- });
- },
- });
-
- const { mutateAsync: deleteTeamEmailVerification, isPending: isDeletingTeamEmailVerification } =
- trpc.team.deleteTeamEmailVerification.useMutation({
- onSuccess: () => {
- toast({
- title: _(msg`Success`),
- description: _(msg`Email verification has been removed`),
- duration: 5000,
- });
- },
- onError: () => {
- toast({
- title: _(msg`Something went wrong`),
- description: _(msg`Unable to remove email verification at this time. Please try again.`),
- variant: 'destructive',
- duration: 10000,
- });
- },
- });
-
- const onRemove = async () => {
- if (team.teamEmail) {
- await deleteTeamEmail({ teamId: team.id });
- }
-
- if (team.emailVerification) {
- await deleteTeamEmailVerification({ teamId: team.id });
- }
-
- router.refresh();
- };
-
- return (
- setOpen(value)}>
-
- {trigger ?? (
-
- Remove team email
-
- )}
-
-
-
-
-
- Are you sure?
-
-
-
-
- You are about to delete the following team email from{' '}
- {teamName} .
-
-
-
-
-
-
- {team.teamEmail?.name || team.emailVerification?.name}
-
- }
- secondaryText={
-
- {team.teamEmail?.email || team.emailVerification?.email}
-
- }
- />
-
-
-
-
- setOpen(false)}>
- Cancel
-
-
- onRemove()}
- >
- Remove
-
-
-
-
-
- );
-};
diff --git a/apps/web/src/components/(teams)/dialogs/transfer-team-dialog.tsx b/apps/web/src/components/(teams)/dialogs/transfer-team-dialog.tsx
deleted file mode 100644
index fa991099b..000000000
--- a/apps/web/src/components/(teams)/dialogs/transfer-team-dialog.tsx
+++ /dev/null
@@ -1,274 +0,0 @@
-'use client';
-
-import { useEffect, useState } from 'react';
-
-import { useRouter } from 'next/navigation';
-
-import { zodResolver } from '@hookform/resolvers/zod';
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { Loader } from 'lucide-react';
-import { useForm } from 'react-hook-form';
-import { z } from 'zod';
-
-import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
-import { trpc } from '@documenso/trpc/react';
-import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
-import { Button } from '@documenso/ui/primitives/button';
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from '@documenso/ui/primitives/dialog';
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from '@documenso/ui/primitives/form/form';
-import { Input } from '@documenso/ui/primitives/input';
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from '@documenso/ui/primitives/select';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-export type TransferTeamDialogProps = {
- teamId: number;
- teamName: string;
- ownerUserId: number;
- trigger?: React.ReactNode;
-};
-
-export const TransferTeamDialog = ({
- trigger,
- teamId,
- teamName,
- ownerUserId,
-}: TransferTeamDialogProps) => {
- const router = useRouter();
- const [open, setOpen] = useState(false);
-
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const { mutateAsync: requestTeamOwnershipTransfer } =
- trpc.team.requestTeamOwnershipTransfer.useMutation();
-
- const {
- data,
- refetch: refetchTeamMembers,
- isLoading: loadingTeamMembers,
- isLoadingError: loadingTeamMembersError,
- } = trpc.team.getTeamMembers.useQuery({
- teamId,
- });
-
- const confirmTransferMessage = _(msg`transfer ${teamName}`);
-
- const ZTransferTeamFormSchema = z.object({
- teamName: z.literal(confirmTransferMessage, {
- errorMap: () => ({ message: `You must enter '${confirmTransferMessage}' to proceed` }),
- }),
- newOwnerUserId: z.string(),
- clearPaymentMethods: z.boolean(),
- });
-
- const form = useForm>({
- resolver: zodResolver(ZTransferTeamFormSchema),
- defaultValues: {
- teamName: '',
- clearPaymentMethods: false,
- },
- });
-
- const onFormSubmit = async ({
- newOwnerUserId,
- clearPaymentMethods,
- }: z.infer) => {
- try {
- await requestTeamOwnershipTransfer({
- teamId,
- newOwnerUserId: Number.parseInt(newOwnerUserId),
- clearPaymentMethods,
- });
-
- router.refresh();
-
- toast({
- title: _(msg`Success`),
- description: _(msg`An email requesting the transfer of this team has been sent.`),
- duration: 5000,
- });
-
- setOpen(false);
- } catch (err) {
- toast({
- title: _(msg`An unknown error occurred`),
- description: _(
- msg`We encountered an unknown error while attempting to request a transfer of this team. Please try again later.`,
- ),
- variant: 'destructive',
- duration: 10000,
- });
- }
- };
-
- useEffect(() => {
- if (!open) {
- form.reset();
- }
- }, [open, form]);
-
- useEffect(() => {
- if (open && loadingTeamMembersError) {
- void refetchTeamMembers();
- }
- }, [open, loadingTeamMembersError, refetchTeamMembers]);
-
- const teamMembers = data
- ? data.filter((teamMember) => teamMember.userId !== ownerUserId)
- : undefined;
-
- return (
- !form.formState.isSubmitting && setOpen(value)}>
-
- {trigger ?? (
-
- Transfer team
-
- )}
-
-
- {teamMembers && teamMembers.length > 0 ? (
-
-
-
- Transfer team
-
-
-
- Transfer ownership of this team to a selected team member.
-
-
-
-
-
-
- (
-
-
- New team owner
-
-
-
-
-
-
-
-
- {teamMembers.map((teamMember) => (
-
- {teamMember.user.name} ({teamMember.user.email})
-
- ))}
-
-
-
-
-
- )}
- />
-
- (
-
-
-
- Confirm by typing{' '}
- {confirmTransferMessage}
-
-
-
-
-
-
-
- )}
- />
-
-
-
-
- {IS_BILLING_ENABLED() && (
-
-
- Any payment methods attached to this team will remain attached to this
- team. Please contact us if you need to update this information.
-
-
- )}
-
-
- The selected team member will receive an email which they must accept
- before the team is transferred
-
-
-
-
-
-
-
- setOpen(false)}>
- Cancel
-
-
-
- Request transfer
-
-
-
-
-
-
- ) : (
-
- {loadingTeamMembers ? (
-
- ) : (
-
- {loadingTeamMembersError ? (
- An error occurred while loading team members. Please try again later.
- ) : (
- You must have at least one other team member to transfer ownership.
- )}
-
- )}
-
- )}
-
- );
-};
diff --git a/apps/web/src/components/(teams)/dialogs/update-team-email-dialog.tsx b/apps/web/src/components/(teams)/dialogs/update-team-email-dialog.tsx
deleted file mode 100644
index 0cdfca00e..000000000
--- a/apps/web/src/components/(teams)/dialogs/update-team-email-dialog.tsx
+++ /dev/null
@@ -1,175 +0,0 @@
-'use client';
-
-import { useEffect, useState } from 'react';
-
-import { useRouter } from 'next/navigation';
-
-import { zodResolver } from '@hookform/resolvers/zod';
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import type * as DialogPrimitive from '@radix-ui/react-dialog';
-import { useForm } from 'react-hook-form';
-import { z } from 'zod';
-
-import type { TeamEmail } from '@documenso/prisma/client';
-import { trpc } from '@documenso/trpc/react';
-import { Button } from '@documenso/ui/primitives/button';
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from '@documenso/ui/primitives/dialog';
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from '@documenso/ui/primitives/form/form';
-import { Input } from '@documenso/ui/primitives/input';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-export type UpdateTeamEmailDialogProps = {
- teamEmail: TeamEmail;
- trigger?: React.ReactNode;
-} & Omit;
-
-const ZUpdateTeamEmailFormSchema = z.object({
- name: z.string().trim().min(1, { message: 'Please enter a valid name.' }),
-});
-
-type TUpdateTeamEmailFormSchema = z.infer;
-
-export const UpdateTeamEmailDialog = ({
- teamEmail,
- trigger,
- ...props
-}: UpdateTeamEmailDialogProps) => {
- const router = useRouter();
-
- const [open, setOpen] = useState(false);
-
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const form = useForm({
- resolver: zodResolver(ZUpdateTeamEmailFormSchema),
- defaultValues: {
- name: teamEmail.name,
- },
- });
-
- const { mutateAsync: updateTeamEmail } = trpc.team.updateTeamEmail.useMutation();
-
- const onFormSubmit = async ({ name }: TUpdateTeamEmailFormSchema) => {
- try {
- await updateTeamEmail({
- teamId: teamEmail.teamId,
- data: {
- name,
- },
- });
-
- toast({
- title: _(msg`Success`),
- description: _(msg`Team email was updated.`),
- duration: 5000,
- });
-
- router.refresh();
-
- setOpen(false);
- } catch (err) {
- toast({
- title: _(msg`An unknown error occurred`),
- description: _(
- msg`We encountered an unknown error while attempting update the team email. Please try again later.`,
- ),
- variant: 'destructive',
- });
- }
- };
-
- useEffect(() => {
- if (!open) {
- form.reset();
- }
- }, [open, form]);
-
- return (
- !form.formState.isSubmitting && setOpen(value)}
- >
- e.stopPropagation()} asChild>
- {trigger ?? (
-
- Update team email
-
- )}
-
-
-
-
-
- Update team email
-
-
-
- To change the email you must remove and add a new email address.
-
-
-
-
-
-
- (
-
-
- Name
-
-
-
-
-
-
- )}
- />
-
-
-
- Email
-
-
-
-
-
-
-
- setOpen(false)}>
- Cancel
-
-
-
- Update
-
-
-
-
-
-
-
- );
-};
diff --git a/apps/web/src/components/(teams)/dialogs/update-team-member-dialog.tsx b/apps/web/src/components/(teams)/dialogs/update-team-member-dialog.tsx
deleted file mode 100644
index 88313e2e4..000000000
--- a/apps/web/src/components/(teams)/dialogs/update-team-member-dialog.tsx
+++ /dev/null
@@ -1,200 +0,0 @@
-'use client';
-
-import { useEffect, useState } from 'react';
-
-import { zodResolver } from '@hookform/resolvers/zod';
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import type * as DialogPrimitive from '@radix-ui/react-dialog';
-import { useForm } from 'react-hook-form';
-import { z } from 'zod';
-
-import { TEAM_MEMBER_ROLE_HIERARCHY, TEAM_MEMBER_ROLE_MAP } from '@documenso/lib/constants/teams';
-import { isTeamRoleWithinUserHierarchy } from '@documenso/lib/utils/teams';
-import { TeamMemberRole } from '@documenso/prisma/client';
-import { trpc } from '@documenso/trpc/react';
-import { Button } from '@documenso/ui/primitives/button';
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from '@documenso/ui/primitives/dialog';
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from '@documenso/ui/primitives/form/form';
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from '@documenso/ui/primitives/select';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-export type UpdateTeamMemberDialogProps = {
- currentUserTeamRole: TeamMemberRole;
- trigger?: React.ReactNode;
- teamId: number;
- teamMemberId: number;
- teamMemberName: string;
- teamMemberRole: TeamMemberRole;
-} & Omit;
-
-const ZUpdateTeamMemberFormSchema = z.object({
- role: z.nativeEnum(TeamMemberRole),
-});
-
-type ZUpdateTeamMemberSchema = z.infer;
-
-export const UpdateTeamMemberDialog = ({
- currentUserTeamRole,
- trigger,
- teamId,
- teamMemberId,
- teamMemberName,
- teamMemberRole,
- ...props
-}: UpdateTeamMemberDialogProps) => {
- const [open, setOpen] = useState(false);
-
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const form = useForm({
- resolver: zodResolver(ZUpdateTeamMemberFormSchema),
- defaultValues: {
- role: teamMemberRole,
- },
- });
-
- const { mutateAsync: updateTeamMember } = trpc.team.updateTeamMember.useMutation();
-
- const onFormSubmit = async ({ role }: ZUpdateTeamMemberSchema) => {
- try {
- await updateTeamMember({
- teamId,
- teamMemberId,
- data: {
- role,
- },
- });
-
- toast({
- title: _(msg`Success`),
- description: _(msg`You have updated ${teamMemberName}.`),
- duration: 5000,
- });
-
- setOpen(false);
- } catch {
- toast({
- title: _(msg`An unknown error occurred`),
- description: _(
- msg`We encountered an unknown error while attempting to update this team member. Please try again later.`,
- ),
- variant: 'destructive',
- });
- }
- };
-
- useEffect(() => {
- if (!open) {
- return;
- }
-
- form.reset();
-
- if (!isTeamRoleWithinUserHierarchy(currentUserTeamRole, teamMemberRole)) {
- setOpen(false);
-
- toast({
- title: _(msg`You cannot modify a team member who has a higher role than you.`),
- variant: 'destructive',
- });
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [open, currentUserTeamRole, teamMemberRole, form, toast]);
-
- return (
- !form.formState.isSubmitting && setOpen(value)}
- >
- e.stopPropagation()} asChild>
- {trigger ?? (
-
- Update team member
-
- )}
-
-
-
-
-
- Update team member
-
-
-
-
- You are currently updating {teamMemberName}.
-
-
-
-
-
-
-
- (
-
-
- Role
-
-
-
-
-
-
-
-
- {TEAM_MEMBER_ROLE_HIERARCHY[currentUserTeamRole].map((role) => (
-
- {_(TEAM_MEMBER_ROLE_MAP[role]) ?? role}
-
- ))}
-
-
-
-
-
- )}
- />
-
-
- setOpen(false)}>
- Cancel
-
-
-
- Update
-
-
-
-
-
-
-
- );
-};
diff --git a/apps/web/src/components/(teams)/forms/update-team-form.tsx b/apps/web/src/components/(teams)/forms/update-team-form.tsx
deleted file mode 100644
index f83c01d73..000000000
--- a/apps/web/src/components/(teams)/forms/update-team-form.tsx
+++ /dev/null
@@ -1,182 +0,0 @@
-'use client';
-
-import { useRouter } from 'next/navigation';
-
-import { zodResolver } from '@hookform/resolvers/zod';
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { AnimatePresence, motion } from 'framer-motion';
-import { useForm } from 'react-hook-form';
-import type { z } from 'zod';
-
-import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
-import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
-import { trpc } from '@documenso/trpc/react';
-import { ZUpdateTeamMutationSchema } from '@documenso/trpc/server/team-router/schema';
-import { Button } from '@documenso/ui/primitives/button';
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from '@documenso/ui/primitives/form/form';
-import { Input } from '@documenso/ui/primitives/input';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-export type UpdateTeamDialogProps = {
- teamId: number;
- teamName: string;
- teamUrl: string;
-};
-
-const ZUpdateTeamFormSchema = ZUpdateTeamMutationSchema.shape.data.pick({
- name: true,
- url: true,
-});
-
-type TUpdateTeamFormSchema = z.infer;
-
-export const UpdateTeamForm = ({ teamId, teamName, teamUrl }: UpdateTeamDialogProps) => {
- const router = useRouter();
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const form = useForm({
- resolver: zodResolver(ZUpdateTeamFormSchema),
- defaultValues: {
- name: teamName,
- url: teamUrl,
- },
- });
-
- const { mutateAsync: updateTeam } = trpc.team.updateTeam.useMutation();
-
- const onFormSubmit = async ({ name, url }: TUpdateTeamFormSchema) => {
- try {
- await updateTeam({
- data: {
- name,
- url,
- },
- teamId,
- });
-
- toast({
- title: _(msg`Success`),
- description: _(msg`Your team has been successfully updated.`),
- duration: 5000,
- });
-
- form.reset({
- name,
- url,
- });
-
- if (url !== teamUrl) {
- router.push(`${NEXT_PUBLIC_WEBAPP_URL()}/t/${url}/settings`);
- }
- } catch (err) {
- const error = AppError.parseError(err);
-
- if (error.code === AppErrorCode.ALREADY_EXISTS) {
- form.setError('url', {
- type: 'manual',
- message: _(msg`This URL is already in use.`),
- });
-
- return;
- }
-
- toast({
- title: _(msg`An unknown error occurred`),
- description: _(
- msg`We encountered an unknown error while attempting to update your team. Please try again later.`,
- ),
- variant: 'destructive',
- });
- }
- };
-
- return (
-
-
-
- (
-
-
- Team Name
-
-
-
-
-
-
- )}
- />
-
- (
-
-
- Team URL
-
-
-
-
- {!form.formState.errors.url && (
-
- {field.value ? (
- `${NEXT_PUBLIC_WEBAPP_URL()}/t/${field.value}`
- ) : (
- A unique URL to identify your team
- )}
-
- )}
-
-
-
- )}
- />
-
-
-
- {form.formState.isDirty && (
-
- form.reset()}>
- Reset
-
-
- )}
-
-
-
- Update team
-
-
-
-
-
- );
-};
diff --git a/apps/web/src/components/(teams)/settings/layout/desktop-nav.tsx b/apps/web/src/components/(teams)/settings/layout/desktop-nav.tsx
deleted file mode 100644
index beb31d9b1..000000000
--- a/apps/web/src/components/(teams)/settings/layout/desktop-nav.tsx
+++ /dev/null
@@ -1,129 +0,0 @@
-'use client';
-
-import type { HTMLAttributes } from 'react';
-
-import Link from 'next/link';
-import { useParams, usePathname } from 'next/navigation';
-
-import { Trans } from '@lingui/macro';
-import { Braces, CreditCard, Globe2Icon, Settings, Settings2, Users, Webhook } from 'lucide-react';
-
-import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
-import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
-import { cn } from '@documenso/ui/lib/utils';
-import { Button } from '@documenso/ui/primitives/button';
-
-export type DesktopNavProps = HTMLAttributes;
-
-export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
- const pathname = usePathname();
- const params = useParams();
-
- const { getFlag } = useFeatureFlags();
-
- const isPublicProfileEnabled = getFlag('app_public_profile');
-
- const teamUrl = typeof params?.teamUrl === 'string' ? params?.teamUrl : '';
-
- const settingsPath = `/t/${teamUrl}/settings`;
- const preferencesPath = `/t/${teamUrl}/settings/preferences`;
- const publicProfilePath = `/t/${teamUrl}/settings/public-profile`;
- const membersPath = `/t/${teamUrl}/settings/members`;
- const tokensPath = `/t/${teamUrl}/settings/tokens`;
- const webhooksPath = `/t/${teamUrl}/settings/webhooks`;
- const billingPath = `/t/${teamUrl}/settings/billing`;
-
- return (
-
-
-
-
- General
-
-
-
-
-
-
-
- Preferences
-
-
-
- {isPublicProfileEnabled && (
-
-
-
- Public Profile
-
-
- )}
-
-
-
-
- Members
-
-
-
-
-
-
- API Tokens
-
-
-
-
-
-
- Webhooks
-
-
-
- {IS_BILLING_ENABLED() && (
-
-
-
- Billing
-
-
- )}
-
- );
-};
diff --git a/apps/web/src/components/(teams)/settings/layout/mobile-nav.tsx b/apps/web/src/components/(teams)/settings/layout/mobile-nav.tsx
deleted file mode 100644
index 81c871fdf..000000000
--- a/apps/web/src/components/(teams)/settings/layout/mobile-nav.tsx
+++ /dev/null
@@ -1,138 +0,0 @@
-'use client';
-
-import type { HTMLAttributes } from 'react';
-
-import Link from 'next/link';
-import { useParams, usePathname } from 'next/navigation';
-
-import { Trans } from '@lingui/macro';
-import { Braces, CreditCard, Globe2Icon, Key, Settings2, User, Webhook } from 'lucide-react';
-
-import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
-import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
-import { cn } from '@documenso/ui/lib/utils';
-import { Button } from '@documenso/ui/primitives/button';
-
-export type MobileNavProps = HTMLAttributes;
-
-export const MobileNav = ({ className, ...props }: MobileNavProps) => {
- const pathname = usePathname();
- const params = useParams();
-
- const { getFlag } = useFeatureFlags();
-
- const isPublicProfileEnabled = getFlag('app_public_profile');
-
- const teamUrl = typeof params?.teamUrl === 'string' ? params?.teamUrl : '';
-
- const settingsPath = `/t/${teamUrl}/settings`;
- const preferencesPath = `/t/${teamUrl}/preferences`;
- const publicProfilePath = `/t/${teamUrl}/settings/public-profile`;
- const membersPath = `/t/${teamUrl}/settings/members`;
- const tokensPath = `/t/${teamUrl}/settings/tokens`;
- const webhooksPath = `/t/${teamUrl}/settings/webhooks`;
- const billingPath = `/t/${teamUrl}/settings/billing`;
-
- return (
-
-
-
-
- General
-
-
-
-
-
-
- Preferences
-
-
-
- {isPublicProfileEnabled && (
-
-
-
- Public Profile
-
-
- )}
-
-
-
-
- Members
-
-
-
-
-
-
- API Tokens
-
-
-
-
-
-
- Webhooks
-
-
-
- {IS_BILLING_ENABLED() && (
-
-
-
- Billing
-
-
- )}
-
- );
-};
diff --git a/apps/web/src/components/(teams)/tables/current-user-teams-data-table.tsx b/apps/web/src/components/(teams)/tables/current-user-teams-data-table.tsx
deleted file mode 100644
index 9715d37d1..000000000
--- a/apps/web/src/components/(teams)/tables/current-user-teams-data-table.tsx
+++ /dev/null
@@ -1,170 +0,0 @@
-'use client';
-
-import { useMemo } from 'react';
-
-import Link from 'next/link';
-import { useSearchParams } from 'next/navigation';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-
-import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
-import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
-import { TEAM_MEMBER_ROLE_MAP } from '@documenso/lib/constants/teams';
-import { ZUrlSearchParamsSchema } from '@documenso/lib/types/search-params';
-import { canExecuteTeamAction } from '@documenso/lib/utils/teams';
-import { trpc } from '@documenso/trpc/react';
-import { AvatarWithText } from '@documenso/ui/primitives/avatar';
-import { Button } from '@documenso/ui/primitives/button';
-import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
-import { DataTable } from '@documenso/ui/primitives/data-table';
-import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
-import { Skeleton } from '@documenso/ui/primitives/skeleton';
-import { TableCell } from '@documenso/ui/primitives/table';
-
-import { LeaveTeamDialog } from '../dialogs/leave-team-dialog';
-
-export const CurrentUserTeamsDataTable = () => {
- const { _, i18n } = useLingui();
-
- const searchParams = useSearchParams();
- const updateSearchParams = useUpdateSearchParams();
-
- const parsedSearchParams = ZUrlSearchParamsSchema.parse(Object.fromEntries(searchParams ?? []));
-
- const { data, isLoading, isLoadingError } = trpc.team.findTeams.useQuery(
- {
- query: parsedSearchParams.query,
- page: parsedSearchParams.page,
- perPage: parsedSearchParams.perPage,
- },
- {
- placeholderData: (previousData) => previousData,
- },
- );
-
- const onPaginationChange = (page: number, perPage: number) => {
- updateSearchParams({
- page,
- perPage,
- });
- };
-
- const results = data ?? {
- data: [],
- perPage: 10,
- currentPage: 1,
- totalPages: 1,
- };
-
- const columns = useMemo(() => {
- return [
- {
- header: _(msg`Team`),
- accessorKey: 'name',
- cell: ({ row }) => (
-
- {row.original.name}
- }
- secondaryText={`${NEXT_PUBLIC_WEBAPP_URL()}/t/${row.original.url}`}
- />
-
- ),
- },
- {
- header: _(msg`Role`),
- accessorKey: 'role',
- cell: ({ row }) =>
- row.original.ownerUserId === row.original.currentTeamMember.userId
- ? _(msg`Owner`)
- : _(TEAM_MEMBER_ROLE_MAP[row.original.currentTeamMember.role]),
- },
- {
- header: _(msg`Member Since`),
- accessorKey: 'createdAt',
- cell: ({ row }) => i18n.date(row.original.createdAt),
- },
- {
- id: 'actions',
- cell: ({ row }) => (
-
- {canExecuteTeamAction('MANAGE_TEAM', row.original.currentTeamMember.role) && (
-
-
- Manage
-
-
- )}
-
- e.preventDefault()}
- >
- Leave
-
- }
- />
-
- ),
- },
- ] satisfies DataTableColumnDef<(typeof results)['data'][number]>[];
- }, []);
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- >
- ),
- }}
- >
- {(table) => }
-
- );
-};
diff --git a/apps/web/src/components/(teams)/tables/pending-user-teams-data-table-actions.tsx b/apps/web/src/components/(teams)/tables/pending-user-teams-data-table-actions.tsx
deleted file mode 100644
index 57c77dbe0..000000000
--- a/apps/web/src/components/(teams)/tables/pending-user-teams-data-table-actions.tsx
+++ /dev/null
@@ -1,58 +0,0 @@
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-
-import { trpc } from '@documenso/trpc/react';
-import { cn } from '@documenso/ui/lib/utils';
-import { Button } from '@documenso/ui/primitives/button';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-export type PendingUserTeamsDataTableActionsProps = {
- className?: string;
- pendingTeamId: number;
- onPayClick: (pendingTeamId: number) => void;
-};
-
-export const PendingUserTeamsDataTableActions = ({
- className,
- pendingTeamId,
- onPayClick,
-}: PendingUserTeamsDataTableActionsProps) => {
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const { mutateAsync: deleteTeamPending, isPending: deletingTeam } =
- trpc.team.deleteTeamPending.useMutation({
- onSuccess: () => {
- toast({
- title: _(msg`Success`),
- description: _(msg`Pending team deleted.`),
- });
- },
- onError: () => {
- toast({
- title: _(msg`Something went wrong`),
- description: _(
- msg`We encountered an unknown error while attempting to delete the pending team. Please try again later.`,
- ),
- duration: 10000,
- variant: 'destructive',
- });
- },
- });
-
- return (
-
- onPayClick(pendingTeamId)}>
- Pay
-
-
- deleteTeamPending({ pendingTeamId: pendingTeamId })}
- >
- Remove
-
-
- );
-};
diff --git a/apps/web/src/components/(teams)/tables/pending-user-teams-data-table.tsx b/apps/web/src/components/(teams)/tables/pending-user-teams-data-table.tsx
deleted file mode 100644
index 81638a434..000000000
--- a/apps/web/src/components/(teams)/tables/pending-user-teams-data-table.tsx
+++ /dev/null
@@ -1,151 +0,0 @@
-'use client';
-
-import { useEffect, useMemo, useState } from 'react';
-
-import { useSearchParams } from 'next/navigation';
-
-import { msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-
-import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
-import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
-import { ZUrlSearchParamsSchema } from '@documenso/lib/types/search-params';
-import { trpc } from '@documenso/trpc/react';
-import { AvatarWithText } from '@documenso/ui/primitives/avatar';
-import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
-import { DataTable } from '@documenso/ui/primitives/data-table';
-import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
-import { Skeleton } from '@documenso/ui/primitives/skeleton';
-import { TableCell } from '@documenso/ui/primitives/table';
-
-import { CreateTeamCheckoutDialog } from '../dialogs/create-team-checkout-dialog';
-import { PendingUserTeamsDataTableActions } from './pending-user-teams-data-table-actions';
-
-export const PendingUserTeamsDataTable = () => {
- const { _, i18n } = useLingui();
-
- const searchParams = useSearchParams();
- const updateSearchParams = useUpdateSearchParams();
-
- const parsedSearchParams = ZUrlSearchParamsSchema.parse(Object.fromEntries(searchParams ?? []));
-
- const [checkoutPendingTeamId, setCheckoutPendingTeamId] = useState(null);
-
- const { data, isLoading, isLoadingError } = trpc.team.findTeamsPending.useQuery(
- {
- query: parsedSearchParams.query,
- page: parsedSearchParams.page,
- perPage: parsedSearchParams.perPage,
- },
- {
- placeholderData: (previousData) => previousData,
- },
- );
-
- const onPaginationChange = (page: number, perPage: number) => {
- updateSearchParams({
- page,
- perPage,
- });
- };
-
- const results = data ?? {
- data: [],
- perPage: 10,
- currentPage: 1,
- totalPages: 1,
- };
-
- const columns = useMemo(() => {
- return [
- {
- header: _(msg`Team`),
- accessorKey: 'name',
- cell: ({ row }) => (
- {row.original.name}
- }
- secondaryText={`${NEXT_PUBLIC_WEBAPP_URL()}/t/${row.original.url}`}
- />
- ),
- },
- {
- header: _(msg`Created on`),
- accessorKey: 'createdAt',
- cell: ({ row }) => i18n.date(row.original.createdAt),
- },
- {
- id: 'actions',
- cell: ({ row }) => (
-
- ),
- },
- ] satisfies DataTableColumnDef<(typeof results)['data'][number]>[];
- }, []);
-
- useEffect(() => {
- const searchParamCheckout = searchParams?.get('checkout');
-
- if (searchParamCheckout && !isNaN(parseInt(searchParamCheckout))) {
- setCheckoutPendingTeamId(parseInt(searchParamCheckout));
- updateSearchParams({ checkout: null });
- }
- }, [searchParams, updateSearchParams]);
-
- return (
- <>
-
-
-
-
-
-
-
-
-
-
-
-
-
- >
- ),
- }}
- >
- {(table) => }
-
-
- setCheckoutPendingTeamId(null)}
- />
- >
- );
-};
diff --git a/apps/web/src/components/(teams)/tables/team-billing-invoices-data-table.tsx b/apps/web/src/components/(teams)/tables/team-billing-invoices-data-table.tsx
deleted file mode 100644
index 81f5c1c49..000000000
--- a/apps/web/src/components/(teams)/tables/team-billing-invoices-data-table.tsx
+++ /dev/null
@@ -1,163 +0,0 @@
-'use client';
-
-import { useMemo } from 'react';
-
-import Link from 'next/link';
-
-import { Plural, Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { File } from 'lucide-react';
-import { DateTime } from 'luxon';
-
-import { trpc } from '@documenso/trpc/react';
-import { Button } from '@documenso/ui/primitives/button';
-import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
-import { DataTable } from '@documenso/ui/primitives/data-table';
-import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
-import { Skeleton } from '@documenso/ui/primitives/skeleton';
-import { TableCell } from '@documenso/ui/primitives/table';
-
-export type TeamBillingInvoicesDataTableProps = {
- teamId: number;
-};
-
-export const TeamBillingInvoicesDataTable = ({ teamId }: TeamBillingInvoicesDataTableProps) => {
- const { _ } = useLingui();
-
- const { data, isLoading, isLoadingError } = trpc.team.findTeamInvoices.useQuery(
- {
- teamId,
- },
- {
- placeholderData: (previousData) => previousData,
- },
- );
-
- const formatCurrency = (currency: string, amount: number) => {
- const formatter = new Intl.NumberFormat('en-US', {
- style: 'currency',
- currency,
- });
-
- return formatter.format(amount);
- };
-
- const results = {
- data: data?.data ?? [],
- perPage: 100,
- currentPage: 1,
- totalPages: 1,
- };
-
- const columns = useMemo(() => {
- return [
- {
- header: _(msg`Invoice`),
- accessorKey: 'created',
- cell: ({ row }) => (
-
-
-
-
-
- {DateTime.fromSeconds(row.original.created).toFormat('MMMM yyyy')}
-
-
-
-
-
-
- ),
- },
- {
- header: _(msg`Status`),
- accessorKey: 'status',
- cell: ({ row }) => {
- const { status, paid } = row.original;
-
- if (!status) {
- return paid ? Paid : Unpaid ;
- }
-
- return status.charAt(0).toUpperCase() + status.slice(1);
- },
- },
- {
- header: _(msg`Amount`),
- accessorKey: 'total',
- cell: ({ row }) => formatCurrency(row.original.currency, row.original.total / 100),
- },
- {
- id: 'actions',
- cell: ({ row }) => (
-
-
-
- View
-
-
-
-
-
- Download
-
-
-
- ),
- },
- ] satisfies DataTableColumnDef<(typeof results)['data'][number]>[];
- }, []);
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- >
- ),
- }}
- >
- {(table) => }
-
- );
-};
diff --git a/apps/web/src/components/(teams)/tables/team-member-invites-data-table.tsx b/apps/web/src/components/(teams)/tables/team-member-invites-data-table.tsx
deleted file mode 100644
index 8a57be81c..000000000
--- a/apps/web/src/components/(teams)/tables/team-member-invites-data-table.tsx
+++ /dev/null
@@ -1,210 +0,0 @@
-'use client';
-
-import { useMemo } from 'react';
-
-import { useSearchParams } from 'next/navigation';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { History, MoreHorizontal, Trash2 } from 'lucide-react';
-
-import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
-import { TEAM_MEMBER_ROLE_MAP } from '@documenso/lib/constants/teams';
-import { ZUrlSearchParamsSchema } from '@documenso/lib/types/search-params';
-import { trpc } from '@documenso/trpc/react';
-import { AvatarWithText } from '@documenso/ui/primitives/avatar';
-import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
-import { DataTable } from '@documenso/ui/primitives/data-table';
-import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuLabel,
- DropdownMenuTrigger,
-} from '@documenso/ui/primitives/dropdown-menu';
-import { Skeleton } from '@documenso/ui/primitives/skeleton';
-import { TableCell } from '@documenso/ui/primitives/table';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-export type TeamMemberInvitesDataTableProps = {
- teamId: number;
-};
-
-export const TeamMemberInvitesDataTable = ({ teamId }: TeamMemberInvitesDataTableProps) => {
- const searchParams = useSearchParams();
- const updateSearchParams = useUpdateSearchParams();
-
- const { _, i18n } = useLingui();
- const { toast } = useToast();
-
- const parsedSearchParams = ZUrlSearchParamsSchema.parse(Object.fromEntries(searchParams ?? []));
-
- const { data, isLoading, isLoadingError } = trpc.team.findTeamMemberInvites.useQuery(
- {
- teamId,
- query: parsedSearchParams.query,
- page: parsedSearchParams.page,
- perPage: parsedSearchParams.perPage,
- },
- {
- placeholderData: (previousData) => previousData,
- },
- );
-
- const { mutateAsync: resendTeamMemberInvitation } =
- trpc.team.resendTeamMemberInvitation.useMutation({
- onSuccess: () => {
- toast({
- title: _(msg`Success`),
- description: _(msg`Invitation has been resent`),
- });
- },
- onError: () => {
- toast({
- title: _(msg`Something went wrong`),
- description: _(msg`Unable to resend invitation. Please try again.`),
- variant: 'destructive',
- });
- },
- });
-
- const { mutateAsync: deleteTeamMemberInvitations } =
- trpc.team.deleteTeamMemberInvitations.useMutation({
- onSuccess: () => {
- toast({
- title: _(msg`Success`),
- description: _(msg`Invitation has been deleted`),
- });
- },
- onError: () => {
- toast({
- title: _(msg`Something went wrong`),
- description: _(msg`Unable to delete invitation. Please try again.`),
- variant: 'destructive',
- });
- },
- });
-
- const onPaginationChange = (page: number, perPage: number) => {
- updateSearchParams({
- page,
- perPage,
- });
- };
-
- const results = data ?? {
- data: [],
- perPage: 10,
- currentPage: 1,
- totalPages: 1,
- };
-
- const columns = useMemo(() => {
- return [
- {
- header: _(msg`Team Member`),
- cell: ({ row }) => {
- return (
- {row.original.email}
- }
- />
- );
- },
- },
- {
- header: _(msg`Role`),
- accessorKey: 'role',
- cell: ({ row }) => _(TEAM_MEMBER_ROLE_MAP[row.original.role]) ?? row.original.role,
- },
- {
- header: _(msg`Invited At`),
- accessorKey: 'createdAt',
- cell: ({ row }) => i18n.date(row.original.createdAt),
- },
- {
- header: _(msg`Actions`),
- cell: ({ row }) => (
-
-
-
-
-
-
-
- Actions
-
-
-
- resendTeamMemberInvitation({
- teamId,
- invitationId: row.original.id,
- })
- }
- >
-
- Resend
-
-
-
- deleteTeamMemberInvitations({
- teamId,
- invitationIds: [row.original.id],
- })
- }
- >
-
- Remove
-
-
-
- ),
- },
- ] satisfies DataTableColumnDef<(typeof results)['data'][number]>[];
- }, []);
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- >
- ),
- }}
- >
- {(table) => }
-
- );
-};
diff --git a/apps/web/src/components/(teams)/tables/team-members-data-table.tsx b/apps/web/src/components/(teams)/tables/team-members-data-table.tsx
deleted file mode 100644
index e92efb727..000000000
--- a/apps/web/src/components/(teams)/tables/team-members-data-table.tsx
+++ /dev/null
@@ -1,218 +0,0 @@
-'use client';
-
-import { useMemo } from 'react';
-
-import { useSearchParams } from 'next/navigation';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { Edit, MoreHorizontal, Trash2 } from 'lucide-react';
-
-import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
-import { TEAM_MEMBER_ROLE_MAP } from '@documenso/lib/constants/teams';
-import { ZUrlSearchParamsSchema } from '@documenso/lib/types/search-params';
-import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
-import { isTeamRoleWithinUserHierarchy } from '@documenso/lib/utils/teams';
-import type { TeamMemberRole } from '@documenso/prisma/client';
-import { trpc } from '@documenso/trpc/react';
-import { AvatarWithText } from '@documenso/ui/primitives/avatar';
-import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
-import { DataTable } from '@documenso/ui/primitives/data-table';
-import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuLabel,
- DropdownMenuTrigger,
-} from '@documenso/ui/primitives/dropdown-menu';
-import { Skeleton } from '@documenso/ui/primitives/skeleton';
-import { TableCell } from '@documenso/ui/primitives/table';
-
-import { DeleteTeamMemberDialog } from '../dialogs/delete-team-member-dialog';
-import { UpdateTeamMemberDialog } from '../dialogs/update-team-member-dialog';
-
-export type TeamMembersDataTableProps = {
- currentUserTeamRole: TeamMemberRole;
- teamOwnerUserId: number;
- teamId: number;
- teamName: string;
-};
-
-export const TeamMembersDataTable = ({
- currentUserTeamRole,
- teamOwnerUserId,
- teamId,
- teamName,
-}: TeamMembersDataTableProps) => {
- const { _, i18n } = useLingui();
-
- const searchParams = useSearchParams();
- const updateSearchParams = useUpdateSearchParams();
-
- const parsedSearchParams = ZUrlSearchParamsSchema.parse(Object.fromEntries(searchParams ?? []));
-
- const { data, isLoading, isLoadingError } = trpc.team.findTeamMembers.useQuery(
- {
- teamId,
- query: parsedSearchParams.query,
- page: parsedSearchParams.page,
- perPage: parsedSearchParams.perPage,
- },
- {
- placeholderData: (previousData) => previousData,
- },
- );
-
- const onPaginationChange = (page: number, perPage: number) => {
- updateSearchParams({
- page,
- perPage,
- });
- };
-
- const results = data ?? {
- data: [],
- perPage: 10,
- currentPage: 1,
- totalPages: 1,
- };
-
- const columns = useMemo(() => {
- return [
- {
- header: _(msg`Team Member`),
- cell: ({ row }) => {
- const avatarFallbackText = row.original.user.name
- ? extractInitials(row.original.user.name)
- : row.original.user.email.slice(0, 1).toUpperCase();
-
- return (
- {row.original.user.name}
- }
- secondaryText={row.original.user.email}
- />
- );
- },
- },
- {
- header: _(msg`Role`),
- accessorKey: 'role',
- cell: ({ row }) =>
- teamOwnerUserId === row.original.userId
- ? _(msg`Owner`)
- : _(TEAM_MEMBER_ROLE_MAP[row.original.role]),
- },
- {
- header: _(msg`Member Since`),
- accessorKey: 'createdAt',
- cell: ({ row }) => i18n.date(row.original.createdAt),
- },
- {
- header: _(msg`Actions`),
- cell: ({ row }) => (
-
-
-
-
-
-
-
- Actions
-
-
- e.preventDefault()}
- title="Update team member role"
- >
-
- Update role
-
- }
- />
-
- e.preventDefault()}
- disabled={
- teamOwnerUserId === row.original.userId ||
- !isTeamRoleWithinUserHierarchy(currentUserTeamRole, row.original.role)
- }
- title={_(msg`Remove team member`)}
- >
-
- Remove
-
- }
- />
-
-
- ),
- },
- ] satisfies DataTableColumnDef<(typeof results)['data'][number]>[];
- }, []);
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
- >
- ),
- }}
- >
- {(table) => }
-
- );
-};
diff --git a/apps/web/src/components/(teams)/tables/teams-member-page-data-table.tsx b/apps/web/src/components/(teams)/tables/teams-member-page-data-table.tsx
deleted file mode 100644
index 496427f39..000000000
--- a/apps/web/src/components/(teams)/tables/teams-member-page-data-table.tsx
+++ /dev/null
@@ -1,102 +0,0 @@
-'use client';
-
-import { useEffect, useState } from 'react';
-
-import Link from 'next/link';
-import { usePathname, useRouter, useSearchParams } from 'next/navigation';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-
-import { useDebouncedValue } from '@documenso/lib/client-only/hooks/use-debounced-value';
-import type { TeamMemberRole } from '@documenso/prisma/client';
-import { Input } from '@documenso/ui/primitives/input';
-import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
-
-import { TeamMemberInvitesDataTable } from '~/components/(teams)/tables/team-member-invites-data-table';
-import { TeamMembersDataTable } from '~/components/(teams)/tables/team-members-data-table';
-
-export type TeamsMemberPageDataTableProps = {
- currentUserTeamRole: TeamMemberRole;
- teamId: number;
- teamName: string;
- teamOwnerUserId: number;
-};
-
-export const TeamsMemberPageDataTable = ({
- currentUserTeamRole,
- teamId,
- teamName,
- teamOwnerUserId,
-}: TeamsMemberPageDataTableProps) => {
- const { _ } = useLingui();
-
- const searchParams = useSearchParams();
- const router = useRouter();
- const pathname = usePathname();
-
- const [searchQuery, setSearchQuery] = useState(() => searchParams?.get('query') ?? '');
-
- const debouncedSearchQuery = useDebouncedValue(searchQuery, 500);
-
- const currentTab = searchParams?.get('tab') === 'invites' ? 'invites' : 'members';
-
- /**
- * Handle debouncing the search query.
- */
- useEffect(() => {
- if (!pathname) {
- return;
- }
-
- const params = new URLSearchParams(searchParams?.toString());
-
- params.set('query', debouncedSearchQuery);
-
- if (debouncedSearchQuery === '') {
- params.delete('query');
- }
-
- router.push(`${pathname}?${params.toString()}`);
- }, [debouncedSearchQuery, pathname, router, searchParams]);
-
- return (
-
-
- setSearchQuery(e.target.value)}
- placeholder={_(msg`Search`)}
- />
-
-
-
-
-
- Active
-
-
-
-
-
- Pending
-
-
-
-
-
-
- {currentTab === 'invites' ? (
-
- ) : (
-
- )}
-
- );
-};
diff --git a/apps/web/src/components/(teams)/tables/user-settings-teams-page-data-table.tsx b/apps/web/src/components/(teams)/tables/user-settings-teams-page-data-table.tsx
deleted file mode 100644
index bac1dbf44..000000000
--- a/apps/web/src/components/(teams)/tables/user-settings-teams-page-data-table.tsx
+++ /dev/null
@@ -1,90 +0,0 @@
-'use client';
-
-import { useEffect, useState } from 'react';
-
-import Link from 'next/link';
-import { usePathname, useRouter, useSearchParams } from 'next/navigation';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-
-import { useDebouncedValue } from '@documenso/lib/client-only/hooks/use-debounced-value';
-import { trpc } from '@documenso/trpc/react';
-import { Input } from '@documenso/ui/primitives/input';
-import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
-
-import { CurrentUserTeamsDataTable } from './current-user-teams-data-table';
-import { PendingUserTeamsDataTable } from './pending-user-teams-data-table';
-
-export const UserSettingsTeamsPageDataTable = () => {
- const { _ } = useLingui();
-
- const searchParams = useSearchParams();
- const router = useRouter();
- const pathname = usePathname();
-
- const [searchQuery, setSearchQuery] = useState(() => searchParams?.get('query') ?? '');
-
- const debouncedSearchQuery = useDebouncedValue(searchQuery, 500);
-
- const currentTab = searchParams?.get('tab') === 'pending' ? 'pending' : 'active';
-
- const { data } = trpc.team.findTeamsPending.useQuery(
- {},
- {
- placeholderData: (previousData) => previousData,
- },
- );
-
- /**
- * Handle debouncing the search query.
- */
- useEffect(() => {
- if (!pathname) {
- return;
- }
-
- const params = new URLSearchParams(searchParams?.toString());
-
- params.set('query', debouncedSearchQuery);
-
- if (debouncedSearchQuery === '') {
- params.delete('query');
- }
-
- router.push(`${pathname}?${params.toString()}`);
- }, [debouncedSearchQuery, pathname, router, searchParams]);
-
- return (
-
-
- setSearchQuery(e.target.value)}
- placeholder={_(msg`Search`)}
- />
-
-
-
-
-
- Active
-
-
-
-
-
- Pending
- {data && data.count > 0 && (
- {data.count}
- )}
-
-
-
-
-
-
- {currentTab === 'pending' ?
:
}
-
- );
-};
diff --git a/apps/web/src/components/(teams)/team-billing-portal-button.tsx b/apps/web/src/components/(teams)/team-billing-portal-button.tsx
deleted file mode 100644
index 7ef4aad29..000000000
--- a/apps/web/src/components/(teams)/team-billing-portal-button.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-'use client';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-
-import { trpc } from '@documenso/trpc/react';
-import { Button } from '@documenso/ui/primitives/button';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-export type TeamBillingPortalButtonProps = {
- buttonProps?: React.ComponentProps;
- teamId: number;
-};
-
-export const TeamBillingPortalButton = ({ buttonProps, teamId }: TeamBillingPortalButtonProps) => {
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const { mutateAsync: createBillingPortal, isPending } =
- trpc.team.createBillingPortal.useMutation();
-
- const handleCreatePortal = async () => {
- try {
- const sessionUrl = await createBillingPortal({ teamId });
-
- window.open(sessionUrl, '_blank');
- } catch (err) {
- toast({
- title: _(msg`Something went wrong`),
- description: _(
- msg`We are unable to proceed to the billing portal at this time. Please try again, or contact support.`,
- ),
- variant: 'destructive',
- duration: 10000,
- });
- }
- };
-
- return (
- handleCreatePortal()} loading={isPending}>
- Manage subscription
-
- );
-};
diff --git a/apps/web/src/components/branding/logo.tsx b/apps/web/src/components/branding/logo.tsx
deleted file mode 100644
index 92087a149..000000000
--- a/apps/web/src/components/branding/logo.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import type { SVGAttributes } from 'react';
-
-export type LogoProps = SVGAttributes;
-
-export const Logo = ({ ...props }: LogoProps) => {
- return (
-
-
-
-
-
-
-
- );
-};
diff --git a/apps/web/src/components/document/document-history-sheet-changes.tsx b/apps/web/src/components/document/document-history-sheet-changes.tsx
deleted file mode 100644
index ef3985a61..000000000
--- a/apps/web/src/components/document/document-history-sheet-changes.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-'use client';
-
-import React from 'react';
-
-import { Badge } from '@documenso/ui/primitives/badge';
-
-export type DocumentHistorySheetChangesProps = {
- values: {
- key: string | React.ReactNode;
- value: string | React.ReactNode;
- }[];
-};
-
-export const DocumentHistorySheetChanges = ({ values }: DocumentHistorySheetChangesProps) => {
- return (
-
- {values.map(({ key, value }, i) => (
-
- {key}:
- {value}
-
- ))}
-
- );
-};
diff --git a/apps/web/src/components/document/document-history-sheet.tsx b/apps/web/src/components/document/document-history-sheet.tsx
deleted file mode 100644
index 8bda3a424..000000000
--- a/apps/web/src/components/document/document-history-sheet.tsx
+++ /dev/null
@@ -1,390 +0,0 @@
-'use client';
-
-import { useMemo, useState } from 'react';
-
-import { Trans } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { ArrowRightIcon, Loader } from 'lucide-react';
-import { DateTime } from 'luxon';
-import { match } from 'ts-pattern';
-import { UAParser } from 'ua-parser-js';
-
-import { DOCUMENT_AUDIT_LOG_EMAIL_FORMAT } from '@documenso/lib/constants/document-audit-logs';
-import { DOCUMENT_AUTH_TYPES } from '@documenso/lib/constants/document-auth';
-import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
-import { formatDocumentAuditLogAction } from '@documenso/lib/utils/document-audit-logs';
-import { trpc } from '@documenso/trpc/react';
-import { cn } from '@documenso/ui/lib/utils';
-import { Avatar, AvatarFallback } from '@documenso/ui/primitives/avatar';
-import { Badge } from '@documenso/ui/primitives/badge';
-import { Button } from '@documenso/ui/primitives/button';
-import { Sheet, SheetContent, SheetTrigger } from '@documenso/ui/primitives/sheet';
-
-import { DocumentHistorySheetChanges } from './document-history-sheet-changes';
-
-export type DocumentHistorySheetProps = {
- documentId: number;
- userId: number;
- isMenuOpen?: boolean;
- onMenuOpenChange?: (_value: boolean) => void;
- children?: React.ReactNode;
-};
-
-export const DocumentHistorySheet = ({
- documentId,
- userId,
- isMenuOpen,
- onMenuOpenChange,
- children,
-}: DocumentHistorySheetProps) => {
- const { _, i18n } = useLingui();
-
- const [isUserDetailsVisible, setIsUserDetailsVisible] = useState(false);
-
- const {
- data,
- isLoading,
- isLoadingError,
- refetch,
- hasNextPage,
- fetchNextPage,
- isFetchingNextPage,
- } = trpc.document.findDocumentAuditLogs.useInfiniteQuery(
- {
- documentId,
- },
- {
- getNextPageParam: (lastPage) => lastPage.nextCursor,
- placeholderData: (previousData) => previousData,
- },
- );
-
- const documentAuditLogs = useMemo(() => (data?.pages ?? []).flatMap((page) => page.data), [data]);
-
- const extractBrowser = (userAgent?: string | null) => {
- if (!userAgent) {
- return 'Unknown';
- }
-
- const parser = new UAParser(userAgent);
-
- parser.setUA(userAgent);
-
- const result = parser.getResult();
-
- return result.browser.name;
- };
-
- /**
- * Applies the following formatting for a given text:
- * - Uppercase first lower, lowercase rest
- * - Replace _ with spaces
- *
- * @param text The text to format
- * @returns The formatted text
- */
- const formatGenericText = (text?: string | null) => {
- if (!text) {
- return '';
- }
-
- return (text.charAt(0).toUpperCase() + text.slice(1).toLowerCase()).replaceAll('_', ' ');
- };
-
- return (
-
- {children && {children} }
-
-
-
-
- Document history
-
- setIsUserDetailsVisible(!isUserDetailsVisible)}
- >
- {isUserDetailsVisible ? (
- Hide additional information
- ) : (
- Show additional information
- )}
-
-
-
- {isLoading && (
-
-
-
- )}
-
- {isLoadingError && (
-
-
- Unable to load document history
-
-
refetch()}
- className="text-foreground/70 hover:text-muted-foreground mt-2 text-sm"
- >
- Click here to retry
-
-
- )}
-
- {data && (
-
- )}
-
-
- );
-};
diff --git a/apps/web/src/components/document/document-read-only-fields.tsx b/apps/web/src/components/document/document-read-only-fields.tsx
deleted file mode 100644
index 926ddaa9d..000000000
--- a/apps/web/src/components/document/document-read-only-fields.tsx
+++ /dev/null
@@ -1,173 +0,0 @@
-'use client';
-
-import { useState } from 'react';
-
-import { Trans } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { Clock, EyeOffIcon } from 'lucide-react';
-import { P, match } from 'ts-pattern';
-
-import {
- DEFAULT_DOCUMENT_DATE_FORMAT,
- convertToLocalSystemFormat,
-} from '@documenso/lib/constants/date-formats';
-import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
-import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones';
-import type { DocumentField } from '@documenso/lib/server-only/field/get-fields-for-document';
-import { parseMessageDescriptor } from '@documenso/lib/utils/i18n';
-import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
-import type { DocumentMeta } from '@documenso/prisma/client';
-import { FieldType, SigningStatus } from '@documenso/prisma/client';
-import { FieldRootContainer } from '@documenso/ui/components/field/field';
-import { SignatureIcon } from '@documenso/ui/icons/signature';
-import { cn } from '@documenso/ui/lib/utils';
-import { Avatar, AvatarFallback } from '@documenso/ui/primitives/avatar';
-import { Badge } from '@documenso/ui/primitives/badge';
-import { FRIENDLY_FIELD_TYPE } from '@documenso/ui/primitives/document-flow/types';
-import { ElementVisible } from '@documenso/ui/primitives/element-visible';
-import { PopoverHover } from '@documenso/ui/primitives/popover';
-
-export type DocumentReadOnlyFieldsProps = {
- fields: DocumentField[];
- documentMeta?: DocumentMeta;
- showFieldStatus?: boolean;
-};
-
-export const DocumentReadOnlyFields = ({
- documentMeta,
- fields,
- showFieldStatus = true,
-}: DocumentReadOnlyFieldsProps) => {
- const { _ } = useLingui();
-
- const [hiddenFieldIds, setHiddenFieldIds] = useState>({});
-
- const handleHideField = (fieldId: string) => {
- setHiddenFieldIds((prev) => ({ ...prev, [fieldId]: true }));
- };
-
- return (
-
- {fields.map(
- (field) =>
- !hiddenFieldIds[field.secondaryId] && (
-
-
-
-
- {extractInitials(field.recipient.name || field.recipient.email)}
-
-
- }
- contentProps={{
- className: 'relative flex w-fit flex-col p-4 text-sm',
- }}
- >
- {showFieldStatus && (
-
- {field.recipient.signingStatus === SigningStatus.SIGNED ? (
- <>
-
- Signed
- >
- ) : (
- <>
-
- Pending
- >
- )}
-
- )}
-
-
- {parseMessageDescriptor(_, FRIENDLY_FIELD_TYPE[field.type])} field
-
-
-
- {field.recipient.name
- ? `${field.recipient.name} (${field.recipient.email})`
- : field.recipient.email}{' '}
-
-
- handleHideField(field.secondaryId)}
- title="Hide field"
- >
-
-
-
-
-
-
- {field.recipient.signingStatus === SigningStatus.SIGNED &&
- match(field)
- .with({ type: FieldType.SIGNATURE }, (field) =>
- field.signature?.signatureImageAsBase64 ? (
-
- ) : (
-
- {field.signature?.typedSignature}
-
- ),
- )
- .with(
- {
- type: P.union(
- FieldType.NAME,
- FieldType.INITIALS,
- FieldType.EMAIL,
- FieldType.NUMBER,
- FieldType.RADIO,
- FieldType.CHECKBOX,
- FieldType.DROPDOWN,
- ),
- },
- () => field.customText,
- )
- .with({ type: FieldType.TEXT }, () => field.customText.substring(0, 20) + '...')
- .with({ type: FieldType.DATE }, () =>
- convertToLocalSystemFormat(
- field.customText,
- documentMeta?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT,
- documentMeta?.timezone ?? DEFAULT_DOCUMENT_TIME_ZONE,
- ),
- )
- .with({ type: FieldType.FREE_SIGNATURE }, () => null)
- .exhaustive()}
-
- {field.recipient.signingStatus === SigningStatus.NOT_SIGNED && (
-
- {parseMessageDescriptor(_, FRIENDLY_FIELD_TYPE[field.type])}
-
- )}
-
-
- ),
- )}
-
- );
-};
diff --git a/apps/web/src/components/document/document-recipient-link-copy-dialog.tsx b/apps/web/src/components/document/document-recipient-link-copy-dialog.tsx
deleted file mode 100644
index bec368f4c..000000000
--- a/apps/web/src/components/document/document-recipient-link-copy-dialog.tsx
+++ /dev/null
@@ -1,151 +0,0 @@
-'use client';
-
-import { useEffect, useState } from 'react';
-
-import { useSearchParams } from 'next/navigation';
-
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-
-import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard';
-import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
-import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
-import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
-import { formatSigningLink } from '@documenso/lib/utils/recipients';
-import type { Recipient } from '@documenso/prisma/client';
-import { RecipientRole } from '@documenso/prisma/client';
-import { CopyTextButton } from '@documenso/ui/components/common/copy-text-button';
-import { AvatarWithText } from '@documenso/ui/primitives/avatar';
-import { Button } from '@documenso/ui/primitives/button';
-import {
- Dialog,
- DialogClose,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from '@documenso/ui/primitives/dialog';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-export type DocumentRecipientLinkCopyDialogProps = {
- trigger?: React.ReactNode;
- recipients: Recipient[];
-};
-
-export const DocumentRecipientLinkCopyDialog = ({
- trigger,
- recipients,
-}: DocumentRecipientLinkCopyDialogProps) => {
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const [, copy] = useCopyToClipboard();
-
- const searchParams = useSearchParams();
- const updateSearchParams = useUpdateSearchParams();
-
- const [open, setOpen] = useState(false);
-
- const actionSearchParam = searchParams?.get('action');
-
- const onBulkCopy = async () => {
- const generatedString = recipients
- .filter((recipient) => recipient.role !== RecipientRole.CC)
- .map((recipient) => `${recipient.email}\n${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`)
- .join('\n\n');
-
- await copy(generatedString).then(() => {
- toast({
- title: _(msg`Copied to clipboard`),
- description: _(msg`All signing links have been copied to your clipboard.`),
- });
- });
- };
-
- useEffect(() => {
- if (actionSearchParam === 'view-signing-links') {
- setOpen(true);
- updateSearchParams({ action: null });
- }
- }, [actionSearchParam, open, setOpen, updateSearchParams]);
-
- return (
- setOpen(value)}>
- e.stopPropagation()}>
- {trigger}
-
-
-
-
-
- Copy Signing Links
-
-
-
-
- You can copy and share these links to recipients so they can action the document.
-
-
-
-
-
- {recipients.length === 0 && (
-
- No recipients
-
- )}
-
- {recipients.map((recipient) => (
-
- {recipient.email}}
- secondaryText={
-
- {_(RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName)}
-
- }
- />
-
- {recipient.role !== RecipientRole.CC && (
- {
- toast({
- title: _(msg`Copied to clipboard`),
- description: _(msg`The signing link has been copied to your clipboard.`),
- });
- }}
- badgeContentUncopied={
-
- Copy
-
- }
- badgeContentCopied={
-
- Copied
-
- }
- />
- )}
-
- ))}
-
-
-
-
-
- Close
-
-
-
-
- Bulk Copy
-
-
-
-
- );
-};
diff --git a/apps/web/src/components/formatter/document-status.tsx b/apps/web/src/components/formatter/document-status.tsx
deleted file mode 100644
index 494a9b627..000000000
--- a/apps/web/src/components/formatter/document-status.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-import type { HTMLAttributes } from 'react';
-
-import type { MessageDescriptor } from '@lingui/core';
-import { msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { CheckCircle2, Clock, File } from 'lucide-react';
-import type { LucideIcon } from 'lucide-react/dist/lucide-react';
-
-import type { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
-import { SignatureIcon } from '@documenso/ui/icons/signature';
-import { cn } from '@documenso/ui/lib/utils';
-
-type FriendlyStatus = {
- label: MessageDescriptor;
- labelExtended: MessageDescriptor;
- icon?: LucideIcon;
- color: string;
-};
-
-export const FRIENDLY_STATUS_MAP: Record = {
- PENDING: {
- label: msg`Pending`,
- labelExtended: msg`Document pending`,
- icon: Clock,
- color: 'text-blue-600 dark:text-blue-300',
- },
- COMPLETED: {
- label: msg`Completed`,
- labelExtended: msg`Document completed`,
- icon: CheckCircle2,
- color: 'text-green-500 dark:text-green-300',
- },
- DRAFT: {
- label: msg`Draft`,
- labelExtended: msg`Document draft`,
- icon: File,
- color: 'text-yellow-500 dark:text-yellow-200',
- },
- INBOX: {
- label: msg`Inbox`,
- labelExtended: msg`Document inbox`,
- icon: SignatureIcon,
- color: 'text-muted-foreground',
- },
- ALL: {
- label: msg`All`,
- labelExtended: msg`Document All`,
- color: 'text-muted-foreground',
- },
-};
-
-export type DocumentStatusProps = HTMLAttributes & {
- status: ExtendedDocumentStatus;
- inheritColor?: boolean;
-};
-
-export const DocumentStatus = ({
- className,
- status,
- inheritColor,
- ...props
-}: DocumentStatusProps) => {
- const { _ } = useLingui();
-
- const { label, icon: Icon, color } = FRIENDLY_STATUS_MAP[status];
-
- return (
-
- {Icon && (
-
- )}
- {_(label)}
-
- );
-};
diff --git a/apps/web/src/components/formatter/template-type.tsx b/apps/web/src/components/formatter/template-type.tsx
deleted file mode 100644
index 03a273470..000000000
--- a/apps/web/src/components/formatter/template-type.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-import type { HTMLAttributes } from 'react';
-
-import type { MessageDescriptor } from '@lingui/core';
-import { msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { Globe2, Lock } from 'lucide-react';
-import type { LucideIcon } from 'lucide-react/dist/lucide-react';
-
-import type { TemplateType as TemplateTypePrisma } from '@documenso/prisma/client';
-import { cn } from '@documenso/ui/lib/utils';
-
-type TemplateTypeIcon = {
- label: MessageDescriptor;
- icon?: LucideIcon;
- color: string;
-};
-
-type TemplateTypes = (typeof TemplateTypePrisma)[keyof typeof TemplateTypePrisma];
-
-const TEMPLATE_TYPES: Record = {
- PRIVATE: {
- label: msg`Private`,
- icon: Lock,
- color: 'text-blue-600 dark:text-blue-300',
- },
- PUBLIC: {
- label: msg`Public`,
- icon: Globe2,
- color: 'text-green-500 dark:text-green-300',
- },
-};
-
-export type TemplateTypeProps = HTMLAttributes & {
- type: TemplateTypes;
- inheritColor?: boolean;
-};
-
-export const TemplateType = ({ className, type, inheritColor, ...props }: TemplateTypeProps) => {
- const { _ } = useLingui();
-
- const { label, icon: Icon, color } = TEMPLATE_TYPES[type];
-
- return (
-
- {Icon && (
-
- )}
- {_(label)}
-
- );
-};
diff --git a/apps/web/src/components/forms/2fa/disable-authenticator-app-dialog.tsx b/apps/web/src/components/forms/2fa/disable-authenticator-app-dialog.tsx
deleted file mode 100644
index 5078a87a0..000000000
--- a/apps/web/src/components/forms/2fa/disable-authenticator-app-dialog.tsx
+++ /dev/null
@@ -1,201 +0,0 @@
-'use client';
-
-import { useState } from 'react';
-
-import { useRouter } from 'next/navigation';
-
-import { zodResolver } from '@hookform/resolvers/zod';
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { flushSync } from 'react-dom';
-import { useForm } from 'react-hook-form';
-import { z } from 'zod';
-
-import { trpc } from '@documenso/trpc/react';
-import { Button } from '@documenso/ui/primitives/button';
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from '@documenso/ui/primitives/dialog';
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from '@documenso/ui/primitives/form/form';
-import { Input } from '@documenso/ui/primitives/input';
-import { PinInput, PinInputGroup, PinInputSlot } from '@documenso/ui/primitives/pin-input';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-export const ZDisable2FAForm = z.object({
- totpCode: z.string().trim().optional(),
- backupCode: z.string().trim().optional(),
-});
-
-export type TDisable2FAForm = z.infer;
-
-export const DisableAuthenticatorAppDialog = () => {
- const router = useRouter();
-
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const [isOpen, setIsOpen] = useState(false);
- const [twoFactorDisableMethod, setTwoFactorDisableMethod] = useState<'totp' | 'backup'>('totp');
-
- const { mutateAsync: disable2FA } = trpc.twoFactorAuthentication.disable.useMutation();
-
- const disable2FAForm = useForm({
- defaultValues: {
- totpCode: '',
- backupCode: '',
- },
- resolver: zodResolver(ZDisable2FAForm),
- });
-
- const onCloseTwoFactorDisableDialog = () => {
- disable2FAForm.reset();
-
- setIsOpen(!isOpen);
- };
-
- const onToggleTwoFactorDisableMethodClick = () => {
- const method = twoFactorDisableMethod === 'totp' ? 'backup' : 'totp';
-
- if (method === 'totp') {
- disable2FAForm.setValue('backupCode', '');
- }
-
- if (method === 'backup') {
- disable2FAForm.setValue('totpCode', '');
- }
-
- setTwoFactorDisableMethod(method);
- };
-
- const { isSubmitting: isDisable2FASubmitting } = disable2FAForm.formState;
-
- const onDisable2FAFormSubmit = async ({ totpCode, backupCode }: TDisable2FAForm) => {
- try {
- await disable2FA({ totpCode, backupCode });
-
- toast({
- title: _(msg`Two-factor authentication disabled`),
- description: _(
- msg`Two-factor authentication has been disabled for your account. You will no longer be required to enter a code from your authenticator app when signing in.`,
- ),
- });
-
- flushSync(() => {
- onCloseTwoFactorDisableDialog();
- });
-
- router.refresh();
- } catch (_err) {
- toast({
- title: _(msg`Unable to disable two-factor authentication`),
- description: _(
- msg`We were unable to disable two-factor authentication for your account. Please ensure that you have entered your password and backup code correctly and try again.`,
- ),
- variant: 'destructive',
- });
- }
- };
-
- return (
-
-
-
- Disable 2FA
-
-
-
-
-
-
- Disable 2FA
-
-
-
-
- Please provide a token from the authenticator, or a backup code. If you do not have a
- backup code available, please contact support.
-
-
-
-
-
-
-
- {twoFactorDisableMethod === 'totp' && (
- (
-
-
-
- {Array(6)
- .fill(null)
- .map((_, i) => (
-
-
-
- ))}
-
-
-
-
- )}
- />
- )}
-
- {twoFactorDisableMethod === 'backup' && (
- (
-
-
- Backup Code
-
-
-
-
-
-
- )}
- />
- )}
-
-
-
- {twoFactorDisableMethod === 'totp' ? (
- Use Backup Code
- ) : (
- Use Authenticator
- )}
-
-
-
- Disable 2FA
-
-
-
-
-
-
-
- );
-};
diff --git a/apps/web/src/components/forms/2fa/enable-authenticator-app-dialog.tsx b/apps/web/src/components/forms/2fa/enable-authenticator-app-dialog.tsx
deleted file mode 100644
index 5965db3d8..000000000
--- a/apps/web/src/components/forms/2fa/enable-authenticator-app-dialog.tsx
+++ /dev/null
@@ -1,274 +0,0 @@
-'use client';
-
-import { useEffect, useState } from 'react';
-
-import { useRouter } from 'next/navigation';
-
-import { zodResolver } from '@hookform/resolvers/zod';
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { useForm } from 'react-hook-form';
-import { renderSVG } from 'uqr';
-import { z } from 'zod';
-
-import { downloadFile } from '@documenso/lib/client-only/download-file';
-import { trpc } from '@documenso/trpc/react';
-import { Button } from '@documenso/ui/primitives/button';
-import {
- Dialog,
- DialogClose,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from '@documenso/ui/primitives/dialog';
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from '@documenso/ui/primitives/form/form';
-import { PinInput, PinInputGroup, PinInputSlot } from '@documenso/ui/primitives/pin-input';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-import { RecoveryCodeList } from './recovery-code-list';
-
-export const ZEnable2FAForm = z.object({
- token: z.string(),
-});
-
-export type TEnable2FAForm = z.infer;
-
-export type EnableAuthenticatorAppDialogProps = {
- onSuccess?: () => void;
-};
-
-export const EnableAuthenticatorAppDialog = ({ onSuccess }: EnableAuthenticatorAppDialogProps) => {
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const router = useRouter();
-
- const [isOpen, setIsOpen] = useState(false);
- const [recoveryCodes, setRecoveryCodes] = useState(null);
-
- const { mutateAsync: enable2FA } = trpc.twoFactorAuthentication.enable.useMutation();
-
- const {
- mutateAsync: setup2FA,
- data: setup2FAData,
- isPending: isSettingUp2FA,
- } = trpc.twoFactorAuthentication.setup.useMutation({
- onError: () => {
- toast({
- title: _(msg`Unable to setup two-factor authentication`),
- description: _(
- msg`We were unable to setup two-factor authentication for your account. Please ensure that you have entered your code correctly and try again.`,
- ),
- variant: 'destructive',
- });
- },
- });
-
- const enable2FAForm = useForm({
- defaultValues: {
- token: '',
- },
- resolver: zodResolver(ZEnable2FAForm),
- });
-
- const { isSubmitting: isEnabling2FA } = enable2FAForm.formState;
-
- const onEnable2FAFormSubmit = async ({ token }: TEnable2FAForm) => {
- try {
- const data = await enable2FA({ code: token });
-
- setRecoveryCodes(data.recoveryCodes);
- onSuccess?.();
-
- toast({
- title: _(msg`Two-factor authentication enabled`),
- description: _(
- msg`You will now be required to enter a code from your authenticator app when signing in.`,
- ),
- });
- } catch (_err) {
- toast({
- title: _(msg`Unable to setup two-factor authentication`),
- description: _(
- msg`We were unable to setup two-factor authentication for your account. Please ensure that you have entered your code correctly and try again.`,
- ),
- variant: 'destructive',
- });
- }
- };
-
- const downloadRecoveryCodes = () => {
- if (recoveryCodes) {
- const blob = new Blob([recoveryCodes.join('\n')], {
- type: 'text/plain',
- });
-
- downloadFile({
- filename: 'documenso-2FA-recovery-codes.txt',
- data: blob,
- });
- }
- };
-
- const handleEnable2FA = async () => {
- if (!setup2FAData) {
- await setup2FA();
- }
-
- setIsOpen(true);
- };
-
- useEffect(() => {
- enable2FAForm.reset();
-
- if (!isOpen && recoveryCodes && recoveryCodes.length > 0) {
- setRecoveryCodes(null);
- router.refresh();
- }
-
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [isOpen]);
-
- return (
-
-
- {
- e.preventDefault();
- void handleEnable2FA();
- }}
- >
- Enable 2FA
-
-
-
-
- {setup2FAData && (
- <>
- {recoveryCodes ? (
-
-
-
- Backup codes
-
-
-
- Your recovery codes are listed below. Please store them in a safe place.
-
-
-
-
-
-
-
-
-
-
-
- Close
-
-
-
-
- Download
-
-
-
- ) : (
-
-
-
-
- Enable Authenticator App
-
-
-
- To enable two-factor authentication, scan the following QR code using your
- authenticator app.
-
-
-
-
-
-
-
-
-
- If your authenticator app does not support QR codes, you can use the
- following code instead:
-
-
-
-
- {setup2FAData?.secret}
-
-
-
-
- Once you have scanned the QR code or entered the code manually, enter the
- code provided by your authenticator app below.
-
-
-
- (
-
-
- Token
-
-
-
- {Array(6)
- .fill(null)
- .map((_, i) => (
-
-
-
- ))}
-
-
-
-
- )}
- />
-
-
-
-
- Cancel
-
-
-
-
- Enable 2FA
-
-
-
-
-
- )}
- >
- )}
-
-
- );
-};
diff --git a/apps/web/src/components/forms/2fa/recovery-code-list.tsx b/apps/web/src/components/forms/2fa/recovery-code-list.tsx
deleted file mode 100644
index 2b72883f2..000000000
--- a/apps/web/src/components/forms/2fa/recovery-code-list.tsx
+++ /dev/null
@@ -1,61 +0,0 @@
-import { msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { Copy } from 'lucide-react';
-
-import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-export type RecoveryCodeListProps = {
- recoveryCodes: string[];
-};
-
-export const RecoveryCodeList = ({ recoveryCodes }: RecoveryCodeListProps) => {
- const { _ } = useLingui();
- const { toast } = useToast();
- const [, copyToClipboard] = useCopyToClipboard();
-
- const onCopyRecoveryCodeClick = async (code: string) => {
- try {
- const result = await copyToClipboard(code);
-
- if (!result) {
- throw new Error('Unable to copy recovery code');
- }
-
- toast({
- title: _(msg`Recovery code copied`),
- description: _(msg`Your recovery code has been copied to your clipboard.`),
- });
- } catch (_err) {
- toast({
- title: _(msg`Unable to copy recovery code`),
- description: _(
- msg`We were unable to copy your recovery code to your clipboard. Please try again.`,
- ),
- variant: 'destructive',
- });
- }
- };
-
- return (
-
- {recoveryCodes.map((code) => (
-
-
{code}
-
-
- void onCopyRecoveryCodeClick(code)}
- >
-
-
-
-
- ))}
-
- );
-};
diff --git a/apps/web/src/components/forms/2fa/view-recovery-codes-dialog.tsx b/apps/web/src/components/forms/2fa/view-recovery-codes-dialog.tsx
deleted file mode 100644
index d7458b627..000000000
--- a/apps/web/src/components/forms/2fa/view-recovery-codes-dialog.tsx
+++ /dev/null
@@ -1,177 +0,0 @@
-'use client';
-
-import { useState } from 'react';
-
-import { zodResolver } from '@hookform/resolvers/zod';
-import { Trans } from '@lingui/macro';
-import { useForm } from 'react-hook-form';
-import { match } from 'ts-pattern';
-import { z } from 'zod';
-
-import { downloadFile } from '@documenso/lib/client-only/download-file';
-import { AppError } from '@documenso/lib/errors/app-error';
-import { trpc } from '@documenso/trpc/react';
-import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
-import { Button } from '@documenso/ui/primitives/button';
-import {
- Dialog,
- DialogClose,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from '@documenso/ui/primitives/dialog';
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormMessage,
-} from '@documenso/ui/primitives/form/form';
-import { PinInput, PinInputGroup, PinInputSlot } from '@documenso/ui/primitives/pin-input';
-
-import { RecoveryCodeList } from './recovery-code-list';
-
-export const ZViewRecoveryCodesForm = z.object({
- token: z.string().min(1, { message: 'Token is required' }),
-});
-
-export type TViewRecoveryCodesForm = z.infer;
-
-export const ViewRecoveryCodesDialog = () => {
- const [isOpen, setIsOpen] = useState(false);
-
- const {
- data: recoveryCodes,
- mutate,
- isPending,
- error,
- } = trpc.twoFactorAuthentication.viewRecoveryCodes.useMutation();
-
- const viewRecoveryCodesForm = useForm({
- defaultValues: {
- token: '',
- },
- resolver: zodResolver(ZViewRecoveryCodesForm),
- });
-
- const downloadRecoveryCodes = () => {
- if (recoveryCodes) {
- const blob = new Blob([recoveryCodes.join('\n')], {
- type: 'text/plain',
- });
-
- downloadFile({
- filename: 'documenso-2FA-recovery-codes.txt',
- data: blob,
- });
- }
- };
-
- return (
-
-
-
- View Codes
-
-
-
-
- {recoveryCodes ? (
-
-
-
- View Recovery Codes
-
-
-
-
- Your recovery codes are listed below. Please store them in a safe place.
-
-
-
-
-
-
-
-
-
- Close
-
-
-
-
- Download
-
-
-
- ) : (
-
- mutate(value))}>
-
-
- View Recovery Codes
-
-
-
- Please provide a token from your authenticator, or a backup code.
-
-
-
-
- (
-
-
-
- {Array(6)
- .fill(null)
- .map((_, i) => (
-
-
-
- ))}
-
-
-
-
- )}
- />
-
- {error && (
-
-
- {match(AppError.parseError(error).message)
- .with('INCORRECT_TWO_FACTOR_CODE', () => (
- Invalid code. Please try again.
- ))
- .otherwise(() => (
- Something went wrong. Please try again or contact support.
- ))}
-
-
- )}
-
-
-
-
- Cancel
-
-
-
-
- View
-
-
-
-
-
- )}
-
-
- );
-};
diff --git a/apps/web/src/components/forms/avatar-image.tsx b/apps/web/src/components/forms/avatar-image.tsx
deleted file mode 100644
index 64e6264c0..000000000
--- a/apps/web/src/components/forms/avatar-image.tsx
+++ /dev/null
@@ -1,193 +0,0 @@
-'use client';
-
-import { useMemo } from 'react';
-
-import { useRouter } from 'next/navigation';
-
-import { zodResolver } from '@hookform/resolvers/zod';
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { ErrorCode, useDropzone } from 'react-dropzone';
-import { useForm } from 'react-hook-form';
-import { match } from 'ts-pattern';
-import { z } from 'zod';
-
-import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
-import { AppError } from '@documenso/lib/errors/app-error';
-import { base64 } from '@documenso/lib/universal/base64';
-import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
-import type { Team, User } from '@documenso/prisma/client';
-import { trpc } from '@documenso/trpc/react';
-import { cn } from '@documenso/ui/lib/utils';
-import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
-import { Button } from '@documenso/ui/primitives/button';
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from '@documenso/ui/primitives/form/form';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-export const ZAvatarImageFormSchema = z.object({
- bytes: z.string().nullish(),
-});
-
-export type TAvatarImageFormSchema = z.infer;
-
-export type AvatarImageFormProps = {
- className?: string;
- user: User;
- team?: Team;
-};
-
-export const AvatarImageForm = ({ className, user, team }: AvatarImageFormProps) => {
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const router = useRouter();
-
- const { mutateAsync: setProfileImage } = trpc.profile.setProfileImage.useMutation();
-
- const initials = extractInitials(team?.name || user.name || '');
-
- const hasAvatarImage = useMemo(() => {
- if (team) {
- return team.avatarImageId !== null;
- }
-
- return user.avatarImageId !== null;
- }, [team, user.avatarImageId]);
-
- const avatarImageId = team ? team.avatarImageId : user.avatarImageId;
-
- const form = useForm({
- values: {
- bytes: null,
- },
- resolver: zodResolver(ZAvatarImageFormSchema),
- });
-
- const { getRootProps, getInputProps } = useDropzone({
- maxSize: 1024 * 1024,
- accept: {
- 'image/*': ['.png', '.jpg', '.jpeg'],
- },
- multiple: false,
- onDropAccepted: ([file]) => {
- void file.arrayBuffer().then((buffer) => {
- const contents = base64.encode(new Uint8Array(buffer));
-
- form.setValue('bytes', contents);
- void form.handleSubmit(onFormSubmit)();
- });
- },
- onDropRejected: ([file]) => {
- form.setError('bytes', {
- type: 'onChange',
- message: match(file.errors[0].code)
- .with(ErrorCode.FileTooLarge, () => _(msg`Uploaded file is too large`))
- .with(ErrorCode.FileTooSmall, () => _(msg`Uploaded file is too small`))
- .with(ErrorCode.FileInvalidType, () => _(msg`Uploaded file not an allowed file type`))
- .otherwise(() => _(msg`An unknown error occurred`)),
- });
- },
- });
-
- const onFormSubmit = async (data: TAvatarImageFormSchema) => {
- try {
- await setProfileImage({
- bytes: data.bytes,
- teamId: team?.id,
- });
-
- toast({
- title: _(msg`Avatar Updated`),
- description: _(msg`Your avatar has been updated successfully.`),
- duration: 5000,
- });
-
- router.refresh();
- } catch (err) {
- const error = AppError.parseError(err);
-
- const errorMessage = match(error.code).otherwise(
- () =>
- msg`We encountered an unknown error while attempting to update your password. Please try again later.`,
- );
-
- toast({
- title: _(msg`An error occurred`),
- description: _(errorMessage),
- variant: 'destructive',
- });
- }
- };
-
- return (
-
-
-
- (
-
-
- Avatar
-
-
-
-
-
-
- {avatarImageId && (
-
- )}
-
- {initials}
-
-
-
- {hasAvatarImage && (
-
void onFormSubmit({ bytes: null })}
- >
- Remove
-
- )}
-
-
-
- Upload Avatar
-
-
-
-
-
-
-
- )}
- />
-
-
-
- );
-};
diff --git a/apps/web/src/components/forms/forgot-password.tsx b/apps/web/src/components/forms/forgot-password.tsx
deleted file mode 100644
index 446e12727..000000000
--- a/apps/web/src/components/forms/forgot-password.tsx
+++ /dev/null
@@ -1,98 +0,0 @@
-'use client';
-
-import { useRouter } from 'next/navigation';
-
-import { zodResolver } from '@hookform/resolvers/zod';
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { useForm } from 'react-hook-form';
-import { z } from 'zod';
-
-import { trpc } from '@documenso/trpc/react';
-import { cn } from '@documenso/ui/lib/utils';
-import { Button } from '@documenso/ui/primitives/button';
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from '@documenso/ui/primitives/form/form';
-import { Input } from '@documenso/ui/primitives/input';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-export const ZForgotPasswordFormSchema = z.object({
- email: z.string().email().min(1),
-});
-
-export type TForgotPasswordFormSchema = z.infer;
-
-export type ForgotPasswordFormProps = {
- className?: string;
-};
-
-export const ForgotPasswordForm = ({ className }: ForgotPasswordFormProps) => {
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const router = useRouter();
-
- const form = useForm({
- values: {
- email: '',
- },
- resolver: zodResolver(ZForgotPasswordFormSchema),
- });
-
- const isSubmitting = form.formState.isSubmitting;
-
- const { mutateAsync: forgotPassword } = trpc.profile.forgotPassword.useMutation();
-
- const onFormSubmit = async ({ email }: TForgotPasswordFormSchema) => {
- await forgotPassword({ email }).catch(() => null);
-
- toast({
- title: _(msg`Reset email sent`),
- description: _(
- msg`A password reset email has been sent, if you have an account you should see it in your inbox shortly.`,
- ),
- duration: 5000,
- });
-
- form.reset();
-
- router.push('/check-email');
- };
-
- return (
-
-
-
- (
-
-
- Email
-
-
-
-
-
-
- )}
- />
-
-
-
- {isSubmitting ? Sending Reset Email... : Reset Password }
-
-
-
- );
-};
diff --git a/apps/web/src/components/forms/password.tsx b/apps/web/src/components/forms/password.tsx
deleted file mode 100644
index c77373972..000000000
--- a/apps/web/src/components/forms/password.tsx
+++ /dev/null
@@ -1,163 +0,0 @@
-'use client';
-
-import { zodResolver } from '@hookform/resolvers/zod';
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { useForm } from 'react-hook-form';
-import { match } from 'ts-pattern';
-import { z } from 'zod';
-
-import { AppError } from '@documenso/lib/errors/app-error';
-import type { User } from '@documenso/prisma/client';
-import { trpc } from '@documenso/trpc/react';
-import { ZCurrentPasswordSchema, ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
-import { cn } from '@documenso/ui/lib/utils';
-import { Button } from '@documenso/ui/primitives/button';
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from '@documenso/ui/primitives/form/form';
-import { PasswordInput } from '@documenso/ui/primitives/password-input';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-export const ZPasswordFormSchema = z
- .object({
- currentPassword: ZCurrentPasswordSchema,
- password: ZPasswordSchema,
- repeatedPassword: ZPasswordSchema,
- })
- .refine((data) => data.password === data.repeatedPassword, {
- message: 'Passwords do not match',
- path: ['repeatedPassword'],
- });
-
-export type TPasswordFormSchema = z.infer;
-
-export type PasswordFormProps = {
- className?: string;
- user: User;
-};
-
-export const PasswordForm = ({ className }: PasswordFormProps) => {
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const form = useForm({
- values: {
- currentPassword: '',
- password: '',
- repeatedPassword: '',
- },
- resolver: zodResolver(ZPasswordFormSchema),
- });
-
- const isSubmitting = form.formState.isSubmitting;
-
- const { mutateAsync: updatePassword } = trpc.profile.updatePassword.useMutation();
-
- const onFormSubmit = async ({ currentPassword, password }: TPasswordFormSchema) => {
- try {
- await updatePassword({
- currentPassword,
- password,
- });
-
- form.reset();
-
- toast({
- title: _(msg`Password updated`),
- description: _(msg`Your password has been updated successfully.`),
- duration: 5000,
- });
- } catch (err) {
- const error = AppError.parseError(err);
-
- const errorMessage = match(error.code)
- .with('NO_PASSWORD', () => msg`User has no password.`)
- .with('INCORRECT_PASSWORD', () => msg`Current password is incorrect.`)
- .with(
- 'SAME_PASSWORD',
- () => msg`Your new password cannot be the same as your old password.`,
- )
- .otherwise(
- () =>
- msg`We encountered an unknown error while attempting to update your password. Please try again later.`,
- );
-
- toast({
- title: _(msg`An error occurred`),
- description: _(errorMessage),
- variant: 'destructive',
- });
- }
- };
-
- return (
-
-
-
- (
-
-
- Current Password
-
-
-
-
-
-
- )}
- />
-
- (
-
-
- Password
-
-
-
-
-
-
- )}
- />
-
- (
-
-
- Repeat Password
-
-
-
-
-
-
- )}
- />
-
-
-
-
- {isSubmitting ? Updating password... : Update password }
-
-
-
-
- );
-};
diff --git a/apps/web/src/components/forms/profile.tsx b/apps/web/src/components/forms/profile.tsx
deleted file mode 100644
index 3d70cf672..000000000
--- a/apps/web/src/components/forms/profile.tsx
+++ /dev/null
@@ -1,147 +0,0 @@
-'use client';
-
-import { useRouter } from 'next/navigation';
-
-import { zodResolver } from '@hookform/resolvers/zod';
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { useForm } from 'react-hook-form';
-import { z } from 'zod';
-
-import type { User } from '@documenso/prisma/client';
-import { trpc } from '@documenso/trpc/react';
-import { cn } from '@documenso/ui/lib/utils';
-import { Button } from '@documenso/ui/primitives/button';
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from '@documenso/ui/primitives/form/form';
-import { Input } from '@documenso/ui/primitives/input';
-import { Label } from '@documenso/ui/primitives/label';
-import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-export const ZProfileFormSchema = z.object({
- name: z.string().trim().min(1, { message: 'Please enter a valid name.' }),
- signature: z.string().min(1, 'Signature Pad cannot be empty'),
-});
-
-export const ZTwoFactorAuthTokenSchema = z.object({
- token: z.string(),
-});
-
-export type TTwoFactorAuthTokenSchema = z.infer;
-export type TProfileFormSchema = z.infer;
-
-export type ProfileFormProps = {
- className?: string;
- user: User;
-};
-
-export const ProfileForm = ({ className, user }: ProfileFormProps) => {
- const router = useRouter();
-
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const form = useForm({
- values: {
- name: user.name ?? '',
- signature: user.signature || '',
- },
- resolver: zodResolver(ZProfileFormSchema),
- });
-
- const isSubmitting = form.formState.isSubmitting;
-
- const { mutateAsync: updateProfile } = trpc.profile.updateProfile.useMutation();
-
- const onFormSubmit = async ({ name, signature }: TProfileFormSchema) => {
- try {
- await updateProfile({
- name,
- signature,
- });
-
- toast({
- title: _(msg`Profile updated`),
- description: _(msg`Your profile has been updated successfully.`),
- duration: 5000,
- });
-
- router.refresh();
- } catch (err) {
- toast({
- title: _(msg`An unknown error occurred`),
- description: _(
- msg`We encountered an unknown error while attempting update your profile. Please try again later.`,
- ),
- variant: 'destructive',
- });
- }
- };
-
- return (
-
-
-
- (
-
-
- Full Name
-
-
-
-
-
-
- )}
- />
-
-
-
- Email
-
-
-
- (
-
-
- Signature
-
-
- onChange(v ?? '')}
- allowTypedSignature={true}
- />
-
-
-
- )}
- />
-
-
-
- {isSubmitting ? Updating profile... : Update profile }
-
-
-
- );
-};
diff --git a/apps/web/src/components/forms/public-profile-claim-dialog.tsx b/apps/web/src/components/forms/public-profile-claim-dialog.tsx
deleted file mode 100644
index a637c7606..000000000
--- a/apps/web/src/components/forms/public-profile-claim-dialog.tsx
+++ /dev/null
@@ -1,201 +0,0 @@
-'use client';
-
-import React, { useState } from 'react';
-
-import Image from 'next/image';
-
-import { zodResolver } from '@hookform/resolvers/zod';
-import { msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { useForm } from 'react-hook-form';
-import { z } from 'zod';
-
-import profileClaimTeaserImage from '@documenso/assets/images/profile-claim-teaser.png';
-import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
-import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
-import type { User } from '@documenso/prisma/client';
-import { trpc } from '@documenso/trpc/react';
-import { cn } from '@documenso/ui/lib/utils';
-import { Button } from '@documenso/ui/primitives/button';
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogHeader,
- DialogTitle,
-} from '@documenso/ui/primitives/dialog';
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from '@documenso/ui/primitives/form/form';
-import { Input } from '@documenso/ui/primitives/input';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-import { UserProfileSkeleton } from '../ui/user-profile-skeleton';
-
-export const ZClaimPublicProfileFormSchema = z.object({
- url: z
- .string()
- .trim()
- .toLowerCase()
- .min(1, { message: 'Please enter a valid username.' })
- .regex(/^[a-z0-9-]+$/, {
- message: 'Username can only container alphanumeric characters and dashes.',
- }),
-});
-
-export type TClaimPublicProfileFormSchema = z.infer;
-
-export type ClaimPublicProfileDialogFormProps = {
- open: boolean;
- onOpenChange?: (open: boolean) => void;
- onClaimed?: () => void;
- user: User;
-};
-
-export const ClaimPublicProfileDialogForm = ({
- open,
- onOpenChange,
- onClaimed,
- user,
-}: ClaimPublicProfileDialogFormProps) => {
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const [claimed, setClaimed] = useState(false);
-
- const baseUrl = new URL(NEXT_PUBLIC_WEBAPP_URL() ?? 'http://localhost:3000');
-
- const form = useForm({
- values: {
- url: user.url || '',
- },
- resolver: zodResolver(ZClaimPublicProfileFormSchema),
- });
-
- const { mutateAsync: updatePublicProfile } = trpc.profile.updatePublicProfile.useMutation();
-
- const isSubmitting = form.formState.isSubmitting;
-
- const onFormSubmit = async ({ url }: TClaimPublicProfileFormSchema) => {
- try {
- await updatePublicProfile({
- url,
- });
-
- setClaimed(true);
- onClaimed?.();
- } catch (err) {
- const error = AppError.parseError(err);
-
- if (error.code === AppErrorCode.PROFILE_URL_TAKEN) {
- form.setError('url', {
- type: 'manual',
- message: _(msg`This username is already taken`),
- });
- } else if (error.code === AppErrorCode.PREMIUM_PROFILE_URL) {
- form.setError('url', {
- type: 'manual',
- message: error.message,
- });
- } else if (error.code !== AppErrorCode.UNKNOWN_ERROR) {
- toast({
- title: 'An error occurred',
- description: error.userMessage ?? error.message,
- variant: 'destructive',
- });
- } else {
- toast({
- title: _(msg`An unknown error occurred`),
- description: _(
- msg`We encountered an unknown error while attempting to save your details. Please try again later.`,
- ),
- variant: 'destructive',
- });
- }
- }
- };
-
- return (
-
-
- {!claimed && (
- <>
-
-
- Introducing public profiles!
-
-
-
- Reserve your Documenso public profile username
-
-
-
-
-
-
-
-
- (
-
- Public profile username
-
-
-
-
-
-
-
-
- {baseUrl.host}/u/{field.value || ''}
-
-
- )}
- />
-
-
-
-
- Claim your username
-
-
-
-
- >
- )}
-
- {claimed && (
- <>
-
- All set!
-
-
- We will let you know as soon as this features is launched
-
-
-
-
-
-
- onOpenChange?.(false)}>
- Can't wait!
-
-
- >
- )}
-
-
- );
-};
diff --git a/apps/web/src/components/forms/public-profile-form.tsx b/apps/web/src/components/forms/public-profile-form.tsx
deleted file mode 100644
index acdb0d350..000000000
--- a/apps/web/src/components/forms/public-profile-form.tsx
+++ /dev/null
@@ -1,286 +0,0 @@
-'use client';
-
-import { useState } from 'react';
-
-import { zodResolver } from '@hookform/resolvers/zod';
-import { Plural, Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { motion } from 'framer-motion';
-import { AnimatePresence } from 'framer-motion';
-import { CheckSquareIcon, CopyIcon } from 'lucide-react';
-import { useForm } from 'react-hook-form';
-import type { z } from 'zod';
-
-import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard';
-import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
-import { formatUserProfilePath } from '@documenso/lib/utils/public-profiles';
-import type { TeamProfile, UserProfile } from '@documenso/prisma/client';
-import {
- MAX_PROFILE_BIO_LENGTH,
- ZUpdatePublicProfileMutationSchema,
-} from '@documenso/trpc/server/profile-router/schema';
-import { cn } from '@documenso/ui/lib/utils';
-import { Button } from '@documenso/ui/primitives/button';
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from '@documenso/ui/primitives/form/form';
-import { Input } from '@documenso/ui/primitives/input';
-import { Textarea } from '@documenso/ui/primitives/textarea';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-export const ZPublicProfileFormSchema = ZUpdatePublicProfileMutationSchema.pick({
- bio: true,
- enabled: true,
- url: true,
-});
-
-export type TPublicProfileFormSchema = z.infer;
-
-export type PublicProfileFormProps = {
- className?: string;
- profileUrl?: string | null;
- teamUrl?: string;
- onProfileUpdate: (data: TPublicProfileFormSchema) => Promise;
- profile: UserProfile | TeamProfile;
-};
-export const PublicProfileForm = ({
- className,
- profileUrl,
- profile,
- teamUrl,
- onProfileUpdate,
-}: PublicProfileFormProps) => {
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const [, copy] = useCopyToClipboard();
-
- const [copiedTimeout, setCopiedTimeout] = useState(null);
-
- const form = useForm({
- values: {
- url: profileUrl ?? '',
- bio: profile?.bio ?? '',
- },
- resolver: zodResolver(ZPublicProfileFormSchema),
- });
-
- const isSubmitting = form.formState.isSubmitting;
-
- const onFormSubmit = async (data: TPublicProfileFormSchema) => {
- try {
- await onProfileUpdate(data);
-
- toast({
- title: _(msg`Success`),
- description: _(msg`Your public profile has been updated.`),
- duration: 5000,
- });
-
- form.reset({
- url: data.url,
- bio: data.bio,
- });
- } catch (err) {
- const error = AppError.parseError(err);
-
- switch (error.code) {
- case AppErrorCode.PREMIUM_PROFILE_URL:
- case AppErrorCode.PROFILE_URL_TAKEN:
- form.setError('url', {
- type: 'manual',
- message: error.message,
- });
-
- break;
-
- default:
- toast({
- title: _(msg`An unknown error occurred`),
- description: _(
- msg`We encountered an unknown error while attempting to update your public profile. Please try again later.`,
- ),
- variant: 'destructive',
- });
- }
- }
- };
-
- const onCopy = async () => {
- await copy(formatUserProfilePath(form.getValues('url') ?? '')).then(() => {
- toast({
- title: _(msg`Copied to clipboard`),
- description: _(msg`The profile link has been copied to your clipboard`),
- });
- });
-
- if (copiedTimeout) {
- clearTimeout(copiedTimeout);
- }
-
- setCopiedTimeout(
- setTimeout(() => {
- setCopiedTimeout(null);
- }, 2000),
- );
- };
-
- return (
-
-
-
- (
-
-
- Public profile URL
-
-
-
-
-
- {teamUrl && (
-
-
- You can update the profile URL by updating the team URL in the general
- settings page.
-
-
- )}
-
-
- {!form.formState.errors.url && (
-
- {field.value ? (
-
-
onCopy()}
- >
-
- {formatUserProfilePath('').replace(/https?:\/\//, '')}
- {field.value}
-
-
-
-
-
- {copiedTimeout ? (
-
- ) : (
-
- )}
-
-
-
-
-
- ) : (
-
- A unique URL to access your profile
-
- )}
-
- )}
-
-
-
-
- )}
- />
-
- {
- const remaningLength = MAX_PROFILE_BIO_LENGTH - (field.value || '').length;
-
- return (
-
- Bio
-
-
-
-
- {!form.formState.errors.bio && (
-
- {remaningLength >= 0 ? (
- # character remaining}
- other={# characters remaining }
- />
- ) : (
- # character over the limit}
- other={# characters over the limit }
- />
- )}
-
- )}
-
-
-
- );
- }}
- />
-
-
-
- {form.formState.isDirty && (
-
- form.reset()}>
- Reset
-
-
- )}
-
-
-
- Update
-
-
-
-
-
- );
-};
diff --git a/apps/web/src/components/forms/reset-password.tsx b/apps/web/src/components/forms/reset-password.tsx
deleted file mode 100644
index fb8580d96..000000000
--- a/apps/web/src/components/forms/reset-password.tsx
+++ /dev/null
@@ -1,148 +0,0 @@
-'use client';
-
-import { useRouter } from 'next/navigation';
-
-import { zodResolver } from '@hookform/resolvers/zod';
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { useForm } from 'react-hook-form';
-import { match } from 'ts-pattern';
-import { z } from 'zod';
-
-import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
-import { trpc } from '@documenso/trpc/react';
-import { ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
-import { cn } from '@documenso/ui/lib/utils';
-import { Button } from '@documenso/ui/primitives/button';
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from '@documenso/ui/primitives/form/form';
-import { PasswordInput } from '@documenso/ui/primitives/password-input';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-export const ZResetPasswordFormSchema = z
- .object({
- password: ZPasswordSchema,
- repeatedPassword: ZPasswordSchema,
- })
- .refine((data) => data.password === data.repeatedPassword, {
- path: ['repeatedPassword'],
- message: "Passwords don't match",
- });
-
-export type TResetPasswordFormSchema = z.infer;
-
-export type ResetPasswordFormProps = {
- className?: string;
- token: string;
-};
-
-export const ResetPasswordForm = ({ className, token }: ResetPasswordFormProps) => {
- const router = useRouter();
-
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const form = useForm({
- values: {
- password: '',
- repeatedPassword: '',
- },
- resolver: zodResolver(ZResetPasswordFormSchema),
- });
-
- const isSubmitting = form.formState.isSubmitting;
-
- const { mutateAsync: resetPassword } = trpc.profile.resetPassword.useMutation();
-
- const onFormSubmit = async ({ password }: Omit) => {
- try {
- await resetPassword({
- password,
- token,
- });
-
- form.reset();
-
- toast({
- title: _(msg`Password updated`),
- description: _(msg`Your password has been updated successfully.`),
- duration: 5000,
- });
-
- router.push('/signin');
- } catch (err) {
- const error = AppError.parseError(err);
-
- const errorMessage = match(error.code)
- .with(AppErrorCode.EXPIRED_CODE, () => msg`Token has expired. Please try again.`)
- .with('INVALID_TOKEN', () => msg`Invalid token provided. Please try again.`)
- .with(
- 'SAME_PASSWORD',
- () => msg`Your new password cannot be the same as your old password.`,
- )
- .otherwise(
- () =>
- msg`We encountered an unknown error while attempting to reset your password. Please try again later.`,
- );
-
- toast({
- title: _(msg`An error occurred`),
- description: _(errorMessage),
- variant: 'destructive',
- });
- }
- };
-
- return (
-
-
-
- (
-
-
- Password
-
-
-
-
-
-
- )}
- />
-
- (
-
-
- Repeat Password
-
-
-
-
-
-
- )}
- />
-
-
-
- {isSubmitting ? Resetting Password... : Reset Password }
-
-
-
- );
-};
diff --git a/apps/web/src/components/forms/search-param-selector.tsx b/apps/web/src/components/forms/search-param-selector.tsx
deleted file mode 100644
index cdd4ef2b2..000000000
--- a/apps/web/src/components/forms/search-param-selector.tsx
+++ /dev/null
@@ -1,50 +0,0 @@
-import React, { useMemo } from 'react';
-
-import { usePathname, useRouter, useSearchParams } from 'next/navigation';
-
-import { Select, SelectContent, SelectTrigger, SelectValue } from '@documenso/ui/primitives/select';
-
-export type SearchParamSelector = {
- paramKey: string;
- isValueValid: (value: unknown) => boolean;
- children: React.ReactNode;
-};
-
-export const SearchParamSelector = ({ children, paramKey, isValueValid }: SearchParamSelector) => {
- const pathname = usePathname();
- const searchParams = useSearchParams();
-
- const router = useRouter();
-
- const value = useMemo(() => {
- const p = searchParams?.get(paramKey) ?? 'all';
-
- return isValueValid(p) ? p : 'all';
- }, [searchParams]);
-
- const onValueChange = (newValue: string) => {
- if (!pathname) {
- return;
- }
-
- const params = new URLSearchParams(searchParams?.toString());
-
- params.set(paramKey, newValue);
-
- if (newValue === '' || newValue === 'all') {
- params.delete(paramKey);
- }
-
- router.push(`${pathname}?${params.toString()}`, { scroll: false });
- };
-
- return (
-
-
-
-
-
- {children}
-
- );
-};
diff --git a/apps/web/src/components/forms/send-confirmation-email.tsx b/apps/web/src/components/forms/send-confirmation-email.tsx
deleted file mode 100644
index a11ee2068..000000000
--- a/apps/web/src/components/forms/send-confirmation-email.tsx
+++ /dev/null
@@ -1,100 +0,0 @@
-'use client';
-
-import { zodResolver } from '@hookform/resolvers/zod';
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { useForm } from 'react-hook-form';
-import { z } from 'zod';
-
-import { trpc } from '@documenso/trpc/react';
-import { cn } from '@documenso/ui/lib/utils';
-import { Button } from '@documenso/ui/primitives/button';
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from '@documenso/ui/primitives/form/form';
-import { Input } from '@documenso/ui/primitives/input';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-export const ZSendConfirmationEmailFormSchema = z.object({
- email: z.string().email().min(1),
-});
-
-export type TSendConfirmationEmailFormSchema = z.infer;
-
-export type SendConfirmationEmailFormProps = {
- className?: string;
-};
-
-export const SendConfirmationEmailForm = ({ className }: SendConfirmationEmailFormProps) => {
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const form = useForm({
- values: {
- email: '',
- },
- resolver: zodResolver(ZSendConfirmationEmailFormSchema),
- });
-
- const isSubmitting = form.formState.isSubmitting;
-
- const { mutateAsync: sendConfirmationEmail } = trpc.profile.sendConfirmationEmail.useMutation();
-
- const onFormSubmit = async ({ email }: TSendConfirmationEmailFormSchema) => {
- try {
- await sendConfirmationEmail({ email });
-
- toast({
- title: _(msg`Confirmation email sent`),
- description: _(
- msg`A confirmation email has been sent, and it should arrive in your inbox shortly.`,
- ),
- duration: 5000,
- });
-
- form.reset();
- } catch (err) {
- toast({
- title: _(msg`An error occurred while sending your confirmation email`),
- description: _(msg`Please try again and make sure you enter the correct email address.`),
- });
- }
- };
-
- return (
-
-
-
- (
-
-
- Email address
-
-
-
-
-
- )}
- />
-
-
-
-
- Send confirmation email
-
-
-
-
- );
-};
diff --git a/apps/web/src/components/forms/signin.tsx b/apps/web/src/components/forms/signin.tsx
deleted file mode 100644
index 82cd64592..000000000
--- a/apps/web/src/components/forms/signin.tsx
+++ /dev/null
@@ -1,520 +0,0 @@
-'use client';
-
-import { useEffect, useMemo, useState } from 'react';
-
-import Link from 'next/link';
-import { useRouter } from 'next/navigation';
-
-import { zodResolver } from '@hookform/resolvers/zod';
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { browserSupportsWebAuthn, startAuthentication } from '@simplewebauthn/browser';
-import { KeyRoundIcon } from 'lucide-react';
-import { signIn } from 'next-auth/react';
-import { useForm } from 'react-hook-form';
-import { FaIdCardClip } from 'react-icons/fa6';
-import { FcGoogle } from 'react-icons/fc';
-import { match } from 'ts-pattern';
-import { z } from 'zod';
-
-import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
-import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
-import { ErrorCode, isErrorCode } from '@documenso/lib/next-auth/error-codes';
-import { trpc } from '@documenso/trpc/react';
-import { ZCurrentPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
-import { cn } from '@documenso/ui/lib/utils';
-import { Button } from '@documenso/ui/primitives/button';
-import {
- Dialog,
- DialogContent,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from '@documenso/ui/primitives/dialog';
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from '@documenso/ui/primitives/form/form';
-import { Input } from '@documenso/ui/primitives/input';
-import { PasswordInput } from '@documenso/ui/primitives/password-input';
-import { PinInput, PinInputGroup, PinInputSlot } from '@documenso/ui/primitives/pin-input';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-const ERROR_MESSAGES: Partial> = {
- [ErrorCode.CREDENTIALS_NOT_FOUND]: 'The email or password provided is incorrect',
- [ErrorCode.INCORRECT_EMAIL_PASSWORD]: 'The email or password provided is incorrect',
- [ErrorCode.USER_MISSING_PASSWORD]:
- 'This account appears to be using a social login method, please sign in using that method',
- [ErrorCode.INCORRECT_TWO_FACTOR_CODE]: 'The two-factor authentication code provided is incorrect',
- [ErrorCode.INCORRECT_TWO_FACTOR_BACKUP_CODE]: 'The backup code provided is incorrect',
- [ErrorCode.UNVERIFIED_EMAIL]:
- 'This account has not been verified. Please verify your account before signing in.',
- [ErrorCode.ACCOUNT_DISABLED]: 'This account has been disabled. Please contact support.',
-};
-
-const TwoFactorEnabledErrorCode = ErrorCode.TWO_FACTOR_MISSING_CREDENTIALS;
-
-const LOGIN_REDIRECT_PATH = '/documents';
-
-export const ZSignInFormSchema = z.object({
- email: z.string().email().min(1),
- password: ZCurrentPasswordSchema,
- totpCode: z.string().trim().optional(),
- backupCode: z.string().trim().optional(),
-});
-
-export type TSignInFormSchema = z.infer;
-
-export type SignInFormProps = {
- className?: string;
- initialEmail?: string;
- isGoogleSSOEnabled?: boolean;
- isOIDCSSOEnabled?: boolean;
- oidcProviderLabel?: string;
- returnTo?: string;
-};
-
-export const SignInForm = ({
- className,
- initialEmail,
- isGoogleSSOEnabled,
- isOIDCSSOEnabled,
- oidcProviderLabel,
- returnTo,
-}: SignInFormProps) => {
- const { _ } = useLingui();
- const { toast } = useToast();
- const { getFlag } = useFeatureFlags();
-
- const router = useRouter();
-
- const [isTwoFactorAuthenticationDialogOpen, setIsTwoFactorAuthenticationDialogOpen] =
- useState(false);
-
- const [twoFactorAuthenticationMethod, setTwoFactorAuthenticationMethod] = useState<
- 'totp' | 'backup'
- >('totp');
-
- const [isPasskeyLoading, setIsPasskeyLoading] = useState(false);
-
- const isPasskeyEnabled = getFlag('app_passkey');
-
- const callbackUrl = useMemo(() => {
- // Handle SSR
- if (typeof window === 'undefined') {
- return LOGIN_REDIRECT_PATH;
- }
-
- let url = new URL(returnTo || LOGIN_REDIRECT_PATH, window.location.origin);
-
- // Don't allow different origins
- if (url.origin !== window.location.origin) {
- url = new URL(LOGIN_REDIRECT_PATH, window.location.origin);
- }
-
- return url.toString();
- }, [returnTo]);
-
- const { mutateAsync: createPasskeySigninOptions } =
- trpc.auth.createPasskeySigninOptions.useMutation();
-
- const form = useForm({
- values: {
- email: initialEmail ?? '',
- password: '',
- totpCode: '',
- backupCode: '',
- },
- resolver: zodResolver(ZSignInFormSchema),
- });
-
- const isSubmitting = form.formState.isSubmitting;
-
- const onCloseTwoFactorAuthenticationDialog = () => {
- form.setValue('totpCode', '');
- form.setValue('backupCode', '');
-
- setIsTwoFactorAuthenticationDialogOpen(false);
- };
-
- const onToggleTwoFactorAuthenticationMethodClick = () => {
- const method = twoFactorAuthenticationMethod === 'totp' ? 'backup' : 'totp';
-
- if (method === 'totp') {
- form.setValue('backupCode', '');
- }
-
- if (method === 'backup') {
- form.setValue('totpCode', '');
- }
-
- setTwoFactorAuthenticationMethod(method);
- };
-
- const onSignInWithPasskey = async () => {
- if (!browserSupportsWebAuthn()) {
- toast({
- title: _(msg`Not supported`),
- description: _(msg`Passkeys are not supported on this browser`),
- duration: 10000,
- variant: 'destructive',
- });
-
- return;
- }
-
- try {
- setIsPasskeyLoading(true);
-
- const options = await createPasskeySigninOptions();
-
- const credential = await startAuthentication(options);
-
- const result = await signIn('webauthn', {
- credential: JSON.stringify(credential),
- callbackUrl,
- redirect: false,
- });
-
- if (!result?.url || result.error) {
- throw new AppError(result?.error ?? '');
- }
-
- window.location.href = result.url;
- } catch (err) {
- setIsPasskeyLoading(false);
-
- if (err.name === 'NotAllowedError') {
- return;
- }
-
- const error = AppError.parseError(err);
-
- const errorMessage = match(error.code)
- .with(
- AppErrorCode.NOT_SETUP,
- () =>
- msg`This passkey is not configured for this application. Please login and add one in the user settings.`,
- )
- .with(AppErrorCode.EXPIRED_CODE, () => msg`This session has expired. Please try again.`)
- .otherwise(() => msg`Please try again later or login using your normal details`);
-
- toast({
- title: 'Something went wrong',
- description: _(errorMessage),
- duration: 10000,
- variant: 'destructive',
- });
- }
- };
-
- const onFormSubmit = async ({ email, password, totpCode, backupCode }: TSignInFormSchema) => {
- try {
- const credentials: Record = {
- email,
- password,
- };
-
- if (totpCode) {
- credentials.totpCode = totpCode;
- }
-
- if (backupCode) {
- credentials.backupCode = backupCode;
- }
-
- const result = await signIn('credentials', {
- ...credentials,
- callbackUrl,
- redirect: false,
- });
-
- if (result?.error && isErrorCode(result.error)) {
- if (result.error === TwoFactorEnabledErrorCode) {
- setIsTwoFactorAuthenticationDialogOpen(true);
- return;
- }
-
- const errorMessage = ERROR_MESSAGES[result.error];
-
- if (result.error === ErrorCode.UNVERIFIED_EMAIL) {
- router.push(`/unverified-account`);
-
- toast({
- title: _(msg`Unable to sign in`),
- description: errorMessage ?? _(msg`An unknown error occurred`),
- });
-
- return;
- }
-
- toast({
- title: _(msg`Unable to sign in`),
- description: errorMessage ?? _(msg`An unknown error occurred`),
- variant: 'destructive',
- });
-
- return;
- }
-
- if (!result?.url) {
- throw new Error('An unknown error occurred');
- }
-
- window.location.href = result.url;
- } catch (err) {
- toast({
- title: _(msg`An unknown error occurred`),
- description: _(
- msg`We encountered an unknown error while attempting to sign you In. Please try again later.`,
- ),
- });
- }
- };
-
- const onSignInWithGoogleClick = async () => {
- try {
- await signIn('google', {
- callbackUrl,
- });
- } catch (err) {
- toast({
- title: _(msg`An unknown error occurred`),
- description: _(
- msg`We encountered an unknown error while attempting to sign you In. Please try again later.`,
- ),
- variant: 'destructive',
- });
- }
- };
-
- const onSignInWithOIDCClick = async () => {
- try {
- await signIn('oidc', {
- callbackUrl,
- });
- } catch (err) {
- toast({
- title: _(msg`An unknown error occurred`),
- description: _(
- msg`We encountered an unknown error while attempting to sign you In. Please try again later.`,
- ),
- variant: 'destructive',
- });
- }
- };
-
- useEffect(() => {
- const hash = window.location.hash.slice(1);
-
- const params = new URLSearchParams(hash);
-
- const email = params.get('email');
-
- if (email) {
- form.setValue('email', email);
- }
- }, [form]);
-
- return (
-
-
-
- (
-
-
- Email
-
-
-
-
-
-
-
-
- )}
- />
-
- (
-
-
- Password
-
-
-
-
-
-
-
-
-
-
- Forgot your password?
-
-
-
- )}
- />
-
-
- {isSubmitting ? Signing in... : Sign In }
-
-
- {(isGoogleSSOEnabled || isPasskeyEnabled || isOIDCSSOEnabled) && (
-
-
-
- Or continue with
-
-
-
- )}
-
- {isGoogleSSOEnabled && (
-
-
- Google
-
- )}
-
- {isOIDCSSOEnabled && (
-
-
- {oidcProviderLabel || 'OIDC'}
-
- )}
-
- {isPasskeyEnabled && (
-
- {!isPasskeyLoading && }
- Passkey
-
- )}
-
-
-
-
-
-
-
- Two-Factor Authentication
-
-
-
-
-
- {twoFactorAuthenticationMethod === 'totp' && (
- (
-
- Token
-
-
- {Array(6)
- .fill(null)
- .map((_, i) => (
-
-
-
- ))}
-
-
-
-
- )}
- />
- )}
-
- {twoFactorAuthenticationMethod === 'backup' && (
- (
-
-
- Backup Code
-
-
-
-
-
-
- )}
- />
- )}
-
-
-
- {twoFactorAuthenticationMethod === 'totp' ? (
- Use Backup Code
- ) : (
- Use Authenticator
- )}
-
-
-
- {isSubmitting ? Signing in... : Sign In }
-
-
-
-
-
-
-
- );
-};
diff --git a/apps/web/src/components/forms/signup.tsx b/apps/web/src/components/forms/signup.tsx
deleted file mode 100644
index 762db4cef..000000000
--- a/apps/web/src/components/forms/signup.tsx
+++ /dev/null
@@ -1,284 +0,0 @@
-'use client';
-
-import { useRouter } from 'next/navigation';
-
-import { zodResolver } from '@hookform/resolvers/zod';
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { signIn } from 'next-auth/react';
-import { useForm } from 'react-hook-form';
-import { FcGoogle } from 'react-icons/fc';
-import { z } from 'zod';
-
-import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
-import { AppError } from '@documenso/lib/errors/app-error';
-import { trpc } from '@documenso/trpc/react';
-import { ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
-import { cn } from '@documenso/ui/lib/utils';
-import { Button } from '@documenso/ui/primitives/button';
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from '@documenso/ui/primitives/form/form';
-import { Input } from '@documenso/ui/primitives/input';
-import { PasswordInput } from '@documenso/ui/primitives/password-input';
-import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-import { signupErrorMessages } from './v2/signup';
-
-const SIGN_UP_REDIRECT_PATH = '/documents';
-
-export const ZSignUpFormSchema = z
- .object({
- name: z.string().trim().min(1, { message: 'Please enter a valid name.' }),
- email: z.string().email().min(1),
- password: ZPasswordSchema,
- signature: z.string().min(1, { message: 'We need your signature to sign documents' }),
- })
- .refine(
- (data) => {
- const { name, email, password } = data;
- return !password.includes(name) && !password.includes(email.split('@')[0]);
- },
- {
- message: 'Password should not be common or based on personal information',
- },
- );
-
-export type TSignUpFormSchema = z.infer;
-
-export type SignUpFormProps = {
- className?: string;
- initialEmail?: string;
- isGoogleSSOEnabled?: boolean;
- isOIDCSSOEnabled?: boolean;
-};
-
-export const SignUpForm = ({
- className,
- initialEmail,
- isGoogleSSOEnabled,
- isOIDCSSOEnabled,
-}: SignUpFormProps) => {
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const analytics = useAnalytics();
- const router = useRouter();
-
- const form = useForm({
- values: {
- name: '',
- email: initialEmail ?? '',
- password: '',
- signature: '',
- },
- resolver: zodResolver(ZSignUpFormSchema),
- });
-
- const isSubmitting = form.formState.isSubmitting;
-
- const { mutateAsync: signup } = trpc.auth.signup.useMutation();
-
- const onFormSubmit = async ({ name, email, password, signature }: TSignUpFormSchema) => {
- try {
- await signup({ name, email, password, signature });
-
- router.push(`/unverified-account`);
-
- toast({
- title: _(msg`Registration Successful`),
- description: _(
- msg`You have successfully registered. Please verify your account by clicking on the link you received in the email.`,
- ),
- duration: 5000,
- });
-
- analytics.capture('App: User Sign Up', {
- email,
- timestamp: new Date().toISOString(),
- });
- } catch (err) {
- const error = AppError.parseError(err);
-
- const errorMessage = signupErrorMessages[error.code] ?? signupErrorMessages.INVALID_REQUEST;
-
- toast({
- title: _(msg`An error occurred`),
- description: _(errorMessage),
- variant: 'destructive',
- });
- }
- };
-
- const onSignUpWithGoogleClick = async () => {
- try {
- await signIn('google', { callbackUrl: SIGN_UP_REDIRECT_PATH });
- } catch (err) {
- toast({
- title: _(msg`An unknown error occurred`),
- description: _(
- msg`We encountered an unknown error while attempting to sign you Up. Please try again later.`,
- ),
- variant: 'destructive',
- });
- }
- };
-
- const onSignUpWithOIDCClick = async () => {
- try {
- await signIn('oidc', { callbackUrl: SIGN_UP_REDIRECT_PATH });
- } catch (err) {
- toast({
- title: _(msg`An unknown error occurred`),
- description: _(
- msg`We encountered an unknown error while attempting to sign you Up. Please try again later.`,
- ),
- variant: 'destructive',
- });
- }
- };
-
- return (
-
-
-
- (
-
-
- Name
-
-
-
-
-
-
- )}
- />
-
- (
-
-
- Email
-
-
-
-
-
-
- )}
- />
-
- (
-
-
- Password
-
-
-
-
-
-
- )}
- />
-
- (
-
-
- Sign Here
-
-
- onChange(v ?? '')}
- />
-
-
-
-
- )}
- />
-
-
-
- {isSubmitting ? Signing up... : Sign Up }
-
-
- {isGoogleSSOEnabled && (
- <>
-
-
-
-
- Sign Up with Google
-
- >
- )}
-
- {isOIDCSSOEnabled && (
- <>
-
-
-
-
- Sign Up with OIDC
-
- >
- )}
-
-
- );
-};
diff --git a/apps/web/src/components/forms/token.tsx b/apps/web/src/components/forms/token.tsx
deleted file mode 100644
index fe2985c52..000000000
--- a/apps/web/src/components/forms/token.tsx
+++ /dev/null
@@ -1,297 +0,0 @@
-'use client';
-
-import { useState, useTransition } from 'react';
-
-import { useRouter } from 'next/navigation';
-
-import { zodResolver } from '@hookform/resolvers/zod';
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { AnimatePresence, motion } from 'framer-motion';
-import { useForm } from 'react-hook-form';
-import { match } from 'ts-pattern';
-import { z } from 'zod';
-
-import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard';
-import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
-import type { ApiToken } from '@documenso/prisma/client';
-import { trpc } from '@documenso/trpc/react';
-import type { TCreateTokenMutationSchema } from '@documenso/trpc/server/api-token-router/schema';
-import { ZCreateTokenMutationSchema } from '@documenso/trpc/server/api-token-router/schema';
-import { cn } from '@documenso/ui/lib/utils';
-import { Button } from '@documenso/ui/primitives/button';
-import { Card, CardContent } from '@documenso/ui/primitives/card';
-import {
- Form,
- FormControl,
- FormDescription,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from '@documenso/ui/primitives/form/form';
-import { Input } from '@documenso/ui/primitives/input';
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from '@documenso/ui/primitives/select';
-import { Switch } from '@documenso/ui/primitives/switch';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-import { EXPIRATION_DATES } from '../(dashboard)/settings/token/contants';
-
-const ZCreateTokenFormSchema = ZCreateTokenMutationSchema.extend({
- enabled: z.boolean(),
-});
-
-type TCreateTokenFormSchema = z.infer;
-
-type NewlyCreatedToken = {
- id: number;
- token: string;
-};
-
-export type ApiTokenFormProps = {
- className?: string;
- teamId?: number;
- tokens?: Pick[];
-};
-
-export const ApiTokenForm = ({ className, teamId, tokens }: ApiTokenFormProps) => {
- const router = useRouter();
- const [isTransitionPending, startTransition] = useTransition();
-
- const [, copy] = useCopyToClipboard();
-
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const [newlyCreatedToken, setNewlyCreatedToken] = useState();
- const [noExpirationDate, setNoExpirationDate] = useState(false);
-
- // This lets us hide the token from being copied if it has been deleted without
- // resorting to a useEffect or any other fanciness. This comes at the cost of it
- // taking slighly longer to appear since it will need to wait for the router.refresh()
- // to finish updating.
- const hasNewlyCreatedToken =
- tokens?.find((token) => token.id === newlyCreatedToken?.id) !== undefined;
-
- const { mutateAsync: createTokenMutation } = trpc.apiToken.createToken.useMutation({
- onSuccess(data) {
- setNewlyCreatedToken(data);
- },
- });
-
- const form = useForm({
- resolver: zodResolver(ZCreateTokenFormSchema),
- defaultValues: {
- tokenName: '',
- expirationDate: '',
- enabled: false,
- },
- });
-
- 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 }: TCreateTokenMutationSchema) => {
- try {
- await createTokenMutation({
- teamId,
- tokenName,
- expirationDate: noExpirationDate ? null : expirationDate,
- });
-
- toast({
- title: _(msg`Token created`),
- description: _(msg`A new token was created successfully.`),
- duration: 5000,
- });
-
- form.reset();
-
- startTransition(() => router.refresh());
- } 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
-
-
-
-
-
-
-
-
-
- {Object.entries(EXPIRATION_DATES).map(([key, date]) => (
-
- {_(date)}
-
- ))}
-
-
-
-
-
-
-
- )}
- />
-
- (
-
-
- Never expire
-
-
-
- {
- setNoExpirationDate((prev) => !prev);
- field.onChange(val);
- }}
- />
-
-
-
-
- )}
- />
-
-
-
- Create token
-
-
-
-
- Create token
-
-
-
-
-
-
-
- {newlyCreatedToken && hasNewlyCreatedToken && (
-
-
-
-
-
- Your token was created successfully! Make sure to copy it because you won't be
- able to see it again!
-
-
-
-
- {newlyCreatedToken.token}
-
-
- void copyToken(newlyCreatedToken.token)}>
- Copy token
-
-
-
-
- )}
-
-
- );
-};
diff --git a/apps/web/src/components/forms/v2/signup.tsx b/apps/web/src/components/forms/v2/signup.tsx
deleted file mode 100644
index b17c295f6..000000000
--- a/apps/web/src/components/forms/v2/signup.tsx
+++ /dev/null
@@ -1,547 +0,0 @@
-'use client';
-
-import { useEffect, useState } from 'react';
-
-import Image from 'next/image';
-import Link from 'next/link';
-import { useRouter, useSearchParams } from 'next/navigation';
-
-import { zodResolver } from '@hookform/resolvers/zod';
-import type { MessageDescriptor } from '@lingui/core';
-import { Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import { AnimatePresence, motion } from 'framer-motion';
-import { signIn } from 'next-auth/react';
-import { useForm } from 'react-hook-form';
-import { FaIdCardClip } from 'react-icons/fa6';
-import { FcGoogle } from 'react-icons/fc';
-import { z } from 'zod';
-
-import communityCardsImage from '@documenso/assets/images/community-cards.png';
-import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
-import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
-import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
-import { trpc } from '@documenso/trpc/react';
-import { ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
-import { cn } from '@documenso/ui/lib/utils';
-import { Button } from '@documenso/ui/primitives/button';
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from '@documenso/ui/primitives/form/form';
-import { Input } from '@documenso/ui/primitives/input';
-import { PasswordInput } from '@documenso/ui/primitives/password-input';
-import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-import { UserProfileSkeleton } from '~/components/ui/user-profile-skeleton';
-import { UserProfileTimur } from '~/components/ui/user-profile-timur';
-
-const SIGN_UP_REDIRECT_PATH = '/documents';
-
-type SignUpStep = 'BASIC_DETAILS' | 'CLAIM_USERNAME';
-
-export const ZSignUpFormV2Schema = z
- .object({
- name: z
- .string()
- .trim()
- .min(1, { message: msg`Please enter a valid name.`.id }),
- email: z.string().email().min(1),
- password: ZPasswordSchema,
- signature: z.string().min(1, { message: msg`We need your signature to sign documents`.id }),
- url: z
- .string()
- .trim()
- .toLowerCase()
- .min(1, { message: msg`We need a username to create your profile`.id })
- .regex(/^[a-z0-9-]+$/, {
- message: msg`Username can only container alphanumeric characters and dashes.`.id,
- }),
- })
- .refine(
- (data) => {
- const { name, email, password } = data;
- return !password.includes(name) && !password.includes(email.split('@')[0]);
- },
- {
- message: msg`Password should not be common or based on personal information`.id,
- path: ['password'],
- },
- );
-
-export const signupErrorMessages: Record = {
- SIGNUP_DISABLED: msg`Signups are disabled.`,
- PROFILE_URL_TAKEN: msg`This username has already been taken`,
- PREMIUM_PROFILE_URL: msg`Only subscribers can have a username shorter than 6 characters`,
- [AppErrorCode.ALREADY_EXISTS]: msg`User with this email already exists. Please use a different email address.`,
- [AppErrorCode.INVALID_REQUEST]: msg`We were unable to create your account. Please review the information you provided and try again.`,
-};
-
-export type TSignUpFormV2Schema = z.infer;
-
-export type SignUpFormV2Props = {
- className?: string;
- initialEmail?: string;
- isGoogleSSOEnabled?: boolean;
- isOIDCSSOEnabled?: boolean;
-};
-
-export const SignUpFormV2 = ({
- className,
- initialEmail,
- isGoogleSSOEnabled,
- isOIDCSSOEnabled,
-}: SignUpFormV2Props) => {
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const analytics = useAnalytics();
- const router = useRouter();
- const searchParams = useSearchParams();
-
- const [step, setStep] = useState('BASIC_DETAILS');
-
- const utmSrc = searchParams?.get('utm_source') ?? null;
-
- const baseUrl = new URL(NEXT_PUBLIC_WEBAPP_URL() ?? 'http://localhost:3000');
-
- const form = useForm({
- values: {
- name: '',
- email: initialEmail ?? '',
- password: '',
- signature: '',
- url: '',
- },
- mode: 'onBlur',
- resolver: zodResolver(ZSignUpFormV2Schema),
- });
-
- const isSubmitting = form.formState.isSubmitting;
-
- const name = form.watch('name');
- const url = form.watch('url');
-
- const { mutateAsync: signup } = trpc.auth.signup.useMutation();
-
- const onFormSubmit = async ({ name, email, password, signature, url }: TSignUpFormV2Schema) => {
- try {
- await signup({ name, email, password, signature, url });
-
- router.push(`/unverified-account`);
-
- toast({
- title: _(msg`Registration Successful`),
- description: _(
- msg`You have successfully registered. Please verify your account by clicking on the link you received in the email.`,
- ),
- duration: 5000,
- });
-
- analytics.capture('App: User Sign Up', {
- email,
- timestamp: new Date().toISOString(),
- custom_campaign_params: { src: utmSrc },
- });
- } catch (err) {
- const error = AppError.parseError(err);
-
- const errorMessage = signupErrorMessages[error.code] ?? signupErrorMessages.INVALID_REQUEST;
-
- if (
- error.code === AppErrorCode.PROFILE_URL_TAKEN ||
- error.code === AppErrorCode.PREMIUM_PROFILE_URL
- ) {
- form.setError('url', {
- type: 'manual',
- message: _(errorMessage),
- });
- } else {
- toast({
- title: _(msg`An error occurred`),
- description: _(errorMessage),
- variant: 'destructive',
- });
- }
- }
- };
-
- const onNextClick = async () => {
- const valid = await form.trigger(['name', 'email', 'password', 'signature']);
-
- if (valid) {
- setStep('CLAIM_USERNAME');
- }
- };
-
- const onSignUpWithGoogleClick = async () => {
- try {
- await signIn('google', { callbackUrl: SIGN_UP_REDIRECT_PATH });
- } catch (err) {
- toast({
- title: _(msg`An unknown error occurred`),
- description: _(
- msg`We encountered an unknown error while attempting to sign you Up. Please try again later.`,
- ),
- variant: 'destructive',
- });
- }
- };
-
- const onSignUpWithOIDCClick = async () => {
- try {
- await signIn('oidc', { callbackUrl: SIGN_UP_REDIRECT_PATH });
- } catch (err) {
- toast({
- title: _(msg`An unknown error occurred`),
- description: _(
- msg`We encountered an unknown error while attempting to sign you Up. Please try again later.`,
- ),
- variant: 'destructive',
- });
- }
- };
-
- useEffect(() => {
- const hash = window.location.hash.slice(1);
-
- const params = new URLSearchParams(hash);
-
- const email = params.get('email');
-
- if (email) {
- form.setValue('email', email);
- }
- }, [form]);
-
- return (
-
-
-
-
-
-
-
-
-
-
- User profiles are here!
-
-
-
- {step === 'BASIC_DETAILS' ? (
-
-
-
- ) : (
-
-
-
- )}
-
-
-
-
-
-
-
- {step === 'BASIC_DETAILS' && (
-
-
- Create a new account
-
-
-
-
- Create your account and start using state-of-the-art document signing. Open and
- beautiful signing is within your grasp.
-
-
-
- )}
-
- {step === 'CLAIM_USERNAME' && (
-
-
- Claim your username now
-
-
-
-
- You will get notified & be able to set up your documenso public profile when we
- launch the feature.
-
-
-
- )}
-
-
-
-
-
- {step === 'BASIC_DETAILS' && (
-
- (
-
-
- Full Name
-
-
-
-
-
-
- )}
- />
-
- (
-
-
- Email Address
-
-
-
-
-
-
- )}
- />
-
- (
-
-
- Password
-
-
-
-
-
-
-
-
- )}
- />
-
- (
-
-
- Sign Here
-
-
- onChange(v ?? '')}
- />
-
-
-
-
- )}
- />
-
- {(isGoogleSSOEnabled || isOIDCSSOEnabled) && (
- <>
-
- >
- )}
-
- {isGoogleSSOEnabled && (
- <>
-
-
- Sign Up with Google
-
- >
- )}
-
- {isOIDCSSOEnabled && (
- <>
-
-
- Sign Up with OIDC
-
- >
- )}
-
-
-
- Already have an account?{' '}
-
- Sign in instead
-
-
-
-
- )}
-
- {step === 'CLAIM_USERNAME' && (
-
- (
-
-
- Public profile username
-
-
-
-
-
-
-
-
-
- {baseUrl.host}/u/{field.value || ''}
-
-
- )}
- />
-
- )}
-
-
- {step === 'BASIC_DETAILS' && (
-
-
- Basic details
- {' '}
- 1/2
-
- )}
-
- {step === 'CLAIM_USERNAME' && (
-
-
- Claim username
- {' '}
- 2/2
-
- )}
-
-
-
-
-
-
-
- {/* Go back button, disabled if step is basic details */}
- setStep('BASIC_DETAILS')}
- >
- Back
-
-
- {/* Continue button */}
- {step === 'BASIC_DETAILS' && (
-
- Next
-
- )}
-
- {/* Sign up button */}
- {step === 'CLAIM_USERNAME' && (
-
- Complete
-
- )}
-
-
-
-
-
- );
-};
diff --git a/apps/web/src/components/general/signing-disclosure.tsx b/apps/web/src/components/general/signing-disclosure.tsx
deleted file mode 100644
index a6257d35f..000000000
--- a/apps/web/src/components/general/signing-disclosure.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import type { HTMLAttributes } from 'react';
-
-import Link from 'next/link';
-
-import { Trans } from '@lingui/macro';
-
-import { cn } from '@documenso/ui/lib/utils';
-
-export type SigningDisclosureProps = HTMLAttributes;
-
-export const SigningDisclosure = ({ className, ...props }: SigningDisclosureProps) => {
- return (
-
-
- By proceeding with your electronic signature, you acknowledge and consent that it will be
- used to sign the given document and holds the same legal validity as a handwritten
- signature. By completing the electronic signing process, you affirm your understanding and
- acceptance of these conditions.
-
-
-
- Read the full{' '}
-
- signature disclosure
-
- .
-
-
-
- );
-};
diff --git a/apps/web/src/components/partials/not-found.tsx b/apps/web/src/components/partials/not-found.tsx
deleted file mode 100644
index e13382fee..000000000
--- a/apps/web/src/components/partials/not-found.tsx
+++ /dev/null
@@ -1,76 +0,0 @@
-'use client';
-
-import Image from 'next/image';
-import { useRouter } from 'next/navigation';
-
-import { Trans } from '@lingui/macro';
-import { motion } from 'framer-motion';
-import { ChevronLeft } from 'lucide-react';
-
-import backgroundPattern from '@documenso/assets/images/background-pattern.png';
-import { cn } from '@documenso/ui/lib/utils';
-import { Button } from '@documenso/ui/primitives/button';
-
-export type NotFoundPartialProps = {
- children?: React.ReactNode;
-};
-
-export default function NotFoundPartial({ children }: NotFoundPartialProps) {
- const router = useRouter();
-
- return (
-
-
-
-
-
-
-
-
-
-
- 404 Page not found
-
-
-
- Oops! Something went wrong.
-
-
-
-
- The page you are looking for was moved, removed, renamed or might never have existed.
-
-
-
-
- {
- void router.back();
- }}
- >
-
- Go Back
-
-
- {children}
-
-
-
-
- );
-}
diff --git a/apps/web/src/components/templates/manage-public-template-dialog.tsx b/apps/web/src/components/templates/manage-public-template-dialog.tsx
deleted file mode 100644
index 67ac27782..000000000
--- a/apps/web/src/components/templates/manage-public-template-dialog.tsx
+++ /dev/null
@@ -1,468 +0,0 @@
-'use client';
-
-import { useEffect, useMemo, useState } from 'react';
-
-import { zodResolver } from '@hookform/resolvers/zod';
-import { Plural, Trans, msg } from '@lingui/macro';
-import { useLingui } from '@lingui/react';
-import type * as DialogPrimitive from '@radix-ui/react-dialog';
-import { CheckCircle2Icon, CircleIcon } from 'lucide-react';
-import { useForm } from 'react-hook-form';
-import { P, match } from 'ts-pattern';
-import { z } from 'zod';
-
-import type { Template, TemplateDirectLink } from '@documenso/prisma/client';
-import { TemplateType } from '@documenso/prisma/client';
-import { trpc } from '@documenso/trpc/react';
-import {
- MAX_TEMPLATE_PUBLIC_DESCRIPTION_LENGTH,
- MAX_TEMPLATE_PUBLIC_TITLE_LENGTH,
-} from '@documenso/trpc/server/template-router/schema';
-import { AnimateGenericFadeInOut } from '@documenso/ui/components/animate/animate-generic-fade-in-out';
-import { Button } from '@documenso/ui/primitives/button';
-import {
- Dialog,
- DialogClose,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from '@documenso/ui/primitives/dialog';
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from '@documenso/ui/primitives/form/form';
-import { Input } from '@documenso/ui/primitives/input';
-import {
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeader,
- TableRow,
-} from '@documenso/ui/primitives/table';
-import { Textarea } from '@documenso/ui/primitives/textarea';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-
-import { useOptionalCurrentTeam } from '~/providers/team';
-
-export type ManagePublicTemplateDialogProps = {
- directTemplates: (Template & {
- directLink: Pick;
- })[];
- initialTemplateId?: number | null;
- initialStep?: ProfileTemplateStep;
- trigger?: React.ReactNode;
- isOpen?: boolean;
- onIsOpenChange?: (value: boolean) => unknown;
-} & Omit;
-
-const ZUpdatePublicTemplateFormSchema = z.object({
- publicTitle: z
- .string()
- .min(1, { message: 'Title is required' })
- .max(MAX_TEMPLATE_PUBLIC_TITLE_LENGTH, {
- message: `Title cannot be longer than ${MAX_TEMPLATE_PUBLIC_TITLE_LENGTH} characters`,
- }),
- publicDescription: z
- .string()
- .min(1, { message: 'Description is required' })
- .max(MAX_TEMPLATE_PUBLIC_DESCRIPTION_LENGTH, {
- message: `Description cannot be longer than ${MAX_TEMPLATE_PUBLIC_DESCRIPTION_LENGTH} characters`,
- }),
-});
-
-type TUpdatePublicTemplateFormSchema = z.infer;
-
-type ProfileTemplateStep = 'SELECT_TEMPLATE' | 'MANAGE' | 'CONFIRM_DISABLE';
-
-export const ManagePublicTemplateDialog = ({
- directTemplates,
- trigger,
- initialTemplateId = null,
- initialStep = 'SELECT_TEMPLATE',
- isOpen = false,
- onIsOpenChange,
- ...props
-}: ManagePublicTemplateDialogProps) => {
- const { _, i18n } = useLingui();
- const { toast } = useToast();
-
- const [open, onOpenChange] = useState(isOpen);
-
- const team = useOptionalCurrentTeam();
-
- const [selectedTemplateId, setSelectedTemplateId] = useState(initialTemplateId);
-
- const [currentStep, setCurrentStep] = useState(() => {
- if (initialStep) {
- return initialStep;
- }
-
- return selectedTemplateId ? 'MANAGE' : 'SELECT_TEMPLATE';
- });
-
- const form = useForm({
- resolver: zodResolver(ZUpdatePublicTemplateFormSchema),
- defaultValues: {
- publicTitle: '',
- publicDescription: '',
- },
- });
-
- const { mutateAsync: updateTemplateSettings, isPending: isUpdatingTemplateSettings } =
- trpc.template.updateTemplate.useMutation();
-
- const setTemplateToPrivate = async (templateId: number) => {
- try {
- await updateTemplateSettings({
- templateId,
- data: {
- type: TemplateType.PRIVATE,
- },
- });
-
- toast({
- title: _(msg`Success`),
- description: _(msg`Template has been removed from your public profile.`),
- duration: 5000,
- });
-
- handleOnOpenChange(false);
- } catch {
- toast({
- title: _(msg`An unknown error occurred`),
- description: _(
- msg`We encountered an unknown error while attempting to remove this template from your profile. Please try again later.`,
- ),
- variant: 'destructive',
- });
- }
- };
-
- const onFormSubmit = async ({
- publicTitle,
- publicDescription,
- }: TUpdatePublicTemplateFormSchema) => {
- if (!selectedTemplateId) {
- return;
- }
-
- try {
- await updateTemplateSettings({
- templateId: selectedTemplateId,
- data: {
- type: TemplateType.PUBLIC,
- publicTitle,
- publicDescription,
- },
- });
-
- toast({
- title: _(msg`Success`),
- description: _(msg`Template has been updated.`),
- duration: 5000,
- });
-
- onOpenChange(false);
- } catch {
- toast({
- title: _(msg`An unknown error occurred`),
- description: _(
- msg`We encountered an unknown error while attempting to update the template. Please try again later.`,
- ),
- variant: 'destructive',
- });
- }
- };
-
- const selectedTemplate = useMemo(
- () => directTemplates.find((template) => template.id === selectedTemplateId),
- [directTemplates, selectedTemplateId],
- );
-
- const onManageStep = () => {
- if (!selectedTemplate) {
- return;
- }
-
- form.reset({
- publicTitle: selectedTemplate.publicTitle,
- publicDescription: selectedTemplate.publicDescription,
- });
-
- setCurrentStep('MANAGE');
- };
-
- const isLoading = isUpdatingTemplateSettings || form.formState.isSubmitting;
-
- useEffect(() => {
- const initialTemplate = directTemplates.find((template) => template.id === initialTemplateId);
-
- if (initialTemplate) {
- setSelectedTemplateId(initialTemplate.id);
-
- form.reset({
- publicTitle: initialTemplate.publicTitle,
- publicDescription: initialTemplate.publicDescription,
- });
- } else {
- setSelectedTemplateId(null);
- }
-
- const step = initialStep || (selectedTemplateId ? 'MANAGE' : 'SELECT_TEMPLATE');
-
- setCurrentStep(step);
-
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [initialTemplateId, initialStep, open, isOpen]);
-
- const handleOnOpenChange = (value: boolean) => {
- if (isLoading || typeof value !== 'boolean') {
- return;
- }
-
- onOpenChange(value);
- onIsOpenChange?.(value);
- };
-
- return (
-
-
- {trigger}
-
-
- {match({ templateId: selectedTemplateId, currentStep })
- .with({ currentStep: 'SELECT_TEMPLATE' }, () => (
-
-
-
- {team?.name ? (
-