mirror of
https://github.com/documenso/documenso.git
synced 2025-11-12 15:53:02 +10:00
Merge branch 'main' into feat/separate-document-page
This commit is contained in:
@ -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
7
.well-known/security.txt
Normal 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
|
||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
7
apps/marketing/public/.well-known/security.txt
Normal file
7
apps/marketing/public/.well-known/security.txt
Normal 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
|
||||||
56591
apps/marketing/public/pdf.worker.min.js
vendored
56591
apps/marketing/public/pdf.worker.min.js
vendored
File diff suppressed because one or more lines are too long
@ -5,14 +5,13 @@ import { allDocuments } from 'contentlayer/generated';
|
|||||||
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 generateStaticParams = () =>
|
export const dynamic = 'force-dynamic';
|
||||||
allDocuments.map((post) => ({ post: post._raw.flattenedPath }));
|
|
||||||
|
|
||||||
export const generateMetadata = ({ params }: { params: { content: string } }) => {
|
export const generateMetadata = ({ params }: { params: { content: string } }) => {
|
||||||
const document = allDocuments.find((post) => post._raw.flattenedPath === params.content);
|
const document = allDocuments.find((doc) => doc._raw.flattenedPath === params.content);
|
||||||
|
|
||||||
if (!document) {
|
if (!document) {
|
||||||
notFound();
|
return { title: 'Not Found' };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { title: document.title };
|
return { title: document.title };
|
||||||
|
|||||||
@ -7,14 +7,15 @@ 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 generateStaticParams = () =>
|
export const dynamic = 'force-dynamic';
|
||||||
allBlogPosts.map((post) => ({ post: post._raw.flattenedPath }));
|
|
||||||
|
|
||||||
export const generateMetadata = ({ params }: { params: { post: string } }) => {
|
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 {
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { allBlogPosts } from 'contentlayer/generated';
|
|||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'Blog',
|
title: 'Blog',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function BlogPage() {
|
export default function BlogPage() {
|
||||||
const blogPosts = allBlogPosts.sort((a, b) => {
|
const blogPosts = allBlogPosts.sort((a, b) => {
|
||||||
const dateA = new Date(a.date);
|
const dateA = new Date(a.date);
|
||||||
|
|||||||
@ -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" />
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>{' '}
|
||||||
|
|||||||
@ -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"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -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
|
||||||
? {
|
? {
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
7
apps/web/public/.well-known/security.txt
Normal file
7
apps/web/public/.well-known/security.txt
Normal 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
|
||||||
@ -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 asChild>
|
<DialogTitle asChild>
|
||||||
<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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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`,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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`,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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),
|
||||||
|
|||||||
@ -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');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,27 @@
|
|||||||
|
import { Mails } from 'lucide-react';
|
||||||
|
|
||||||
|
import { SendConfirmationEmailForm } from '~/components/forms/send-confirmation-email';
|
||||||
|
|
||||||
|
export default function UnverifiedAccount() {
|
||||||
|
return (
|
||||||
|
<div className="flex w-full items-start">
|
||||||
|
<div className="mr-4 mt-1 hidden md:block">
|
||||||
|
<Mails className="text-primary h-10 w-10" strokeWidth={2} />
|
||||||
|
</div>
|
||||||
|
<div className="">
|
||||||
|
<h2 className="text-2xl font-bold md:text-4xl">Confirm email</h2>
|
||||||
|
|
||||||
|
<p className="text-muted-foreground mt-4">
|
||||||
|
To gain access to your account, please confirm your email address by clicking on the
|
||||||
|
confirmation link from your inbox.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p className="text-muted-foreground mt-4">
|
||||||
|
If you don't find the confirmation link in your inbox, you can request a new one below.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<SendConfirmationEmailForm />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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.',
|
||||||
|
|||||||
@ -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')
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -1,14 +1,12 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { flushSync } from 'react-dom';
|
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
import { renderSVG } from 'uqr';
|
import { renderSVG } from 'uqr';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { downloadFile } from '@documenso/lib/client-only/download-file';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import {
|
import {
|
||||||
@ -54,14 +52,16 @@ export const EnableAuthenticatorAppDialog = ({
|
|||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
}: EnableAuthenticatorAppDialogProps) => {
|
}: EnableAuthenticatorAppDialogProps) => {
|
||||||
const router = useRouter();
|
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const { mutateAsync: setupTwoFactorAuthentication, data: setupTwoFactorAuthenticationData } =
|
const { mutateAsync: setupTwoFactorAuthentication, data: setupTwoFactorAuthenticationData } =
|
||||||
trpc.twoFactorAuthentication.setup.useMutation();
|
trpc.twoFactorAuthentication.setup.useMutation();
|
||||||
|
|
||||||
const { mutateAsync: enableTwoFactorAuthentication, data: enableTwoFactorAuthenticationData } =
|
const {
|
||||||
trpc.twoFactorAuthentication.enable.useMutation();
|
mutateAsync: enableTwoFactorAuthentication,
|
||||||
|
data: enableTwoFactorAuthenticationData,
|
||||||
|
isLoading: isEnableTwoFactorAuthenticationDataLoading,
|
||||||
|
} = trpc.twoFactorAuthentication.enable.useMutation();
|
||||||
|
|
||||||
const setupTwoFactorAuthenticationForm = useForm<TSetupTwoFactorAuthenticationForm>({
|
const setupTwoFactorAuthenticationForm = useForm<TSetupTwoFactorAuthenticationForm>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@ -115,6 +115,19 @@ export const EnableAuthenticatorAppDialog = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const downloadRecoveryCodes = () => {
|
||||||
|
if (enableTwoFactorAuthenticationData && enableTwoFactorAuthenticationData.recoveryCodes) {
|
||||||
|
const blob = new Blob([enableTwoFactorAuthenticationData.recoveryCodes.join('\n')], {
|
||||||
|
type: 'text/plain',
|
||||||
|
});
|
||||||
|
|
||||||
|
downloadFile({
|
||||||
|
filename: 'documenso-2FA-recovery-codes.txt',
|
||||||
|
data: blob,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const onEnableTwoFactorAuthenticationFormSubmit = async ({
|
const onEnableTwoFactorAuthenticationFormSubmit = async ({
|
||||||
token,
|
token,
|
||||||
}: TEnableTwoFactorAuthenticationForm) => {
|
}: TEnableTwoFactorAuthenticationForm) => {
|
||||||
@ -136,14 +149,6 @@ export const EnableAuthenticatorAppDialog = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onCompleteClick = () => {
|
|
||||||
flushSync(() => {
|
|
||||||
onOpenChange(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.refresh();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className="w-full max-w-xl md:max-w-xl lg:max-w-xl">
|
<DialogContent className="w-full max-w-xl md:max-w-xl lg:max-w-xl">
|
||||||
@ -270,9 +275,16 @@ export const EnableAuthenticatorAppDialog = ({
|
|||||||
<RecoveryCodeList recoveryCodes={enableTwoFactorAuthenticationData.recoveryCodes} />
|
<RecoveryCodeList recoveryCodes={enableTwoFactorAuthenticationData.recoveryCodes} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="mt-4 flex w-full flex-row-reverse items-center justify-between">
|
<div className="mt-4 flex flex-row-reverse items-center gap-2">
|
||||||
<Button type="button" onClick={() => onCompleteClick()}>
|
<Button onClick={() => onOpenChange(false)}>Complete</Button>
|
||||||
Complete
|
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={downloadRecoveryCodes}
|
||||||
|
disabled={!enableTwoFactorAuthenticationData?.recoveryCodes}
|
||||||
|
loading={isEnableTwoFactorAuthenticationDataLoading}
|
||||||
|
>
|
||||||
|
Download
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { useForm } from 'react-hook-form';
|
|||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { downloadFile } from '@documenso/lib/client-only/download-file';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import {
|
import {
|
||||||
@ -42,8 +43,11 @@ export type ViewRecoveryCodesDialogProps = {
|
|||||||
export const ViewRecoveryCodesDialog = ({ open, onOpenChange }: ViewRecoveryCodesDialogProps) => {
|
export const ViewRecoveryCodesDialog = ({ open, onOpenChange }: ViewRecoveryCodesDialogProps) => {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const { mutateAsync: viewRecoveryCodes, data: viewRecoveryCodesData } =
|
const {
|
||||||
trpc.twoFactorAuthentication.viewRecoveryCodes.useMutation();
|
mutateAsync: viewRecoveryCodes,
|
||||||
|
data: viewRecoveryCodesData,
|
||||||
|
isLoading: isViewRecoveryCodesDataLoading,
|
||||||
|
} = trpc.twoFactorAuthentication.viewRecoveryCodes.useMutation();
|
||||||
|
|
||||||
const viewRecoveryCodesForm = useForm<TViewRecoveryCodesForm>({
|
const viewRecoveryCodesForm = useForm<TViewRecoveryCodesForm>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@ -62,6 +66,19 @@ export const ViewRecoveryCodesDialog = ({ open, onOpenChange }: ViewRecoveryCode
|
|||||||
return 'view';
|
return 'view';
|
||||||
}, [viewRecoveryCodesData, isViewRecoveryCodesSubmitting]);
|
}, [viewRecoveryCodesData, isViewRecoveryCodesSubmitting]);
|
||||||
|
|
||||||
|
const downloadRecoveryCodes = () => {
|
||||||
|
if (viewRecoveryCodesData && viewRecoveryCodesData.recoveryCodes) {
|
||||||
|
const blob = new Blob([viewRecoveryCodesData.recoveryCodes.join('\n')], {
|
||||||
|
type: 'text/plain',
|
||||||
|
});
|
||||||
|
|
||||||
|
downloadFile({
|
||||||
|
filename: 'documenso-2FA-recovery-codes.txt',
|
||||||
|
data: blob,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const onViewRecoveryCodesFormSubmit = async ({ password }: TViewRecoveryCodesForm) => {
|
const onViewRecoveryCodesFormSubmit = async ({ password }: TViewRecoveryCodesForm) => {
|
||||||
try {
|
try {
|
||||||
await viewRecoveryCodes({ password });
|
await viewRecoveryCodes({ password });
|
||||||
@ -139,8 +156,17 @@ export const ViewRecoveryCodesDialog = ({ open, onOpenChange }: ViewRecoveryCode
|
|||||||
<RecoveryCodeList recoveryCodes={viewRecoveryCodesData.recoveryCodes} />
|
<RecoveryCodeList recoveryCodes={viewRecoveryCodesData.recoveryCodes} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="mt-4 flex flex-row-reverse items-center justify-between">
|
<div className="mt-4 flex flex-row-reverse items-center gap-2">
|
||||||
<Button onClick={() => onOpenChange(false)}>Complete</Button>
|
<Button onClick={() => onOpenChange(false)}>Complete</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
disabled={!viewRecoveryCodesData?.recoveryCodes}
|
||||||
|
loading={isViewRecoveryCodesDataLoading}
|
||||||
|
onClick={downloadRecoveryCodes}
|
||||||
|
>
|
||||||
|
Download
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
|
|||||||
95
apps/web/src/components/forms/send-confirmation-email.tsx
Normal file
95
apps/web/src/components/forms/send-confirmation-email.tsx
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { trpc } from '@documenso/trpc/react';
|
||||||
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from '@documenso/ui/primitives/form/form';
|
||||||
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
|
export const ZSendConfirmationEmailFormSchema = z.object({
|
||||||
|
email: z.string().email().min(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TSendConfirmationEmailFormSchema = z.infer<typeof ZSendConfirmationEmailFormSchema>;
|
||||||
|
|
||||||
|
export type SendConfirmationEmailFormProps = {
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SendConfirmationEmailForm = ({ className }: SendConfirmationEmailFormProps) => {
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const form = useForm<TSendConfirmationEmailFormSchema>({
|
||||||
|
values: {
|
||||||
|
email: '',
|
||||||
|
},
|
||||||
|
resolver: zodResolver(ZSendConfirmationEmailFormSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
const isSubmitting = form.formState.isSubmitting;
|
||||||
|
|
||||||
|
const { mutateAsync: sendConfirmationEmail } = trpc.profile.sendConfirmationEmail.useMutation();
|
||||||
|
|
||||||
|
const onFormSubmit = async ({ email }: TSendConfirmationEmailFormSchema) => {
|
||||||
|
try {
|
||||||
|
await sendConfirmationEmail({ email });
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: 'Confirmation email sent',
|
||||||
|
description:
|
||||||
|
'A confirmation email has been sent, and it should arrive in your inbox shortly.',
|
||||||
|
duration: 5000,
|
||||||
|
});
|
||||||
|
|
||||||
|
form.reset();
|
||||||
|
} catch (err) {
|
||||||
|
toast({
|
||||||
|
title: 'An error occurred while sending your confirmation email',
|
||||||
|
description: 'Please try again and make sure you enter the correct email address.',
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
className={cn('mt-6 flex w-full flex-col gap-y-4', className)}
|
||||||
|
onSubmit={form.handleSubmit(onFormSubmit)}
|
||||||
|
>
|
||||||
|
<fieldset className="flex w-full flex-col gap-y-4" disabled={isSubmitting}>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="email"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Email address</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="email" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
|
||||||
|
<Button size="lg" type="submit" disabled={isSubmitting} loading={isSubmitting}>
|
||||||
|
Send confirmation email
|
||||||
|
</Button>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { signIn } from 'next-auth/react';
|
import { signIn } from 'next-auth/react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
@ -38,6 +40,8 @@ const ERROR_MESSAGES: Partial<Record<keyof typeof ErrorCode, string>> = {
|
|||||||
'This account appears to be using a social login method, please sign in using that method',
|
'This account appears to be using a social login method, please sign in using that method',
|
||||||
[ErrorCode.INCORRECT_TWO_FACTOR_CODE]: 'The two-factor authentication code provided is incorrect',
|
[ErrorCode.INCORRECT_TWO_FACTOR_CODE]: 'The two-factor authentication code provided is incorrect',
|
||||||
[ErrorCode.INCORRECT_TWO_FACTOR_BACKUP_CODE]: 'The backup code provided is incorrect',
|
[ErrorCode.INCORRECT_TWO_FACTOR_BACKUP_CODE]: 'The backup code provided is incorrect',
|
||||||
|
[ErrorCode.UNVERIFIED_EMAIL]:
|
||||||
|
'This account has not been verified. Please verify your account before signing in.',
|
||||||
};
|
};
|
||||||
|
|
||||||
const TwoFactorEnabledErrorCode = ErrorCode.TWO_FACTOR_MISSING_CREDENTIALS;
|
const TwoFactorEnabledErrorCode = ErrorCode.TWO_FACTOR_MISSING_CREDENTIALS;
|
||||||
@ -63,6 +67,7 @@ export const SignInForm = ({ className, initialEmail, isGoogleSSOEnabled }: Sign
|
|||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [isTwoFactorAuthenticationDialogOpen, setIsTwoFactorAuthenticationDialogOpen] =
|
const [isTwoFactorAuthenticationDialogOpen, setIsTwoFactorAuthenticationDialogOpen] =
|
||||||
useState(false);
|
useState(false);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const [twoFactorAuthenticationMethod, setTwoFactorAuthenticationMethod] = useState<
|
const [twoFactorAuthenticationMethod, setTwoFactorAuthenticationMethod] = useState<
|
||||||
'totp' | 'backup'
|
'totp' | 'backup'
|
||||||
@ -130,6 +135,17 @@ export const SignInForm = ({ className, initialEmail, isGoogleSSOEnabled }: Sign
|
|||||||
|
|
||||||
const errorMessage = ERROR_MESSAGES[result.error];
|
const errorMessage = ERROR_MESSAGES[result.error];
|
||||||
|
|
||||||
|
if (result.error === ErrorCode.UNVERIFIED_EMAIL) {
|
||||||
|
router.push(`/unverified-account`);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: 'Unable to sign in',
|
||||||
|
description: errorMessage ?? 'An unknown error occurred',
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
title: 'Unable to sign in',
|
title: 'Unable to sign in',
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { signIn } from 'next-auth/react';
|
import { signIn } from 'next-auth/react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
@ -55,6 +57,7 @@ export type SignUpFormProps = {
|
|||||||
export const SignUpForm = ({ className, initialEmail, isGoogleSSOEnabled }: SignUpFormProps) => {
|
export const SignUpForm = ({ className, initialEmail, isGoogleSSOEnabled }: SignUpFormProps) => {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const analytics = useAnalytics();
|
const analytics = useAnalytics();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const form = useForm<TSignUpFormSchema>({
|
const form = useForm<TSignUpFormSchema>({
|
||||||
values: {
|
values: {
|
||||||
@ -74,10 +77,13 @@ export const SignUpForm = ({ className, initialEmail, isGoogleSSOEnabled }: Sign
|
|||||||
try {
|
try {
|
||||||
await signup({ name, email, password, signature });
|
await signup({ name, email, password, signature });
|
||||||
|
|
||||||
await signIn('credentials', {
|
router.push(`/unverified-account`);
|
||||||
email,
|
|
||||||
password,
|
toast({
|
||||||
callbackUrl: SIGN_UP_REDIRECT_PATH,
|
title: 'Registration Successful',
|
||||||
|
description:
|
||||||
|
'You have successfully registered. Please verify your account by clicking on the link you received in the email.',
|
||||||
|
duration: 5000,
|
||||||
});
|
});
|
||||||
|
|
||||||
analytics.capture('App: User Sign Up', {
|
analytics.capture('App: User Sign Up', {
|
||||||
|
|||||||
@ -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());
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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
16
package-lock.json
generated
@ -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",
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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]);
|
||||||
|
};
|
||||||
13
packages/ee/server-only/stripe/get-enterprise-plan-prices.ts
Normal file
13
packages/ee/server-only/stripe/get-enterprise-plan-prices.ts
Normal 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);
|
||||||
|
};
|
||||||
@ -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');
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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]);
|
||||||
|
};
|
||||||
17
packages/ee/server-only/stripe/get-team-related-prices.ts
Normal file
17
packages/ee/server-only/stripe/get-team-related-prices.ts
Normal 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));
|
||||||
|
};
|
||||||
@ -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;
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
19
packages/lib/client-only/download-file.ts
Normal file
19
packages/lib/client-only/download-file.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
export type DownloadFileOptions = {
|
||||||
|
filename: string;
|
||||||
|
data: Blob;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const downloadFile = ({ filename, data }: DownloadFileOptions) => {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
throw new Error('downloadFile can only be called in browser environments');
|
||||||
|
}
|
||||||
|
|
||||||
|
const link = window.document.createElement('a');
|
||||||
|
|
||||||
|
link.href = window.URL.createObjectURL(data);
|
||||||
|
link.download = filename;
|
||||||
|
|
||||||
|
link.click();
|
||||||
|
|
||||||
|
window.URL.revokeObjectURL(link.href);
|
||||||
|
};
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import type { DocumentData } from '@documenso/prisma/client';
|
import type { DocumentData } from '@documenso/prisma/client';
|
||||||
|
|
||||||
import { getFile } from '../universal/upload/get-file';
|
import { getFile } from '../universal/upload/get-file';
|
||||||
|
import { downloadFile } from './download-file';
|
||||||
|
|
||||||
type DownloadPDFProps = {
|
type DownloadPDFProps = {
|
||||||
documentData: DocumentData;
|
documentData: DocumentData;
|
||||||
@ -14,16 +15,12 @@ export const downloadPDF = async ({ documentData, fileName }: DownloadPDFProps)
|
|||||||
type: 'application/pdf',
|
type: 'application/pdf',
|
||||||
});
|
});
|
||||||
|
|
||||||
const link = window.document.createElement('a');
|
|
||||||
|
|
||||||
const [baseTitle] = fileName?.includes('.pdf')
|
const [baseTitle] = fileName?.includes('.pdf')
|
||||||
? fileName.split('.pdf')
|
? fileName.split('.pdf')
|
||||||
: [fileName ?? 'document'];
|
: [fileName ?? 'document'];
|
||||||
|
|
||||||
link.href = window.URL.createObjectURL(blob);
|
downloadFile({
|
||||||
link.download = `${baseTitle}_signed.pdf`;
|
filename: baseTitle,
|
||||||
|
data: blob,
|
||||||
link.click();
|
});
|
||||||
|
|
||||||
window.URL.revokeObjectURL(link.href);
|
|
||||||
};
|
};
|
||||||
|
|||||||
13
packages/lib/client-only/hooks/use-effect-once.ts
Normal file
13
packages/lib/client-only/hooks/use-effect-once.ts
Normal 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, []);
|
||||||
|
};
|
||||||
@ -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';
|
||||||
|
|||||||
@ -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';
|
|
||||||
|
|||||||
@ -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,7 +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,
|
app_teams: true,
|
||||||
app_document_page_view_history_sheet: false,
|
app_document_page_view_history_sheet: false,
|
||||||
marketing_header_single_player_mode: false,
|
marketing_header_single_player_mode: false,
|
||||||
@ -26,8 +31,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;
|
||||||
|
|||||||
@ -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`;
|
||||||
|
|||||||
@ -7,13 +7,16 @@ 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';
|
||||||
|
|
||||||
import { isTwoFactorAuthenticationEnabled } from '../server-only/2fa/is-2fa-availble';
|
import { isTwoFactorAuthenticationEnabled } from '../server-only/2fa/is-2fa-availble';
|
||||||
import { validateTwoFactorAuthentication } from '../server-only/2fa/validate-2fa';
|
import { validateTwoFactorAuthentication } from '../server-only/2fa/validate-2fa';
|
||||||
|
import { getMostRecentVerificationTokenByUserId } from '../server-only/user/get-most-recent-verification-token-by-user-id';
|
||||||
import { getUserByEmail } from '../server-only/user/get-user-by-email';
|
import { getUserByEmail } from '../server-only/user/get-user-by-email';
|
||||||
|
import { sendConfirmationToken } from '../server-only/user/send-confirmation-token';
|
||||||
import { extractNextAuthRequestMetadata } from '../universal/extract-request-metadata';
|
import { extractNextAuthRequestMetadata } from '../universal/extract-request-metadata';
|
||||||
import { ErrorCode } from './error-codes';
|
import { ErrorCode } from './error-codes';
|
||||||
|
|
||||||
@ -90,6 +93,22 @@ export const NEXT_AUTH_OPTIONS: AuthOptions = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!user.emailVerified) {
|
||||||
|
const mostRecentToken = await getMostRecentVerificationTokenByUserId({
|
||||||
|
userId: user.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
!mostRecentToken ||
|
||||||
|
mostRecentToken.expires.valueOf() <= Date.now() ||
|
||||||
|
DateTime.fromJSDate(mostRecentToken.createdAt).diffNow('minutes').minutes > -5
|
||||||
|
) {
|
||||||
|
await sendConfirmationToken({ email });
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(ErrorCode.UNVERIFIED_EMAIL);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: Number(user.id),
|
id: Number(user.id),
|
||||||
email: user.email,
|
email: user.email,
|
||||||
@ -203,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;
|
||||||
|
|||||||
@ -19,4 +19,5 @@ export const ErrorCode = {
|
|||||||
INCORRECT_PASSWORD: 'INCORRECT_PASSWORD',
|
INCORRECT_PASSWORD: 'INCORRECT_PASSWORD',
|
||||||
MISSING_ENCRYPTION_KEY: 'MISSING_ENCRYPTION_KEY',
|
MISSING_ENCRYPTION_KEY: 'MISSING_ENCRYPTION_KEY',
|
||||||
MISSING_BACKUP_CODE: 'MISSING_BACKUP_CODE',
|
MISSING_BACKUP_CODE: 'MISSING_BACKUP_CODE',
|
||||||
|
UNVERIFIED_EMAIL: 'UNVERIFIED_EMAIL',
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@ -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());
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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';
|
||||||
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';
|
||||||
@ -94,7 +95,7 @@ export const deleteDocument = async ({
|
|||||||
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,
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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) => {
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -0,0 +1,18 @@
|
|||||||
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
|
export type GetMostRecentVerificationTokenByUserIdOptions = {
|
||||||
|
userId: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMostRecentVerificationTokenByUserId = async ({
|
||||||
|
userId,
|
||||||
|
}: GetMostRecentVerificationTokenByUserIdOptions) => {
|
||||||
|
return await prisma.verificationToken.findFirst({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
createdAt: 'desc',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -1,13 +1,20 @@
|
|||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
|
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
import { ONE_HOUR } from '../../constants/time';
|
import { ONE_HOUR } from '../../constants/time';
|
||||||
import { sendConfirmationEmail } from '../auth/send-confirmation-email';
|
import { sendConfirmationEmail } from '../auth/send-confirmation-email';
|
||||||
|
import { getMostRecentVerificationTokenByUserId } from './get-most-recent-verification-token-by-user-id';
|
||||||
|
|
||||||
const IDENTIFIER = 'confirmation-email';
|
const IDENTIFIER = 'confirmation-email';
|
||||||
|
|
||||||
export const sendConfirmationToken = async ({ email }: { email: string }) => {
|
type SendConfirmationTokenOptions = { email: string; force?: boolean };
|
||||||
|
|
||||||
|
export const sendConfirmationToken = async ({
|
||||||
|
email,
|
||||||
|
force = false,
|
||||||
|
}: SendConfirmationTokenOptions) => {
|
||||||
const token = crypto.randomBytes(20).toString('hex');
|
const token = crypto.randomBytes(20).toString('hex');
|
||||||
|
|
||||||
const user = await prisma.user.findFirst({
|
const user = await prisma.user.findFirst({
|
||||||
@ -20,6 +27,21 @@ export const sendConfirmationToken = async ({ email }: { email: string }) => {
|
|||||||
throw new Error('User not found');
|
throw new Error('User not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (user.emailVerified) {
|
||||||
|
throw new Error('Email verified');
|
||||||
|
}
|
||||||
|
|
||||||
|
const mostRecentToken = await getMostRecentVerificationTokenByUserId({ userId: user.id });
|
||||||
|
|
||||||
|
// If we've sent a token in the last 5 minutes, don't send another one
|
||||||
|
if (
|
||||||
|
!force &&
|
||||||
|
mostRecentToken?.createdAt &&
|
||||||
|
DateTime.fromJSDate(mostRecentToken.createdAt).diffNow('minutes').minutes > -5
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const createdToken = await prisma.verificationToken.create({
|
const createdToken = await prisma.verificationToken.create({
|
||||||
data: {
|
data: {
|
||||||
identifier: IDENTIFIER,
|
identifier: IDENTIFIER,
|
||||||
@ -37,5 +59,11 @@ export const sendConfirmationToken = async ({ email }: { email: string }) => {
|
|||||||
throw new Error(`Failed to create the verification token`);
|
throw new Error(`Failed to create the verification token`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return sendConfirmationEmail({ userId: user.id });
|
try {
|
||||||
|
await sendConfirmationEmail({ userId: user.id });
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(`Failed to send the confirmation email`);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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}`;
|
||||||
|
|||||||
@ -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: {
|
||||||
|
|||||||
@ -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));
|
||||||
|
|
||||||
|
|||||||
@ -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');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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),
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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.',
|
||||||
|
|||||||
@ -141,7 +141,7 @@ export const profileRouter = router({
|
|||||||
try {
|
try {
|
||||||
const { email } = input;
|
const { email } = input;
|
||||||
|
|
||||||
return sendConfirmationToken({ email });
|
return await sendConfirmationToken({ email });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
let message = 'We were unable to send a confirmation email. Please try again.';
|
let message = 'We were unable to send a confirmation email. Please try again.';
|
||||||
|
|
||||||
|
|||||||
@ -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([
|
||||||
|
|||||||
@ -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"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -380,7 +380,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>
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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');
|
||||||
|
|||||||
16
scripts/copy-wellknown.cjs
Normal file
16
scripts/copy-wellknown.cjs
Normal 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,
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user