Compare commits

..

38 Commits

Author SHA1 Message Date
Ephraim Atta-Duncan
986030cc38 chore: failed attempt at adding custom field labels
I have spent all day on it and for some reason, I can't figure it out
2024-02-18 16:01:03 +00:00
David Nguyen
2815b1a809 feat: add enterprise billing (#939)
## Description

Add support for enterprise billing plans.

Enterprise billing plans by default get access to everything early
adopters do:
- Unlimited teams
- Unlimited documents

They will also get additional features in the future.

## Notes

Pending webhook updates to support enterprise onboarding.
Rolled back env changes `NEXT_PUBLIC_PROJECT` since it doesn't seem to
work.
2024-02-17 12:42:00 +11:00
Lucas Smith
960914aeb5 fix: undo operation on signature pad (#868)
fixes: #864
2024-02-16 22:57:14 +11:00
Lucas Smith
d83769b410 chore: use unsafe effect 2024-02-16 11:56:02 +00:00
Lucas Smith
a0cf2a2c75 fix: improved document-dropzone ui for small vertical screens (#857)
improved document-dropzone ui for small vertical screens (screens less
than 800px vertically)
Although it can still become congested on really small vertical screens,
but possibility is really low.

fixes: #840
2024-02-16 22:03:24 +11:00
Lucas Smith
a30b73ce86 fix: update css 2024-02-16 11:02:04 +00:00
Lucas Smith
46d163d9d6 fix: highlighting issue in recipient selection (#937)
fixes: #920 

<img width="391" alt="image"
src="https://github.com/documenso/documenso/assets/75713174/08b2f5ab-4a6f-423a-a2fa-8f7b04789bb8">
2024-02-16 21:50:53 +11:00
Lucas Smith
681a89cfe1 chore: minor lint fixes (#934) 2024-02-16 21:48:45 +11:00
Lucas Smith
e5f4edc120 chore: create security.txt (#878)
Adding a security.txt file enables security researchers to quickly and
easily see where they can submit security issues and know that they are
being taken serious. From the proposal website:

> "When security risks in web services are discovered by independent
security researchers who understand the severity of the risk, they often
lack the channels to disclose them properly. As a result, security
issues may be left unreported. security.txt defines a standard to help
organizations define the process for security researchers to disclose
security vulnerabilities securely.”

See also https://securitytxt.org
2024-02-16 12:34:41 +11:00
Sumit Bisht
25291b64eb fix: highlighting issue in recipient selection 2024-02-15 22:25:23 +05:30
Lucas Smith
fe2093fe7c feat: add next-runtime-env (#869)
This PR adds the package
[next-runtime-env](https://github.com/expatfile/next-runtime-env/) to
populate the public environment variables at runtime.
2024-02-15 22:10:21 +11:00
Ephraim Atta-Duncan
49cddfab38 chore: lint with oxc 2024-02-15 06:11:50 +00:00
Timur Ercan
3e12a05ab8 chore: more grammar 2024-02-14 17:19:48 +01:00
Timur Ercan
a76504c0a4 Merge branch 'main' into chore-security-text 2024-02-14 17:16:44 +01:00
Timur Ercan
abab0c0a22 chore: grammer and format 2024-02-14 17:14:43 +01:00
Lucas Smith
51608ed390 fix: lint issue 2024-02-12 02:02:43 +00:00
Lucas Smith
8ebef831ac Merge branch 'main' into feat/add-runtime-env 2024-02-12 12:30:35 +11:00
Lucas Smith
20e2976731 fix: build issues 2024-02-12 01:29:22 +00:00
Lucas Smith
748bf6de6b fix: add dropped constants from merge 2024-02-08 22:12:04 +11:00
Lucas Smith
d13cf743bf Merge branch 'main' into feat/add-runtime-env 2024-02-08 22:06:59 +11:00
apoorv taneja
c970abc871 added onchange handler 2024-02-02 20:46:54 +05:30
apoorv taneja
d5b3df1648 fixed variable declaration 2024-02-02 19:32:39 +05:30
apoorv taneja
142c1c003e changed useEffect variables 2024-02-02 18:16:54 +05:30
apoorv taneja
a06c628653 Merge branch 'main' of https://github.com/plxity/documenso into fix/undo-button-in-canvas 2024-02-02 17:56:58 +05:30
apoorv taneja
7ca3697303 Merge branch 'main' of https://github.com/plxity/documenso into fix/undo-button-in-canvas 2024-02-02 14:49:06 +05:30
Lucas Smith
8ac2209493 Merge branch 'main' into chore-security-text 2024-02-02 16:16:25 +11:00
Lucas Smith
9c4ec34a3c fix: add precommit step for .well-known 2024-02-02 04:00:28 +00:00
Timur Ercan
1f142e334a Merge branch 'main' into chore-security-text 2024-01-31 20:31:34 +01:00
Mythie
08f82b23dc fix: update env entries to evaluate at runtime 2024-01-31 22:32:42 +11:00
Timur Ercan
747a7b0aea chore: security contacts and descr 2024-01-30 16:15:32 +01:00
Timur Ercan
375df71f5c Merge branch 'main' into chore-security-text 2024-01-29 16:43:57 +01:00
Catalin Pit
c0bb5205e1 chore: merged main 2024-01-29 10:14:56 +02:00
Tangerine Kugelmann
927a656c57 Create security.txt
See also https://securitytxt.org
2024-01-28 01:00:07 +01:00
Catalin Pit
751fb5275c Merge branch 'main' into feat/add-runtime-env 2024-01-26 14:04:54 +02:00
Catalin Pit
2f18518961 chore: merged main 2024-01-25 10:53:05 +02:00
Catalin Pit
d451a7acce feat: add next-runtime-env 2024-01-25 10:48:20 +02:00
apoorv taneja
d8aecc4092 fixed undo operation on signature pad 2024-01-25 13:21:55 +05:30
Sumit Bisht
e5c2263e92 fix: imporoved document-dropzone ui for small vertical screens 2024-01-23 18:37:02 +05:30
95 changed files with 637 additions and 56905 deletions

View File

@@ -1,4 +1,16 @@
#!/usr/bin/env sh #!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh" . "$(dirname -- "$0")/_/husky.sh"
SCRIPT_DIR="$(readlink -f "$(dirname "$0")")"
MONOREPO_ROOT="$(readlink -f "$SCRIPT_DIR/../")"
echo "Copying pdf.js"
npm run copy:pdfjs --workspace apps/**
echo "Copying .well-known/ contents"
node "$MONOREPO_ROOT/scripts/copy-wellknown.cjs"
git add "$MONOREPO_ROOT/apps/web/public/"
git add "$MONOREPO_ROOT/apps/marketing/public/"
npx lint-staged npx lint-staged

7
.well-known/security.txt Normal file
View File

@@ -0,0 +1,7 @@
# General Issues
Contact: https://github.com/documenso/documenso/issues/new?assignees=&labels=bug&projects=&template=bug-report.yml
# Report critical issues privately to let us take appropriate action before publishing.
Contact: mailto:security@documenso.com
Preferred-Languages: en
Canonical: https://documenso.com/.well-known/security.txt

View File

@@ -5,7 +5,7 @@ authorName: 'Timur Ercan'
authorImage: '/blog/blog-author-timur.jpeg' authorImage: '/blog/blog-author-timur.jpeg'
authorRole: 'Co-Founder' authorRole: 'Co-Founder'
date: 2024-01-25 date: 2024-01-25
Tags: tags:
- Vision - Vision
- Mission - Mission
- Open Source - Open Source

View File

@@ -5,7 +5,7 @@ authorName: 'Timur Ercan'
authorImage: '/blog/blog-author-timur.jpeg' authorImage: '/blog/blog-author-timur.jpeg'
authorRole: 'Co-Founder' authorRole: 'Co-Founder'
date: 2024-01-10 date: 2024-01-10
Tags: tags:
- GitHub - GitHub
- Backlog - Backlog
- Roadmap - Roadmap

View File

@@ -5,7 +5,7 @@ authorName: 'Timur Ercan'
authorImage: '/blog/blog-author-timur.jpeg' authorImage: '/blog/blog-author-timur.jpeg'
authorRole: 'Co-Founder' authorRole: 'Co-Founder'
date: 2024-02-06 date: 2024-02-06
Tags: tags:
- Founders - Founders
- Mission - Mission
- Open Source - Open Source

View File

@@ -0,0 +1,7 @@
# General Issues
Contact: https://github.com/documenso/documenso/issues/new?assignees=&labels=bug&projects=&template=bug-report.yml
# Report critical issues privately to let us take appropriate action before publishing.
Contact: mailto:security@documenso.com
Preferred-Languages: en
Canonical: https://documenso.com/.well-known/security.txt

File diff suppressed because one or more lines are too long

View File

@@ -12,7 +12,7 @@ export const generateMetadata = ({ params }: { params: { content: string } }) =>
const document = allDocuments.find((post) => post._raw.flattenedPath === params.content); const document = allDocuments.find((post) => post._raw.flattenedPath === params.content);
if (!document) { if (!document) {
notFound(); return { title: 'Not Found' };
} }
return { title: document.title }; return { title: document.title };

View File

@@ -7,6 +7,8 @@ import { ChevronLeft } from 'lucide-react';
import type { MDXComponents } from 'mdx/types'; import type { MDXComponents } from 'mdx/types';
import { useMDXComponent } from 'next-contentlayer/hooks'; import { useMDXComponent } from 'next-contentlayer/hooks';
export const dynamic = 'force-dynamic';
export const generateStaticParams = () => export const generateStaticParams = () =>
allBlogPosts.map((post) => ({ post: post._raw.flattenedPath })); allBlogPosts.map((post) => ({ post: post._raw.flattenedPath }));
@@ -14,7 +16,9 @@ export const generateMetadata = ({ params }: { params: { post: string } }) => {
const blogPost = allBlogPosts.find((post) => post._raw.flattenedPath === `blog/${params.post}`); const blogPost = allBlogPosts.find((post) => post._raw.flattenedPath === `blog/${params.post}`);
if (!blogPost) { if (!blogPost) {
notFound(); return {
title: 'Not Found',
};
} }
return { return {

View File

@@ -4,6 +4,7 @@ import { redirect } from 'next/navigation';
import { ArrowRight } from 'lucide-react'; import { ArrowRight } from 'lucide-react';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { redis } from '@documenso/lib/server-only/redis'; import { redis } from '@documenso/lib/server-only/redis';
import { stripe } from '@documenso/lib/server-only/stripe'; import { stripe } from '@documenso/lib/server-only/stripe';
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
@@ -12,6 +13,8 @@ import { Button } from '@documenso/ui/primitives/button';
import { PasswordReveal } from '~/components/(marketing)/password-reveal'; import { PasswordReveal } from '~/components/(marketing)/password-reveal';
export const dynamic = 'force-dynamic';
const fontCaveat = Caveat({ const fontCaveat = Caveat({
weight: ['500'], weight: ['500'],
subsets: ['latin'], subsets: ['latin'],
@@ -175,11 +178,7 @@ export default async function ClaimedPlanPage({ searchParams = {} }: ClaimedPlan
This is a temporary password. Please change it as soon as possible. This is a temporary password. Please change it as soon as possible.
</p> </p>
<Link <Link href={`${NEXT_PUBLIC_WEBAPP_URL()}/signin`} target="_blank" className="mt-4 block">
href={`${process.env.NEXT_PUBLIC_WEBAPP_URL}/signin`}
target="_blank"
className="mt-4 block"
>
<Button size="lg" className="text-base"> <Button size="lg" className="text-base">
Let's get started! Let's get started!
<ArrowRight className="ml-2 h-5 w-5" /> <ArrowRight className="ml-2 h-5 w-5" />

View File

@@ -147,7 +147,12 @@ export default async function OpenPage() {
<p className="text-muted-foreground mt-4 max-w-[60ch] text-center text-lg leading-normal"> <p className="text-muted-foreground mt-4 max-w-[60ch] text-center text-lg leading-normal">
All our metrics, finances, and learnings are public. We believe in transparency and want All our metrics, finances, and learnings are public. We believe in transparency and want
to share our journey with you. You can read more about why here:{' '} to share our journey with you. You can read more about why here:{' '}
<a className="font-bold" href="https://documenso.com/blog/pre-seed" target="_blank"> <a
className="font-bold"
href="https://documenso.com/blog/pre-seed"
target="_blank"
rel="noreferrer"
>
Announcing Open Metrics Announcing Open Metrics
</a> </a>
</p> </p>

View File

@@ -15,6 +15,8 @@ export const metadata: Metadata = {
title: 'Pricing', title: 'Pricing',
}; };
export const dynamic = 'force-dynamic';
export type PricingPageProps = { export type PricingPageProps = {
searchParams?: { searchParams?: {
planId?: string; planId?: string;
@@ -53,7 +55,7 @@ export default function PricingPage() {
<div className="mt-4 flex justify-center"> <div className="mt-4 flex justify-center">
<Button variant="outline" size="lg" className="rounded-full hover:cursor-pointer" asChild> <Button variant="outline" size="lg" className="rounded-full hover:cursor-pointer" asChild>
<Link href="https://github.com/documenso/documenso" target="_blank"> <Link href="https://github.com/documenso/documenso" target="_blank" rel="noreferrer">
Get Started Get Started
</Link> </Link>
</Button> </Button>
@@ -166,6 +168,7 @@ export default function PricingPage() {
<Link <Link
className="text-documenso-700 font-bold" className="text-documenso-700 font-bold"
target="_blank" target="_blank"
rel="noreferrer"
href="mailto:support@documenso.com" href="mailto:support@documenso.com"
> >
support@documenso.com support@documenso.com
@@ -175,6 +178,7 @@ export default function PricingPage() {
className="text-documenso-700 font-bold" className="text-documenso-700 font-bold"
href="https://documen.so/discord" href="https://documen.so/discord"
target="_blank" target="_blank"
rel="noreferrer"
> >
in our Discord-Support-Channel in our Discord-Support-Channel
</a>{' '} </a>{' '}

View File

@@ -6,6 +6,7 @@ import Link from 'next/link';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics'; import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { base64 } from '@documenso/lib/universal/base64'; import { base64 } from '@documenso/lib/universal/base64';
import { putFile } from '@documenso/lib/universal/upload/put-file'; import { putFile } from '@documenso/lib/universal/upload/put-file';
import type { Field, Recipient } from '@documenso/prisma/client'; import type { Field, Recipient } from '@documenso/prisma/client';
@@ -190,7 +191,7 @@ export const SinglePlayerClient = () => {
<p className="text-foreground mx-auto mt-4 max-w-[50ch] text-lg leading-normal"> <p className="text-foreground mx-auto mt-4 max-w-[50ch] text-lg leading-normal">
Create a{' '} Create a{' '}
<Link <Link
href={`${process.env.NEXT_PUBLIC_WEBAPP_URL}/signup`} href={`${NEXT_PUBLIC_WEBAPP_URL()}/signup`}
target="_blank" target="_blank"
className="hover:text-foreground/80 font-semibold transition-colors" className="hover:text-foreground/80 font-semibold transition-colors"
> >

View File

@@ -7,6 +7,7 @@ export const metadata: Metadata = {
}; };
export const revalidate = 0; export const revalidate = 0;
export const dynamic = 'force-dynamic';
// !: This entire file is a hack to get around failed prerendering of // !: This entire file is a hack to get around failed prerendering of
// !: the Single Player Mode page. This regression was introduced during // !: the Single Player Mode page. This regression was introduced during

View File

@@ -3,6 +3,7 @@ import { Suspense } from 'react';
import { Caveat, Inter } from 'next/font/google'; import { Caveat, Inter } from 'next/font/google';
import { FeatureFlagProvider } from '@documenso/lib/client-only/providers/feature-flag'; import { FeatureFlagProvider } from '@documenso/lib/client-only/providers/feature-flag';
import { NEXT_PUBLIC_MARKETING_URL } from '@documenso/lib/constants/app';
import { getAllAnonymousFlags } from '@documenso/lib/universal/get-feature-flag'; import { getAllAnonymousFlags } from '@documenso/lib/universal/get-feature-flag';
import { TrpcProvider } from '@documenso/trpc/react'; import { TrpcProvider } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
@@ -17,32 +18,35 @@ import './globals.css';
const fontInter = Inter({ subsets: ['latin'], variable: '--font-sans' }); const fontInter = Inter({ subsets: ['latin'], variable: '--font-sans' });
const fontCaveat = Caveat({ subsets: ['latin'], variable: '--font-signature' }); const fontCaveat = Caveat({ subsets: ['latin'], variable: '--font-signature' });
export const metadata = { export function generateMetadata() {
title: { return {
template: '%s - Documenso', title: {
default: 'Documenso', 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',
openGraph: {
title: 'Documenso - The Open Source DocuSign Alternative',
description: 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.', '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', keywords:
images: [`${process.env.NEXT_PUBLIC_MARKETING_URL}/opengraph-image.jpg`], 'Documenso, open source, DocuSign alternative, document signing, open signing infrastructure, open-source community, fast signing, beautiful signing, smart templates',
}, authors: { name: 'Documenso, Inc.' },
twitter: { robots: 'index, follow',
site: '@documenso', metadataBase: new URL(NEXT_PUBLIC_MARKETING_URL() ?? 'http://localhost:3000'),
card: 'summary_large_image', openGraph: {
images: [`${process.env.NEXT_PUBLIC_MARKETING_URL}/opengraph-image.jpg`], title: 'Documenso - The Open Source DocuSign Alternative',
description: 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.', '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 }) { export default async function RootLayout({ children }: { children: React.ReactNode }) {
const flags = await getAllAnonymousFlags(); const flags = await getAllAnonymousFlags();

View File

@@ -8,6 +8,7 @@ import Link from 'next/link';
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion';
import { usePlausible } from 'next-plausible'; import { usePlausible } from 'next-plausible';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
@@ -82,11 +83,7 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
</p> </p>
<Button className="rounded-full text-base" asChild> <Button className="rounded-full text-base" asChild>
<Link <Link href={`${NEXT_PUBLIC_WEBAPP_URL()}/signup`} target="_blank" className="mt-6">
href={`${process.env.NEXT_PUBLIC_WEBAPP_URL}/signup`}
target="_blank"
className="mt-6"
>
Signup Now Signup Now
</Link> </Link>
</Button> </Button>
@@ -117,13 +114,13 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
</p> </p>
<Button className="mt-6 rounded-full text-base" asChild> <Button className="mt-6 rounded-full text-base" asChild>
<Link href={`${process.env.NEXT_PUBLIC_WEBAPP_URL}/signup`}>Signup Now</Link> <Link href={`${NEXT_PUBLIC_WEBAPP_URL()}/signup`}>Signup Now</Link>
</Button> </Button>
<div className="mt-8 flex w-full flex-col divide-y"> <div className="mt-8 flex w-full flex-col divide-y">
<p className="text-foreground py-4 font-medium"> <p className="text-foreground py-4 font-medium">
{' '} {' '}
<a href="https://documenso.com/blog/early-adopters" target="_blank"> <a href="https://documenso.com/blog/early-adopters" target="_blank" rel="noreferrer">
The Early Adopter Deal: The Early Adopter Deal:
</a> </a>
</p> </p>
@@ -133,7 +130,11 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
<p className="text-foreground py-4"> <p className="text-foreground py-4">
<strong> <strong>
{' '} {' '}
<a href="https://documenso.com/blog/early-adopters" target="_blank"> <a
href="https://documenso.com/blog/early-adopters"
target="_blank"
rel="noreferrer"
>
Includes all upcoming features Includes all upcoming features
</a> </a>
</strong> </strong>

View File

@@ -6,6 +6,7 @@ import Link from 'next/link';
import signingCelebration from '@documenso/assets/images/signing-celebration.png'; import signingCelebration from '@documenso/assets/images/signing-celebration.png';
import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag'; import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import type { Signature } from '@documenso/prisma/client'; import type { Signature } from '@documenso/prisma/client';
import { DocumentStatus } from '@documenso/prisma/client'; import { DocumentStatus } from '@documenso/prisma/client';
import type { DocumentWithRecipient } from '@documenso/prisma/types/document-with-recipient'; import type { DocumentWithRecipient } from '@documenso/prisma/types/document-with-recipient';
@@ -85,7 +86,7 @@ export const SinglePlayerModeSuccess = ({
<p className="text-muted-foreground/60 mt-16 text-center text-sm"> <p className="text-muted-foreground/60 mt-16 text-center text-sm">
Create a{' '} Create a{' '}
<Link <Link
href={`${process.env.NEXT_PUBLIC_WEBAPP_URL}/signup`} href={`${NEXT_PUBLIC_WEBAPP_URL()}/signup`}
target="_blank" target="_blank"
className="text-documenso-700 hover:text-documenso-600 whitespace-nowrap" className="text-documenso-700 hover:text-documenso-600 whitespace-nowrap"
> >

View File

@@ -7,6 +7,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion';
import { Loader } from 'lucide-react'; import { Loader } from 'lucide-react';
import { usePlausible } from 'next-plausible'; import { usePlausible } from 'next-plausible';
import { env } from 'next-runtime-env';
import { Controller, useForm } from 'react-hook-form'; import { Controller, useForm } from 'react-hook-form';
import { z } from 'zod'; import { z } from 'zod';
@@ -144,7 +145,11 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
setTimeout(resolve, 1000); setTimeout(resolve, 1000);
}); });
const planId = process.env.NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID; const planId = env('NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID');
if (!planId) {
throw new Error('No plan ID found.');
}
const claimPlanInput = signatureDataUrl const claimPlanInput = signatureDataUrl
? { ? {

View File

@@ -1,13 +1,15 @@
import { NextApiRequest, NextApiResponse } from 'next'; import type { NextApiRequest, NextApiResponse } from 'next';
import { randomUUID } from 'crypto'; import { randomUUID } from 'crypto';
import { TEarlyAdopterCheckoutMetadataSchema } from '@documenso/ee/server-only/stripe/webhook/early-adopter-checkout-metadata'; import type { TEarlyAdopterCheckoutMetadataSchema } from '@documenso/ee/server-only/stripe/webhook/early-adopter-checkout-metadata';
import { NEXT_PUBLIC_MARKETING_URL, NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { redis } from '@documenso/lib/server-only/redis'; import { redis } from '@documenso/lib/server-only/redis';
import { stripe } from '@documenso/lib/server-only/stripe'; import { stripe } from '@documenso/lib/server-only/stripe';
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
import { TClaimPlanResponseSchema, ZClaimPlanRequestSchema } from '~/api/claim-plan/types'; import type { TClaimPlanResponseSchema } from '~/api/claim-plan/types';
import { ZClaimPlanRequestSchema } from '~/api/claim-plan/types';
export default async function handler( export default async function handler(
req: NextApiRequest, req: NextApiRequest,
@@ -40,7 +42,7 @@ export default async function handler(
if (user) { if (user) {
return res.status(200).json({ return res.status(200).json({
redirectUrl: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/signin`, redirectUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/signin`,
}); });
} }
@@ -77,8 +79,8 @@ export default async function handler(
mode: 'subscription', mode: 'subscription',
metadata, metadata,
allow_promotion_codes: true, allow_promotion_codes: true,
success_url: `${process.env.NEXT_PUBLIC_MARKETING_URL}/claimed?sessionId={CHECKOUT_SESSION_ID}`, success_url: `${NEXT_PUBLIC_MARKETING_URL()}/claimed?sessionId={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_MARKETING_URL}`, cancel_url: `${NEXT_PUBLIC_MARKETING_URL()}`,
}); });
if (!checkout.url) { if (!checkout.url) {

View File

@@ -0,0 +1,7 @@
# General Issues
Contact: https://github.com/documenso/documenso/issues/new?assignees=&labels=bug&projects=&template=bug-report.yml
# Report critical issues privately to let us take appropriate action before publishing.
Contact: mailto:security@documenso.com
Preferred-Languages: en
Canonical: https://documenso.com/.well-known/security.txt

View File

@@ -108,88 +108,86 @@ export const ResendDocumentActionItem = ({
}; };
return ( return (
<> <Dialog open={isOpen} onOpenChange={setIsOpen}>
<Dialog open={isOpen} onOpenChange={setIsOpen}> <DialogTrigger asChild>
<DialogTrigger asChild> <DropdownMenuItem disabled={isDisabled} onSelect={(e) => e.preventDefault()}>
<DropdownMenuItem disabled={isDisabled} onSelect={(e) => e.preventDefault()}> <History className="mr-2 h-4 w-4" />
<History className="mr-2 h-4 w-4" /> Resend
Resend </DropdownMenuItem>
</DropdownMenuItem> </DialogTrigger>
</DialogTrigger>
<DialogContent className="sm:max-w-sm" hideClose> <DialogContent className="sm:max-w-sm" hideClose>
<DialogHeader> <DialogHeader>
<DialogTitle> <DialogTitle>
<h1 className="text-center text-xl">Who do you want to remind?</h1> <h1 className="text-center text-xl">Who do you want to remind?</h1>
</DialogTitle> </DialogTitle>
</DialogHeader> </DialogHeader>
<Form {...form}> <Form {...form}>
<form id={FORM_ID} onSubmit={handleSubmit(onFormSubmit)} className="px-3"> <form id={FORM_ID} onSubmit={handleSubmit(onFormSubmit)} className="px-3">
<FormField <FormField
control={form.control} control={form.control}
name="recipients" name="recipients"
render={({ field: { value, onChange } }) => ( render={({ field: { value, onChange } }) => (
<> <>
{recipients.map((recipient) => ( {recipients.map((recipient) => (
<FormItem <FormItem
key={recipient.id} key={recipient.id}
className="flex flex-row items-center justify-between gap-x-3" className="flex flex-row items-center justify-between gap-x-3"
>
<FormLabel
className={cn('my-2 flex items-center gap-2 font-normal', {
'opacity-50': !value.includes(recipient.id),
})}
> >
<FormLabel <StackAvatar
className={cn('my-2 flex items-center gap-2 font-normal', { key={recipient.id}
'opacity-50': !value.includes(recipient.id), type={getRecipientType(recipient)}
})} fallbackText={recipientAbbreviation(recipient)}
> />
<StackAvatar {recipient.email}
key={recipient.id} </FormLabel>
type={getRecipientType(recipient)}
fallbackText={recipientAbbreviation(recipient)}
/>
{recipient.email}
</FormLabel>
<FormControl> <FormControl>
<Checkbox <Checkbox
className="h-5 w-5 rounded-full data-[state=checked]:border-black data-[state=checked]:bg-black " className="h-5 w-5 rounded-full data-[state=checked]:border-black data-[state=checked]:bg-black "
checkClassName="text-white" checkClassName="text-white"
value={recipient.id} value={recipient.id}
checked={value.includes(recipient.id)} checked={value.includes(recipient.id)}
onCheckedChange={(checked: boolean) => onCheckedChange={(checked: boolean) =>
checked checked
? onChange([...value, recipient.id]) ? onChange([...value, recipient.id])
: onChange(value.filter((v) => v !== recipient.id)) : onChange(value.filter((v) => v !== recipient.id))
} }
/> />
</FormControl> </FormControl>
</FormItem> </FormItem>
))} ))}
</> </>
)} )}
/> />
</form> </form>
</Form> </Form>
<DialogFooter> <DialogFooter>
<div className="flex w-full flex-1 flex-nowrap gap-4"> <div className="flex w-full flex-1 flex-nowrap gap-4">
<DialogClose asChild> <DialogClose asChild>
<Button <Button
type="button" type="button"
className="dark:bg-muted dark:hover:bg-muted/80 flex-1 bg-black/5 hover:bg-black/10" className="dark:bg-muted dark:hover:bg-muted/80 flex-1 bg-black/5 hover:bg-black/10"
variant="secondary" variant="secondary"
disabled={isSubmitting} disabled={isSubmitting}
> >
Cancel Cancel
</Button>
</DialogClose>
<Button className="flex-1" loading={isSubmitting} type="submit" form={FORM_ID}>
Send reminder
</Button> </Button>
</div> </DialogClose>
</DialogFooter>
</DialogContent> <Button className="flex-1" loading={isSubmitting} type="submit" form={FORM_ID}>
</Dialog> Send reminder
</> </Button>
</div>
</DialogFooter>
</DialogContent>
</Dialog>
); );
}; };

View File

@@ -117,7 +117,7 @@ export const UploadDocument = ({ className, team }: UploadDocumentProps) => {
return ( return (
<div className={cn('relative', className)}> <div className={cn('relative', className)}>
<DocumentDropzone <DocumentDropzone
className="min-h-[40vh]" className="h-[min(400px,50vh)]"
disabled={remaining.documents === 0 || !session?.user.emailVerified} disabled={remaining.documents === 0 || !session?.user.emailVerified}
disabledMessage={disabledMessage} disabledMessage={disabledMessage}
onDrop={onFileDrop} onDrop={onFileDrop}

View File

@@ -2,6 +2,7 @@
import { getStripeCustomerByUser } from '@documenso/ee/server-only/stripe/get-customer'; import { getStripeCustomerByUser } from '@documenso/ee/server-only/stripe/get-customer';
import { getPortalSession } from '@documenso/ee/server-only/stripe/get-portal-session'; 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 { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
export const createBillingPortal = async () => { export const createBillingPortal = async () => {
@@ -11,6 +12,6 @@ export const createBillingPortal = async () => {
return getPortalSession({ return getPortalSession({
customerId: stripeCustomer.id, customerId: stripeCustomer.id,
returnUrl: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/settings/billing`, returnUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/settings/billing`,
}); });
}; };

View File

@@ -3,6 +3,7 @@
import { getCheckoutSession } from '@documenso/ee/server-only/stripe/get-checkout-session'; import { getCheckoutSession } from '@documenso/ee/server-only/stripe/get-checkout-session';
import { getStripeCustomerByUser } from '@documenso/ee/server-only/stripe/get-customer'; import { getStripeCustomerByUser } from '@documenso/ee/server-only/stripe/get-customer';
import { getPortalSession } from '@documenso/ee/server-only/stripe/get-portal-session'; 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 { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
import { getSubscriptionsByUserId } from '@documenso/lib/server-only/subscription/get-subscriptions-by-user-id'; import { getSubscriptionsByUserId } from '@documenso/lib/server-only/subscription/get-subscriptions-by-user-id';
@@ -27,13 +28,13 @@ export const createCheckout = async ({ priceId }: CreateCheckoutOptions) => {
if (foundSubscription) { if (foundSubscription) {
return getPortalSession({ return getPortalSession({
customerId: stripeCustomer.id, customerId: stripeCustomer.id,
returnUrl: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/settings/billing`, returnUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/settings/billing`,
}); });
} }
return getCheckoutSession({ return getCheckoutSession({
customerId: stripeCustomer.id, customerId: stripeCustomer.id,
priceId, priceId,
returnUrl: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/settings/billing`, returnUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/settings/billing`,
}); });
}; };

View File

@@ -5,7 +5,7 @@ import { match } from 'ts-pattern';
import { getStripeCustomerByUser } from '@documenso/ee/server-only/stripe/get-customer'; import { getStripeCustomerByUser } from '@documenso/ee/server-only/stripe/get-customer';
import { getPricesByInterval } from '@documenso/ee/server-only/stripe/get-prices-by-interval'; import { getPricesByInterval } from '@documenso/ee/server-only/stripe/get-prices-by-interval';
import { getPricesByPlan } from '@documenso/ee/server-only/stripe/get-prices-by-plan'; 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 { getProductByPriceId } from '@documenso/ee/server-only/stripe/get-product-by-price-id';
import { STRIPE_PLAN_TYPE } from '@documenso/lib/constants/billing'; import { STRIPE_PLAN_TYPE } from '@documenso/lib/constants/billing';
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session'; import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
@@ -37,23 +37,23 @@ export default async function BillingSettingsPage() {
user = await getStripeCustomerByUser(user).then((result) => result.user); user = await getStripeCustomerByUser(user).then((result) => result.user);
} }
const [subscriptions, prices, communityPlanPrices] = await Promise.all([ const [subscriptions, prices, primaryAccountPlanPrices] = await Promise.all([
getSubscriptionsByUserId({ userId: user.id }), getSubscriptionsByUserId({ userId: user.id }),
getPricesByInterval({ plan: STRIPE_PLAN_TYPE.COMMUNITY }), getPricesByInterval({ plan: STRIPE_PLAN_TYPE.COMMUNITY }),
getPricesByPlan(STRIPE_PLAN_TYPE.COMMUNITY), getPrimaryAccountPlanPrices(),
]); ]);
const communityPlanPriceIds = communityPlanPrices.map(({ id }) => id); const primaryAccountPlanPriceIds = primaryAccountPlanPrices.map(({ id }) => id);
let subscriptionProduct: Stripe.Product | null = null; let subscriptionProduct: Stripe.Product | null = null;
const communityPlanUserSubscriptions = subscriptions.filter(({ priceId }) => const primaryAccountPlanSubscriptions = subscriptions.filter(({ priceId }) =>
communityPlanPriceIds.includes(priceId), primaryAccountPlanPriceIds.includes(priceId),
); );
const subscription = const subscription =
communityPlanUserSubscriptions.find(({ status }) => status === SubscriptionStatus.ACTIVE) ?? primaryAccountPlanSubscriptions.find(({ status }) => status === SubscriptionStatus.ACTIVE) ??
communityPlanUserSubscriptions[0]; primaryAccountPlanSubscriptions[0];
if (subscription?.priceId) { if (subscription?.priceId) {
subscriptionProduct = await getProductByPriceId({ priceId: subscription.priceId }).catch( subscriptionProduct = await getProductByPriceId({ priceId: subscription.priceId }).catch(

View File

@@ -3,6 +3,8 @@ import { NextResponse } from 'next/server';
import { P, match } from 'ts-pattern'; import { P, match } from 'ts-pattern';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import type { ShareHandlerAPIResponse } from '~/pages/api/share'; import type { ShareHandlerAPIResponse } from '~/pages/api/share';
export const runtime = 'edge'; export const runtime = 'edge';
@@ -37,7 +39,7 @@ export async function GET(_request: Request, { params: { slug } }: SharePageOpen
), ),
]); ]);
const baseUrl = process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000'; const baseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
const recipientOrSender: ShareHandlerAPIResponse = await fetch( const recipientOrSender: ShareHandlerAPIResponse = await fetch(
new URL(`/api/share?slug=${slug}`, baseUrl), new URL(`/api/share?slug=${slug}`, baseUrl),

View File

@@ -1,8 +1,8 @@
import { Metadata } from 'next'; import type { Metadata } from 'next';
import { headers } from 'next/headers'; import { headers } from 'next/headers';
import { redirect } from 'next/navigation'; import { redirect } from 'next/navigation';
import { APP_BASE_URL } from '@documenso/lib/constants/app'; import { NEXT_PUBLIC_MARKETING_URL } from '@documenso/lib/constants/app';
type SharePageProps = { type SharePageProps = {
params: { slug: string }; params: { slug: string };
@@ -16,12 +16,12 @@ export function generateMetadata({ params: { slug } }: SharePageProps) {
title: 'Documenso - Join the open source signing revolution', title: 'Documenso - Join the open source signing revolution',
description: 'I just signed with Documenso!', description: 'I just signed with Documenso!',
type: 'website', type: 'website',
images: [`${APP_BASE_URL}/share/${slug}/opengraph`], images: [`/share/${slug}/opengraph`],
}, },
twitter: { twitter: {
site: '@documenso', site: '@documenso',
card: 'summary_large_image', card: 'summary_large_image',
images: [`${APP_BASE_URL}/share/${slug}/opengraph`], images: [`/share/${slug}/opengraph`],
description: 'I just signed with Documenso!', description: 'I just signed with Documenso!',
}, },
} satisfies Metadata; } satisfies Metadata;
@@ -35,5 +35,5 @@ export default function SharePage() {
return null; return null;
} }
redirect(process.env.NEXT_PUBLIC_MARKETING_URL ?? 'http://localhost:3001'); redirect(NEXT_PUBLIC_MARKETING_URL() ?? 'http://localhost:3001');
} }

View File

@@ -2,6 +2,8 @@ import type { Metadata } from 'next';
import Link from 'next/link'; import Link from 'next/link';
import { redirect } from 'next/navigation'; import { redirect } from 'next/navigation';
import { env } from 'next-runtime-env';
import { IS_GOOGLE_SSO_ENABLED } from '@documenso/lib/constants/auth'; import { IS_GOOGLE_SSO_ENABLED } from '@documenso/lib/constants/auth';
import { decryptSecondaryData } from '@documenso/lib/server-only/crypto/decrypt'; import { decryptSecondaryData } from '@documenso/lib/server-only/crypto/decrypt';
@@ -18,6 +20,8 @@ type SignInPageProps = {
}; };
export default function SignInPage({ searchParams }: SignInPageProps) { export default function SignInPage({ searchParams }: SignInPageProps) {
const NEXT_PUBLIC_DISABLE_SIGNUP = env('NEXT_PUBLIC_DISABLE_SIGNUP');
const rawEmail = typeof searchParams.email === 'string' ? searchParams.email : undefined; const rawEmail = typeof searchParams.email === 'string' ? searchParams.email : undefined;
const email = rawEmail ? decryptSecondaryData(rawEmail) : null; const email = rawEmail ? decryptSecondaryData(rawEmail) : null;
@@ -39,7 +43,7 @@ export default function SignInPage({ searchParams }: SignInPageProps) {
isGoogleSSOEnabled={IS_GOOGLE_SSO_ENABLED} isGoogleSSOEnabled={IS_GOOGLE_SSO_ENABLED}
/> />
{process.env.NEXT_PUBLIC_DISABLE_SIGNUP !== 'true' && ( {NEXT_PUBLIC_DISABLE_SIGNUP !== 'true' && (
<p className="text-muted-foreground mt-6 text-center text-sm"> <p className="text-muted-foreground mt-6 text-center text-sm">
Don't have an account?{' '} Don't have an account?{' '}
<Link href="/signup" className="text-primary duration-200 hover:opacity-70"> <Link href="/signup" className="text-primary duration-200 hover:opacity-70">

View File

@@ -2,6 +2,8 @@ import type { Metadata } from 'next';
import Link from 'next/link'; import Link from 'next/link';
import { redirect } from 'next/navigation'; import { redirect } from 'next/navigation';
import { env } from 'next-runtime-env';
import { IS_GOOGLE_SSO_ENABLED } from '@documenso/lib/constants/auth'; import { IS_GOOGLE_SSO_ENABLED } from '@documenso/lib/constants/auth';
import { decryptSecondaryData } from '@documenso/lib/server-only/crypto/decrypt'; import { decryptSecondaryData } from '@documenso/lib/server-only/crypto/decrypt';
@@ -18,7 +20,9 @@ type SignUpPageProps = {
}; };
export default function SignUpPage({ searchParams }: SignUpPageProps) { export default function SignUpPage({ searchParams }: SignUpPageProps) {
if (process.env.NEXT_PUBLIC_DISABLE_SIGNUP === 'true') { const NEXT_PUBLIC_DISABLE_SIGNUP = env('NEXT_PUBLIC_DISABLE_SIGNUP');
if (NEXT_PUBLIC_DISABLE_SIGNUP === 'true') {
redirect('/signin'); redirect('/signin');
} }

View File

@@ -2,8 +2,11 @@ import { Suspense } from 'react';
import { Caveat, Inter } from 'next/font/google'; import { Caveat, Inter } from 'next/font/google';
import { PublicEnvScript } from 'next-runtime-env';
import { FeatureFlagProvider } from '@documenso/lib/client-only/providers/feature-flag'; import { FeatureFlagProvider } from '@documenso/lib/client-only/providers/feature-flag';
import { LocaleProvider } from '@documenso/lib/client-only/providers/locale'; import { LocaleProvider } from '@documenso/lib/client-only/providers/locale';
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 { getServerComponentAllFlags } from '@documenso/lib/server-only/feature-flags/get-server-component-feature-flag';
import { getLocale } from '@documenso/lib/server-only/headers/get-locale'; import { getLocale } from '@documenso/lib/server-only/headers/get-locale';
import { TrpcProvider } from '@documenso/trpc/react'; import { TrpcProvider } from '@documenso/trpc/react';
@@ -19,32 +22,35 @@ import './globals.css';
const fontInter = Inter({ subsets: ['latin'], variable: '--font-sans' }); const fontInter = Inter({ subsets: ['latin'], variable: '--font-sans' });
const fontCaveat = Caveat({ subsets: ['latin'], variable: '--font-signature' }); const fontCaveat = Caveat({ subsets: ['latin'], variable: '--font-signature' });
export const metadata = { export function generateMetadata() {
title: { return {
template: '%s - Documenso', title: {
default: 'Documenso', 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',
openGraph: {
title: 'Documenso - The Open Source DocuSign Alternative',
description: 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.', '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', keywords:
images: [`${process.env.NEXT_PUBLIC_WEBAPP_URL}/opengraph-image.jpg`], 'Documenso, open source, DocuSign alternative, document signing, open signing infrastructure, open-source community, fast signing, beautiful signing, smart templates',
}, authors: { name: 'Documenso, Inc.' },
twitter: { robots: 'index, follow',
site: '@documenso', metadataBase: new URL(NEXT_PUBLIC_WEBAPP_URL() ?? 'http://localhost:3000'),
card: 'summary_large_image', openGraph: {
images: [`${process.env.NEXT_PUBLIC_WEBAPP_URL}/opengraph-image.jpg`], title: 'Documenso - The Open Source DocuSign Alternative',
description: 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.', '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 }) { export default async function RootLayout({ children }: { children: React.ReactNode }) {
const flags = await getServerComponentAllFlags(); const flags = await getServerComponentAllFlags();
@@ -62,6 +68,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" /> <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" /> <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
<link rel="manifest" href="/site.webmanifest" /> <link rel="manifest" href="/site.webmanifest" />
<PublicEnvScript />
</head> </head>
<Suspense> <Suspense>

View File

@@ -4,6 +4,7 @@ import React from 'react';
import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard'; import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard';
import { getRecipientType } from '@documenso/lib/client-only/recipient-type'; 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 { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter'; import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter';
import type { Recipient } from '@documenso/prisma/client'; import type { Recipient } from '@documenso/prisma/client';
@@ -25,7 +26,7 @@ export function AvatarWithRecipient({ recipient }: AvatarWithRecipientProps) {
return; return;
} }
void copy(`${process.env.NEXT_PUBLIC_WEBAPP_URL}/sign/${recipient.token}`).then(() => { void copy(`${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`).then(() => {
toast({ toast({
title: 'Copied to clipboard', title: 'Copied to clipboard',
description: 'The signing link has been copied to your clipboard.', description: 'The signing link has been copied to your clipboard.',

View File

@@ -238,7 +238,7 @@ export const TransferTeamDialog = ({
<Alert variant="neutral"> <Alert variant="neutral">
<AlertDescription> <AlertDescription>
<ul className="list-outside list-disc space-y-2 pl-4"> <ul className="list-outside list-disc space-y-2 pl-4">
{IS_BILLING_ENABLED && ( {IS_BILLING_ENABLED() && (
// Temporary removed. // Temporary removed.
// <li> // <li>
// {form.getValues('clearPaymentMethods') // {form.getValues('clearPaymentMethods')

View File

@@ -48,7 +48,7 @@ export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
</Button> </Button>
</Link> </Link>
{IS_BILLING_ENABLED && ( {IS_BILLING_ENABLED() && (
<Link href={billingPath}> <Link href={billingPath}>
<Button <Button
variant="ghost" variant="ghost"

View File

@@ -56,7 +56,7 @@ export const MobileNav = ({ className, ...props }: MobileNavProps) => {
</Button> </Button>
</Link> </Link>
{IS_BILLING_ENABLED && ( {IS_BILLING_ENABLED() && (
<Link href={billingPath}> <Link href={billingPath}>
<Button <Button
variant="ghost" variant="ghost"

View File

@@ -67,7 +67,7 @@ export const TeamsMemberPageDataTable = ({
<Tabs value={currentTab} className="flex-shrink-0 overflow-x-auto"> <Tabs value={currentTab} className="flex-shrink-0 overflow-x-auto">
<TabsList> <TabsList>
<TabsTrigger className="min-w-[60px]" value="members" asChild> <TabsTrigger className="min-w-[60px]" value="members" asChild>
<Link href={pathname ?? '/'}>All</Link> <Link href={pathname ?? '/'}>Active</Link>
</TabsTrigger> </TabsTrigger>
<TabsTrigger className="min-w-[60px]" value="invites" asChild> <TabsTrigger className="min-w-[60px]" value="invites" asChild>

View File

@@ -1,3 +1,5 @@
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
/** /**
* getAssetBuffer is used to retrieve array buffers for various assets * getAssetBuffer is used to retrieve array buffers for various assets
* that are hosted in the `public` folder. * that are hosted in the `public` folder.
@@ -8,7 +10,7 @@
* @param path The path to the asset, relative to the `public` folder. * @param path The path to the asset, relative to the `public` folder.
*/ */
export const getAssetBuffer = async (path: string) => { export const getAssetBuffer = async (path: string) => {
const baseUrl = process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000'; const baseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
return fetch(new URL(path, baseUrl)).then(async (res) => res.arrayBuffer()); return fetch(new URL(path, baseUrl)).then(async (res) => res.arrayBuffer());
}; };

View File

@@ -1,7 +1,7 @@
/** @type {import('lint-staged').Config} */ /** @type {import('lint-staged').Config} */
module.exports = { module.exports = {
'**/*.{ts,tsx,cts,mts}': (files) => `eslint --fix ${files.join(' ')}`, '**/*.{ts,tsx,cts,mts}': (files) => files.map((file) => `eslint --fix ${file}`),
'**/*.{js,jsx,cjs,mjs}': (files) => `prettier --write ${files.join(' ')}`, '**/*.{js,jsx,cjs,mjs}': (files) => files.map((file) => `prettier --write ${file}`),
'**/*.{yml,mdx}': (files) => `prettier --write ${files.join(' ')}`, '**/*.{yml,mdx}': (files) => files.map((file) => `prettier --write ${file}`),
'**/*/package.json': 'npm run precommit', '**/*/package.json': 'npm run precommit',
}; };

16
package-lock.json generated
View File

@@ -9,6 +9,9 @@
"apps/*", "apps/*",
"packages/*" "packages/*"
], ],
"dependencies": {
"next-runtime-env": "^3.2.0"
},
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^17.7.1", "@commitlint/cli": "^17.7.1",
"@commitlint/config-conventional": "^17.7.0", "@commitlint/config-conventional": "^17.7.0",
@@ -14404,6 +14407,19 @@
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
} }
}, },
"node_modules/next-runtime-env": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/next-runtime-env/-/next-runtime-env-3.2.0.tgz",
"integrity": "sha512-rwe3flUgSRm51hzRN4Vt5MMSYMS4aDMEPJa0r+CMONA3UyUZl8Y5O8zjHSIlaNb3yquTCttZ0ahObPyPprBj9g==",
"dependencies": {
"next": "^14",
"react": "^18"
},
"peerDependencies": {
"next": "^14",
"react": "^18"
}
},
"node_modules/next-themes": { "node_modules/next-themes": {
"version": "0.2.1", "version": "0.2.1",
"resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.2.1.tgz", "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.2.1.tgz",

View File

@@ -47,7 +47,9 @@
"apps/*", "apps/*",
"packages/*" "packages/*"
], ],
"dependencies": {}, "dependencies": {
"next-runtime-env": "^3.2.0"
},
"overrides": { "overrides": {
"next-auth": { "next-auth": {
"next": "14.0.3" "next": "14.0.3"

View File

@@ -12,7 +12,7 @@ export type GetLimitsOptions = {
export const getLimits = async ({ headers, teamId }: GetLimitsOptions = {}) => { export const getLimits = async ({ headers, teamId }: GetLimitsOptions = {}) => {
const requestHeaders = headers ?? {}; const requestHeaders = headers ?? {};
const url = new URL(`${APP_BASE_URL}/api/limits`); const url = new URL('/api/limits', APP_BASE_URL() ?? 'http://localhost:3000');
if (teamId) { if (teamId) {
requestHeaders['team-id'] = teamId.toString(); requestHeaders['team-id'] = teamId.toString();

View File

@@ -1,11 +1,10 @@
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app'; import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
import { STRIPE_PLAN_TYPE } from '@documenso/lib/constants/billing';
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
import { SubscriptionStatus } from '@documenso/prisma/client'; import { SubscriptionStatus } from '@documenso/prisma/client';
import { getPricesByPlan } from '../stripe/get-prices-by-plan'; import { getDocumentRelatedPrices } from '../stripe/get-document-related-prices.ts';
import { FREE_PLAN_LIMITS, SELFHOSTED_PLAN_LIMITS, TEAM_PLAN_LIMITS } from './constants'; import { FREE_PLAN_LIMITS, SELFHOSTED_PLAN_LIMITS, TEAM_PLAN_LIMITS } from './constants';
import { ERROR_CODES } from './errors'; import { ERROR_CODES } from './errors';
import { ZLimitsSchema } from './schema'; import { ZLimitsSchema } from './schema';
@@ -16,7 +15,7 @@ export type GetServerLimitsOptions = {
}; };
export const getServerLimits = async ({ email, teamId }: GetServerLimitsOptions) => { export const getServerLimits = async ({ email, teamId }: GetServerLimitsOptions) => {
if (!IS_BILLING_ENABLED) { if (!IS_BILLING_ENABLED()) {
return { return {
quota: SELFHOSTED_PLAN_LIMITS, quota: SELFHOSTED_PLAN_LIMITS,
remaining: SELFHOSTED_PLAN_LIMITS, remaining: SELFHOSTED_PLAN_LIMITS,
@@ -56,10 +55,11 @@ const handleUserLimits = async ({ email }: HandleUserLimitsOptions) => {
); );
if (activeSubscriptions.length > 0) { if (activeSubscriptions.length > 0) {
const communityPlanPrices = await getPricesByPlan(STRIPE_PLAN_TYPE.COMMUNITY); const documentPlanPrices = await getDocumentRelatedPrices();
for (const subscription of activeSubscriptions) { for (const subscription of activeSubscriptions) {
const price = communityPlanPrices.find((price) => price.id === subscription.priceId); const price = documentPlanPrices.find((price) => price.id === subscription.priceId);
if (!price || typeof price.product === 'string' || price.product.deleted) { if (!price || typeof price.product === 'string' || price.product.deleted) {
continue; continue;
} }

View File

@@ -0,0 +1,10 @@
import { STRIPE_PLAN_TYPE } from '@documenso/lib/constants/billing';
import { getPricesByPlan } from './get-prices-by-plan';
/**
* Returns the Stripe prices of items that affect the amount of documents a user can create.
*/
export const getDocumentRelatedPrices = async () => {
return await getPricesByPlan([STRIPE_PLAN_TYPE.COMMUNITY, STRIPE_PLAN_TYPE.ENTERPRISE]);
};

View File

@@ -0,0 +1,13 @@
import { STRIPE_PLAN_TYPE } from '@documenso/lib/constants/billing';
import { getPricesByPlan } from './get-prices-by-plan';
export const getEnterprisePlanPrices = async () => {
return await getPricesByPlan(STRIPE_PLAN_TYPE.ENTERPRISE);
};
export const getEnterprisePlanPriceIds = async () => {
const prices = await getEnterprisePlanPrices();
return prices.map((price) => price.id);
};

View File

@@ -1,14 +1,18 @@
import type { STRIPE_PLAN_TYPE } from '@documenso/lib/constants/billing'; import type { STRIPE_PLAN_TYPE } from '@documenso/lib/constants/billing';
import { stripe } from '@documenso/lib/server-only/stripe'; import { stripe } from '@documenso/lib/server-only/stripe';
export const getPricesByPlan = async ( type PlanType = (typeof STRIPE_PLAN_TYPE)[keyof typeof STRIPE_PLAN_TYPE];
plan: (typeof STRIPE_PLAN_TYPE)[keyof typeof STRIPE_PLAN_TYPE],
) => { export const getPricesByPlan = async (plan: PlanType | PlanType[]) => {
const planTypes = typeof plan === 'string' ? [plan] : plan;
const query = planTypes.map((planType) => `metadata['plan']:'${planType}'`).join(' OR ');
const { data: prices } = await stripe.prices.search({ const { data: prices } = await stripe.prices.search({
query: `metadata['plan']:'${plan}' type:'recurring'`, query,
expand: ['data.product'], expand: ['data.product'],
limit: 100, limit: 100,
}); });
return prices; return prices.filter((price) => price.type === 'recurring');
}; };

View File

@@ -0,0 +1,10 @@
import { STRIPE_PLAN_TYPE } from '@documenso/lib/constants/billing';
import { getPricesByPlan } from './get-prices-by-plan';
/**
* Returns the prices of items that count as the account's primary plan.
*/
export const getPrimaryAccountPlanPrices = async () => {
return await getPricesByPlan([STRIPE_PLAN_TYPE.COMMUNITY, STRIPE_PLAN_TYPE.ENTERPRISE]);
};

View File

@@ -0,0 +1,17 @@
import { STRIPE_PLAN_TYPE } from '@documenso/lib/constants/billing';
import { getPricesByPlan } from './get-prices-by-plan';
/**
* Returns the Stripe prices of items that affect the amount of teams a user can create.
*/
export const getTeamRelatedPrices = async () => {
return await getPricesByPlan([STRIPE_PLAN_TYPE.COMMUNITY, STRIPE_PLAN_TYPE.ENTERPRISE]);
};
/**
* Returns the Stripe price IDs of items that affect the amount of teams a user can create.
*/
export const getTeamRelatedPriceIds = async () => {
return await getTeamRelatedPrices().then((prices) => prices.map((price) => price.id));
};

View File

@@ -2,13 +2,13 @@ import type Stripe from 'stripe';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { stripe } from '@documenso/lib/server-only/stripe'; import { stripe } from '@documenso/lib/server-only/stripe';
import { subscriptionsContainsActiveCommunityPlan } from '@documenso/lib/utils/billing'; import { subscriptionsContainsActivePlan } from '@documenso/lib/utils/billing';
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
import { type Subscription, type Team, type User } from '@documenso/prisma/client'; import { type Subscription, type Team, type User } from '@documenso/prisma/client';
import { deleteCustomerPaymentMethods } from './delete-customer-payment-methods'; import { deleteCustomerPaymentMethods } from './delete-customer-payment-methods';
import { getCommunityPlanPriceIds } from './get-community-plan-prices';
import { getTeamPrices } from './get-team-prices'; import { getTeamPrices } from './get-team-prices';
import { getTeamRelatedPriceIds } from './get-team-related-prices';
type TransferStripeSubscriptionOptions = { type TransferStripeSubscriptionOptions = {
/** /**
@@ -46,14 +46,14 @@ export const transferTeamSubscription = async ({
throw new AppError(AppErrorCode.NOT_FOUND, 'Missing customer ID.'); throw new AppError(AppErrorCode.NOT_FOUND, 'Missing customer ID.');
} }
const [communityPlanIds, teamSeatPrices] = await Promise.all([ const [teamRelatedPlanPriceIds, teamSeatPrices] = await Promise.all([
getCommunityPlanPriceIds(), getTeamRelatedPriceIds(),
getTeamPrices(), getTeamPrices(),
]); ]);
const teamSubscriptionRequired = !subscriptionsContainsActiveCommunityPlan( const teamSubscriptionRequired = !subscriptionsContainsActivePlan(
user.Subscription, user.Subscription,
communityPlanIds, teamRelatedPlanPriceIds,
); );
let teamSubscription: Stripe.Subscription | null = null; let teamSubscription: Stripe.Subscription | null = null;

View File

@@ -1,3 +1,5 @@
import { env } from 'next-runtime-env';
import { Button, Column, Img, Link, Section, Text } from '../components'; import { Button, Column, Img, Link, Section, Text } from '../components';
import { TemplateDocumentImage } from './template-document-image'; import { TemplateDocumentImage } from './template-document-image';
@@ -10,7 +12,9 @@ export const TemplateDocumentSelfSigned = ({
documentName, documentName,
assetBaseUrl, assetBaseUrl,
}: TemplateDocumentSelfSignedProps) => { }: TemplateDocumentSelfSignedProps) => {
const signUpUrl = `${process.env.NEXT_PUBLIC_WEBAPP_URL ?? 'http://localhost:3000'}/signup`; const NEXT_PUBLIC_WEBAPP_URL = env('NEXT_PUBLIC_WEBAPP_URL');
const signUpUrl = `${NEXT_PUBLIC_WEBAPP_URL ?? 'http://localhost:3000'}/signup`;
const getAssetUrl = (path: string) => { const getAssetUrl = (path: string) => {
return new URL(path, assetBaseUrl).toString(); return new URL(path, assetBaseUrl).toString();

View File

@@ -1,3 +1,5 @@
import { env } from 'next-runtime-env';
import { Button, Section, Text } from '../components'; import { Button, Section, Text } from '../components';
import { TemplateDocumentImage } from './template-document-image'; import { TemplateDocumentImage } from './template-document-image';
@@ -8,6 +10,8 @@ export interface TemplateResetPasswordProps {
} }
export const TemplateResetPassword = ({ assetBaseUrl }: TemplateResetPasswordProps) => { export const TemplateResetPassword = ({ assetBaseUrl }: TemplateResetPasswordProps) => {
const NEXT_PUBLIC_WEBAPP_URL = env('NEXT_PUBLIC_WEBAPP_URL');
return ( return (
<> <>
<TemplateDocumentImage className="mt-6" assetBaseUrl={assetBaseUrl} /> <TemplateDocumentImage className="mt-6" assetBaseUrl={assetBaseUrl} />
@@ -24,7 +28,7 @@ export const TemplateResetPassword = ({ assetBaseUrl }: TemplateResetPasswordPro
<Section className="mb-6 mt-8 text-center"> <Section className="mb-6 mt-8 text-center">
<Button <Button
className="bg-documenso-500 inline-flex items-center justify-center rounded-lg px-6 py-3 text-center text-sm font-medium text-black no-underline" className="bg-documenso-500 inline-flex items-center justify-center rounded-lg px-6 py-3 text-center text-sm font-medium text-black no-underline"
href={`${process.env.NEXT_PUBLIC_WEBAPP_URL ?? 'http://localhost:3000'}/signin`} href={`${NEXT_PUBLIC_WEBAPP_URL ?? 'http://localhost:3000'}/signin`}
> >
Sign In Sign In
</Button> </Button>

View File

@@ -0,0 +1,13 @@
import type { EffectCallback } from 'react';
import { useEffect } from 'react';
/**
* Dangerously runs an effect "once" by ignoring the depedencies of a given effect.
*
* DANGER: The effect will run twice in concurrent react and development environments.
*/
export const unsafe_useEffectOnce = (callback: EffectCallback) => {
// Intentionally avoiding exhaustive deps and rule of hooks here
// eslint-disable-next-line react-hooks/exhaustive-deps, react-hooks/rules-of-hooks
return useEffect(callback, []);
};

View File

@@ -1,16 +1,19 @@
export const IS_APP_MARKETING = process.env.NEXT_PUBLIC_PROJECT === 'marketing'; import { env } from 'next-runtime-env';
export const IS_APP_WEB = process.env.NEXT_PUBLIC_PROJECT === 'web';
export const IS_BILLING_ENABLED = process.env.NEXT_PUBLIC_FEATURE_BILLING_ENABLED === 'true';
export const APP_DOCUMENT_UPLOAD_SIZE_LIMIT = export const APP_DOCUMENT_UPLOAD_SIZE_LIMIT =
Number(process.env.NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT) || 50; Number(process.env.NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT) || 50;
export const APP_FOLDER = IS_APP_MARKETING ? 'marketing' : 'web'; export const NEXT_PUBLIC_WEBAPP_URL = () => env('NEXT_PUBLIC_WEBAPP_URL');
export const NEXT_PUBLIC_MARKETING_URL = () => env('NEXT_PUBLIC_MARKETING_URL');
export const APP_BASE_URL = IS_APP_WEB export const IS_APP_MARKETING = process.env.NEXT_PUBLIC_PROJECT === 'marketing';
? process.env.NEXT_PUBLIC_WEBAPP_URL export const IS_APP_WEB = process.env.NEXT_PUBLIC_PROJECT === 'web';
: process.env.NEXT_PUBLIC_MARKETING_URL; export const IS_BILLING_ENABLED = () => env('NEXT_PUBLIC_FEATURE_BILLING_ENABLED') === 'true';
export const WEBAPP_BASE_URL = process.env.NEXT_PUBLIC_WEBAPP_URL ?? 'http://localhost:3000'; export const APP_FOLDER = () => (IS_APP_MARKETING ? 'marketing' : 'web');
export const MARKETING_BASE_URL = process.env.NEXT_PUBLIC_MARKETING_URL ?? 'http://localhost:3001'; export const APP_BASE_URL = () =>
IS_APP_WEB ? NEXT_PUBLIC_WEBAPP_URL() : NEXT_PUBLIC_MARKETING_URL();
export const WEBAPP_BASE_URL = NEXT_PUBLIC_WEBAPP_URL() ?? 'http://localhost:3000';
export const MARKETING_BASE_URL = NEXT_PUBLIC_MARKETING_URL() ?? 'http://localhost:3001';

View File

@@ -6,6 +6,5 @@ export enum STRIPE_CUSTOMER_TYPE {
export enum STRIPE_PLAN_TYPE { export enum STRIPE_PLAN_TYPE {
TEAM = 'team', TEAM = 'team',
COMMUNITY = 'community', COMMUNITY = 'community',
ENTERPRISE = 'enterprise',
} }
export const TEAM_BILLING_DOMAIN = 'billing.team.documenso.com';

View File

@@ -1,5 +1,10 @@
import { env } from 'next-runtime-env';
import { APP_BASE_URL } from './app'; import { APP_BASE_URL } from './app';
const NEXT_PUBLIC_FEATURE_BILLING_ENABLED = () => env('NEXT_PUBLIC_FEATURE_BILLING_ENABLED');
const NEXT_PUBLIC_POSTHOG_KEY = () => env('NEXT_PUBLIC_POSTHOG_KEY');
/** /**
* The flag name for global session recording feature flag. * The flag name for global session recording feature flag.
*/ */
@@ -16,8 +21,7 @@ export const FEATURE_FLAG_POLL_INTERVAL = 30000;
* Does not take any person or group properties into account. * Does not take any person or group properties into account.
*/ */
export const LOCAL_FEATURE_FLAGS: Record<string, boolean> = { export const LOCAL_FEATURE_FLAGS: Record<string, boolean> = {
app_billing: process.env.NEXT_PUBLIC_FEATURE_BILLING_ENABLED === 'true', app_billing: NEXT_PUBLIC_FEATURE_BILLING_ENABLED() === 'true',
app_teams: true,
marketing_header_single_player_mode: false, marketing_header_single_player_mode: false,
} as const; } as const;
@@ -25,8 +29,8 @@ export const LOCAL_FEATURE_FLAGS: Record<string, boolean> = {
* Extract the PostHog configuration from the environment. * Extract the PostHog configuration from the environment.
*/ */
export function extractPostHogConfig(): { key: string; host: string } | null { export function extractPostHogConfig(): { key: string; host: string } | null {
const postHogKey = process.env.NEXT_PUBLIC_POSTHOG_KEY; const postHogKey = NEXT_PUBLIC_POSTHOG_KEY();
const postHogHost = `${APP_BASE_URL}/ingest`; const postHogHost = `${APP_BASE_URL()}/ingest`;
if (!postHogKey || !postHogHost) { if (!postHogKey || !postHogHost) {
return null; return null;

View File

@@ -6,4 +6,4 @@ export const DEFAULT_HANDWRITING_FONT_SIZE = 50;
export const MIN_STANDARD_FONT_SIZE = 8; export const MIN_STANDARD_FONT_SIZE = 8;
export const MIN_HANDWRITING_FONT_SIZE = 20; export const MIN_HANDWRITING_FONT_SIZE = 20;
export const CAVEAT_FONT_PATH = `${APP_BASE_URL}/fonts/caveat.ttf`; export const CAVEAT_FONT_PATH = () => `${APP_BASE_URL()}/fonts/caveat.ttf`;

View File

@@ -7,6 +7,7 @@ import type { JWT } from 'next-auth/jwt';
import CredentialsProvider from 'next-auth/providers/credentials'; import CredentialsProvider from 'next-auth/providers/credentials';
import type { GoogleProfile } from 'next-auth/providers/google'; import type { GoogleProfile } from 'next-auth/providers/google';
import GoogleProvider from 'next-auth/providers/google'; import GoogleProvider from 'next-auth/providers/google';
import { env } from 'next-runtime-env';
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
import { IdentityProvider, UserSecurityAuditLogType } from '@documenso/prisma/client'; import { IdentityProvider, UserSecurityAuditLogType } from '@documenso/prisma/client';
@@ -221,7 +222,7 @@ export const NEXT_AUTH_OPTIONS: AuthOptions = {
async signIn({ user }) { async signIn({ user }) {
// We do this to stop OAuth providers from creating an account // We do this to stop OAuth providers from creating an account
// when signups are disabled // when signups are disabled
if (process.env.NEXT_PUBLIC_DISABLE_SIGNUP === 'true') { if (env('NEXT_PUBLIC_DISABLE_SIGNUP') === 'true') {
const userData = await getUserByEmail({ email: user.email! }); const userData = await getUserByEmail({ email: user.email! });
return !!userData; return !!userData;

View File

@@ -43,7 +43,7 @@ export const setupTwoFactorAuthentication = async ({
const secret = crypto.randomBytes(10); const secret = crypto.randomBytes(10);
const backupCodes = new Array(10) const backupCodes = Array.from({ length: 10 })
.fill(null) .fill(null)
.map(() => crypto.randomBytes(5).toString('hex')) .map(() => crypto.randomBytes(5).toString('hex'))
.map((code) => `${code.slice(0, 5)}-${code.slice(5)}`.toUpperCase()); .map((code) => `${code.slice(0, 5)}-${code.slice(5)}`.toUpperCase());

View File

@@ -5,11 +5,16 @@ import { render } from '@documenso/email/render';
import { ConfirmEmailTemplate } from '@documenso/email/templates/confirm-email'; import { ConfirmEmailTemplate } from '@documenso/email/templates/confirm-email';
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
export interface SendConfirmationEmailProps { export interface SendConfirmationEmailProps {
userId: number; userId: number;
} }
export const sendConfirmationEmail = async ({ userId }: SendConfirmationEmailProps) => { export const sendConfirmationEmail = async ({ userId }: SendConfirmationEmailProps) => {
const NEXT_PRIVATE_SMTP_FROM_NAME = process.env.NEXT_PRIVATE_SMTP_FROM_NAME;
const NEXT_PRIVATE_SMTP_FROM_ADDRESS = process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS;
const user = await prisma.user.findFirstOrThrow({ const user = await prisma.user.findFirstOrThrow({
where: { where: {
id: userId, id: userId,
@@ -30,10 +35,10 @@ export const sendConfirmationEmail = async ({ userId }: SendConfirmationEmailPro
throw new Error('Verification token not found for the user'); throw new Error('Verification token not found for the user');
} }
const assetBaseUrl = process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000'; const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
const confirmationLink = `${assetBaseUrl}/verify-email/${verificationToken.token}`; const confirmationLink = `${assetBaseUrl}/verify-email/${verificationToken.token}`;
const senderName = process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso'; const senderName = NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso';
const senderAdress = process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com'; const senderAdress = NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com';
const confirmationTemplate = createElement(ConfirmEmailTemplate, { const confirmationTemplate = createElement(ConfirmEmailTemplate, {
assetBaseUrl, assetBaseUrl,

View File

@@ -5,6 +5,8 @@ import { render } from '@documenso/email/render';
import { ForgotPasswordTemplate } from '@documenso/email/templates/forgot-password'; import { ForgotPasswordTemplate } from '@documenso/email/templates/forgot-password';
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
export interface SendForgotPasswordOptions { export interface SendForgotPasswordOptions {
userId: number; userId: number;
} }
@@ -29,8 +31,8 @@ export const sendForgotPassword = async ({ userId }: SendForgotPasswordOptions)
} }
const token = user.PasswordResetToken[0].token; const token = user.PasswordResetToken[0].token;
const assetBaseUrl = process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000'; const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
const resetPasswordLink = `${process.env.NEXT_PUBLIC_WEBAPP_URL}/reset-password/${token}`; const resetPasswordLink = `${NEXT_PUBLIC_WEBAPP_URL()}/reset-password/${token}`;
const template = createElement(ForgotPasswordTemplate, { const template = createElement(ForgotPasswordTemplate, {
assetBaseUrl, assetBaseUrl,

View File

@@ -5,6 +5,8 @@ import { render } from '@documenso/email/render';
import { ResetPasswordTemplate } from '@documenso/email/templates/reset-password'; import { ResetPasswordTemplate } from '@documenso/email/templates/reset-password';
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
export interface SendResetPasswordOptions { export interface SendResetPasswordOptions {
userId: number; userId: number;
} }
@@ -16,7 +18,7 @@ export const sendResetPassword = async ({ userId }: SendResetPasswordOptions) =>
}, },
}); });
const assetBaseUrl = process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000'; const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
const template = createElement(ResetPasswordTemplate, { const template = createElement(ResetPasswordTemplate, {
assetBaseUrl, assetBaseUrl,

View File

@@ -8,6 +8,7 @@ import DocumentCancelTemplate from '@documenso/email/templates/document-cancel';
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
import { DocumentStatus } from '@documenso/prisma/client'; import { DocumentStatus } from '@documenso/prisma/client';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { FROM_ADDRESS, FROM_NAME } from '../../constants/email'; import { FROM_ADDRESS, FROM_NAME } from '../../constants/email';
export type DeleteDocumentOptions = { export type DeleteDocumentOptions = {
@@ -49,7 +50,7 @@ export const deleteDocument = async ({ id, userId, status }: DeleteDocumentOptio
if (document.Recipient.length > 0) { if (document.Recipient.length > 0) {
await Promise.all( await Promise.all(
document.Recipient.map(async (recipient) => { document.Recipient.map(async (recipient) => {
const assetBaseUrl = process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000'; const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
const template = createElement(DocumentCancelTemplate, { const template = createElement(DocumentCancelTemplate, {
documentName: document.title, documentName: document.title,

View File

@@ -18,6 +18,8 @@ import type { Prisma } from '@documenso/prisma/client';
import { getDocumentWhereInput } from './get-document-by-id'; import { getDocumentWhereInput } from './get-document-by-id';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
export type ResendDocumentOptions = { export type ResendDocumentOptions = {
documentId: number; documentId: number;
userId: number; userId: number;
@@ -94,8 +96,8 @@ export const resendDocument = async ({
'document.name': document.title, 'document.name': document.title,
}; };
const assetBaseUrl = process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000'; const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
const signDocumentLink = `${process.env.NEXT_PUBLIC_WEBAPP_URL}/sign/${recipient.token}`; const signDocumentLink = `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`;
const template = createElement(DocumentInviteEmailTemplate, { const template = createElement(DocumentInviteEmailTemplate, {
documentName: document.title, documentName: document.title,

View File

@@ -5,6 +5,7 @@ import { render } from '@documenso/email/render';
import { DocumentCompletedEmailTemplate } from '@documenso/email/templates/document-completed'; import { DocumentCompletedEmailTemplate } from '@documenso/email/templates/document-completed';
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs'; import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
import type { RequestMetadata } from '../../universal/extract-request-metadata'; import type { RequestMetadata } from '../../universal/extract-request-metadata';
import { getFile } from '../../universal/upload/get-file'; import { getFile } from '../../universal/upload/get-file';
@@ -40,12 +41,12 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
document.Recipient.map(async (recipient) => { document.Recipient.map(async (recipient) => {
const { email, name, token } = recipient; const { email, name, token } = recipient;
const assetBaseUrl = process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000'; const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
const template = createElement(DocumentCompletedEmailTemplate, { const template = createElement(DocumentCompletedEmailTemplate, {
documentName: document.title, documentName: document.title,
assetBaseUrl, assetBaseUrl,
downloadLink: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/sign/${token}/complete`, downloadLink: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${token}/complete`,
}); });
await prisma.$transaction(async (tx) => { await prisma.$transaction(async (tx) => {

View File

@@ -4,10 +4,6 @@ import { mailer } from '@documenso/email/mailer';
import { render } from '@documenso/email/render'; import { render } from '@documenso/email/render';
import { DocumentInviteEmailTemplate } from '@documenso/email/templates/document-invite'; import { DocumentInviteEmailTemplate } from '@documenso/email/templates/document-invite';
import { FROM_ADDRESS, FROM_NAME } from '@documenso/lib/constants/email'; import { FROM_ADDRESS, FROM_NAME } from '@documenso/lib/constants/email';
import {
RECIPIENT_ROLES_DESCRIPTION,
RECIPIENT_ROLE_TO_EMAIL_TYPE,
} from '@documenso/lib/constants/recipient-roles';
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs'; import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata'; import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs'; import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
@@ -15,6 +11,12 @@ import { renderCustomEmailTemplate } from '@documenso/lib/utils/render-custom-em
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
import { DocumentStatus, RecipientRole, SendStatus } from '@documenso/prisma/client'; import { DocumentStatus, RecipientRole, SendStatus } from '@documenso/prisma/client';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import {
RECIPIENT_ROLES_DESCRIPTION,
RECIPIENT_ROLE_TO_EMAIL_TYPE,
} from '../../constants/recipient-roles';
export type SendDocumentOptions = { export type SendDocumentOptions = {
documentId: number; documentId: number;
userId: number; userId: number;
@@ -91,8 +93,8 @@ export const sendDocument = async ({
'document.name': document.title, 'document.name': document.title,
}; };
const assetBaseUrl = process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000'; const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
const signDocumentLink = `${process.env.NEXT_PUBLIC_WEBAPP_URL}/sign/${recipient.token}`; const signDocumentLink = `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`;
const template = createElement(DocumentInviteEmailTemplate, { const template = createElement(DocumentInviteEmailTemplate, {
documentName: document.title, documentName: document.title,

View File

@@ -5,6 +5,8 @@ import { render } from '@documenso/email/render';
import { DocumentPendingEmailTemplate } from '@documenso/email/templates/document-pending'; import { DocumentPendingEmailTemplate } from '@documenso/email/templates/document-pending';
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
export interface SendPendingEmailOptions { export interface SendPendingEmailOptions {
documentId: number; documentId: number;
recipientId: number; recipientId: number;
@@ -41,7 +43,7 @@ export const sendPendingEmail = async ({ documentId, recipientId }: SendPendingE
const { email, name } = recipient; const { email, name } = recipient;
const assetBaseUrl = process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000'; const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
const template = createElement(DocumentPendingEmailTemplate, { const template = createElement(DocumentPendingEmailTemplate, {
documentName: document.title, documentName: document.title,

View File

@@ -5,6 +5,7 @@ import { getToken } from 'next-auth/jwt';
import { LOCAL_FEATURE_FLAGS } from '@documenso/lib/constants/feature-flags'; import { LOCAL_FEATURE_FLAGS } from '@documenso/lib/constants/feature-flags';
import PostHogServerClient from '@documenso/lib/server-only/feature-flags/get-post-hog-server-client'; import PostHogServerClient from '@documenso/lib/server-only/feature-flags/get-post-hog-server-client';
import { NEXT_PUBLIC_MARKETING_URL, NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { extractDistinctUserId, mapJwtToFlagProperties } from './get'; import { extractDistinctUserId, mapJwtToFlagProperties } from './get';
/** /**
@@ -38,11 +39,11 @@ export default async function handlerFeatureFlagAll(req: Request) {
const origin = req.headers.get('origin'); const origin = req.headers.get('origin');
if (origin) { if (origin) {
if (origin.startsWith(process.env.NEXT_PUBLIC_WEBAPP_URL ?? 'http://localhost:3000')) { if (origin.startsWith(NEXT_PUBLIC_WEBAPP_URL() ?? 'http://localhost:3000')) {
res.headers.set('Access-Control-Allow-Origin', origin); res.headers.set('Access-Control-Allow-Origin', origin);
} }
if (origin.startsWith(process.env.NEXT_PUBLIC_MARKETING_URL ?? 'http://localhost:3001')) { if (origin.startsWith(NEXT_PUBLIC_MARKETING_URL() ?? 'http://localhost:3001')) {
res.headers.set('Access-Control-Allow-Origin', origin); res.headers.set('Access-Control-Allow-Origin', origin);
} }
} }

View File

@@ -1,11 +1,14 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import { JWT, getToken } from 'next-auth/jwt'; import type { JWT } from 'next-auth/jwt';
import { getToken } from 'next-auth/jwt';
import { LOCAL_FEATURE_FLAGS, extractPostHogConfig } from '@documenso/lib/constants/feature-flags'; import { LOCAL_FEATURE_FLAGS, extractPostHogConfig } from '@documenso/lib/constants/feature-flags';
import PostHogServerClient from '@documenso/lib/server-only/feature-flags/get-post-hog-server-client'; import PostHogServerClient from '@documenso/lib/server-only/feature-flags/get-post-hog-server-client';
import { NEXT_PUBLIC_MARKETING_URL, NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
/** /**
* Evaluate a single feature flag based on the current user if possible. * Evaluate a single feature flag based on the current user if possible.
* *
@@ -57,11 +60,11 @@ export default async function handleFeatureFlagGet(req: Request) {
const origin = req.headers.get('Origin'); const origin = req.headers.get('Origin');
if (origin) { if (origin) {
if (origin.startsWith(process.env.NEXT_PUBLIC_WEBAPP_URL ?? 'http://localhost:3000')) { if (origin.startsWith(NEXT_PUBLIC_WEBAPP_URL() ?? 'http://localhost:3000')) {
res.headers.set('Access-Control-Allow-Origin', origin); res.headers.set('Access-Control-Allow-Origin', origin);
} }
if (origin.startsWith(process.env.NEXT_PUBLIC_MARKETING_URL ?? 'http://localhost:3001')) { if (origin.startsWith(NEXT_PUBLIC_MARKETING_URL() ?? 'http://localhost:3001')) {
res.headers.set('Access-Control-Allow-Origin', origin); res.headers.set('Access-Control-Allow-Origin', origin);
} }
} }

View File

@@ -20,6 +20,7 @@ export interface SetFieldsForDocumentOptions {
pageY: number; pageY: number;
pageWidth: number; pageWidth: number;
pageHeight: number; pageHeight: number;
label: string;
}[]; }[];
requestMetadata?: RequestMetadata; requestMetadata?: RequestMetadata;
} }

View File

@@ -12,7 +12,7 @@ export async function insertTextInPDF(
useHandwritingFont = true, useHandwritingFont = true,
): Promise<string> { ): Promise<string> {
// Fetch the font file from the public URL. // Fetch the font file from the public URL.
const fontResponse = await fetch(CAVEAT_FONT_PATH); const fontResponse = await fetch(CAVEAT_FONT_PATH());
const fontCaveat = await fontResponse.arrayBuffer(); const fontCaveat = await fontResponse.arrayBuffer();
const pdfDoc = await PDFDocument.load(pdfAsBase64); const pdfDoc = await PDFDocument.load(pdfAsBase64);

View File

@@ -46,7 +46,7 @@ export const acceptTeamInvitation = async ({ userId, teamId }: AcceptTeamInvitat
}, },
}); });
if (IS_BILLING_ENABLED && team.subscription) { if (IS_BILLING_ENABLED() && team.subscription) {
const numberOfSeats = await tx.teamMember.count({ const numberOfSeats = await tx.teamMember.count({
where: { where: {
teamId: teamMemberInvite.teamId, teamId: teamMemberInvite.teamId,

View File

@@ -12,7 +12,7 @@ export const createTeamBillingPortal = async ({
userId, userId,
teamId, teamId,
}: CreateTeamBillingPortalOptions) => { }: CreateTeamBillingPortalOptions) => {
if (!IS_BILLING_ENABLED) { if (!IS_BILLING_ENABLED()) {
throw new Error('Billing is not enabled'); throw new Error('Billing is not enabled');
} }

View File

@@ -2,11 +2,11 @@ import type Stripe from 'stripe';
import { z } from 'zod'; import { z } from 'zod';
import { createTeamCustomer } from '@documenso/ee/server-only/stripe/create-team-customer'; import { createTeamCustomer } from '@documenso/ee/server-only/stripe/create-team-customer';
import { getCommunityPlanPriceIds } from '@documenso/ee/server-only/stripe/get-community-plan-prices'; import { getTeamRelatedPrices } from '@documenso/ee/server-only/stripe/get-team-related-prices';
import { mapStripeSubscriptionToPrismaUpsertAction } from '@documenso/ee/server-only/stripe/webhook/on-subscription-updated'; import { mapStripeSubscriptionToPrismaUpsertAction } from '@documenso/ee/server-only/stripe/webhook/on-subscription-updated';
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app'; import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { subscriptionsContainsActiveCommunityPlan } from '@documenso/lib/utils/billing'; import { subscriptionsContainsActivePlan } from '@documenso/lib/utils/billing';
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
import { Prisma, TeamMemberRole } from '@documenso/prisma/client'; import { Prisma, TeamMemberRole } from '@documenso/prisma/client';
@@ -57,17 +57,16 @@ export const createTeam = async ({
}, },
}); });
let isPaymentRequired = IS_BILLING_ENABLED; let isPaymentRequired = IS_BILLING_ENABLED();
let customerId: string | null = null; let customerId: string | null = null;
if (IS_BILLING_ENABLED) { if (IS_BILLING_ENABLED()) {
const communityPlanPriceIds = await getCommunityPlanPriceIds(); const teamRelatedPriceIds = await getTeamRelatedPrices().then((prices) =>
prices.map((price) => price.id),
isPaymentRequired = !subscriptionsContainsActiveCommunityPlan(
user.Subscription,
communityPlanPriceIds,
); );
isPaymentRequired = !subscriptionsContainsActivePlan(user.Subscription, teamRelatedPriceIds);
customerId = await createTeamCustomer({ customerId = await createTeamCustomer({
name: user.name ?? teamName, name: user.name ?? teamName,
email: user.email, email: user.email,

View File

@@ -85,7 +85,7 @@ export const deleteTeamMembers = async ({
}, },
}); });
if (IS_BILLING_ENABLED && team.subscription) { if (IS_BILLING_ENABLED() && team.subscription) {
const numberOfSeats = await tx.teamMember.count({ const numberOfSeats = await tx.teamMember.count({
where: { where: {
teamId, teamId,

View File

@@ -42,7 +42,7 @@ export const leaveTeam = async ({ userId, teamId }: LeaveTeamOptions) => {
}, },
}); });
if (IS_BILLING_ENABLED && team.subscription) { if (IS_BILLING_ENABLED() && team.subscription) {
const numberOfSeats = await tx.teamMember.count({ const numberOfSeats = await tx.teamMember.count({
where: { where: {
teamId, teamId,

View File

@@ -49,7 +49,7 @@ export const transferTeamOwnership = async ({ token }: TransferTeamOwnershipOpti
let teamSubscription: Stripe.Subscription | null = null; let teamSubscription: Stripe.Subscription | null = null;
if (IS_BILLING_ENABLED) { if (IS_BILLING_ENABLED()) {
teamSubscription = await transferTeamSubscription({ teamSubscription = await transferTeamSubscription({
user: newOwnerUser, user: newOwnerUser,
team, team,

View File

@@ -68,7 +68,7 @@ export const createUser = async ({ name, email, password, signature }: CreateUse
}, },
}); });
if (!IS_BILLING_ENABLED) { if (!IS_BILLING_ENABLED()) {
return; return;
} }
@@ -108,7 +108,7 @@ export const createUser = async ({ name, email, password, signature }: CreateUse
); );
// Update the user record with a new or existing Stripe customer record. // Update the user record with a new or existing Stripe customer record.
if (IS_BILLING_ENABLED) { if (IS_BILLING_ENABLED()) {
try { try {
return await getStripeCustomerByUser(user).then((session) => session.user); return await getStripeCustomerByUser(user).then((session) => session.user);
} catch (err) { } catch (err) {

View File

@@ -1,4 +1,6 @@
/* eslint-disable turbo/no-undeclared-env-vars */ /* eslint-disable turbo/no-undeclared-env-vars */
import { NEXT_PUBLIC_WEBAPP_URL } from '../constants/app';
export const getBaseUrl = () => { export const getBaseUrl = () => {
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
return ''; return '';
@@ -8,8 +10,10 @@ export const getBaseUrl = () => {
return `https://${process.env.VERCEL_URL}`; return `https://${process.env.VERCEL_URL}`;
} }
if (process.env.NEXT_PUBLIC_WEBAPP_URL) { const webAppUrl = NEXT_PUBLIC_WEBAPP_URL();
return process.env.NEXT_PUBLIC_WEBAPP_URL;
if (webAppUrl) {
return webAppUrl;
} }
return `http://localhost:${process.env.PORT ?? 3000}`; return `http://localhost:${process.env.PORT ?? 3000}`;

View File

@@ -22,7 +22,7 @@ export const getFlag = async (
return LOCAL_FEATURE_FLAGS[flag] ?? true; return LOCAL_FEATURE_FLAGS[flag] ?? true;
} }
const url = new URL(`${APP_BASE_URL}/api/feature-flag/get`); const url = new URL(`${APP_BASE_URL()}/api/feature-flag/get`);
url.searchParams.set('flag', flag); url.searchParams.set('flag', flag);
const response = await fetch(url, { const response = await fetch(url, {
@@ -55,7 +55,7 @@ export const getAllFlags = async (
return LOCAL_FEATURE_FLAGS; return LOCAL_FEATURE_FLAGS;
} }
const url = new URL(`${APP_BASE_URL}/api/feature-flag/all`); const url = new URL(`${APP_BASE_URL()}/api/feature-flag/all`);
return fetch(url, { return fetch(url, {
headers: { headers: {
@@ -80,7 +80,7 @@ export const getAllAnonymousFlags = async (): Promise<Record<string, TFeatureFla
return LOCAL_FEATURE_FLAGS; return LOCAL_FEATURE_FLAGS;
} }
const url = new URL(`${APP_BASE_URL}/api/feature-flag/all`); const url = new URL(`${APP_BASE_URL()}/api/feature-flag/all`);
return fetch(url, { return fetch(url, {
next: { next: {

View File

@@ -1,4 +1,5 @@
import { base64 } from '@scure/base'; import { base64 } from '@scure/base';
import { env } from 'next-runtime-env';
import { match } from 'ts-pattern'; import { match } from 'ts-pattern';
import { DocumentDataType } from '@documenso/prisma/client'; import { DocumentDataType } from '@documenso/prisma/client';
@@ -12,7 +13,9 @@ type File = {
}; };
export const putFile = async (file: File) => { export const putFile = async (file: File) => {
const { type, data } = await match(process.env.NEXT_PUBLIC_UPLOAD_TRANSPORT) const NEXT_PUBLIC_UPLOAD_TRANSPORT = env('NEXT_PUBLIC_UPLOAD_TRANSPORT');
const { type, data } = await match(NEXT_PUBLIC_UPLOAD_TRANSPORT)
.with('s3', async () => putFileInS3(file)) .with('s3', async () => putFileInS3(file))
.otherwise(async () => putFileInDatabase(file)); .otherwise(async () => putFileInDatabase(file));

View File

@@ -11,6 +11,7 @@ import {
} from '@aws-sdk/client-s3'; } from '@aws-sdk/client-s3';
import slugify from '@sindresorhus/slugify'; import slugify from '@sindresorhus/slugify';
import { type JWT, getToken } from 'next-auth/jwt'; import { type JWT, getToken } from 'next-auth/jwt';
import { env } from 'next-runtime-env';
import path from 'node:path'; import path from 'node:path';
import { APP_BASE_URL } from '../../constants/app'; import { APP_BASE_URL } from '../../constants/app';
@@ -25,8 +26,10 @@ export const getPresignPostUrl = async (fileName: string, contentType: string) =
let token: JWT | null = null; let token: JWT | null = null;
try { try {
const baseUrl = APP_BASE_URL() ?? 'http://localhost:3000';
token = await getToken({ token = await getToken({
req: new NextRequest(APP_BASE_URL ?? 'http://localhost:3000', { req: new NextRequest(baseUrl, {
headers: headers(), headers: headers(),
}), }),
}); });
@@ -117,7 +120,9 @@ export const deleteS3File = async (key: string) => {
}; };
const getS3Client = () => { const getS3Client = () => {
if (process.env.NEXT_PUBLIC_UPLOAD_TRANSPORT !== 's3') { const NEXT_PUBLIC_UPLOAD_TRANSPORT = env('NEXT_PUBLIC_UPLOAD_TRANSPORT');
if (NEXT_PUBLIC_UPLOAD_TRANSPORT !== 's3') {
throw new Error('Invalid upload transport'); throw new Error('Invalid upload transport');
} }

View File

@@ -2,15 +2,14 @@ import type { Subscription } from '.prisma/client';
import { SubscriptionStatus } from '.prisma/client'; import { SubscriptionStatus } from '.prisma/client';
/** /**
* Returns true if there is a subscription that is active and is a community plan. * Returns true if there is a subscription that is active and is one of the provided price IDs.
*/ */
export const subscriptionsContainsActiveCommunityPlan = ( export const subscriptionsContainsActivePlan = (
subscriptions: Subscription[], subscriptions: Subscription[],
communityPlanPriceIds: string[], priceIds: string[],
) => { ) => {
return subscriptions.some( return subscriptions.some(
(subscription) => (subscription) =>
subscription.status === SubscriptionStatus.ACTIVE && subscription.status === SubscriptionStatus.ACTIVE && priceIds.includes(subscription.priceId),
communityPlanPriceIds.includes(subscription.priceId),
); );
}; };

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Field" ADD COLUMN "label" TEXT;

View File

@@ -210,15 +210,15 @@ model DocumentData {
} }
model DocumentMeta { model DocumentMeta {
id String @id @default(cuid()) id String @id @default(cuid())
subject String? subject String?
message String? message String?
timezone String? @default("Etc/UTC") @db.Text timezone String? @default("Etc/UTC") @db.Text
password String? password String?
dateFormat String? @default("yyyy-MM-dd hh:mm a") @db.Text dateFormat String? @default("yyyy-MM-dd hh:mm a") @db.Text
documentId Int @unique documentId Int @unique
document Document @relation(fields: [documentId], references: [id], onDelete: Cascade) document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
redirectUrl String? redirectUrl String?
} }
enum ReadStatus { enum ReadStatus {
@@ -283,6 +283,7 @@ model Field {
documentId Int? documentId Int?
templateId Int? templateId Int?
recipientId Int? recipientId Int?
label String?
type FieldType type FieldType
page Int page Int
positionX Decimal @default(0) positionX Decimal @default(0)

View File

@@ -1,4 +1,5 @@
import { TRPCError } from '@trpc/server'; import { TRPCError } from '@trpc/server';
import { env } from 'next-runtime-env';
import { ErrorCode } from '@documenso/lib/next-auth/error-codes'; import { ErrorCode } from '@documenso/lib/next-auth/error-codes';
import { compareSync } from '@documenso/lib/server-only/auth/hash'; import { compareSync } from '@documenso/lib/server-only/auth/hash';
@@ -8,10 +9,12 @@ import { sendConfirmationToken } from '@documenso/lib/server-only/user/send-conf
import { authenticatedProcedure, procedure, router } from '../trpc'; import { authenticatedProcedure, procedure, router } from '../trpc';
import { ZSignUpMutationSchema, ZVerifyPasswordMutationSchema } from './schema'; import { ZSignUpMutationSchema, ZVerifyPasswordMutationSchema } from './schema';
const NEXT_PUBLIC_DISABLE_SIGNUP = () => env('NEXT_PUBLIC_DISABLE_SIGNUP');
export const authRouter = router({ export const authRouter = router({
signup: procedure.input(ZSignUpMutationSchema).mutation(async ({ input }) => { signup: procedure.input(ZSignUpMutationSchema).mutation(async ({ input }) => {
try { try {
if (process.env.NEXT_PUBLIC_DISABLE_SIGNUP === 'true') { if (NEXT_PUBLIC_DISABLE_SIGNUP() === 'true') {
throw new TRPCError({ throw new TRPCError({
code: 'BAD_REQUEST', code: 'BAD_REQUEST',
message: 'Signups are disabled.', message: 'Signups are disabled.',

View File

@@ -33,6 +33,7 @@ export const fieldRouter = router({
pageY: field.pageY, pageY: field.pageY,
pageWidth: field.pageWidth, pageWidth: field.pageWidth,
pageHeight: field.pageHeight, pageHeight: field.pageHeight,
label: field.label,
})), })),
requestMetadata: extractNextApiRequestMetadata(ctx.req), requestMetadata: extractNextApiRequestMetadata(ctx.req),
}); });

View File

@@ -15,6 +15,7 @@ export const ZAddFieldsMutationSchema = z.object({
pageY: z.number().min(0), pageY: z.number().min(0),
pageWidth: z.number().min(0), pageWidth: z.number().min(0),
pageHeight: z.number().min(0), pageHeight: z.number().min(0),
label: z.string(),
}), }),
), ),
}); });

View File

@@ -5,6 +5,7 @@ import { PDFDocument } from 'pdf-lib';
import { mailer } from '@documenso/email/mailer'; import { mailer } from '@documenso/email/mailer';
import { renderAsync } from '@documenso/email/render'; import { renderAsync } from '@documenso/email/render';
import { DocumentSelfSignedEmailTemplate } from '@documenso/email/templates/document-self-signed'; import { DocumentSelfSignedEmailTemplate } from '@documenso/email/templates/document-self-signed';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { FROM_ADDRESS, FROM_NAME, SERVICE_USER_EMAIL } from '@documenso/lib/constants/email'; import { FROM_ADDRESS, FROM_NAME, SERVICE_USER_EMAIL } from '@documenso/lib/constants/email';
import { insertFieldInPDF } from '@documenso/lib/server-only/pdf/insert-field-in-pdf'; import { insertFieldInPDF } from '@documenso/lib/server-only/pdf/insert-field-in-pdf';
import { alphaid } from '@documenso/lib/universal/id'; import { alphaid } from '@documenso/lib/universal/id';
@@ -149,7 +150,7 @@ export const singleplayerRouter = router({
const template = createElement(DocumentSelfSignedEmailTemplate, { const template = createElement(DocumentSelfSignedEmailTemplate, {
documentName: documentName, documentName: documentName,
assetBaseUrl: process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000', assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000',
}); });
const [html, text] = await Promise.all([ const [html, text] = await Promise.all([

View File

@@ -7,6 +7,7 @@ import { Copy, Sparkles } from 'lucide-react';
import { FaXTwitter } from 'react-icons/fa6'; import { FaXTwitter } from 'react-icons/fa6';
import { useCopyShareLink } from '@documenso/lib/client-only/hooks/use-copy-share-link'; import { useCopyShareLink } from '@documenso/lib/client-only/hooks/use-copy-share-link';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { import {
TOAST_DOCUMENT_SHARE_ERROR, TOAST_DOCUMENT_SHARE_ERROR,
TOAST_DOCUMENT_SHARE_SUCCESS, TOAST_DOCUMENT_SHARE_SUCCESS,
@@ -68,7 +69,7 @@ export const DocumentShareButton = ({
const onCopyClick = async () => { const onCopyClick = async () => {
if (shareLink) { if (shareLink) {
await copyShareLink(`${process.env.NEXT_PUBLIC_WEBAPP_URL}/share/${shareLink.slug}`); await copyShareLink(`${NEXT_PUBLIC_WEBAPP_URL()}/share/${shareLink.slug}`);
} else { } else {
await createAndCopyShareLink({ await createAndCopyShareLink({
token, token,
@@ -92,7 +93,7 @@ export const DocumentShareButton = ({
} }
// Ensuring we've prewarmed the opengraph image for the Twitter // Ensuring we've prewarmed the opengraph image for the Twitter
await fetch(`${process.env.NEXT_PUBLIC_WEBAPP_URL}/share/${slug}/opengraph`, { await fetch(`${NEXT_PUBLIC_WEBAPP_URL()}/share/${slug}/opengraph`, {
// We don't care about the response, so we can use no-cors // We don't care about the response, so we can use no-cors
mode: 'no-cors', mode: 'no-cors',
}); });
@@ -100,7 +101,7 @@ export const DocumentShareButton = ({
window.open( window.open(
generateTwitterIntent( generateTwitterIntent(
`I just ${token ? 'signed' : 'sent'} a document in style with @documenso. Check it out!`, `I just ${token ? 'signed' : 'sent'} a document in style with @documenso. Check it out!`,
`${process.env.NEXT_PUBLIC_WEBAPP_URL}/share/${slug}`, `${NEXT_PUBLIC_WEBAPP_URL()}/share/${slug}`,
), ),
'_blank', '_blank',
); );
@@ -148,7 +149,7 @@ export const DocumentShareButton = ({
'animate-pulse': !shareLink?.slug, 'animate-pulse': !shareLink?.slug,
})} })}
> >
{process.env.NEXT_PUBLIC_WEBAPP_URL}/share/{shareLink?.slug || '...'} {NEXT_PUBLIC_WEBAPP_URL()}/share/{shareLink?.slug || '...'}
</span> </span>
<div <div
className={cn( className={cn(
@@ -160,7 +161,7 @@ export const DocumentShareButton = ({
> >
{shareLink?.slug && ( {shareLink?.slug && (
<img <img
src={`${process.env.NEXT_PUBLIC_WEBAPP_URL}/share/${shareLink.slug}/opengraph`} src={`${NEXT_PUBLIC_WEBAPP_URL()}/share/${shareLink.slug}/opengraph`}
alt="sharing link" alt="sharing link"
className="h-full w-full object-cover" className="h-full w-full object-cover"
/> />

View File

@@ -121,7 +121,7 @@ const CommandItem = React.forwardRef<
<CommandPrimitive.Item <CommandPrimitive.Item
ref={ref} ref={ref}
className={cn( className={cn(
'aria-selected:bg-accent aria-selected:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50', 'hover:bg-accent hover:text-accent-foreground aria-selected:bg-accent aria-selected:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className, className,
)} )}
{...props} {...props}

View File

@@ -4,6 +4,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Caveat } from 'next/font/google'; import { Caveat } from 'next/font/google';
import { Label } from '@radix-ui/react-label';
import { Check, ChevronsUpDown, Info } from 'lucide-react'; import { Check, ChevronsUpDown, Info } from 'lucide-react';
import { useFieldArray, useForm } from 'react-hook-form'; import { useFieldArray, useForm } from 'react-hook-form';
@@ -20,6 +21,7 @@ import { cn } from '../../lib/utils';
import { Button } from '../button'; import { Button } from '../button';
import { Card, CardContent } from '../card'; import { Card, CardContent } from '../card';
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem } from '../command'; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem } from '../command';
import { Input } from '../input';
import { Popover, PopoverContent, PopoverTrigger } from '../popover'; import { Popover, PopoverContent, PopoverTrigger } from '../popover';
import { useStep } from '../stepper'; import { useStep } from '../stepper';
import { Tooltip, TooltipContent, TooltipTrigger } from '../tooltip'; import { Tooltip, TooltipContent, TooltipTrigger } from '../tooltip';
@@ -32,6 +34,7 @@ import {
DocumentFlowFormContainerStep, DocumentFlowFormContainerStep,
} from './document-flow-root'; } from './document-flow-root';
import { FieldItem } from './field-item'; import { FieldItem } from './field-item';
import type { TDocumentFlowFormSchema } from './types';
import { type DocumentFlowStep, FRIENDLY_FIELD_TYPE } from './types'; import { type DocumentFlowStep, FRIENDLY_FIELD_TYPE } from './types';
const fontCaveat = Caveat({ const fontCaveat = Caveat({
@@ -47,6 +50,8 @@ const DEFAULT_WIDTH_PERCENT = 15;
const MIN_HEIGHT_PX = 60; const MIN_HEIGHT_PX = 60;
const MIN_WIDTH_PX = 200; const MIN_WIDTH_PX = 200;
type ActiveField = TDocumentFlowFormSchema['fields'][0];
export type AddFieldsFormProps = { export type AddFieldsFormProps = {
documentFlow: DocumentFlowStep; documentFlow: DocumentFlowStep;
hideRecipients?: boolean; hideRecipients?: boolean;
@@ -69,6 +74,7 @@ export const AddFieldsFormPartial = ({
control, control,
handleSubmit, handleSubmit,
formState: { isSubmitting }, formState: { isSubmitting },
setValue,
} = useForm<TAddFieldsFormSchema>({ } = useForm<TAddFieldsFormSchema>({
defaultValues: { defaultValues: {
fields: fields.map((field) => ({ fields: fields.map((field) => ({
@@ -82,11 +88,23 @@ export const AddFieldsFormPartial = ({
pageHeight: Number(field.height), pageHeight: Number(field.height),
signerEmail: signerEmail:
recipients.find((recipient) => recipient.id === field.recipientId)?.email ?? '', recipients.find((recipient) => recipient.id === field.recipientId)?.email ?? '',
label: field.label ?? '',
})), })),
}, },
}); });
// const addLabelForm = useForm<TAddCustomLabelFormSchema>({
// defaultValues: {
// label: '',
// },
// });
// const onCustomLabelFormSubmit = (data: TAddCustomLabelFormSchema) => {
// console.log('Custom label', data);
// };
const onFormSubmit = handleSubmit(onSubmit); const onFormSubmit = handleSubmit(onSubmit);
// const onAddCustomLabelFormSubmit = addLabelForm.handleSubmit(onCustomLabelFormSubmit);
const { const {
append, append,
@@ -101,6 +119,8 @@ export const AddFieldsFormPartial = ({
const [selectedField, setSelectedField] = useState<FieldType | null>(null); const [selectedField, setSelectedField] = useState<FieldType | null>(null);
const [selectedSigner, setSelectedSigner] = useState<Recipient | null>(null); const [selectedSigner, setSelectedSigner] = useState<Recipient | null>(null);
const [showRecipientsSelector, setShowRecipientsSelector] = useState(false); const [showRecipientsSelector, setShowRecipientsSelector] = useState(false);
const [activeField, setActiveField] = useState<ActiveField | null>(null);
const [fieldLabel, setFieldLabel] = useState<Record<string, string> | null>({});
const hasSelectedSignerBeenSent = selectedSigner?.sendStatus === SendStatus.SENT; const hasSelectedSignerBeenSent = selectedSigner?.sendStatus === SendStatus.SENT;
@@ -186,12 +206,13 @@ export const AddFieldsFormPartial = ({
pageWidth: fieldPageWidth, pageWidth: fieldPageWidth,
pageHeight: fieldPageHeight, pageHeight: fieldPageHeight,
signerEmail: selectedSigner.email, signerEmail: selectedSigner.email,
label: activeField?.label ?? fieldLabel?.[activeField?.formId ?? ''] ?? '',
}); });
setIsFieldWithinBounds(false); setIsFieldWithinBounds(false);
setSelectedField(null); setSelectedField(null);
}, },
[append, isWithinPageBounds, selectedField, selectedSigner, getPage], [append, isWithinPageBounds, selectedField, selectedSigner, activeField, fieldLabel, getPage],
); );
const onFieldResize = useCallback( const onFieldResize = useCallback(
@@ -257,7 +278,7 @@ export const AddFieldsFormPartial = ({
window.removeEventListener('mousemove', onMouseMove); window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('mouseup', onMouseClick); window.removeEventListener('mouseup', onMouseClick);
}; };
}, [onMouseClick, onMouseMove, selectedField]); }, [onMouseClick, onMouseMove, selectedField, activeField]);
useEffect(() => { useEffect(() => {
const observer = new MutationObserver((_mutations) => { const observer = new MutationObserver((_mutations) => {
@@ -311,6 +332,8 @@ export const AddFieldsFormPartial = ({
); );
}, [recipientsByRole]); }, [recipientsByRole]);
console.log(localFields[0].label);
return ( return (
<> <>
<DocumentFlowFormContainerHeader <DocumentFlowFormContainerHeader
@@ -342,19 +365,24 @@ export const AddFieldsFormPartial = ({
</Card> </Card>
)} )}
{localFields.map((field, index) => ( {localFields.map((field, index) => {
<FieldItem return (
key={index} <FieldItem
field={field} key={index}
disabled={selectedSigner?.email !== field.signerEmail || hasSelectedSignerBeenSent} field={field}
minHeight={fieldBounds.current.height} disabled={selectedSigner?.email !== field.signerEmail || hasSelectedSignerBeenSent}
minWidth={fieldBounds.current.width} minHeight={fieldBounds.current.height}
passive={isFieldWithinBounds && !!selectedField} minWidth={fieldBounds.current.width}
onResize={(options) => onFieldResize(options, index)} passive={isFieldWithinBounds && !!selectedField}
onMove={(options) => onFieldMove(options, index)} onResize={(options) => onFieldResize(options, index)}
onRemove={() => remove(index)} onClick={(field) => {
/> setActiveField(field);
))} }}
onMove={(options) => onFieldMove(options, index)}
onRemove={() => remove(index)}
/>
);
})}
{!hideRecipients && ( {!hideRecipients && (
<Popover open={showRecipientsSelector} onOpenChange={setShowRecipientsSelector}> <Popover open={showRecipientsSelector} onOpenChange={setShowRecipientsSelector}>
@@ -380,7 +408,7 @@ export const AddFieldsFormPartial = ({
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="p-0" align="start"> <PopoverContent className="p-0" align="start">
<Command> <Command value={selectedSigner?.email}>
<CommandInput /> <CommandInput />
<CommandEmpty> <CommandEmpty>
@@ -462,7 +490,7 @@ export const AddFieldsFormPartial = ({
</Popover> </Popover>
)} )}
<div className="-mx-2 flex-1 overflow-y-auto px-2"> <div className="-mx-2 flex-1 px-2">
<fieldset disabled={isFieldsDisabled} className="grid grid-cols-2 gap-x-4 gap-y-8"> <fieldset disabled={isFieldsDisabled} className="grid grid-cols-2 gap-x-4 gap-y-8">
<button <button
type="button" type="button"
@@ -554,6 +582,40 @@ export const AddFieldsFormPartial = ({
</button> </button>
</fieldset> </fieldset>
</div> </div>
{activeField && (
<div className="-mx-2 my-8 flex-1 gap-1.5 px-2">
<Label htmlFor="form-label">Custom Label</Label>
<div className="mt-2 flex w-full items-center space-x-2">
<Input
type="text"
className="w-full"
placeholder="Label"
id="form-label"
value={fieldLabel?.[activeField.formId] ?? activeField.label}
onChange={(e) => {
setFieldLabel((prev) => ({
...prev,
[activeField.formId]: e.target.value,
}));
setValue(
'fields',
localFields.map((field) => {
if (field.formId === activeField.formId) {
return {
...field,
label: fieldLabel?.[activeField.formId] ?? activeField.label ?? '',
};
}
return field;
}),
);
}}
/>
</div>
</div>
)}
</div> </div>
</DocumentFlowFormContainerContent> </DocumentFlowFormContainerContent>

View File

@@ -14,6 +14,7 @@ export const ZAddFieldsFormSchema = z.object({
pageY: z.number().min(0), pageY: z.number().min(0),
pageWidth: z.number().min(0), pageWidth: z.number().min(0),
pageHeight: z.number().min(0), pageHeight: z.number().min(0),
label: z.string().min(1),
}), }),
), ),
}); });

View File

@@ -23,6 +23,7 @@ export type FieldItemProps = {
minWidth?: number; minWidth?: number;
onResize?: (_node: HTMLElement) => void; onResize?: (_node: HTMLElement) => void;
onMove?: (_node: HTMLElement) => void; onMove?: (_node: HTMLElement) => void;
onClick?: (field: Field) => void;
onRemove?: () => void; onRemove?: () => void;
}; };
@@ -35,6 +36,7 @@ export const FieldItem = ({
onResize, onResize,
onMove, onMove,
onRemove, onRemove,
onClick,
}: FieldItemProps) => { }: FieldItemProps) => {
const [active, setActive] = useState(false); const [active, setActive] = useState(false);
const [coords, setCoords] = useState({ const [coords, setCoords] = useState({
@@ -106,7 +108,10 @@ export const FieldItem = ({
width: coords.pageWidth, width: coords.pageWidth,
}} }}
bounds={`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${field.pageNumber}"]`} bounds={`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${field.pageNumber}"]`}
onDragStart={() => setActive(true)} onDragStart={() => {
setActive(true);
onClick?.(field);
}}
onResizeStart={() => setActive(true)} onResizeStart={() => setActive(true)}
onResizeStop={(_e, _d, ref) => { onResizeStop={(_e, _d, ref) => {
setActive(false); setActive(false);

View File

@@ -30,6 +30,7 @@ export const ZDocumentFlowFormSchema = z.object({
pageY: z.number().min(0), pageY: z.number().min(0),
pageWidth: z.number().min(0), pageWidth: z.number().min(0),
pageHeight: z.number().min(0), pageHeight: z.number().min(0),
label: z.string().optional(),
}), }),
), ),

View File

@@ -7,6 +7,8 @@ import { Undo2 } from 'lucide-react';
import type { StrokeOptions } from 'perfect-freehand'; import type { StrokeOptions } from 'perfect-freehand';
import { getStroke } from 'perfect-freehand'; import { getStroke } from 'perfect-freehand';
import { unsafe_useEffectOnce } from '@documenso/lib/client-only/hooks/use-effect-once';
import { cn } from '../../lib/utils'; import { cn } from '../../lib/utils';
import { getSvgPathFromStroke } from './helper'; import { getSvgPathFromStroke } from './helper';
import { Point } from './point'; import { Point } from './point';
@@ -28,6 +30,7 @@ export const SignaturePad = ({
...props ...props
}: SignaturePadProps) => { }: SignaturePadProps) => {
const $el = useRef<HTMLCanvasElement>(null); const $el = useRef<HTMLCanvasElement>(null);
const $imageData = useRef<ImageData | null>(null);
const [isPressed, setIsPressed] = useState(false); const [isPressed, setIsPressed] = useState(false);
const [lines, setLines] = useState<Point[][]>([]); const [lines, setLines] = useState<Point[][]>([]);
@@ -134,7 +137,6 @@ export const SignaturePad = ({
}); });
onChange?.($el.current.toDataURL()); onChange?.($el.current.toDataURL());
ctx.save(); ctx.save();
} }
} }
@@ -163,6 +165,7 @@ export const SignaturePad = ({
const ctx = $el.current.getContext('2d'); const ctx = $el.current.getContext('2d');
ctx?.clearRect(0, 0, $el.current.width, $el.current.height); ctx?.clearRect(0, 0, $el.current.width, $el.current.height);
$imageData.current = null;
} }
onChange?.(null); onChange?.(null);
@@ -176,19 +179,25 @@ export const SignaturePad = ({
return; return;
} }
const newLines = [...lines]; const newLines = lines.slice(0, -1);
newLines.pop(); // Remove the last line
setLines(newLines); setLines(newLines);
// Clear the canvas // Clear the canvas
if ($el.current) { if ($el.current) {
const ctx = $el.current.getContext('2d'); const ctx = $el.current.getContext('2d');
ctx?.clearRect(0, 0, $el.current.width, $el.current.height); const { width, height } = $el.current;
ctx?.clearRect(0, 0, width, height);
if (typeof defaultValue === 'string' && $imageData.current) {
ctx?.putImageData($imageData.current, 0, 0);
}
newLines.forEach((line) => { newLines.forEach((line) => {
const pathData = new Path2D(getSvgPathFromStroke(getStroke(line, perfectFreehandOptions))); const pathData = new Path2D(getSvgPathFromStroke(getStroke(line, perfectFreehandOptions)));
ctx?.fill(pathData); ctx?.fill(pathData);
}); });
onChange?.($el.current.toDataURL());
} }
}; };
@@ -199,7 +208,7 @@ export const SignaturePad = ({
} }
}, []); }, []);
useEffect(() => { unsafe_useEffectOnce(() => {
if ($el.current && typeof defaultValue === 'string') { if ($el.current && typeof defaultValue === 'string') {
const ctx = $el.current.getContext('2d'); const ctx = $el.current.getContext('2d');
@@ -209,11 +218,15 @@ export const SignaturePad = ({
img.onload = () => { img.onload = () => {
ctx?.drawImage(img, 0, 0, Math.min(width, img.width), Math.min(height, img.height)); ctx?.drawImage(img, 0, 0, Math.min(width, img.width), Math.min(height, img.height));
const defaultImageData = ctx?.getImageData(0, 0, width, height) || null;
$imageData.current = defaultImageData;
}; };
img.src = defaultValue; img.src = defaultValue;
} }
}, [defaultValue]); });
return ( return (
<div <div

View File

@@ -8,4 +8,5 @@ const pdfjsDistPath = path.dirname(require.resolve('pdfjs-dist/package.json'));
const pdfWorkerPath = path.join(pdfjsDistPath, 'build', 'pdf.worker.min.js'); const pdfWorkerPath = path.join(pdfjsDistPath, 'build', 'pdf.worker.min.js');
console.log(`Copying pdf.js to: ${path.resolve('./public/pdf.worker.min.js')}`);
fs.copyFileSync(pdfWorkerPath, './public/pdf.worker.min.js'); fs.copyFileSync(pdfWorkerPath, './public/pdf.worker.min.js');

View File

@@ -0,0 +1,16 @@
#!/usr/bin/env node
/* eslint-disable @typescript-eslint/no-var-requires */
const path = require('path');
const fs = require('fs');
const wellKnownPath = path.join(__dirname, '../.well-known');
console.log('Copying .well-known/ contents to apps');
fs.cpSync(wellKnownPath, path.join(__dirname, '../apps/web/public/.well-known'), {
recursive: true,
});
fs.cpSync(wellKnownPath, path.join(__dirname, '../apps/marketing/public/.well-known'), {
recursive: true,
});