diff --git a/.env.example b/.env.example
index fb22bbedf..9ea1ae4b9 100644
--- a/.env.example
+++ b/.env.example
@@ -10,6 +10,9 @@ NEXT_PRIVATE_GOOGLE_CLIENT_SECRET=""
NEXT_PUBLIC_WEBAPP_URL="http://localhost:3000"
NEXT_PUBLIC_MARKETING_URL="http://localhost:3001"
+# [[MARKETING]]
+NEXT_PUBLIC_MARKETING_SITE_URL="http://localhost:3001"
+
# [[DATABASE]]
NEXT_PRIVATE_DATABASE_URL="postgres://documenso:password@127.0.0.1:54320/documenso"
# Defines the URL to use for the database when running migrations and other commands that won't work with a connection pool.
diff --git a/apps/marketing/next.config.js b/apps/marketing/next.config.js
index 2783e4063..e74f7d545 100644
--- a/apps/marketing/next.config.js
+++ b/apps/marketing/next.config.js
@@ -2,7 +2,7 @@
const path = require('path');
const { withContentlayer } = require('next-contentlayer');
-const { parsed: env } = require('dotenv').config({
+require('dotenv').config({
path: path.join(__dirname, '../../.env.local'),
});
@@ -10,9 +10,13 @@ const { parsed: env } = require('dotenv').config({
const config = {
experimental: {
serverActions: true,
+ serverActionsBodySizeLimit: '10mb',
},
reactStrictMode: true,
transpilePackages: ['@documenso/lib', '@documenso/prisma', '@documenso/trpc', '@documenso/ui'],
+ env: {
+ NEXT_PUBLIC_PROJECT: 'marketing',
+ },
modularizeImports: {
'lucide-react': {
transform: 'lucide-react/dist/esm/icons/{{ kebabCase member }}',
diff --git a/apps/marketing/package.json b/apps/marketing/package.json
index c76c5e631..b2bce26c6 100644
--- a/apps/marketing/package.json
+++ b/apps/marketing/package.json
@@ -18,14 +18,16 @@
"@hookform/resolvers": "^3.1.0",
"contentlayer": "^0.3.4",
"framer-motion": "^10.12.8",
- "lucide-react": "^0.214.0",
+ "lucide-react": "^0.277.0",
"micro": "^10.0.1",
- "next": "13.4.12",
+ "next": "13.4.19",
"next-auth": "4.22.3",
"next-contentlayer": "^0.3.4",
"next-plausible": "^3.10.1",
"perfect-freehand": "^1.2.0",
+ "posthog-js": "^1.77.3",
"react": "18.2.0",
+ "react-confetti": "^6.1.0",
"react-dom": "18.2.0",
"react-hook-form": "^7.43.9",
"react-icons": "^4.8.0",
diff --git a/apps/marketing/src/app/(marketing)/open/page.tsx b/apps/marketing/src/app/(marketing)/open/page.tsx
index 2d5bc2aa4..37d390223 100644
--- a/apps/marketing/src/app/(marketing)/open/page.tsx
+++ b/apps/marketing/src/app/(marketing)/open/page.tsx
@@ -66,7 +66,7 @@ export default async function OpenPage() {
.then((res) => ZStargazersLiveResponse.parse(res));
return (
-
+
Open Startup
diff --git a/apps/marketing/src/app/(marketing)/pricing/page.tsx b/apps/marketing/src/app/(marketing)/pricing/page.tsx
index 238fa11a4..0e927b836 100644
--- a/apps/marketing/src/app/(marketing)/pricing/page.tsx
+++ b/apps/marketing/src/app/(marketing)/pricing/page.tsx
@@ -20,7 +20,7 @@ export type PricingPageProps = {
export default function PricingPage() {
return (
-
+
Pricing
diff --git a/apps/marketing/src/app/(marketing)/single-player-mode/[token]/success/page.tsx b/apps/marketing/src/app/(marketing)/single-player-mode/[token]/success/page.tsx
new file mode 100644
index 000000000..138591a7e
--- /dev/null
+++ b/apps/marketing/src/app/(marketing)/single-player-mode/[token]/success/page.tsx
@@ -0,0 +1,29 @@
+import { notFound } from 'next/navigation';
+
+import { getDocumentAndRecipientByToken } from '@documenso/lib/server-only/document/get-document-by-token';
+
+import SinglePlayerModeSuccess from '~/components/(marketing)/single-player-mode/single-player-mode-success';
+
+export type SinglePlayerModeSuccessPageProps = {
+ params: {
+ token?: string;
+ };
+};
+
+export default async function SinglePlayerModeSuccessPage({
+ params: { token },
+}: SinglePlayerModeSuccessPageProps) {
+ if (!token) {
+ return notFound();
+ }
+
+ const document = await getDocumentAndRecipientByToken({
+ token,
+ }).catch(() => null);
+
+ if (!document || document.status !== 'COMPLETED') {
+ return notFound();
+ }
+
+ return
;
+}
diff --git a/apps/marketing/src/app/(marketing)/single-player-mode/page.tsx b/apps/marketing/src/app/(marketing)/single-player-mode/page.tsx
new file mode 100644
index 000000000..02e9cff63
--- /dev/null
+++ b/apps/marketing/src/app/(marketing)/single-player-mode/page.tsx
@@ -0,0 +1,237 @@
+'use client';
+
+import { useEffect, useState } from 'react';
+
+import Link from 'next/link';
+import { useRouter } from 'next/navigation';
+
+import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
+import { Field, Prisma, Recipient } from '@documenso/prisma/client';
+import { Card, CardContent } from '@documenso/ui/primitives/card';
+import { DocumentDropzone } from '@documenso/ui/primitives/document-dropzone';
+import { AddFieldsFormPartial } from '@documenso/ui/primitives/document-flow/add-fields';
+import { TAddFieldsFormSchema } from '@documenso/ui/primitives/document-flow/add-fields.types';
+import { AddSignatureFormPartial } from '@documenso/ui/primitives/document-flow/add-signature';
+import { TAddSignatureFormSchema } from '@documenso/ui/primitives/document-flow/add-signature.types';
+import {
+ DocumentFlowFormContainer,
+ DocumentFlowFormContainerHeader,
+} from '@documenso/ui/primitives/document-flow/document-flow-root';
+import { DocumentFlowStep } from '@documenso/ui/primitives/document-flow/types';
+import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
+import { useToast } from '@documenso/ui/primitives/use-toast';
+
+import { createSinglePlayerDocument } from '~/components/(marketing)/single-player-mode/create-single-player-document.action';
+
+type SinglePlayerModeStep = 'fields' | 'sign';
+
+export default function SinglePlayerModePage() {
+ const analytics = useAnalytics();
+ const router = useRouter();
+
+ const { toast } = useToast();
+
+ const [uploadedFile, setUploadedFile] = useState<{ name: string; file: string } | null>();
+
+ const [step, setStep] = useState
('fields');
+ const [fields, setFields] = useState([]);
+
+ const documentFlow: Record = {
+ fields: {
+ title: 'Add document',
+ description: 'Upload a document and add fields.',
+ stepIndex: 1,
+ onBackStep: uploadedFile
+ ? () => {
+ setUploadedFile(null);
+ setFields([]);
+ }
+ : undefined,
+ onNextStep: () => setStep('sign'),
+ },
+ sign: {
+ title: 'Sign',
+ description: 'Enter your details.',
+ stepIndex: 2,
+ onBackStep: () => setStep('fields'),
+ },
+ };
+
+ const currentDocumentFlow = documentFlow[step];
+
+ useEffect(() => {
+ analytics.startSessionRecording('marketing_session_recording_spm');
+
+ return () => {
+ analytics.stopSessionRecording();
+ };
+ }, [analytics]);
+
+ /**
+ * Insert the selected fields into the local state.
+ */
+ const onFieldsSubmit = (data: TAddFieldsFormSchema) => {
+ if (!uploadedFile) {
+ return;
+ }
+
+ setFields(
+ data.fields.map((field, i) => ({
+ id: i,
+ documentId: -1,
+ recipientId: -1,
+ type: field.type,
+ page: field.pageNumber,
+ positionX: new Prisma.Decimal(field.pageX),
+ positionY: new Prisma.Decimal(field.pageY),
+ width: new Prisma.Decimal(field.pageWidth),
+ height: new Prisma.Decimal(field.pageHeight),
+ customText: '',
+ inserted: false,
+ })),
+ );
+
+ analytics.capture('Marketing: SPM - Fields added');
+
+ documentFlow.fields.onNextStep?.();
+ };
+
+ /**
+ * Create, sign and send the document.
+ */
+ const onSignSubmit = async (data: TAddSignatureFormSchema) => {
+ if (!uploadedFile) {
+ return;
+ }
+
+ try {
+ const documentToken = await createSinglePlayerDocument({
+ document: uploadedFile.file,
+ documentName: uploadedFile.name,
+ signer: data,
+ fields: fields.map((field) => ({
+ page: field.page,
+ type: field.type,
+ positionX: field.positionX.toNumber(),
+ positionY: field.positionY.toNumber(),
+ width: field.width.toNumber(),
+ height: field.height.toNumber(),
+ })),
+ });
+
+ analytics.capture('Marketing: SPM - Document signed', {
+ signer: data.email,
+ });
+
+ router.push(`/single-player-mode/${documentToken}/success`);
+ } catch {
+ toast({
+ title: 'Something went wrong',
+ description: 'Please try again later.',
+ variant: 'destructive',
+ });
+ }
+ };
+
+ const placeholderRecipient: Recipient = {
+ id: -1,
+ documentId: -1,
+ email: '',
+ name: '',
+ token: '',
+ expired: null,
+ signedAt: null,
+ readStatus: 'OPENED',
+ signingStatus: 'NOT_SIGNED',
+ sendStatus: 'NOT_SENT',
+ };
+
+ const onFileDrop = async (file: File) => {
+ try {
+ const arrayBuffer = await file.arrayBuffer();
+ const base64String = Buffer.from(arrayBuffer).toString('base64');
+
+ setUploadedFile({
+ name: file.name,
+ file: `data:application/pdf;base64,${base64String}`,
+ });
+
+ analytics.capture('Marketing: SPM - Document uploaded');
+ } catch {
+ toast({
+ title: 'Something went wrong',
+ description: 'Please try again later.',
+ variant: 'destructive',
+ });
+ }
+ };
+
+ return (
+
+
+
Single Player Mode
+
+
+ View our{' '}
+
+ community plan
+ {' '}
+ for exclusive features, including the ability to collaborate with multiple signers.
+
+
+
+
+
+ {uploadedFile ? (
+
+
+
+
+
+ ) : (
+
+ )}
+
+
+
+
e.preventDefault()}>
+
+
+ {/* Add fields to PDF page. */}
+ {step === 'fields' && (
+
+ )}
+
+ {/* Enter user details and signature. */}
+ {step === 'sign' && (
+ field.type === 'NAME'))}
+ requireSignature={Boolean(fields.find((field) => field.type === 'SIGNATURE'))}
+ />
+ )}
+
+
+
+
+ );
+}
diff --git a/apps/marketing/src/app/layout.tsx b/apps/marketing/src/app/layout.tsx
index 46d9a3d32..350fb8c2d 100644
--- a/apps/marketing/src/app/layout.tsx
+++ b/apps/marketing/src/app/layout.tsx
@@ -1,12 +1,19 @@
-import { Inter } from 'next/font/google';
+import { Suspense } from 'react';
+import { Caveat, Inter } from 'next/font/google';
+
+import { FeatureFlagProvider } from '@documenso/lib/client-only/providers/feature-flag';
+import { getAllAnonymousFlags } from '@documenso/lib/universal/get-feature-flag';
+import { cn } from '@documenso/ui/lib/utils';
import { Toaster } from '@documenso/ui/primitives/toaster';
import { PlausibleProvider } from '~/providers/plausible';
+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 const metadata = {
title: 'Documenso - The Open Source DocuSign Alternative',
@@ -32,9 +39,15 @@ export const metadata = {
},
};
-export default function RootLayout({ children }: { children: React.ReactNode }) {
+export default async function RootLayout({ children }: { children: React.ReactNode }) {
+ const flags = await getAllAnonymousFlags();
+
return (
-
+
@@ -42,9 +55,15 @@ export default function RootLayout({ children }: { children: React.ReactNode })
+
+
+
+
- {children}
-
+
+ {children}
+
+
);
diff --git a/apps/marketing/src/assets/signing-celebration.png b/apps/marketing/src/assets/signing-celebration.png
new file mode 100644
index 000000000..a3fb5bc65
Binary files /dev/null and b/apps/marketing/src/assets/signing-celebration.png differ
diff --git a/apps/marketing/src/components/(marketing)/claim-plan-dialog.tsx b/apps/marketing/src/components/(marketing)/claim-plan-dialog.tsx
index 7de30bba3..31b13abfc 100644
--- a/apps/marketing/src/components/(marketing)/claim-plan-dialog.tsx
+++ b/apps/marketing/src/components/(marketing)/claim-plan-dialog.tsx
@@ -10,6 +10,7 @@ import { usePlausible } from 'next-plausible';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
+import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import {
@@ -43,9 +44,11 @@ export type ClaimPlanDialogProps = {
export const ClaimPlanDialog = ({ className, planId, children }: ClaimPlanDialogProps) => {
const params = useSearchParams();
- const { toast } = useToast();
+ const analytics = useAnalytics();
const event = usePlausible();
+ const { toast } = useToast();
+
const [open, setOpen] = useState(() => params?.get('cancelled') === 'true');
const {
@@ -73,10 +76,12 @@ export const ClaimPlanDialog = ({ className, planId, children }: ClaimPlanDialog
]);
event('claim-plan-pricing');
+ analytics.capture('Marketing: Claim plan', { planId, email });
window.location.href = redirectUrl;
} catch (error) {
event('claim-plan-failed');
+ analytics.capture('Marketing: Claim plan failure', { planId, email });
toast({
title: 'Something went wrong',
diff --git a/apps/marketing/src/components/(marketing)/confetti-screen.tsx b/apps/marketing/src/components/(marketing)/confetti-screen.tsx
new file mode 100644
index 000000000..9843a0df0
--- /dev/null
+++ b/apps/marketing/src/components/(marketing)/confetti-screen.tsx
@@ -0,0 +1,46 @@
+'use client';
+
+import React, { useEffect, useState } from 'react';
+
+import Confetti from 'react-confetti';
+import { createPortal } from 'react-dom';
+
+import { useIsMounted } from '@documenso/lib/client-only/hooks/use-is-mounted';
+import { useWindowSize } from '@documenso/lib/client-only/hooks/use-window-size';
+
+export default function ConfettiScreen({
+ numberOfPieces: numberOfPiecesProp = 200,
+ ...props
+}: React.ComponentPropsWithoutRef & { duration?: number }) {
+ const isMounted = useIsMounted();
+ const { width, height } = useWindowSize();
+
+ const [numberOfPieces, setNumberOfPieces] = useState(numberOfPiecesProp);
+
+ useEffect(() => {
+ if (!props.duration) {
+ return;
+ }
+
+ const timer = setTimeout(() => {
+ setNumberOfPieces(0);
+ }, props.duration);
+
+ return () => clearTimeout(timer);
+ }, [props.duration]);
+
+ if (!isMounted) {
+ return null;
+ }
+
+ return createPortal(
+ ,
+ document.body,
+ );
+}
diff --git a/apps/marketing/src/components/(marketing)/footer.tsx b/apps/marketing/src/components/(marketing)/footer.tsx
index ab0dd6e24..18032a167 100644
--- a/apps/marketing/src/components/(marketing)/footer.tsx
+++ b/apps/marketing/src/components/(marketing)/footer.tsx
@@ -17,6 +17,7 @@ const SOCIAL_LINKS = [
const FOOTER_LINKS = [
{ href: '/pricing', text: 'Pricing' },
+ { href: '/single-player-mode', text: 'Single Player Mode' },
{ href: '/blog', text: 'Blog' },
{ href: '/open', text: 'Open' },
{ href: 'https://shop.documenso.com', text: 'Shop', target: '_blank' },
diff --git a/apps/marketing/src/components/(marketing)/header.tsx b/apps/marketing/src/components/(marketing)/header.tsx
index 4b9458ce2..d5c6d505a 100644
--- a/apps/marketing/src/components/(marketing)/header.tsx
+++ b/apps/marketing/src/components/(marketing)/header.tsx
@@ -5,6 +5,7 @@ import { HTMLAttributes, useState } from 'react';
import Image from 'next/image';
import Link from 'next/link';
+import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
import { cn } from '@documenso/ui/lib/utils';
import { HamburgerMenu } from './mobile-hamburger';
@@ -15,11 +16,26 @@ export type HeaderProps = HTMLAttributes;
export const Header = ({ className, ...props }: HeaderProps) => {
const [isHamburgerMenuOpen, setIsHamburgerMenuOpen] = useState(false);
+ const { getFlag } = useFeatureFlags();
+
+ const isSinglePlayerModeMarketingEnabled = getFlag('marketing_header_single_player_mode');
+
return (
- setIsHamburgerMenuOpen(false)}>
-
-
+
+ setIsHamburgerMenuOpen(false)}>
+
+
+
+ {isSinglePlayerModeMarketingEnabled && (
+
+ Try now!
+
+ )}
+
diff --git a/apps/marketing/src/components/(marketing)/hero.tsx b/apps/marketing/src/components/(marketing)/hero.tsx
index b406b51cc..9367e02e7 100644
--- a/apps/marketing/src/components/(marketing)/hero.tsx
+++ b/apps/marketing/src/components/(marketing)/hero.tsx
@@ -6,7 +6,9 @@ import Link from 'next/link';
import { Variants, motion } from 'framer-motion';
import { Github } from 'lucide-react';
import { usePlausible } from 'next-plausible';
+import { match } from 'ts-pattern';
+import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
@@ -51,6 +53,10 @@ const HeroTitleVariants: Variants = {
export const Hero = ({ className, ...props }: HeroProps) => {
const event = usePlausible();
+ const { getFlag } = useFeatureFlags();
+
+ const heroMarketingCTA = getFlag('marketing_landing_hero_cta');
+
const onSignUpClick = () => {
const el = document.getElementById('email');
@@ -122,23 +128,40 @@ export const Hero = ({ className, ...props }: HeroProps) => {
-
-
-
-
-
+ {match(heroMarketingCTA)
+ .with('spm', () => (
+
+
+ Single Player Mode
+ Self sign documents here
+
+
+ ))
+ .with('productHunt', () => (
+
+
+
+
+
+ ))
+ .otherwise(() => null)}
;
+
+/**
+ * Create and self signs a document.
+ *
+ * Returns the document token.
+ */
+export const createSinglePlayerDocument = async (
+ value: TCreateSinglePlayerDocumentSchema,
+): Promise => {
+ const { signer, fields, document, documentName } = ZCreateSinglePlayerDocumentSchema.parse(value);
+
+ const doc = await PDFDocument.load(document);
+ const createdAt = new Date();
+
+ const isBase64 = signer.signature.startsWith('data:image/png;base64,');
+ const signatureImageAsBase64 = isBase64 ? signer.signature : null;
+ const typedSignature = !isBase64 ? signer.signature : null;
+
+ // Update the document with the fields inserted.
+ for (const field of fields) {
+ const isSignatureField = field.type === FieldType.SIGNATURE;
+
+ await insertFieldInPDF(doc, {
+ ...mapField(field, signer),
+ Signature: isSignatureField
+ ? {
+ created: createdAt,
+ signatureImageAsBase64,
+ typedSignature,
+ // Dummy data.
+ id: -1,
+ recipientId: -1,
+ fieldId: -1,
+ }
+ : null,
+ // Dummy data.
+ id: -1,
+ documentId: -1,
+ recipientId: -1,
+ });
+ }
+
+ const pdfBytes = await doc.save();
+
+ const documentToken = await prisma.$transaction(async (tx) => {
+ const documentToken = nanoid();
+
+ // Fetch service user who will be the owner of the document.
+ const serviceUser = await tx.user.findFirstOrThrow({
+ where: {
+ email: SERVICE_USER_EMAIL,
+ },
+ });
+
+ const documentDataBytes = Buffer.from(pdfBytes).toString('base64');
+
+ const { id: documentDataId } = await tx.documentData.create({
+ data: {
+ type: DocumentDataType.BYTES_64,
+ data: documentDataBytes,
+ initialData: documentDataBytes,
+ },
+ });
+
+ // Create document.
+ const document = await tx.document.create({
+ data: {
+ title: documentName,
+ status: DocumentStatus.COMPLETED,
+ documentDataId,
+ userId: serviceUser.id,
+ createdAt,
+ },
+ });
+
+ // Create recipient.
+ const recipient = await tx.recipient.create({
+ data: {
+ documentId: document.id,
+ name: signer.name,
+ email: signer.email,
+ token: documentToken,
+ signedAt: createdAt,
+ readStatus: ReadStatus.OPENED,
+ signingStatus: SigningStatus.SIGNED,
+ sendStatus: SendStatus.SENT,
+ },
+ });
+
+ // Create fields and signatures.
+ await Promise.all(
+ fields.map(async (field) => {
+ const insertedField = await tx.field.create({
+ data: {
+ documentId: document.id,
+ recipientId: recipient.id,
+ ...mapField(field, signer),
+ },
+ });
+
+ if (field.type === FieldType.SIGNATURE || field.type === FieldType.FREE_SIGNATURE) {
+ await tx.signature.create({
+ data: {
+ fieldId: insertedField.id,
+ signatureImageAsBase64,
+ typedSignature,
+ recipientId: recipient.id,
+ },
+ });
+ }
+ }),
+ );
+
+ return documentToken;
+ });
+
+ // Todo: Handle `downloadLink`
+ const template = createElement(DocumentSelfSignedEmailTemplate, {
+ downloadLink: 'https://documenso.com',
+ documentName: documentName,
+ assetBaseUrl: process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000',
+ });
+
+ // Send email to signer.
+ await mailer.sendMail({
+ to: {
+ address: signer.email,
+ name: signer.name,
+ },
+ from: {
+ name: FROM_NAME,
+ address: FROM_ADDRESS,
+ },
+ subject: 'Document signed',
+ html: render(template),
+ text: render(template, { plainText: true }),
+ });
+
+ return documentToken;
+};
+
+/**
+ * Map the fields provided by the user to fields compatible with Prisma.
+ *
+ * Signature fields are handled separately.
+ *
+ * @param field The field passed in by the user.
+ * @param signer The details of the person who is signing this document.
+ * @returns A field compatible with Prisma.
+ */
+const mapField = (
+ field: TCreateSinglePlayerDocumentSchema['fields'][number],
+ signer: TCreateSinglePlayerDocumentSchema['signer'],
+) => {
+ const customText = match(field.type)
+ .with(FieldType.DATE, () => DateTime.now().toFormat('yyyy-MM-dd hh:mm a'))
+ .with(FieldType.EMAIL, () => signer.email)
+ .with(FieldType.NAME, () => signer.name)
+ .otherwise(() => '');
+
+ return {
+ type: field.type,
+ page: field.page,
+ positionX: new Prisma.Decimal(field.positionX),
+ positionY: new Prisma.Decimal(field.positionY),
+ width: new Prisma.Decimal(field.width),
+ height: new Prisma.Decimal(field.height),
+ customText,
+ inserted: true,
+ };
+};
diff --git a/apps/marketing/src/components/(marketing)/single-player-mode/single-player-mode-success.tsx b/apps/marketing/src/components/(marketing)/single-player-mode/single-player-mode-success.tsx
new file mode 100644
index 000000000..8dfd1fc74
--- /dev/null
+++ b/apps/marketing/src/components/(marketing)/single-player-mode/single-player-mode-success.tsx
@@ -0,0 +1,129 @@
+'use client';
+
+import { useEffect, useState } from 'react';
+
+import Link from 'next/link';
+
+import { Share } from 'lucide-react';
+
+import { getFile } from '@documenso/lib/universal/upload/get-file';
+import { DocumentWithRecipient } from '@documenso/prisma/types/document-with-recipient';
+import DocumentDialog from '@documenso/ui/components/document/document-dialog';
+import { DocumentDownloadButton } from '@documenso/ui/components/document/document-download-button';
+import { SigningCard3D } from '@documenso/ui/components/signing-card';
+import { cn } from '@documenso/ui/lib/utils';
+import { Button } from '@documenso/ui/primitives/button';
+import { useToast } from '@documenso/ui/primitives/use-toast';
+
+import signingCelebration from '~/assets/signing-celebration.png';
+import ConfettiScreen from '~/components/(marketing)/confetti-screen';
+
+import { DocumentStatus } from '.prisma/client';
+
+interface SinglePlayerModeSuccessProps {
+ className?: string;
+ document: DocumentWithRecipient;
+}
+
+export default function SinglePlayerModeSuccess({
+ className,
+ document,
+}: SinglePlayerModeSuccessProps) {
+ const [showDocumentDialog, setShowDocumentDialog] = useState(false);
+ const [isFetchingDocumentFile, setIsFetchingDocumentFile] = useState(false);
+ const [documentFile, setDocumentFile] = useState(null);
+
+ const { toast } = useToast();
+
+ const handleShowDocumentDialog = async () => {
+ if (isFetchingDocumentFile) {
+ return;
+ }
+
+ setIsFetchingDocumentFile(true);
+
+ try {
+ const data = await getFile(document.documentData);
+
+ setDocumentFile(Buffer.from(data).toString('base64'));
+
+ setShowDocumentDialog(true);
+ } catch {
+ toast({
+ title: 'Something went wrong.',
+ description: 'We were unable to retrieve the document at this time. Please try again.',
+ variant: 'destructive',
+ duration: 7500,
+ });
+ }
+
+ setIsFetchingDocumentFile(false);
+ };
+
+ useEffect(() => {
+ window.scrollTo({ top: 0 });
+ }, []);
+
+ return (
+
+
+
+
+ You have signed
+
+
+ {document.title}
+
+
+
+
+
+
+
+ {/* TODO: Hook this up */}
+
+
+
+
+
+
+
+
+
+
+ View the{' '}
+
+ community plan
+ {' '}
+ to access the full range of features provided by Documenso
+
+
+
+
+ );
+}
diff --git a/apps/marketing/src/pages/api/feature-flag/all.ts b/apps/marketing/src/pages/api/feature-flag/all.ts
new file mode 100644
index 000000000..f4d0aa3e9
--- /dev/null
+++ b/apps/marketing/src/pages/api/feature-flag/all.ts
@@ -0,0 +1,7 @@
+import handlerFeatureFlagAll from '@documenso/lib/server-only/feature-flags/all';
+
+export const config = {
+ runtime: 'edge',
+};
+
+export default handlerFeatureFlagAll;
diff --git a/apps/marketing/src/pages/api/feature-flag/get.ts b/apps/marketing/src/pages/api/feature-flag/get.ts
new file mode 100644
index 000000000..938dfbbcd
--- /dev/null
+++ b/apps/marketing/src/pages/api/feature-flag/get.ts
@@ -0,0 +1,7 @@
+import handlerFeatureFlagGet from '@documenso/lib/server-only/feature-flags/get';
+
+export const config = {
+ runtime: 'edge',
+};
+
+export default handlerFeatureFlagGet;
diff --git a/apps/marketing/src/providers/posthog.tsx b/apps/marketing/src/providers/posthog.tsx
new file mode 100644
index 000000000..a4019bfb5
--- /dev/null
+++ b/apps/marketing/src/providers/posthog.tsx
@@ -0,0 +1,39 @@
+'use client';
+
+import { useEffect } from 'react';
+
+import { usePathname, useSearchParams } from 'next/navigation';
+
+import posthog from 'posthog-js';
+
+import { extractPostHogConfig } from '@documenso/lib/constants/feature-flags';
+
+export function PostHogPageview() {
+ const postHogConfig = extractPostHogConfig();
+
+ const pathname = usePathname();
+ const searchParams = useSearchParams();
+
+ if (typeof window !== 'undefined' && postHogConfig) {
+ posthog.init(postHogConfig.key, {
+ api_host: postHogConfig.host,
+ disable_session_recording: true,
+ });
+ }
+
+ useEffect(() => {
+ if (!postHogConfig || !pathname) {
+ return;
+ }
+
+ let url = window.origin + pathname;
+ if (searchParams && searchParams.toString()) {
+ url = url + `?${searchParams.toString()}`;
+ }
+ posthog.capture('$pageview', {
+ $current_url: url,
+ });
+ }, [pathname, searchParams, postHogConfig]);
+
+ return null;
+}
diff --git a/apps/web/next.config.js b/apps/web/next.config.js
index be51b51fc..c4359060b 100644
--- a/apps/web/next.config.js
+++ b/apps/web/next.config.js
@@ -2,7 +2,7 @@
const path = require('path');
const { version } = require('./package.json');
-const { parsed: env } = require('dotenv').config({
+require('dotenv').config({
path: path.join(__dirname, '../../.env.local'),
});
@@ -22,6 +22,7 @@ const config = {
],
env: {
APP_VERSION: version,
+ NEXT_PUBLIC_PROJECT: 'web',
},
modularizeImports: {
'lucide-react': {
diff --git a/apps/web/package.json b/apps/web/package.json
index d3ab34f96..eeacec152 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -21,10 +21,11 @@
"@tanstack/react-query": "^4.29.5",
"formidable": "^2.1.1",
"framer-motion": "^10.12.8",
- "lucide-react": "^0.214.0",
+ "lucide-react": "^0.277.0",
"luxon": "^3.4.0",
"micro": "^10.0.1",
- "next": "13.4.12",
+ "nanoid": "^4.0.2",
+ "next": "13.4.19",
"next-auth": "4.22.3",
"next-plausible": "^3.10.1",
"next-themes": "^0.2.1",
@@ -49,4 +50,4 @@
"@types/react": "18.2.18",
"@types/react-dom": "18.2.7"
}
-}
+}
\ No newline at end of file
diff --git a/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx b/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx
index b4837ab23..c4d0ca2c0 100644
--- a/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx
+++ b/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx
@@ -55,21 +55,18 @@ export const EditDocumentForm = ({
title: 'Add Signers',
description: 'Add the people who will sign the document.',
stepIndex: 1,
- onSubmit: () => onAddSignersFormSubmit,
},
fields: {
title: 'Add Fields',
description: 'Add all relevant fields for each recipient.',
stepIndex: 2,
onBackStep: () => setStep('signers'),
- onSubmit: () => onAddFieldsFormSubmit,
},
subject: {
title: 'Add Subject',
description: 'Add the subject and message you wish to send to signers.',
stepIndex: 3,
onBackStep: () => setStep('fields'),
- onSubmit: () => onAddSubjectFormSubmit,
},
};
diff --git a/apps/web/src/app/(dashboard)/settings/billing/page.tsx b/apps/web/src/app/(dashboard)/settings/billing/page.tsx
index 7e6694491..28c8b8122 100644
--- a/apps/web/src/app/(dashboard)/settings/billing/page.tsx
+++ b/apps/web/src/app/(dashboard)/settings/billing/page.tsx
@@ -4,12 +4,12 @@ import { redirect } from 'next/navigation';
import { createCustomer } from '@documenso/ee/server-only/stripe/create-customer';
import { getPortalSession } from '@documenso/ee/server-only/stripe/get-portal-session';
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
+import { getServerComponentFlag } from '@documenso/lib/server-only/feature-flags/get-server-component-feature-flag';
import { getSubscriptionByUserId } from '@documenso/lib/server-only/subscription/get-subscription-by-user-id';
import { SubscriptionStatus } from '@documenso/prisma/client';
import { Button } from '@documenso/ui/primitives/button';
import { LocaleDate } from '~/components/formatter/locale-date';
-import { getServerComponentFlag } from '~/helpers/get-server-component-feature-flag';
export default async function BillingSettingsPage() {
const user = await getRequiredServerComponentSession();
diff --git a/apps/web/src/app/(signing)/sign/[token]/complete/page.tsx b/apps/web/src/app/(signing)/sign/[token]/complete/page.tsx
index 71a368da5..c4f62e9e1 100644
--- a/apps/web/src/app/(signing)/sign/[token]/complete/page.tsx
+++ b/apps/web/src/app/(signing)/sign/[token]/complete/page.tsx
@@ -8,10 +8,11 @@ import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document
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 { DocumentStatus, FieldType } from '@documenso/prisma/client';
+import { DocumentDownloadButton } from '@documenso/ui/components/document/document-download-button';
+import { SigningCard } from '@documenso/ui/components/signing-card';
import { Button } from '@documenso/ui/primitives/button';
-import { DownloadButton } from './download-button';
-import { SigningCard } from './signing-card';
+import signingCelebration from '~/assets/signing-celebration.png';
export type CompletedSigningPageProps = {
params: {
@@ -53,7 +54,7 @@ export default async function CompletedSigningPage({
return (
{/* Card with recipient */}
-
+
{match(document.status)
@@ -94,7 +95,7 @@ export default async function CompletedSigningPage({
Share
-
- Want so send slick signing links like this one?{' '}
+ Want to send slick signing links like this one?{' '}
Check out Documenso.
diff --git a/apps/web/src/app/(signing)/sign/[token]/complete/signing-card.tsx b/apps/web/src/app/(signing)/sign/[token]/complete/signing-card.tsx
deleted file mode 100644
index 791c61231..000000000
--- a/apps/web/src/app/(signing)/sign/[token]/complete/signing-card.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-'use client';
-
-import Image from 'next/image';
-
-import { motion } from 'framer-motion';
-
-import { Card, CardContent } from '@documenso/ui/primitives/card';
-
-import signingCelebration from '~/assets/signing-celebration.png';
-
-export type SigningCardProps = {
- name: string;
-};
-
-export const SigningCard = ({ name }: SigningCardProps) => {
- return (
-
-
-
-
- {name}
-
-
-
-
-
-
-
-
- );
-};
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
index d5efcb3df..749ab660f 100644
--- a/apps/web/src/app/(signing)/sign/[token]/signing-field-container.tsx
+++ b/apps/web/src/app/(signing)/sign/[token]/signing-field-container.tsx
@@ -2,12 +2,11 @@
import React from 'react';
+import { useFieldPageCoords } from '@documenso/lib/client-only/hooks/use-field-page-coords';
import { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
import { cn } from '@documenso/ui/lib/utils';
import { Card, CardContent } from '@documenso/ui/primitives/card';
-import { useFieldPageCoords } from '~/hooks/use-field-page-coords';
-
export type SignatureFieldProps = {
field: FieldWithSignature;
loading?: boolean;
diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx
index 2a1d082f9..5ff1b88b6 100644
--- a/apps/web/src/app/layout.tsx
+++ b/apps/web/src/app/layout.tsx
@@ -2,15 +2,15 @@ import { Suspense } from 'react';
import { Caveat, Inter } from 'next/font/google';
+import { FeatureFlagProvider } from '@documenso/lib/client-only/providers/feature-flag';
import { LocaleProvider } from '@documenso/lib/client-only/providers/locale';
+import { getServerComponentAllFlags } from '@documenso/lib/server-only/feature-flags/get-server-component-feature-flag';
import { getLocale } from '@documenso/lib/server-only/headers/get-locale';
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 { getServerComponentAllFlags } from '~/helpers/get-server-component-feature-flag';
-import { FeatureFlagProvider } from '~/providers/feature-flag';
import { ThemeProvider } from '~/providers/next-theme';
import { PlausibleProvider } from '~/providers/plausible';
import { PostHogPageview } from '~/providers/posthog';
diff --git a/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx b/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx
index 9ae9b4297..d3073e7c1 100644
--- a/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx
+++ b/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx
@@ -16,6 +16,7 @@ import {
import { signOut } from 'next-auth/react';
import { useTheme } from 'next-themes';
+import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
import { isAdmin } from '@documenso/lib/next-auth/guards/is-admin';
import { recipientInitials } from '@documenso/lib/utils/recipient-formatter';
import { User } from '@documenso/prisma/client';
@@ -30,8 +31,6 @@ import {
DropdownMenuTrigger,
} from '@documenso/ui/primitives/dropdown-menu';
-import { useFeatureFlags } from '~/providers/feature-flag';
-
export type ProfileDropdownProps = {
user: User;
};
diff --git a/apps/web/src/components/(dashboard)/settings/layout/desktop-nav.tsx b/apps/web/src/components/(dashboard)/settings/layout/desktop-nav.tsx
index 5a85680c8..901c6a5ae 100644
--- a/apps/web/src/components/(dashboard)/settings/layout/desktop-nav.tsx
+++ b/apps/web/src/components/(dashboard)/settings/layout/desktop-nav.tsx
@@ -7,11 +7,10 @@ import { usePathname } from 'next/navigation';
import { CreditCard, Key, User } 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';
-import { useFeatureFlags } from '~/providers/feature-flag';
-
export type DesktopNavProps = HTMLAttributes;
export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
diff --git a/apps/web/src/components/(dashboard)/settings/layout/mobile-nav.tsx b/apps/web/src/components/(dashboard)/settings/layout/mobile-nav.tsx
index 19bbefdc9..ffe2b0d80 100644
--- a/apps/web/src/components/(dashboard)/settings/layout/mobile-nav.tsx
+++ b/apps/web/src/components/(dashboard)/settings/layout/mobile-nav.tsx
@@ -7,11 +7,10 @@ import { usePathname } from 'next/navigation';
import { CreditCard, Key, User } 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';
-import { useFeatureFlags } from '~/providers/feature-flag';
-
export type MobileNavProps = HTMLAttributes;
export const MobileNav = ({ className, ...props }: MobileNavProps) => {
diff --git a/apps/web/src/pages/api/feature-flag/all.ts b/apps/web/src/pages/api/feature-flag/all.ts
index 54efbd7fc..f4d0aa3e9 100644
--- a/apps/web/src/pages/api/feature-flag/all.ts
+++ b/apps/web/src/pages/api/feature-flag/all.ts
@@ -1,44 +1,7 @@
-import { NextRequest, NextResponse } from 'next/server';
-
-import { getToken } from 'next-auth/jwt';
-
-import { LOCAL_FEATURE_FLAGS } from '@documenso/lib/constants/feature-flags';
-
-import PostHogServerClient from '~/helpers/get-post-hog-server-client';
-
-import { extractDistinctUserId, mapJwtToFlagProperties } from './get';
+import handlerFeatureFlagAll from '@documenso/lib/server-only/feature-flags/all';
export const config = {
runtime: 'edge',
};
-/**
- * Get all the evaluated feature flags based on the current user if possible.
- */
-export default async function handler(req: Request) {
- const requestHeaders = Object.fromEntries(req.headers.entries());
-
- const nextReq = new NextRequest(req, {
- headers: requestHeaders,
- });
-
- const token = await getToken({ req: nextReq });
-
- const postHog = PostHogServerClient();
-
- // Return the local feature flags if PostHog is not enabled, true by default.
- // The front end should not call this API if PostHog is not enabled to reduce network requests.
- if (!postHog) {
- return NextResponse.json(LOCAL_FEATURE_FLAGS);
- }
-
- const distinctId = extractDistinctUserId(token, nextReq);
-
- const featureFlags = await postHog.getAllFlags(distinctId, mapJwtToFlagProperties(token));
-
- const res = NextResponse.json(featureFlags);
-
- res.headers.set('Cache-Control', 'public, s-maxage=60, stale-while-revalidate=300');
-
- return res;
-}
+export default handlerFeatureFlagAll;
diff --git a/apps/web/src/pages/api/feature-flag/get.ts b/apps/web/src/pages/api/feature-flag/get.ts
index 6e45b5a18..938dfbbcd 100644
--- a/apps/web/src/pages/api/feature-flag/get.ts
+++ b/apps/web/src/pages/api/feature-flag/get.ts
@@ -1,122 +1,7 @@
-import { NextRequest, NextResponse } from 'next/server';
-
-import { JWT, getToken } from 'next-auth/jwt';
-
-import { LOCAL_FEATURE_FLAGS, extractPostHogConfig } from '@documenso/lib/constants/feature-flags';
-import { nanoid } from '@documenso/lib/universal/id';
-
-import PostHogServerClient from '~/helpers/get-post-hog-server-client';
+import handlerFeatureFlagGet from '@documenso/lib/server-only/feature-flags/get';
export const config = {
runtime: 'edge',
};
-/**
- * Evaluate a single feature flag based on the current user if possible.
- *
- * @param req The request with a query parameter `flag`. Example request URL: /api/feature-flag/get?flag=flag-name
- * @returns A Response with the feature flag value.
- */
-export default async function handler(req: Request) {
- const { searchParams } = new URL(req.url ?? '');
- const flag = searchParams.get('flag');
-
- const requestHeaders = Object.fromEntries(req.headers.entries());
-
- const nextReq = new NextRequest(req, {
- headers: requestHeaders,
- });
-
- const token = await getToken({ req: nextReq });
-
- if (!flag) {
- return NextResponse.json(
- {
- error: 'Missing flag query parameter.',
- },
- {
- status: 400,
- headers: {
- 'content-type': 'application/json',
- },
- },
- );
- }
-
- const postHog = PostHogServerClient();
-
- // Return the local feature flags if PostHog is not enabled, true by default.
- // The front end should not call this API if PostHog is disabled to reduce network requests.
- if (!postHog) {
- return NextResponse.json(LOCAL_FEATURE_FLAGS[flag] ?? true);
- }
-
- const distinctId = extractDistinctUserId(token, nextReq);
-
- const featureFlag = await postHog.getFeatureFlag(flag, distinctId, mapJwtToFlagProperties(token));
-
- const res = NextResponse.json(featureFlag);
-
- res.headers.set('Cache-Control', 'public, s-maxage=60, stale-while-revalidate=300');
-
- return res;
-}
-
-/**
- * Map a JWT to properties which are consumed by PostHog to evaluate feature flags.
- *
- * @param jwt The JWT of the current user.
- * @returns A map of properties which are consumed by PostHog.
- */
-export const mapJwtToFlagProperties = (
- jwt?: JWT | null,
-): {
- groups?: Record;
- personProperties?: Record;
- groupProperties?: Record>;
-} => {
- return {
- personProperties: {
- email: jwt?.email ?? '',
- },
- groupProperties: {
- // Add properties to group users into different groups, such as billing plan.
- },
- };
-};
-
-/**
- * Extract a distinct ID from a JWT and request.
- *
- * Will fallback to a random ID if no ID could be extracted from either the JWT or request.
- *
- * @param jwt The JWT of the current user.
- * @param request Request potentially containing a PostHog `distinct_id` cookie.
- * @returns A distinct user ID.
- */
-export const extractDistinctUserId = (jwt: JWT | null, request: NextRequest): string => {
- const config = extractPostHogConfig();
-
- const email = jwt?.email;
- const userId = jwt?.id.toString();
-
- let fallbackDistinctId = nanoid();
-
- if (config) {
- try {
- const postHogCookie = JSON.parse(
- request.cookies.get(`ph_${config.key}_posthog`)?.value ?? '',
- );
-
- const postHogDistinctId = postHogCookie['distinct_id'];
-
- if (typeof postHogDistinctId === 'string') {
- fallbackDistinctId = postHogDistinctId;
- }
- } catch {
- // Do nothing.
- }
- }
-
- return email ?? userId ?? fallbackDistinctId;
-};
+export default handlerFeatureFlagGet;
diff --git a/package-lock.json b/package-lock.json
index c6558b466..faaa7705c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -41,14 +41,16 @@
"@hookform/resolvers": "^3.1.0",
"contentlayer": "^0.3.4",
"framer-motion": "^10.12.8",
- "lucide-react": "^0.214.0",
+ "lucide-react": "^0.277.0",
"micro": "^10.0.1",
- "next": "13.4.12",
+ "next": "13.4.19",
"next-auth": "4.22.3",
"next-contentlayer": "^0.3.4",
"next-plausible": "^3.10.1",
"perfect-freehand": "^1.2.0",
+ "posthog-js": "^1.77.3",
"react": "18.2.0",
+ "react-confetti": "^6.1.0",
"react-dom": "18.2.0",
"react-hook-form": "^7.43.9",
"react-icons": "^4.8.0",
@@ -78,10 +80,11 @@
"@tanstack/react-query": "^4.29.5",
"formidable": "^2.1.1",
"framer-motion": "^10.12.8",
- "lucide-react": "^0.214.0",
+ "lucide-react": "^0.277.0",
"luxon": "^3.4.0",
"micro": "^10.0.1",
- "next": "13.4.12",
+ "nanoid": "^4.0.2",
+ "next": "13.4.19",
"next-auth": "4.22.3",
"next-plausible": "^3.10.1",
"next-themes": "^0.2.1",
@@ -2444,9 +2447,9 @@
}
},
"node_modules/@hookform/resolvers": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.1.1.tgz",
- "integrity": "sha512-tS16bAUkqjITNSvbJuO1x7MXbn7Oe8ZziDTJdA9mMvsoYthnOOiznOTGBYwbdlYBgU+tgpI/BtTU3paRbCuSlg==",
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.3.0.tgz",
+ "integrity": "sha512-tgK3nWlfFLlqhqpXZmFMP3RN5E7mlbGfnM2h2ILVsW1TNGuFSod0ePW0grlIY2GAbL4pJdtmOT4HQSZsTwOiKg==",
"peerDependencies": {
"react-hook-form": "^7.0.0"
}
@@ -2818,22 +2821,22 @@
}
},
"node_modules/@next/env": {
- "version": "13.4.12",
- "resolved": "https://registry.npmjs.org/@next/env/-/env-13.4.12.tgz",
- "integrity": "sha512-RmHanbV21saP/6OEPBJ7yJMuys68cIf8OBBWd7+uj40LdpmswVAwe1uzeuFyUsd6SfeITWT3XnQfn6wULeKwDQ=="
+ "version": "13.4.19",
+ "resolved": "https://registry.npmjs.org/@next/env/-/env-13.4.19.tgz",
+ "integrity": "sha512-FsAT5x0jF2kkhNkKkukhsyYOrRqtSxrEhfliniIq0bwWbuXLgyt3Gv0Ml+b91XwjwArmuP7NxCiGd++GGKdNMQ=="
},
"node_modules/@next/eslint-plugin-next": {
- "version": "13.4.12",
- "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-13.4.12.tgz",
- "integrity": "sha512-6rhK9CdxEgj/j1qvXIyLTWEaeFv7zOK8yJMulz3Owel0uek0U9MJCGzmKgYxM3aAUBo3gKeywCZKyQnJKto60A==",
+ "version": "13.4.19",
+ "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-13.4.19.tgz",
+ "integrity": "sha512-N/O+zGb6wZQdwu6atMZHbR7T9Np5SUFUjZqCbj0sXm+MwQO35M8TazVB4otm87GkXYs2l6OPwARd3/PUWhZBVQ==",
"dependencies": {
"glob": "7.1.7"
}
},
"node_modules/@next/swc-darwin-arm64": {
- "version": "13.4.12",
- "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.4.12.tgz",
- "integrity": "sha512-deUrbCXTMZ6ZhbOoloqecnUeNpUOupi8SE2tx4jPfNS9uyUR9zK4iXBvH65opVcA/9F5I/p8vDXSYbUlbmBjZg==",
+ "version": "13.4.19",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.4.19.tgz",
+ "integrity": "sha512-vv1qrjXeGbuF2mOkhkdxMDtv9np7W4mcBtaDnHU+yJG+bBwa6rYsYSCI/9Xm5+TuF5SbZbrWO6G1NfTh1TMjvQ==",
"cpu": [
"arm64"
],
@@ -2846,9 +2849,9 @@
}
},
"node_modules/@next/swc-darwin-x64": {
- "version": "13.4.12",
- "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.12.tgz",
- "integrity": "sha512-WRvH7RxgRHlC1yb5oG0ZLx8F7uci9AivM5/HGGv9ZyG2Als8Ij64GC3d+mQ5sJhWjusyU6T6V1WKTUoTmOB0zQ==",
+ "version": "13.4.19",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.19.tgz",
+ "integrity": "sha512-jyzO6wwYhx6F+7gD8ddZfuqO4TtpJdw3wyOduR4fxTUCm3aLw7YmHGYNjS0xRSYGAkLpBkH1E0RcelyId6lNsw==",
"cpu": [
"x64"
],
@@ -2861,9 +2864,9 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
- "version": "13.4.12",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.4.12.tgz",
- "integrity": "sha512-YEKracAWuxp54tKiAvvq73PUs9lok57cc8meYRibTWe/VdPB2vLgkTVWFcw31YDuRXdEhdX0fWS6Q+ESBhnEig==",
+ "version": "13.4.19",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.4.19.tgz",
+ "integrity": "sha512-vdlnIlaAEh6H+G6HrKZB9c2zJKnpPVKnA6LBwjwT2BTjxI7e0Hx30+FoWCgi50e+YO49p6oPOtesP9mXDRiiUg==",
"cpu": [
"arm64"
],
@@ -2876,9 +2879,9 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
- "version": "13.4.12",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.4.12.tgz",
- "integrity": "sha512-LhJR7/RAjdHJ2Isl2pgc/JaoxNk0KtBgkVpiDJPVExVWA1c6gzY57+3zWuxuyWzTG+fhLZo2Y80pLXgIJv7g3g==",
+ "version": "13.4.19",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.4.19.tgz",
+ "integrity": "sha512-aU0HkH2XPgxqrbNRBFb3si9Ahu/CpaR5RPmN2s9GiM9qJCiBBlZtRTiEca+DC+xRPyCThTtWYgxjWHgU7ZkyvA==",
"cpu": [
"arm64"
],
@@ -2891,9 +2894,9 @@
}
},
"node_modules/@next/swc-linux-x64-gnu": {
- "version": "13.4.12",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.4.12.tgz",
- "integrity": "sha512-1DWLL/B9nBNiQRng+1aqs3OaZcxC16Nf+mOnpcrZZSdyKHek3WQh6j/fkbukObgNGwmCoVevLUa/p3UFTTqgqg==",
+ "version": "13.4.19",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.4.19.tgz",
+ "integrity": "sha512-htwOEagMa/CXNykFFeAHHvMJeqZfNQEoQvHfsA4wgg5QqGNqD5soeCer4oGlCol6NGUxknrQO6VEustcv+Md+g==",
"cpu": [
"x64"
],
@@ -2906,9 +2909,9 @@
}
},
"node_modules/@next/swc-linux-x64-musl": {
- "version": "13.4.12",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.4.12.tgz",
- "integrity": "sha512-kEAJmgYFhp0VL+eRWmUkVxLVunn7oL9Mdue/FS8yzRBVj7Z0AnIrHpTIeIUl1bbdQq1VaoOztnKicAjfkLTRCQ==",
+ "version": "13.4.19",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.4.19.tgz",
+ "integrity": "sha512-4Gj4vvtbK1JH8ApWTT214b3GwUh9EKKQjY41hH/t+u55Knxi/0wesMzwQRhppK6Ddalhu0TEttbiJ+wRcoEj5Q==",
"cpu": [
"x64"
],
@@ -2921,9 +2924,9 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
- "version": "13.4.12",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.4.12.tgz",
- "integrity": "sha512-GMLuL/loR6yIIRTnPRY6UGbLL9MBdw2anxkOnANxvLvsml4F0HNIgvnU3Ej4BjbqMTNjD4hcPFdlEow4XHPdZA==",
+ "version": "13.4.19",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.4.19.tgz",
+ "integrity": "sha512-bUfDevQK4NsIAHXs3/JNgnvEY+LRyneDN788W2NYiRIIzmILjba7LaQTfihuFawZDhRtkYCv3JDC3B4TwnmRJw==",
"cpu": [
"arm64"
],
@@ -2936,9 +2939,9 @@
}
},
"node_modules/@next/swc-win32-ia32-msvc": {
- "version": "13.4.12",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.4.12.tgz",
- "integrity": "sha512-PhgNqN2Vnkm7XaMdRmmX0ZSwZXQAtamBVSa9A/V1dfKQCV1rjIZeiy/dbBnVYGdj63ANfsOR/30XpxP71W0eww==",
+ "version": "13.4.19",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.4.19.tgz",
+ "integrity": "sha512-Y5kikILFAr81LYIFaw6j/NrOtmiM4Sf3GtOc0pn50ez2GCkr+oejYuKGcwAwq3jiTKuzF6OF4iT2INPoxRycEA==",
"cpu": [
"ia32"
],
@@ -2951,9 +2954,9 @@
}
},
"node_modules/@next/swc-win32-x64-msvc": {
- "version": "13.4.12",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.4.12.tgz",
- "integrity": "sha512-Z+56e/Ljt0bUs+T+jPjhFyxYBcdY2RIq9ELFU+qAMQMteHo7ymbV7CKmlcX59RI9C4YzN8PgMgLyAoi916b5HA==",
+ "version": "13.4.19",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.4.19.tgz",
+ "integrity": "sha512-YzA78jBDXMYiINdPdJJwGgPNT3YqBNNGhsthsDoWHL9p24tEJn9ViQf/ZqTbwSpX/RrkPupLfuuTH2sf73JBAw==",
"cpu": [
"x64"
],
@@ -6272,9 +6275,9 @@
"integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA=="
},
"node_modules/@types/luxon": {
- "version": "3.3.1",
- "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.3.1.tgz",
- "integrity": "sha512-XOS5nBcgEeP2PpcqJHjCWhUCAzGfXIU8ILOSLpx2FhxqMW9KdxgCGXNOEKGVBfveKtIpztHzKK5vSRVLyW/NqA==",
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.3.2.tgz",
+ "integrity": "sha512-l5cpE57br4BIjK+9BSkFBOsWtwv6J9bJpC7gdXIzZyI0vuKvNTk0wZZrkQxMGsUAuGW9+WMNWF2IJMD7br2yeQ==",
"dev": true
},
"node_modules/@types/mdast": {
@@ -9594,19 +9597,19 @@
}
},
"node_modules/eslint-config-next": {
- "version": "13.4.12",
- "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-13.4.12.tgz",
- "integrity": "sha512-ZF0r5vxKaVazyZH/37Au/XItiG7qUOBw+HaH3PeyXltIMwXorsn6bdrl0Nn9N5v5v9spc+6GM2ryjugbjF6X2g==",
+ "version": "13.4.19",
+ "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-13.4.19.tgz",
+ "integrity": "sha512-WE8367sqMnjhWHvR5OivmfwENRQ1ixfNE9hZwQqNCsd+iM3KnuMc1V8Pt6ytgjxjf23D+xbesADv9x3xaKfT3g==",
"dependencies": {
- "@next/eslint-plugin-next": "13.4.12",
+ "@next/eslint-plugin-next": "13.4.19",
"@rushstack/eslint-patch": "^1.1.3",
- "@typescript-eslint/parser": "^5.42.0",
+ "@typescript-eslint/parser": "^5.4.2 || ^6.0.0",
"eslint-import-resolver-node": "^0.3.6",
"eslint-import-resolver-typescript": "^3.5.2",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-react": "^7.31.7",
- "eslint-plugin-react-hooks": "5.0.0-canary-7118f5dd7-20230705"
+ "eslint-plugin-react-hooks": "^4.5.0 || 5.0.0-canary-7118f5dd7-20230705"
},
"peerDependencies": {
"eslint": "^7.23.0 || ^8.0.0",
@@ -12685,17 +12688,17 @@
}
},
"node_modules/lucide-react": {
- "version": "0.214.0",
- "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.214.0.tgz",
- "integrity": "sha512-/vRi1wnFV2lqyIIkghQ3dDLu0eA9zykRQN9GZBwydzv+kB/2Q3S4X6OYB+aRqLXwl438vfVBqyYov2z0LJeoqA==",
+ "version": "0.277.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.277.0.tgz",
+ "integrity": "sha512-9epmznme+vW14V9d2rsMeLr3fMnf59lYDUOVUg6s7oVN22Zq8h4B30+3CIdFFV9UXCjPG5ZNKHfO/hf96cl46A==",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/luxon": {
- "version": "3.4.0",
- "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.0.tgz",
- "integrity": "sha512-7eDo4Pt7aGhoCheGFIuq4Xa2fJm4ZpmldpGhjTYBNUYNCN6TIEP6v7chwwwt3KRp7YR+rghbfvjyo3V5y9hgBw==",
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.2.tgz",
+ "integrity": "sha512-uBoAVCVcajsrqy3pv7eo5jEUz1oeLmCcnMv8n4AJpT5hbpN9lUssAXibNElpbLce3Mhm9dyBzwYLs9zctM/0tA==",
"engines": {
"node": ">=12"
}
@@ -13981,11 +13984,11 @@
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
},
"node_modules/next": {
- "version": "13.4.12",
- "resolved": "https://registry.npmjs.org/next/-/next-13.4.12.tgz",
- "integrity": "sha512-eHfnru9x6NRmTMcjQp6Nz0J4XH9OubmzOa7CkWL+AUrUxpibub3vWwttjduu9No16dug1kq04hiUUpo7J3m3Xw==",
+ "version": "13.4.19",
+ "resolved": "https://registry.npmjs.org/next/-/next-13.4.19.tgz",
+ "integrity": "sha512-HuPSzzAbJ1T4BD8e0bs6B9C1kWQ6gv8ykZoRWs5AQoiIuqbGHHdQO7Ljuvg05Q0Z24E2ABozHe6FxDvI6HfyAw==",
"dependencies": {
- "@next/env": "13.4.12",
+ "@next/env": "13.4.19",
"@swc/helpers": "0.5.1",
"busboy": "1.6.0",
"caniuse-lite": "^1.0.30001406",
@@ -14001,19 +14004,18 @@
"node": ">=16.8.0"
},
"optionalDependencies": {
- "@next/swc-darwin-arm64": "13.4.12",
- "@next/swc-darwin-x64": "13.4.12",
- "@next/swc-linux-arm64-gnu": "13.4.12",
- "@next/swc-linux-arm64-musl": "13.4.12",
- "@next/swc-linux-x64-gnu": "13.4.12",
- "@next/swc-linux-x64-musl": "13.4.12",
- "@next/swc-win32-arm64-msvc": "13.4.12",
- "@next/swc-win32-ia32-msvc": "13.4.12",
- "@next/swc-win32-x64-msvc": "13.4.12"
+ "@next/swc-darwin-arm64": "13.4.19",
+ "@next/swc-darwin-x64": "13.4.19",
+ "@next/swc-linux-arm64-gnu": "13.4.19",
+ "@next/swc-linux-arm64-musl": "13.4.19",
+ "@next/swc-linux-x64-gnu": "13.4.19",
+ "@next/swc-linux-x64-musl": "13.4.19",
+ "@next/swc-win32-arm64-msvc": "13.4.19",
+ "@next/swc-win32-ia32-msvc": "13.4.19",
+ "@next/swc-win32-x64-msvc": "13.4.19"
},
"peerDependencies": {
"@opentelemetry/api": "^1.1.0",
- "fibers": ">= 3.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"sass": "^1.3.0"
@@ -14022,9 +14024,6 @@
"@opentelemetry/api": {
"optional": true
},
- "fibers": {
- "optional": true
- },
"sass": {
"optional": true
}
@@ -15043,9 +15042,9 @@
}
},
"node_modules/posthog-js": {
- "version": "1.75.3",
- "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.75.3.tgz",
- "integrity": "sha512-q5xP4R/Tx8E6H0goZQjY+URMLATFiYXc2raHA+31aNvpBs118fPTmExa4RK6MgRZDFhBiMUBZNT6aj7dM3SyUQ==",
+ "version": "1.77.3",
+ "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.77.3.tgz",
+ "integrity": "sha512-DKsGpBIUjQSihhGruEW8wpVCkeDxU4jz7gADdXX2jEWV6bl4WpUPxjo1ukidVDFvvc/ihCM5PQWMQrItexdpSA==",
"dependencies": {
"fflate": "^0.4.1"
}
@@ -15559,6 +15558,20 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-confetti": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/react-confetti/-/react-confetti-6.1.0.tgz",
+ "integrity": "sha512-7Ypx4vz0+g8ECVxr88W9zhcQpbeujJAVqL14ZnXJ3I23mOI9/oBVTQ3dkJhUmB0D6XOtCZEM6N0Gm9PMngkORw==",
+ "dependencies": {
+ "tween-functions": "^1.2.0"
+ },
+ "engines": {
+ "node": ">=10.18"
+ },
+ "peerDependencies": {
+ "react": "^16.3.0 || ^17.0.1 || ^18.0.0"
+ }
+ },
"node_modules/react-day-picker": {
"version": "8.8.0",
"resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-8.8.0.tgz",
@@ -16089,9 +16102,9 @@
}
},
"node_modules/react-hook-form": {
- "version": "7.45.2",
- "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.45.2.tgz",
- "integrity": "sha512-9s45OdTaKN+4NSTbXVqeDITd/nwIg++nxJGL8+OD5uf1DxvhsXQ641kaYHk5K28cpIOTYm71O/fYk7rFaygb3A==",
+ "version": "7.45.4",
+ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.45.4.tgz",
+ "integrity": "sha512-HGDV1JOOBPZj10LB3+OZgfDBTn+IeEsNOKiq/cxbQAIbKaiJUe/KV8DBUzsx0Gx/7IG/orWqRRm736JwOfUSWQ==",
"engines": {
"node": ">=12.22.0"
},
@@ -18639,6 +18652,11 @@
"node": ">= 6"
}
},
+ "node_modules/tween-functions": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/tween-functions/-/tween-functions-1.2.0.tgz",
+ "integrity": "sha512-PZBtLYcCLtEcjL14Fzb1gSxPBeL7nWvGhO5ZFPGqziCcr8uvHp0NDmdjBchp6KHL+tExcg0m3NISmKxhU394dA=="
+ },
"node_modules/typanion": {
"version": "3.13.0",
"resolved": "https://registry.npmjs.org/typanion/-/typanion-3.13.0.tgz",
@@ -19439,7 +19457,7 @@
"@typescript-eslint/eslint-plugin": "^5.59.2",
"@typescript-eslint/parser": "^5.59.2",
"eslint": "^8.40.0",
- "eslint-config-next": "13.4.12",
+ "eslint-config-next": "13.4.19",
"eslint-config-prettier": "^8.8.0",
"eslint-config-turbo": "^1.9.3",
"eslint-plugin-package-json": "^0.1.4",
@@ -19466,7 +19484,7 @@
"bcrypt": "^5.1.0",
"luxon": "^3.4.0",
"nanoid": "^4.0.2",
- "next": "13.4.12",
+ "next": "13.4.19",
"next-auth": "4.22.3",
"pdf-lib": "^1.17.1",
"react": "18.2.0",
@@ -19549,6 +19567,7 @@
"license": "MIT",
"dependencies": {
"@documenso/lib": "*",
+ "@hookform/resolvers": "^3.3.0",
"@radix-ui/react-accordion": "^1.1.1",
"@radix-ui/react-alert-dialog": "^1.0.3",
"@radix-ui/react-aspect-ratio": "^1.0.2",
@@ -19569,6 +19588,7 @@
"@radix-ui/react-select": "^1.2.1",
"@radix-ui/react-separator": "^1.0.2",
"@radix-ui/react-slider": "^1.1.1",
+ "@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-switch": "^1.0.2",
"@radix-ui/react-tabs": "^1.0.3",
"@radix-ui/react-toast": "^1.1.3",
@@ -19579,17 +19599,21 @@
"clsx": "^1.2.1",
"cmdk": "^0.2.0",
"framer-motion": "^10.12.8",
- "lucide-react": "^0.214.0",
- "next": "13.4.12",
+ "lucide-react": "^0.277.0",
+ "luxon": "^3.4.2",
+ "next": "13.4.19",
"pdfjs-dist": "3.6.172",
"react-day-picker": "^8.7.1",
+ "react-hook-form": "^7.45.4",
"react-pdf": "^7.3.3",
+ "react-rnd": "^10.4.1",
"tailwind-merge": "^1.12.0",
"tailwindcss-animate": "^1.0.5"
},
"devDependencies": {
"@documenso/tailwind-config": "*",
"@documenso/tsconfig": "*",
+ "@types/luxon": "^3.3.2",
"@types/react": "18.2.18",
"@types/react-dom": "18.2.7",
"react": "18.2.0",
diff --git a/packages/email/template-components/template-document-completed.tsx b/packages/email/template-components/template-document-completed.tsx
index b64b13cff..b37b75d4f 100644
--- a/packages/email/template-components/template-document-completed.tsx
+++ b/packages/email/template-components/template-document-completed.tsx
@@ -4,14 +4,12 @@ import * as config from '@documenso/tailwind-config';
export interface TemplateDocumentCompletedProps {
downloadLink: string;
- reviewLink: string;
documentName: string;
assetBaseUrl: string;
}
export const TemplateDocumentCompleted = ({
downloadLink,
- reviewLink,
documentName,
assetBaseUrl,
}: TemplateDocumentCompletedProps) => {
@@ -44,17 +42,17 @@ export const TemplateDocumentCompleted = ({
- Continue by downloading or reviewing the document.
+ Continue by downloading the document.
-
+ */}